@skein-code/cli 0.3.22 → 0.3.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -96,7 +96,7 @@ To build, verify, and install a local package artifact from this checkout:
96
96
 
97
97
  ```bash
98
98
  npm run verify:package -- --output-dir artifacts/package
99
- npm install -g ./artifacts/package/skein-code-cli-0.3.22.tgz
99
+ npm install -g ./artifacts/package/skein-code-cli-0.3.23.tgz
100
100
  ```
101
101
 
102
102
  To install the published package from npm:
@@ -214,6 +214,13 @@ only to the originating session, expire after seven days, and are removed when
214
214
  that session is deleted. A receipt marked `source-truncated` is honest about
215
215
  bytes already omitted by the producing tool; those bytes cannot be recovered.
216
216
 
217
+ Normal chat runs discover MCP servers lazily. Startup exposes only a compact
218
+ `mcp_activate` catalog of configured servers; selecting one connects that
219
+ server, runs remote discovery, and loads at most eight request-relevant schemas
220
+ into the live tool registry. All activated MCP tools remain `network`
221
+ operations. The explicit `skein mcp status` diagnostic still connects
222
+ configured servers eagerly.
223
+
217
224
  At 96 columns and wider, a fresh session uses a two-column welcome surface. The
218
225
  left side keeps Skein's brand, workspace path, and next actions close to the
219
226
  composer; the right side reports the actual model, mode, local index file/chunk
package/dist/cli.js CHANGED
@@ -220,7 +220,7 @@ function isSqliteBusy(error) {
220
220
  // package.json
221
221
  var package_default = {
222
222
  name: "@skein-code/cli",
223
- version: "0.3.22",
223
+ version: "0.3.23",
224
224
  description: "A context-first, model-agnostic coding agent with an auditable terminal workspace.",
225
225
  type: "module",
226
226
  license: "MIT",
@@ -236,6 +236,9 @@ var package_default = {
236
236
  },
237
237
  skein: {
238
238
  releaseNotes: [
239
+ "Normal chat startup now exposes a compact mcp_activate catalog instead of eagerly connecting MCP servers",
240
+ "MCP activation connects one selected server, discovers remote tools, and loads at most eight relevant schemas",
241
+ "Explicit MCP diagnostics can still connect eagerly while all MCP activation and remote calls remain network operations",
239
242
  "OpenAI, Anthropic, and Gemini usage now normalizes cached input, cache-write input, and reasoning tokens when reported",
240
243
  "Token Ledger, session totals, and structured usage events preserve provider cache and reasoning counts without prompt content",
241
244
  "Streaming, non-streaming, explicit-zero, and legacy-session compatibility paths are covered without claiming cache activation",
@@ -1938,6 +1941,7 @@ var agentTeamConfigSchema = z2.object({
1938
1941
  var mcpServerSchema = z2.object({
1939
1942
  enabled: z2.boolean().optional(),
1940
1943
  transport: z2.enum(["stdio", "http"]).optional(),
1944
+ description: z2.string().min(1).max(500).optional(),
1941
1945
  command: z2.string().min(1).max(512).optional(),
1942
1946
  args: z2.array(z2.string().max(4e3)).max(64).optional(),
1943
1947
  cwd: z2.string().max(4e3).optional(),
@@ -19940,12 +19944,24 @@ var MAX_TOOLS_PER_SERVER = 256;
19940
19944
  var MAX_LIST_PAGES = 16;
19941
19945
  var DEFAULT_CONNECT_TIMEOUT = 12e3;
19942
19946
  var DEFAULT_TOOL_TIMEOUT = 6e4;
19947
+ var LAZY_SCHEMA_LIMIT = 8;
19943
19948
  var McpManager = class {
19944
19949
  constructor(config, options = {}) {
19945
19950
  this.config = config;
19946
19951
  this.options = options;
19947
- for (const [name, server] of Object.entries(config.servers ?? {})) {
19952
+ const entries = Object.entries(config.servers ?? {});
19953
+ for (const [index, [name, server]] of entries.entries()) {
19948
19954
  const transport = server.transport ?? "stdio";
19955
+ if (index >= MAX_SERVERS) {
19956
+ this.statuses.set(name, {
19957
+ name,
19958
+ state: "error",
19959
+ transport,
19960
+ toolCount: 0,
19961
+ error: `MCP server limit exceeded (maximum ${MAX_SERVERS})`
19962
+ });
19963
+ continue;
19964
+ }
19949
19965
  const state = config.enabled === false || server.enabled === false ? "disabled" : "disconnected";
19950
19966
  this.statuses.set(name, { name, state, transport, toolCount: 0 });
19951
19967
  }
@@ -19960,6 +19976,75 @@ var McpManager = class {
19960
19976
  options;
19961
19977
  shutdownController = new AbortController();
19962
19978
  closed = false;
19979
+ /** Compact model-visible catalog; transport and remote discovery stay lazy. */
19980
+ activationTool(registry) {
19981
+ const names = this.activatableServerNames();
19982
+ if (!names.length) return;
19983
+ const catalog = names.map((name) => {
19984
+ const server = this.config.servers[name];
19985
+ const description = sanitizeCatalogText(server?.description) || `${server?.transport ?? "stdio"} MCP server`;
19986
+ return `${name}: ${description}`;
19987
+ }).join("; ");
19988
+ return {
19989
+ definition: {
19990
+ name: "mcp_activate",
19991
+ description: `Connect to one configured MCP server only when the current task needs it, discover its tools, and load at most ${LAZY_SCHEMA_LIMIT} relevant schemas. Available servers: ${catalog}`,
19992
+ category: "network",
19993
+ inputSchema: {
19994
+ type: "object",
19995
+ properties: {
19996
+ server: { type: "string", enum: names, description: "Configured MCP server to activate." },
19997
+ query: { type: "string", minLength: 1, maxLength: 500, description: "Capability needed from that server." }
19998
+ },
19999
+ required: ["server", "query"],
20000
+ additionalProperties: false
20001
+ }
20002
+ },
20003
+ permissionCategories: () => ["network"],
20004
+ execute: async (arguments_, context) => {
20005
+ const server = typeof arguments_.server === "string" ? arguments_.server : "";
20006
+ const query = typeof arguments_.query === "string" ? arguments_.query.trim() : "";
20007
+ if (!names.includes(server)) throw new ToolInputError("MCP server is not available for activation");
20008
+ if (!query || query.length > 500) {
20009
+ throw new ToolInputError("MCP activation query must contain 1 to 500 characters");
20010
+ }
20011
+ const result = await this.activate(server, query, registry, context.signal);
20012
+ if (!result.ok) {
20013
+ return {
20014
+ ok: false,
20015
+ content: `MCP server ${server} could not be activated: ${result.status.error ?? result.status.state}`,
20016
+ metadata: activationMetadata(result)
20017
+ };
20018
+ }
20019
+ const loaded = result.registeredTools.length ? result.registeredTools.join(", ") : "no matching tool schemas";
20020
+ const deferred = result.deferredTools ? ` ${result.deferredTools} additional schemas remain deferred; activate again with a narrower query if needed.` : "";
20021
+ return {
20022
+ content: `Activated MCP server ${server}. Loaded: ${loaded}.${deferred}`,
20023
+ metadata: activationMetadata(result)
20024
+ };
20025
+ }
20026
+ };
20027
+ }
20028
+ /** Connect/discover one server, then register only request-relevant schemas. */
20029
+ async activate(name, query, registry, signal) {
20030
+ const connected = await this.connect(name, signal);
20031
+ if (!connected.ok) {
20032
+ return { ...connected, registeredTools: [], availableTools: 0, deferredTools: 0 };
20033
+ }
20034
+ const connection = this.connections.get(name);
20035
+ if (!connection) {
20036
+ return { ...connected, ok: false, registeredTools: [], availableTools: 0, deferredTools: 0 };
20037
+ }
20038
+ const tools = [...connection.tools.values()];
20039
+ const selected = tools.length <= LAZY_SCHEMA_LIMIT ? tools : selectRelevantTools(tools, query, LAZY_SCHEMA_LIMIT);
20040
+ this.registerSelectedTools(registry, selected);
20041
+ return {
20042
+ ...connected,
20043
+ registeredTools: selected.map((tool) => tool.definition.name),
20044
+ availableTools: tools.length,
20045
+ deferredTools: Math.max(0, tools.length - selected.length)
20046
+ };
20047
+ }
19963
20048
  /** Connect enabled servers with a small concurrency bound. */
19964
20049
  async connectAll(signal) {
19965
20050
  if (this.closed) throw new Error("MCP manager is closed");
@@ -19968,15 +20053,6 @@ var McpManager = class {
19968
20053
  if (this.config.enabled === false) {
19969
20054
  return configuredNames.map((name) => this.resultFor(name, false, 0));
19970
20055
  }
19971
- for (const name of configuredNames.slice(MAX_SERVERS)) {
19972
- const server = this.config.servers[name];
19973
- this.setStatus(name, {
19974
- state: "error",
19975
- transport: server?.transport ?? "stdio",
19976
- toolCount: 0,
19977
- error: `MCP server limit exceeded (maximum ${MAX_SERVERS})`
19978
- });
19979
- }
19980
20056
  const results = [];
19981
20057
  let cursor = 0;
19982
20058
  const worker = async () => {
@@ -19992,6 +20068,10 @@ var McpManager = class {
19992
20068
  /** Connect one configured server. Connection errors are captured in status. */
19993
20069
  async connect(name, signal) {
19994
20070
  if (this.closed) throw new Error("MCP manager is closed");
20071
+ const status = this.statuses.get(name);
20072
+ if (status?.state === "error" && status.error?.includes("server limit exceeded")) {
20073
+ return this.resultFor(name, false, 0);
20074
+ }
19995
20075
  const existing = this.pending.get(name);
19996
20076
  if (existing) return existing;
19997
20077
  const connectionController = new AbortController();
@@ -20076,8 +20156,11 @@ var McpManager = class {
20076
20156
  }
20077
20157
  /** Register connected MCP tools, preserving idempotency for the same adapter. */
20078
20158
  registerTools(registry) {
20159
+ return this.registerSelectedTools(registry, this.tools());
20160
+ }
20161
+ registerSelectedTools(registry, tools) {
20079
20162
  const registered = [];
20080
- for (const tool of this.tools()) {
20163
+ for (const tool of tools) {
20081
20164
  const existing = registry.get(tool.definition.name);
20082
20165
  if (existing) {
20083
20166
  if (existing !== tool) {
@@ -20090,6 +20173,10 @@ var McpManager = class {
20090
20173
  }
20091
20174
  return registered;
20092
20175
  }
20176
+ activatableServerNames() {
20177
+ if (this.config.enabled === false) return [];
20178
+ return [...this.statuses.values()].filter((status) => status.state !== "disabled" && status.state !== "error").map((status) => status.name).sort((left, right) => left.localeCompare(right));
20179
+ }
20093
20180
  async connectInternal(name, signal) {
20094
20181
  const configured = this.config.servers?.[name];
20095
20182
  if (!configured) throw new Error(`Unknown MCP server: ${name}`);
@@ -20341,6 +20428,28 @@ var McpManager = class {
20341
20428
  return { name, ok, status: { ...status }, skippedTools };
20342
20429
  }
20343
20430
  };
20431
+ function activationMetadata(result) {
20432
+ return {
20433
+ mcpServer: result.name,
20434
+ state: result.status.state,
20435
+ availableTools: result.availableTools,
20436
+ loadedTools: result.registeredTools,
20437
+ deferredTools: result.deferredTools,
20438
+ skippedTools: result.skippedTools
20439
+ };
20440
+ }
20441
+ function selectRelevantTools(tools, query, limit) {
20442
+ const terms = new Set(query.toLocaleLowerCase().match(/[\p{L}\p{N}_-]{2,}/gu) ?? []);
20443
+ return tools.map((tool) => {
20444
+ const searchable = `${tool.definition.name.replaceAll("_", " ")} ${tool.definition.description}`.toLocaleLowerCase();
20445
+ let score = 0;
20446
+ for (const term of terms) if (searchable.includes(term)) score += term.length;
20447
+ return { tool, score };
20448
+ }).sort((left, right) => right.score - left.score || left.tool.definition.name.localeCompare(right.tool.definition.name)).slice(0, limit).map(({ tool }) => tool);
20449
+ }
20450
+ function sanitizeCatalogText(value) {
20451
+ return value ? stripAnsi5(value).replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim().slice(0, 200) : "";
20452
+ }
20344
20453
  function requestOptions(timeoutMs, signal) {
20345
20454
  return {
20346
20455
  timeout: timeoutMs,
@@ -20850,7 +20959,7 @@ var ExtensionRuntime = class _ExtensionRuntime {
20850
20959
  throw error;
20851
20960
  }
20852
20961
  }
20853
- async initialize(registry, signal) {
20962
+ async initialize(registry, _signal) {
20854
20963
  if (this.initialized) return;
20855
20964
  if (this.memory) {
20856
20965
  await this.memory.open();
@@ -20862,8 +20971,8 @@ var ExtensionRuntime = class _ExtensionRuntime {
20862
20971
  }
20863
20972
  await this.skills?.discover();
20864
20973
  if (this.mcp) {
20865
- await this.mcp.connectAll(signal);
20866
- this.mcp.registerTools(registry);
20974
+ const activationTool = this.mcp.activationTool(registry);
20975
+ if (activationTool) registry.register(activationTool);
20867
20976
  }
20868
20977
  await this.profiles?.discover();
20869
20978
  if (this.config.agents?.enabled && this.profiles && this.options.provider && this.options.contextEngine) {