From 59969abef5964cca8fc3efdd905c09ff2509d366 Mon Sep 17 00:00:00 2001 From: Juha Litola Date: Fri, 31 Jul 2026 08:53:41 +0300 Subject: [PATCH 1/2] fix: improve agent tool guidance Expose code_grep context limits in the MCP schema and clarify docs navigation, public repository scope, and pinned source examples. --- docs/implementation/tools.md | 6 +++- packages/mcp/src/mcp/instructions.ts | 6 ++-- packages/mcp/src/mcp/server.test.ts | 23 +++++++++++++ .../mcp/src/shared/grep-repo-request.test.ts | 21 ++++++++++++ packages/mcp/src/shared/grep-repo-request.ts | 12 ++++--- packages/mcp/src/tools/grep-repo.ts | 32 +++++++++++++++++-- .../mcp/src/tools/read-package-doc.test.ts | 10 ++++++ packages/mcp/src/tools/read-package-doc.ts | 6 ++-- skills/githits-code/SKILL.md | 15 +++++---- src/commands/init/guidance-assets.ts | 2 +- src/commands/mcp-instructions.test.ts | 18 +++++++++++ 11 files changed, 130 insertions(+), 21 deletions(-) diff --git a/docs/implementation/tools.md b/docs/implementation/tools.md index 87d29d30..dfc51fe4 100644 --- a/docs/implementation/tools.md +++ b/docs/implementation/tools.md @@ -247,6 +247,10 @@ code_grep | matches in files | pattern="..." [regex,case-sensitive] Standard grep -A/-B notation: `:` separator on match lines, `-` on context lines. Non-adjacent blocks within the same file are separated by `--`. The `()` after the file path is the per-file match count; the header sums across files. Header flags (`regex`, `case-sensitive`) appear only when the request used them. Scope filters are not echoed in text mode; agents already have the tool call arguments in context, and `format: "json"` preserves exact request/filter metadata for programmatic use. Match-line offsets, file content hashes, file intent, and symbol metadata are dropped in text mode — agents that need them can request `format: "json"`. +`context_lines`, `context_lines_before`, and `context_lines_after` accept integers from 0 through 10. The MCP JSON Schema advertises the range so agent clients reject invalid calls before dispatch; direct CLI/internal callers retain the same request-builder validation. The asymmetric fields override the corresponding side of `context_lines`. + +`docs_read` text output is capped at 150 lines per call, including explicit larger ranges. Its response reports the actual returned range and total line count for the next bounded read; JSON mode preserves explicitly requested ranges. + **Errors in text mode.** `search` errors render as text in `text-v1` mode: `search | ERROR | code= [| retryable]\n` followed by an indented `details:` block when present. `code_files` and `code_grep` keep errors JSON-formatted in either mode for now — revisit if agent feedback warrants. ## Server instructions @@ -255,7 +259,7 @@ The MCP server advertises a short, cross-tool orientation via the protocol's ser `packages/mcp/src/mcp/instructions.ts` owns the server-level instruction sections: -- **Core block** — always loaded. Introduces GitHits, expands trigger criteria to include comparative cross-OSS questions and "how does X actually implement this" archaeology, and walks through the `get_example` / `search_language` / `feedback` workflow. +- **Core block** — always loaded. Introduces GitHits, defines its public-only scope, expands trigger criteria to include comparative cross-OSS questions and "how does X actually implement this" archaeology, and walks through the `get_example` / `search_language` / `feedback` workflow. - **External-content block** — included by default from `packages/mcp/src/tools/guardrails.ts`; tells agents to treat third-party prose as data, not instructions. - **Package-tools block** — always appended. Contains a preamble plus one bullet per package/code tool, plus two cross-tool tips: - **Delegate multi-call work**: anticipate 3+ code-navigation calls? Use a sub-agent and ask for a compact synthesis. diff --git a/packages/mcp/src/mcp/instructions.ts b/packages/mcp/src/mcp/instructions.ts index bedee5ff..f00ecd77 100644 --- a/packages/mcp/src/mcp/instructions.ts +++ b/packages/mcp/src/mcp/instructions.ts @@ -17,6 +17,8 @@ const CORE_BLOCK = `GitHits provides verified open-source examples plus indexed Routing: use \`get_example\` for canonical cross-project examples; use \`search\` / \`code_*\` / \`docs_*\` / \`pkg_*\` for a known dependency, repository, stack trace, package adoption question, or upgrade review; use both for comparative OSS questions or when package-scoped evidence needs broader examples. Use \`search_language\` only to disambiguate a \`get_example\` language. Use \`feedback\` after helpful or flawed results. +GitHits indexes public OSS/package evidence, not local workspaces, private repositories, uncommitted changes, or proprietary code. Do not attempt private repository targets; they return \`REPOSITORY_NOT_FOUND\`. + When presenting \`get_example\` output, include source repository provenance/citations from GitHits' generated references/provenance section whenever present.`; const PACKAGE_TOOLS_PREAMBLE = `Indexed package/source tools inspect third-party dependency source, docs, and registry metadata. Package targets use \`registry:name[@version]\`; repo targets use GitHub URLs. Prefer the default compact \`text-v1\` output; request JSON only when exact structured fields are necessary.`; @@ -43,10 +45,10 @@ const CODE_FILES_BULLET = "- `code_files` — list/discover file paths; first choice for directory enumeration before `code_read` or scoped `code_grep`."; const DOCS_LIST_BULLET = - '- `docs_list` — browse available documentation pages; use `search` with `source:"docs"` for topic search.'; + '- `docs_list` — browse documentation pages available for a package; for a docs topic, use `search` with `source:"docs"`, then pass its `pageId` to `docs_read`.'; const DOCS_READ_BULLET = - "- `docs_read` — read a documentation page by pageId from `docs_list` or docs `search` results."; + "- `docs_read` — read a documentation page by pageId from `docs_list` or docs `search` results; text reads are capped at 150 lines per call."; const PKG_INFO_BULLET = "- `pkg_info` — latest package health/adoption overview: license, repo health, downloads, publish age, latest vulnerability status."; diff --git a/packages/mcp/src/mcp/server.test.ts b/packages/mcp/src/mcp/server.test.ts index b1619afc..fcf4572f 100644 --- a/packages/mcp/src/mcp/server.test.ts +++ b/packages/mcp/src/mcp/server.test.ts @@ -62,3 +62,26 @@ describe("MCP output format", () => { } }); }); + +describe("MCP code_grep schema", () => { + it("advertises context as integers from zero through ten", () => { + const descriptor = getMcpToolDescriptors().find( + (candidate) => candidate.name === "code_grep", + ); + expect(descriptor).toBeDefined(); + + const inputSchema = z.toJSONSchema(z.object(descriptor?.schema ?? {})); + for (const field of [ + "context_lines", + "context_lines_before", + "context_lines_after", + ]) { + expect(inputSchema.properties?.[field], field).toMatchObject({ + type: "integer", + minimum: 0, + maximum: 10, + description: expect.stringContaining("integer 0-10"), + }); + } + }); +}); diff --git a/packages/mcp/src/shared/grep-repo-request.test.ts b/packages/mcp/src/shared/grep-repo-request.test.ts index 9db7e6b4..74172f3e 100644 --- a/packages/mcp/src/shared/grep-repo-request.test.ts +++ b/packages/mcp/src/shared/grep-repo-request.test.ts @@ -61,6 +61,27 @@ describe("buildGrepRepoParams", () => { expect(asymmetric.params.contextLinesAfter).toBe(5); }); + it("accepts context boundaries and rejects values outside them", () => { + const boundary = buildGrepRepoParams({ + target, + pattern: "middleware", + contextLinesBefore: 0, + contextLinesAfter: 10, + }); + expect(boundary.params.contextLinesBefore).toBe(0); + expect(boundary.params.contextLinesAfter).toBe(10); + + for (const contextLines of [-1, 11, 1.5]) { + expect(() => + buildGrepRepoParams({ + target, + pattern: "middleware", + contextLines, + }), + ).toThrow(/context_lines.*integer between 0 and 10/); + } + }); + it("passes symbol fields through when requested", () => { const { params, explicit } = buildGrepRepoParams({ target, diff --git a/packages/mcp/src/shared/grep-repo-request.ts b/packages/mcp/src/shared/grep-repo-request.ts index 587bb4b2..c2229762 100644 --- a/packages/mcp/src/shared/grep-repo-request.ts +++ b/packages/mcp/src/shared/grep-repo-request.ts @@ -10,8 +10,8 @@ import { import { InvalidPackageSpecError } from "./package-spec.js"; const PATTERN_MAX = 200; -const CONTEXT_MIN = 0; -const CONTEXT_MAX = 10; +export const GREP_REPO_CONTEXT_MIN = 0; +export const GREP_REPO_CONTEXT_MAX = 10; const LIMIT_MIN = 1; const LIMIT_MAX = 1000; const LIMIT_DEFAULT = 50; @@ -254,9 +254,13 @@ function normalizeOptionalContext( field: string, ): number | undefined { if (value === undefined) return undefined; - if (!Number.isInteger(value) || value < CONTEXT_MIN || value > CONTEXT_MAX) { + if ( + !Number.isInteger(value) || + value < GREP_REPO_CONTEXT_MIN || + value > GREP_REPO_CONTEXT_MAX + ) { throw new InvalidPackageSpecError( - `\`${field}\` must be an integer between ${CONTEXT_MIN} and ${CONTEXT_MAX}. Got ${value}.`, + `\`${field}\` must be an integer between ${GREP_REPO_CONTEXT_MIN} and ${GREP_REPO_CONTEXT_MAX}. Got ${value}.`, ); } return value; diff --git a/packages/mcp/src/tools/grep-repo.ts b/packages/mcp/src/tools/grep-repo.ts index 26d6ba83..999d303f 100644 --- a/packages/mcp/src/tools/grep-repo.ts +++ b/packages/mcp/src/tools/grep-repo.ts @@ -4,6 +4,8 @@ import { z } from "zod"; import { mapCodeNavigationError } from "../shared/code-navigation-error-map.js"; import { buildGrepRepoParams, + GREP_REPO_CONTEXT_MAX, + GREP_REPO_CONTEXT_MIN, GREP_REPO_PATTERN_NOTE, GREP_REPO_SYMBOL_FIELDS, GREP_REPO_SYMBOL_FIELDS_NOTE, @@ -76,9 +78,33 @@ const schema: ZodRawShape = { case_sensitive: z.boolean().optional(), exclude_doc_files: z.boolean().optional(), exclude_test_files: z.boolean().optional(), - context_lines: z.number().optional(), - context_lines_before: z.number().optional(), - context_lines_after: z.number().optional(), + context_lines: z + .number() + .int() + .min(GREP_REPO_CONTEXT_MIN) + .max(GREP_REPO_CONTEXT_MAX) + .optional() + .describe( + `Context lines on both sides of each match (integer ${GREP_REPO_CONTEXT_MIN}-${GREP_REPO_CONTEXT_MAX}). \`context_lines_before\` or \`context_lines_after\` overrides the corresponding side.`, + ), + context_lines_before: z + .number() + .int() + .min(GREP_REPO_CONTEXT_MIN) + .max(GREP_REPO_CONTEXT_MAX) + .optional() + .describe( + `Context lines before each match (integer ${GREP_REPO_CONTEXT_MIN}-${GREP_REPO_CONTEXT_MAX}). Overrides \`context_lines\` for the before side.`, + ), + context_lines_after: z + .number() + .int() + .min(GREP_REPO_CONTEXT_MIN) + .max(GREP_REPO_CONTEXT_MAX) + .optional() + .describe( + `Context lines after each match (integer ${GREP_REPO_CONTEXT_MIN}-${GREP_REPO_CONTEXT_MAX}). Overrides \`context_lines\` for the after side.`, + ), max_matches: z.number().optional(), max_matches_per_file: z.number().optional(), cursor: z.string().optional(), diff --git a/packages/mcp/src/tools/read-package-doc.test.ts b/packages/mcp/src/tools/read-package-doc.test.ts index 8bce97b0..fd54b6d1 100644 --- a/packages/mcp/src/tools/read-package-doc.test.ts +++ b/packages/mcp/src/tools/read-package-doc.test.ts @@ -20,6 +20,16 @@ describe("createReadPackageDocTool", () => { "end_line", "format", ]); + expect(tool.description).toContain("capped at 150 lines per call"); + expect(tool.schema.start_line?.description).toContain( + "at most 150 lines per call", + ); + expect(tool.schema.end_line?.description).toContain( + "In text mode, omitting it returns at most 150 lines", + ); + expect(tool.schema.end_line?.description).toContain( + "in JSON mode, omitting it reads to the end", + ); }); it("calls service.readPackageDoc with the page ID", async () => { diff --git a/packages/mcp/src/tools/read-package-doc.ts b/packages/mcp/src/tools/read-package-doc.ts index 3f53f9f6..2b9739a9 100644 --- a/packages/mcp/src/tools/read-package-doc.ts +++ b/packages/mcp/src/tools/read-package-doc.ts @@ -32,13 +32,13 @@ const schema: ZodRawShape = { .number() .optional() .describe( - "Starting line (1-indexed). Omit for the full page. Use with `end_line` to bound how much content the tool returns when a page is large.", + "Starting line (1-indexed). Omit to start at line 1. Text output returns at most 150 lines per call even when a larger explicit range is requested.", ), end_line: z .number() .optional() .describe( - "Ending line (inclusive). Omit for end of page. Must be ≥ `start_line` when both are set.", + "Ending line (inclusive). In text mode, omitting it returns at most 150 lines from `start_line`; in JSON mode, omitting it reads to the end of the page. Must be ≥ `start_line` when both are set. Text output clamps larger ranges and reports the returned range.", ), format: z .enum(["text-v1", "text", "json"]) @@ -50,7 +50,7 @@ const schema: ZodRawShape = { export const DESCRIPTION: string = "Read a documentation page by page ID. Works for both hosted/crawled docs and repository-backed docs. " + - "Pass `start_line` / `end_line` to fetch only a slice when a page is too long — response carries `totalLines` so you can target the next slice. " + + "Pass `start_line` / `end_line` to fetch a slice when a page is too long. Text output is capped at 150 lines per call, including explicit larger ranges; the response carries the returned range and `totalLines` so you can target the next slice. " + "Repo-backed results additionally include exact file follow-up metadata for `code_read`." + `\n\n${DOCS_GUARDRAIL}`; diff --git a/skills/githits-code/SKILL.md b/skills/githits-code/SKILL.md index e2e1107d..95cf4aa8 100644 --- a/skills/githits-code/SKILL.md +++ b/skills/githits-code/SKILL.md @@ -24,7 +24,7 @@ Use GitHits for evidence from real open-source code instead of guessing from mod - Exact language name uncertain for `example --lang`: run `githits languages ` first. - Inspecting a known dependency or GitHub repo: start with `githits search` scoped by `--in`. - Need file/path enumeration: use `githits code files`; do not probe directories with `code read`. -- Know the exact text or regex to match: use `githits code grep`; use `githits search` for discovery. +- Know the exact text to match: use `githits code grep` (literal by default). Pass `--regex` for RE2 syntax; lookaround and backreferences are unsupported. Use `githits search` for discovery. - Need documentation pages: use `githits search "" --source docs --in ` for topic search, or `githits docs list ` to browse available pages. ## Core Commands @@ -34,15 +34,15 @@ githits example "how to use express middleware" githits example "react hooks patterns" --lang typescript githits languages type -githits search "router middleware" --in npm:express -githits search "debounce" --in npm:lodash --source symbol +githits search "router middleware" --in npm:express@5.2.1 +githits search "debounce" --in npm:lodash@4.18.1 --source symbol githits search '"body parser" OR multer' --in npm:express --source docs --json githits search-status -githits code files npm:express lib/ --ext js --limit 100 -githits code read npm:express lib/express.js --lines 1-90 -githits code grep npm:express "process_params" lib/ -C 3 -githits code grep --repo-url https://github.com/expressjs/express --git-ref HEAD "Router" lib/ +githits code files npm:express@5.2.1 lib/ --ext js --limit 100 +githits code read npm:express@5.2.1 lib/express.js --lines 1-90 +githits code grep npm:express@5.2.1 "require('router')" lib/ -C 3 +githits code grep --repo-url https://github.com/expressjs/express --git-ref v5.2.1 "require('router')" lib/ githits docs list npm:express --limit 20 githits docs read --lines 20-120 @@ -52,6 +52,7 @@ githits docs read --lines 20-120 - For behavioral claims, prefer source, symbols, tests, and call sites over docs prose. - For `githits example` results, report the source repositories/citations shown in GitHits' generated references/provenance section; they are core evidence for the synthesized pattern. +- Package targets inspect published artifacts and omitted versions resolve to the latest release; repository targets inspect repository trees. For source-layout questions, always pin and report the package version or Git ref. - For source work, locate symbols or matches first, then read a focused window with explicit `--lines`. - For multi-step code/docs investigations, keep raw CLI output out of the final answer unless it is the evidence the user needs. - If output says it used recent/stale indexed evidence, treat the displayed served target as provenance; if freshness matters, retry with a longer `--wait` or use one of the displayed `queryable now` versions/refs, or inspect JSON `targetResolution` for structured candidates. diff --git a/src/commands/init/guidance-assets.ts b/src/commands/init/guidance-assets.ts index 2348f371..1011a3b6 100644 --- a/src/commands/init/guidance-assets.ts +++ b/src/commands/init/guidance-assets.ts @@ -14,4 +14,4 @@ export const CLAUDE_GITHITS_MCP_SKILL_RELATIVE_PATH = [ ] as const; export const GITHITS_GUIDANCE_BLOCK = - "GitHits has been installed to the system. For public OSS/package questions, prefer the installed githits-mcp skill and GitHits MCP tools when external evidence is useful. GitHits does not index local workspaces, private repositories, uncommitted changes, or proprietary code. For known public dependencies or repositories, use search/docs_* for docs and code_files/code_grep/code_read for source and call sites. Use get_example for broad cross-OSS scans of vague issues, unfamiliar errors, cross-library patterns, how others solved something, and rare real-world examples that may appear in only one or a few repos. Use pkg_* for package metadata, security, dependencies, changelogs, and upgrades. Prefer default compact text tool output; request JSON only when exact structured fields are necessary. Ground answers in fetched GitHits evidence and cite package, repository, file, docs page, or version facts when available."; + "GitHits has been installed to the system. For public OSS/package questions, prefer the installed githits-mcp skill and GitHits MCP tools when external evidence is useful. GitHits does not index local workspaces, private repositories, uncommitted changes, or proprietary code; do not attempt private repository targets. For known public dependencies or repositories, use search/docs_* for docs and code_files/code_grep/code_read for source and call sites. Use get_example for broad cross-OSS scans of vague issues, unfamiliar errors, cross-library patterns, how others solved something, and rare real-world examples that may appear in only one or a few repos. Use pkg_* for package metadata, security, dependencies, changelogs, and upgrades. Prefer default compact text tool output; request JSON only when exact structured fields are necessary. Ground answers in fetched GitHits evidence and cite package, repository, file, docs page, or version facts when available."; diff --git a/src/commands/mcp-instructions.test.ts b/src/commands/mcp-instructions.test.ts index 2a5dfd63..83654428 100644 --- a/src/commands/mcp-instructions.test.ts +++ b/src/commands/mcp-instructions.test.ts @@ -109,6 +109,24 @@ describe("buildMcpInstructions", () => { ); }); + it("excludes local and private repository targets", () => { + const instructions = buildMcpInstructions(); + expect(instructions).toContain( + "not local workspaces, private repositories", + ); + expect(instructions).toContain("Do not attempt private repository targets"); + expect(instructions).toContain("`REPOSITORY_NOT_FOUND`"); + }); + + it("makes indexed documentation discovery explicit", () => { + const instructions = buildMcpInstructions(); + expect(instructions).toContain( + "documentation pages available for a package", + ); + expect(instructions).toContain('`search` with `source:"docs"`'); + expect(instructions).toContain("pass its `pageId` to `docs_read`"); + }); + it("keeps the core block first", () => { const instructions = buildMcpInstructions(); From a5eae3439a3ccd78151293015bced85741dbd926 Mon Sep 17 00:00:00 2001 From: Juha Litola Date: Fri, 31 Jul 2026 09:16:34 +0300 Subject: [PATCH 2/2] docs: label docs read cap guidance Separate the docs_read cap note from the surrounding code_grep anatomy documentation. --- docs/implementation/tools.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/implementation/tools.md b/docs/implementation/tools.md index dfc51fe4..f29e4ee1 100644 --- a/docs/implementation/tools.md +++ b/docs/implementation/tools.md @@ -249,7 +249,7 @@ Standard grep -A/-B notation: `:` separator on match lines, `-` on context lines `context_lines`, `context_lines_before`, and `context_lines_after` accept integers from 0 through 10. The MCP JSON Schema advertises the range so agent clients reject invalid calls before dispatch; direct CLI/internal callers retain the same request-builder validation. The asymmetric fields override the corresponding side of `context_lines`. -`docs_read` text output is capped at 150 lines per call, including explicit larger ranges. Its response reports the actual returned range and total line count for the next bounded read; JSON mode preserves explicitly requested ranges. +**Docs read cap.** `docs_read` text output is capped at 150 lines per call, including explicit larger ranges. Its response reports the actual returned range and total line count for the next bounded read; JSON mode preserves explicitly requested ranges. **Errors in text mode.** `search` errors render as text in `text-v1` mode: `search | ERROR | code= [| retryable]\n` followed by an indented `details:` block when present. `code_files` and `code_grep` keep errors JSON-formatted in either mode for now — revisit if agent feedback warrants.