context-mode 1.0.143 → 1.0.145

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.
@@ -13,6 +13,7 @@ import { readFileSync, mkdirSync, accessSync, existsSync, constants, } from "nod
13
13
  import { resolve, join } from "node:path";
14
14
  import { homedir } from "node:os";
15
15
  import { CopilotBaseAdapter } from "../copilot-base.js";
16
+ import { resolveContextModeDataRoot } from "../base.js";
16
17
  // ─────────────────────────────────────────────────────────
17
18
  // Hook constants (re-exported from hooks.ts)
18
19
  // ─────────────────────────────────────────────────────────
@@ -45,6 +46,16 @@ export class VSCodeCopilotAdapter extends CopilotBaseAdapter {
45
46
  return process.env.CLAUDE_PROJECT_DIR || process.cwd();
46
47
  }
47
48
  getSessionDir() {
49
+ // Issue #649: CONTEXT_MODE_DATA_DIR wins over both the .github project
50
+ // dir and the ~/.vscode fallback so dev-container/CI users can pin
51
+ // storage to a writable volume regardless of whether a .github tree
52
+ // happens to exist in cwd.
53
+ const override = resolveContextModeDataRoot();
54
+ if (override) {
55
+ const overrideDir = join(override, "context-mode", "sessions");
56
+ mkdirSync(overrideDir, { recursive: true });
57
+ return overrideDir;
58
+ }
48
59
  // Prefer .github/context-mode/sessions/ if .github exists,
49
60
  // otherwise fall back to ~/.vscode/context-mode/sessions/
50
61
  const githubDir = resolve(".github", "context-mode", "sessions");
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: [] }));
package/build/store.js CHANGED
@@ -524,7 +524,7 @@ export class ContentStore {
524
524
  highlight(chunks, 1, char(2), char(3)) AS highlighted
525
525
  FROM chunks
526
526
  JOIN sources ON sources.id = chunks.source_id
527
- WHERE chunks MATCH ? AND sources.label LIKE ?
527
+ WHERE chunks MATCH ? AND sources.label LIKE ? ESCAPE '\\'
528
528
  ORDER BY rank
529
529
  LIMIT ?
530
530
  `);
@@ -569,7 +569,7 @@ export class ContentStore {
569
569
  highlight(chunks_trigram, 1, char(2), char(3)) AS highlighted
570
570
  FROM chunks_trigram
571
571
  JOIN sources ON sources.id = chunks_trigram.source_id
572
- WHERE chunks_trigram MATCH ? AND sources.label LIKE ?
572
+ WHERE chunks_trigram MATCH ? AND sources.label LIKE ? ESCAPE '\\'
573
573
  ORDER BY rank
574
574
  LIMIT ?
575
575
  `);
@@ -615,7 +615,7 @@ export class ContentStore {
615
615
  highlight(chunks, 1, char(2), char(3)) AS highlighted
616
616
  FROM chunks
617
617
  JOIN sources ON sources.id = chunks.source_id
618
- WHERE chunks MATCH ? AND sources.label LIKE ? AND chunks.content_type = ?
618
+ WHERE chunks MATCH ? AND sources.label LIKE ? ESCAPE '\\' AND chunks.content_type = ?
619
619
  ORDER BY rank
620
620
  LIMIT ?
621
621
  `);
@@ -660,7 +660,7 @@ export class ContentStore {
660
660
  highlight(chunks_trigram, 1, char(2), char(3)) AS highlighted
661
661
  FROM chunks_trigram
662
662
  JOIN sources ON sources.id = chunks_trigram.source_id
663
- WHERE chunks_trigram MATCH ? AND sources.label LIKE ? AND chunks_trigram.content_type = ?
663
+ WHERE chunks_trigram MATCH ? AND sources.label LIKE ? ESCAPE '\\' AND chunks_trigram.content_type = ?
664
664
  ORDER BY rank
665
665
  LIMIT ?
666
666
  `);
@@ -859,7 +859,19 @@ export class ContentStore {
859
859
  }));
860
860
  }
861
861
  #sourceFilterParam(source, sourceMatchMode) {
862
- return sourceMatchMode === "exact" ? source : `%${source}%`;
862
+ if (sourceMatchMode === "exact")
863
+ return source;
864
+ // Escape SQLite LIKE metacharacters so user-supplied source labels
865
+ // containing `_`, `%`, or `\` are matched literally rather than as
866
+ // wildcards. Backslash must be replaced first (otherwise subsequent
867
+ // escapes would themselves be re-escaped). Paired with `ESCAPE '\'`
868
+ // in the four prepared LIKE statements (#stmtSearchPorter*,
869
+ // #stmtSearchTrigram*). Regression: #646.
870
+ const escaped = source
871
+ .replace(/\\/g, "\\\\")
872
+ .replace(/%/g, "\\%")
873
+ .replace(/_/g, "\\_");
874
+ return `%${escaped}%`;
863
875
  }
864
876
  search(query, limit = 3, source, mode = "AND", contentType, sourceMatchMode = "like") {
865
877
  const sanitized = sanitizeQuery(query, mode);