diff --git a/.changeset/vivid-ibex-hop.md b/.changeset/vivid-ibex-hop.md new file mode 100644 index 000000000..3143dbdc0 --- /dev/null +++ b/.changeset/vivid-ibex-hop.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 2963 +--- +**Research agents no longer call a context7 tool that doesn't exist** — four shipped docs instructed agents to call `mcp__context7__get-library-docs`, a tool the context7 MCP server does not register (it exposes only `resolve-library-id` and `query-docs`). Every research workflow that loaded the canonical doc-lookup reference either errored, fell back to the `ctx7` CLI, or fabricated a result. All sites now name `query-docs` with the registered `libraryId`/`query` params, the CLI-fallback rationale now describes the real project-scoped `.mcp.json` mechanism, and a parity guard fails the build if the banned name returns. (#2943) diff --git a/agents/gsd-executor.md b/agents/gsd-executor.md index 58b741b3a..058a7df22 100644 --- a/agents/gsd-executor.md +++ b/agents/gsd-executor.md @@ -26,10 +26,12 @@ When you need library or framework documentation, check in this order: 1. If Context7 MCP tools (`mcp__context7__*, mcp__plugin_context7_context7__*`) are available in your environment, use them: - Resolve library ID: `mcp__context7__resolve-library-id` with `libraryName` - - Fetch docs: `mcp__context7__get-library-docs` with `context7CompatibleLibraryId` and `topic` + - Fetch docs: `mcp__context7__query-docs` with `libraryId` (the ID from step 1) and `query` -2. If Context7 MCP is not available (upstream bug anthropics/claude-code#13898 strips MCP - tools from agents with a `tools:` frontmatter restriction), use the CLI fallback via Bash: +2. If Context7 MCP is not available (custom subagents cannot see project-scoped + `.mcp.json` servers — they only inherit user-scoped `~/.claude/mcp.json`, so a + context7 server configured at the project scope is invisible to spawned + agents), use the CLI fallback via Bash: Step 1 — Resolve library ID: ```bash diff --git a/gsd-core/references/research-documentation-lookup.md b/gsd-core/references/research-documentation-lookup.md index ecc1d1545..79dd742a9 100644 --- a/gsd-core/references/research-documentation-lookup.md +++ b/gsd-core/references/research-documentation-lookup.md @@ -2,10 +2,12 @@ When you need library or framework documentation, check in this order: 1. If Context7 MCP tools (`mcp__context7__*`) are available in your environment, use them: - Resolve library ID: `mcp__context7__resolve-library-id` with `libraryName` - - Fetch docs: `mcp__context7__get-library-docs` with `context7CompatibleLibraryId` and `topic` + - Fetch docs: `mcp__context7__query-docs` with `libraryId` (the ID from step 1) and `query` -2. If Context7 MCP is not available (upstream bug anthropics/claude-code#13898 strips MCP - tools from agents with a `tools:` frontmatter restriction), use the CLI fallback via Bash: +2. If Context7 MCP is not available (custom subagents cannot see project-scoped + `.mcp.json` servers — they only inherit user-scoped `~/.claude/mcp.json`, so a + context7 server configured at the project scope is invisible to spawned + agents), use the CLI fallback via Bash: Step 1 — Resolve library ID: ```bash diff --git a/gsd-core/workflows/discovery-phase.md b/gsd-core/workflows/discovery-phase.md index dc83520c1..b80b12339 100644 --- a/gsd-core/workflows/discovery-phase.md +++ b/gsd-core/workflows/discovery-phase.md @@ -65,9 +65,9 @@ For: Single known library, confirming syntax/version still correct. 2. Fetch relevant docs: ``` - mcp__context7__get-library-docs with: - - context7CompatibleLibraryID: [from step 1] - - topic: [specific concern] + mcp__context7__query-docs with: + - libraryId: [from step 1] + - query: [specific concern] ``` 3. Verify: @@ -101,7 +101,7 @@ For: Choosing between options, new external integration. ``` For each library/framework: - mcp__context7__resolve-library-id - - mcp__context7__get-library-docs (mode: "code" for API, "info" for concepts) + - mcp__context7__query-docs (frame the `query` for API usage vs concepts) ``` 3. **Official docs** for anything Context7 lacks. diff --git a/tests/context7-tool-name-parity.test.cjs b/tests/context7-tool-name-parity.test.cjs new file mode 100644 index 000000000..fc08d7a86 --- /dev/null +++ b/tests/context7-tool-name-parity.test.cjs @@ -0,0 +1,113 @@ +'use strict'; + +// Regression guard for #2943 — the context7 MCP server registers exactly two +// tools (`resolve-library-id` and `query-docs`); it does NOT register +// `get-library-docs`. That stale name survived in shipped prose (copied from +// upstream's own stale README), so agents were instructed to call a tool the +// server does not register — they errored, fell through to the ctx7 CLI +// fallback, or fabricated a result. +// +// This is the SECOND context7 naming drift after #2017 (which guarded the +// plugin-marketplace PREFIX). #2017's guard (context7-plugin-grant-parity) only +// checks `tools:` frontmatter lines; it does not scan prose bodies, which is +// where the broken tool NAME lived. This guard scans the shipped prose surface +// and fails if any artifact tells an agent to call the nonexistent tool. +// +// Allowed (registered upstream): mcp__context7__resolve-library-id, +// mcp__context7__query-docs, and the mcp__context7__* / plugin-marketplace +// grants. Banned: mcp__context7__get-library-docs. + +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const path = require('path'); + +const REPO_ROOT = path.join(__dirname, '..'); + +// Shipped prose directories an agent can be instructed by. Tests/, CHANGELOG, +// RELEASE-NOTES-LEGACY (historical record), and node_modules are deliberately +// excluded — a test fixture may use the banned name as a negative input, and +// history must not be rewritten. +const SCAN_DIRS = [ + 'agents', + 'gsd-core/references', + 'gsd-core/workflows', + 'commands/gsd', + 'skills', + 'docs', +]; + +// Files that are historical record (must not be rewritten to satisfy this +// guard) or generated changelog. RELEASE-NOTES-LEGACY.md carries the old #13898 +// attribution as shipped history; it is out of scope. +const EXCLUDED_FILES = new Set([ + path.join(REPO_ROOT, 'docs', 'RELEASE-NOTES-LEGACY.md'), + path.join(REPO_ROOT, 'CHANGELOG.md'), +]); + +const BANNED = 'mcp__context7__get-library-docs'; + +function listMarkdown(dir) { + const abs = path.join(REPO_ROOT, dir); + if (!fs.existsSync(abs)) return []; + const out = []; + for (const entry of fs.readdirSync(abs, { withFileTypes: true })) { + const full = path.join(abs, entry.name); + if (entry.isDirectory()) { + out.push(...listMarkdown(path.join(dir, entry.name))); + } else if (entry.isFile() && entry.name.endsWith('.md')) { + out.push(full); + } + } + return out; +} + +describe('#2943 — no shipped artifact references the nonexistent get-library-docs tool', () => { + const files = SCAN_DIRS.flatMap(listMarkdown) + .filter((f) => !EXCLUDED_FILES.has(f)); + assert.ok(files.length > 50, `scan surface sanity check (found ${files.length} markdown files)`); + + const offenders = []; + for (const file of files) { + const content = fs.readFileSync(file, 'utf8'); + if (content.includes(BANNED)) { + // Report every offending line for actionable failures. + const rel = path.relative(REPO_ROOT, file); + for (const [i, line] of content.split(/\r?\n/).entries()) { + if (line.includes(BANNED)) offenders.push(`${rel}:${i + 1}`); + } + } + } + + test(`no shipped prose references ${BANNED}`, () => { + assert.deepEqual(offenders, [], + `Shipped artifacts must not instruct agents to call ${BANNED} — the ` + + `context7 MCP server does not register it (only resolve-library-id and ` + + `query-docs exist). Offending sites:\n${offenders.join('\n')}`); + }); + + test('the canonical reference names the registered query-docs tool', () => { + const ref = path.join(REPO_ROOT, 'gsd-core', 'references', 'research-documentation-lookup.md'); + const content = fs.readFileSync(ref, 'utf8'); + assert.ok(content.includes('mcp__context7__query-docs'), + 'research-documentation-lookup.md must name mcp__context7__query-docs'); + // The canonical param names (upstream query-docs: libraryId + query). The + // alias context7CompatibleLibraryID is tolerated upstream but the canonical + // spelling removes the drift source. + assert.ok(/query-docs` with `libraryId`/.test(content) || /libraryId` and `query`/.test(content), + 'query-docs must be documented with the libraryId / query params'); + }); + + test('resolve-library-id tool name is preserved (still registered upstream)', () => { + // Negative-space guard: this fix renames get-library-docs only. + // resolve-library-id is a different, still-valid tool and must survive. + let any = false; + for (const file of files) { + if (fs.readFileSync(file, 'utf8').includes('mcp__context7__resolve-library-id')) { + any = true; + break; + } + } + assert.ok(any, 'mcp__context7__resolve-library-id must still appear in shipped artifacts'); + }); +}); diff --git a/tests/emitted-drift-acks/2943-context7-tool-name.json b/tests/emitted-drift-acks/2943-context7-tool-name.json new file mode 100644 index 000000000..8dd00b733 --- /dev/null +++ b/tests/emitted-drift-acks/2943-context7-tool-name.json @@ -0,0 +1,6 @@ +{ + "version": 1, + "paths": { + "gsd-executor.md": "#2943: the context7 doc-lookup block's ctx7 CLI-fallback rationale was rewritten to describe the real mechanism (custom subagents cannot see project-scoped .mcp.json; they inherit only user-scoped ~/.claude/mcp.json), replacing the wrong anthropics/claude-code#13898 'tools: frontmatter restriction' attribution. The tool name was also corrected (get-library-docs -> query-docs) and the params renamed (context7CompatibleLibraryId/topic -> libraryId/query). The +95 bytes is the longer-but-accurate mechanism description; it is the literal fix for the mis-attribution, not incidental prose growth, and the rationale must be correct because agents read it to decide when to fall back to the CLI." + } +} diff --git a/tests/no-bare-gsd-tools-command-position.test.cjs b/tests/no-bare-gsd-tools-command-position.test.cjs index d96cd7f46..135c328ad 100644 --- a/tests/no-bare-gsd-tools-command-position.test.cjs +++ b/tests/no-bare-gsd-tools-command-position.test.cjs @@ -82,7 +82,7 @@ const BARE_COMMAND_RE = new RegExp( // Each entry MUST carry a one-line reason; the test prints the allowlist on // failure so a reviewer can see exactly what is sanctioned. const PROSE_ALLOWLIST = [ - { file: 'agents/gsd-executor.md', line: 791, reason: 'describes the SDK return envelope of `gsd-tools query commit`; not an instruction to run the bare word' }, + { file: 'agents/gsd-executor.md', line: 793, reason: 'describes the SDK return envelope of `gsd-tools query commit`; not an instruction to run the bare word' }, { file: 'agents/gsd-phase-researcher.md', line: 33, reason: 'package-legitimacy provenance rule names the command as the source of an OK verdict; descriptive' }, { file: 'agents/gsd-roadmapper.md', line: 624, reason: 'parenthetical "e.g." naming SDK queries a user *could* run; not an agent instruction' }, { file: 'agents/gsd-intel-updater.md', line: 40, reason: 'cross-platform note names the `gsd-tools intel ` CLI surface descriptively ("CLI invocations go through..."); not an agent instruction' },