context-mode 1.0.143 → 1.0.144

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.
@@ -6,14 +6,14 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Claude Code plugins by Mert Koseoğlu",
9
- "version": "1.0.143"
9
+ "version": "1.0.144"
10
10
  },
11
11
  "plugins": [
12
12
  {
13
13
  "name": "context-mode",
14
14
  "source": "./",
15
15
  "description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
16
- "version": "1.0.143",
16
+ "version": "1.0.144",
17
17
  "author": {
18
18
  "name": "Mert Koseoğlu"
19
19
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "1.0.143",
3
+ "version": "1.0.144",
4
4
  "description": "MCP server that saves 98% of your context window with session continuity. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and automatic state restore across compactions.",
5
5
  "author": {
6
6
  "name": "Mert Koseoğlu",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "1.0.143",
3
+ "version": "1.0.144",
4
4
  "description": "MCP server that saves 98% of your context window with session continuity. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and automatic state restore across compactions.",
5
5
  "author": {
6
6
  "name": "Mert Koseoğlu",
@@ -3,7 +3,7 @@
3
3
  "name": "Context Mode",
4
4
  "kind": "tool",
5
5
  "description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
6
- "version": "1.0.143",
6
+ "version": "1.0.144",
7
7
  "sandbox": {
8
8
  "mode": "permissive",
9
9
  "filesystem_access": "full",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "1.0.143",
3
+ "version": "1.0.144",
4
4
  "description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
5
5
  "author": {
6
6
  "name": "Mert Koseoğlu",
@@ -92,10 +92,16 @@ export function resolveJsRuntimeForBridge(deps = {}) {
92
92
  return execPath;
93
93
  return null;
94
94
  }
95
+ // Bridge-imposed timeout for protocol-handshake methods (initialize,
96
+ // tools/list). These MUST be bounded: a server that never replies to
97
+ // initialize would otherwise block Pi's bridge bootstrap indefinitely.
98
+ // `tools/call` deliberately has NO bridge ceiling (#643) — long-running
99
+ // ctx_execute (test suites, builds, cargo test) was rejected by a 120s
100
+ // hardcoded bound even though the executor child would have finished.
101
+ // Responsibility for bounding a tool call belongs to the executor
102
+ // layer (per-tool timeout / background mode / Pi-level cancel), not
103
+ // to the transport.
95
104
  const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
96
- // Tools/call may run shell commands or fetch URLs — wider window than
97
- // initialize/list, but still bounded so a hung server can't block Pi.
98
- const DEFAULT_CALL_TIMEOUT_MS = 120_000;
99
105
  class PiTextComponent {
100
106
  text;
101
107
  constructor(text = "") {
@@ -349,19 +355,29 @@ export class MCPStdioClient {
349
355
  throw new Error("MCP client not started");
350
356
  const id = ++this.requestId;
351
357
  return new Promise((resolve, reject) => {
352
- const timer = setTimeout(() => {
353
- if (!this.pending.has(id))
354
- return;
355
- this.pending.delete(id);
356
- reject(new Error(`MCP request timeout after ${timeoutMs}ms: ${method}`));
357
- }, timeoutMs);
358
+ // Gate the timer on a finite ms value so callers can pass
359
+ // `Number.POSITIVE_INFINITY` to mean "no bridge ceiling" (#643).
360
+ // Node coerces both `undefined` and `Infinity` to a 1ms delay
361
+ // (TimeoutOverflowWarning), so we can't just pass them through —
362
+ // we must skip the setTimeout entirely. tools/call uses this path
363
+ // because long-running ctx_execute must not be bounded here.
364
+ const timer = Number.isFinite(timeoutMs)
365
+ ? setTimeout(() => {
366
+ if (!this.pending.has(id))
367
+ return;
368
+ this.pending.delete(id);
369
+ reject(new Error(`MCP request timeout after ${timeoutMs}ms: ${method}`));
370
+ }, timeoutMs)
371
+ : null;
358
372
  this.pending.set(id, {
359
373
  resolve: (v) => {
360
- clearTimeout(timer);
374
+ if (timer)
375
+ clearTimeout(timer);
361
376
  resolve(v);
362
377
  },
363
378
  reject: (e) => {
364
- clearTimeout(timer);
379
+ if (timer)
380
+ clearTimeout(timer);
365
381
  reject(e);
366
382
  },
367
383
  });
@@ -445,7 +461,15 @@ export class MCPStdioClient {
445
461
  // one layer covers `listTools` / `initialize` paths too, with a
446
462
  // single-flight guard against orphan child processes from
447
463
  // concurrent callers.
448
- return this.request("tools/call", { name, arguments: args ?? {} }, DEFAULT_CALL_TIMEOUT_MS);
464
+ //
465
+ // No bridge-imposed timeout for tools/call (#643). The previous
466
+ // 120s ceiling rejected legitimate long-running ctx_execute calls
467
+ // (test suites, builds, large `cargo test`) even though the
468
+ // executor child would have finished. Bounding belongs to the
469
+ // executor layer (per-tool timeout / background mode / Pi cancel),
470
+ // not the transport. `Number.POSITIVE_INFINITY` instructs
471
+ // `request()` to skip the setTimeout entirely — see the gate there.
472
+ return this.request("tools/call", { name, arguments: args ?? {} }, Number.POSITIVE_INFINITY);
449
473
  }
450
474
  /**
451
475
  * Respawn the MCP child after an exit (clean shutdown or crash).
package/build/server.d.ts CHANGED
@@ -21,6 +21,41 @@ export declare function emitSuppressionDiagnostic(opts?: {
21
21
  }): void;
22
22
  /** Test-only: reset the one-shot emission flag so suites can re-exercise. */
23
23
  export declare function __resetSuppressionDiagnosticForTests(): void;
24
+ /**
25
+ * Issue #637 — register an explicit empty `tools/list` handler on the McpServer.
26
+ *
27
+ * Background: when `suppressMcpToolsForNativePluginHost` is true, every
28
+ * `server.registerTool()` call is short-circuited (returns `undefined` above).
29
+ * The MCP SDK only installs the SDK-default `tools/list` handler when at least
30
+ * one `registerTool()` reaches `setToolRequestHandlers()` internally
31
+ * (mcp.js:56-67). Suppressing every registration leaves `tools/list`
32
+ * unregistered, and the framework's RPC layer answers it with
33
+ * `-32601 "Method not found"`.
34
+ *
35
+ * The reporter of #637 (SquirrelRat) inspected the suppressed child via
36
+ * `tools/list` and read the JSON-RPC error as "the plugin never registers any
37
+ * ctx_* tools" — when in fact the plugin DOES register all 11 tools natively
38
+ * (verified at `src/adapters/opencode/plugin.ts:469` and
39
+ * `tests/opencode-plugin.test.ts:88`). The misleading -32601 is the seed of
40
+ * the #637 perception.
41
+ *
42
+ * This helper installs an explicit handler that returns `{tools: []}` — a
43
+ * spec-compliant empty list. Paired with the existing #623 stderr diagnostic,
44
+ * an operator now sees:
45
+ * - wire response: `{tools: []}` (matches expectation, no JSON-RPC error)
46
+ * - stderr: `[context-mode] ctx_* tools/list intentionally empty… (#623)`
47
+ *
48
+ * Idempotent: throws inside SDK if called twice on the same server because
49
+ * `assertCanSetRequestHandler` (mcp.js:60) rejects duplicate registrations;
50
+ * we therefore install the SDK's default tool handlers FIRST (via a no-op
51
+ * registerTool of a fake tool, immediately removed) only if needed. To keep
52
+ * the public surface minimal, we just call `server.server.setRequestHandler`
53
+ * directly — that is the same low-level call used for prompts/resources at
54
+ * server.ts:259-261 and avoids the SDK guard entirely.
55
+ *
56
+ * Exported for test (#637 in-memory regression guard).
57
+ */
58
+ export declare function registerEmptyToolsListHandler(target?: McpServer): void;
24
59
  type ToolContextOverride = {
25
60
  projectDir: string;
26
61
  sessionId?: string;
package/build/server.js CHANGED
@@ -186,6 +186,44 @@ export function emitSuppressionDiagnostic(opts = {}) {
186
186
  export function __resetSuppressionDiagnosticForTests() {
187
187
  __suppressionDiagnosticEmitted = false;
188
188
  }
189
+ /**
190
+ * Issue #637 — register an explicit empty `tools/list` handler on the McpServer.
191
+ *
192
+ * Background: when `suppressMcpToolsForNativePluginHost` is true, every
193
+ * `server.registerTool()` call is short-circuited (returns `undefined` above).
194
+ * The MCP SDK only installs the SDK-default `tools/list` handler when at least
195
+ * one `registerTool()` reaches `setToolRequestHandlers()` internally
196
+ * (mcp.js:56-67). Suppressing every registration leaves `tools/list`
197
+ * unregistered, and the framework's RPC layer answers it with
198
+ * `-32601 "Method not found"`.
199
+ *
200
+ * The reporter of #637 (SquirrelRat) inspected the suppressed child via
201
+ * `tools/list` and read the JSON-RPC error as "the plugin never registers any
202
+ * ctx_* tools" — when in fact the plugin DOES register all 11 tools natively
203
+ * (verified at `src/adapters/opencode/plugin.ts:469` and
204
+ * `tests/opencode-plugin.test.ts:88`). The misleading -32601 is the seed of
205
+ * the #637 perception.
206
+ *
207
+ * This helper installs an explicit handler that returns `{tools: []}` — a
208
+ * spec-compliant empty list. Paired with the existing #623 stderr diagnostic,
209
+ * an operator now sees:
210
+ * - wire response: `{tools: []}` (matches expectation, no JSON-RPC error)
211
+ * - stderr: `[context-mode] ctx_* tools/list intentionally empty… (#623)`
212
+ *
213
+ * Idempotent: throws inside SDK if called twice on the same server because
214
+ * `assertCanSetRequestHandler` (mcp.js:60) rejects duplicate registrations;
215
+ * we therefore install the SDK's default tool handlers FIRST (via a no-op
216
+ * registerTool of a fake tool, immediately removed) only if needed. To keep
217
+ * the public surface minimal, we just call `server.server.setRequestHandler`
218
+ * directly — that is the same low-level call used for prompts/resources at
219
+ * server.ts:259-261 and avoids the SDK guard entirely.
220
+ *
221
+ * Exported for test (#637 in-memory regression guard).
222
+ */
223
+ export function registerEmptyToolsListHandler(target = server) {
224
+ target.server.registerCapabilities({ tools: { listChanged: false } });
225
+ target.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [] }));
226
+ }
189
227
  const originalRegisterTool = server.registerTool.bind(server);
190
228
  server.registerTool = (...args) => {
191
229
  const [name, config, handler] = args;
@@ -196,6 +234,16 @@ server.registerTool = (...args) => {
196
234
  REGISTERED_CTX_TOOLS.push({ name, config, handler });
197
235
  return originalRegisterTool(...args);
198
236
  };
237
+ // Issue #637 — when suppression is active, install the empty tools/list handler
238
+ // once at module-init time so the suppressed MCP child responds with
239
+ // `{tools: []}` instead of JSON-RPC `-32601 Method not found`. Pair with the
240
+ // #623 stderr diagnostic that explains WHY the list is empty. Skipped for the
241
+ // embedded plugin-import path because the embedded process is not the stdio
242
+ // MCP child an operator would inspect — it lives inside the OpenCode/Kilo
243
+ // host and never speaks JSON-RPC over stdio.
244
+ if (suppressMcpToolsForNativePluginHost && process.env.CONTEXT_MODE_EMBEDDED_PLUGIN_TOOLS !== "1") {
245
+ registerEmptyToolsListHandler(server);
246
+ }
199
247
  const projectDirOverride = new AsyncLocalStorage();
200
248
  export async function withProjectDirOverride(projectDir, fn) {
201
249
  const ctx = typeof projectDir === "string" ? { projectDir } : projectDir;
@@ -204,7 +252,7 @@ export async function withProjectDirOverride(projectDir, fn) {
204
252
  // Register empty prompts/resources handlers so MCP clients don't get -32601 (#168).
205
253
  // OpenCode calls listPrompts()/listResources() unconditionally — the error can poison
206
254
  // the SDK transport layer, causing subsequent listTools() calls to fail permanently.
207
- import { ListPromptsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema } from "@modelcontextprotocol/sdk/types.js";
255
+ import { ListPromptsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
208
256
  server.server.registerCapabilities({ prompts: { listChanged: false }, resources: { listChanged: false } });
209
257
  server.server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: [] }));
210
258
  server.server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [] }));