Skip to content

feat: [ENG-2124] Improve MCP tool descriptions#423

Merged
bao-byterover merged 3 commits intomainfrom
feat/ENG-2124
Apr 16, 2026
Merged

feat: [ENG-2124] Improve MCP tool descriptions#423
bao-byterover merged 3 commits intomainfrom
feat/ENG-2124

Conversation

@bao-byterover
Copy link
Copy Markdown
Collaborator

Summary

  • Problem: MCP tool descriptions for brv-curate and brv-query framed the cwd field around server runtime state (project-mode vs global-mode) — information LLMs cannot detect. Descriptions were also duplicated between the two tools, risking drift.
  • Why it matters: Coding agents (Cursor, Cline, Zed, Claude Code) couldn't reliably decide when to pass cwd, leading to either regression (omitting it inappropriately) or guessed/invented paths. No server-level onboarding existed before the first tool call.
  • What changed:
    • Added src/server/infra/mcp/tools/shared-schema.ts exporting CWD_DESCRIPTION and cwdField — a single source of truth for the cwd Zod field.
    • Reframed CWD_DESCRIPTION to be capability-first: instructs the LLM based on whether its own runtime exposes a workspace/project root (checkable), not the server's launch mode (not checkable).
    • Refactored brv-curate-tool.ts and brv-query-tool.ts to use cwdField instead of inline duplicated schemas.
    • Added a short server-level instructions string on the McpServer constructor as a pointer to the per-tool cwd description (avoids drift, minimal tokens).
  • What did NOT change (scope boundary):
    • No Zod type changes — cwd remains z.string().optional().
    • No handler logic, transport payloads, project resolution, or error paths modified.
    • context, files, folder, query fields untouched.
    • No changes to server mode detection, resolveClientCwd, or resolveMcpTaskContext.

Type of change

  • Bug fix
  • New feature
  • Refactor (no behavior change)
  • Documentation
  • Test
  • Chore (build, dependencies, CI)

Scope (select all touched areas)

  • TUI / REPL
  • Agent / Tools
  • LLM Providers
  • Server / Daemon
  • Shared (constants, types, transport events)
  • CLI Commands (oclif)
  • Hub / Connectors
  • Cloud Sync
  • CI/CD / Infra

Linked issues

  • Closes #
  • Related #

Root cause (bug fixes only, otherwise write N/A)

  • Root cause: N/A
  • Why this was not caught earlier: N/A

Test plan

  • Coverage added:
    • Unit test
    • Integration test
    • Manual verification only
  • Test file(s): Existing test/unit/infra/mcp/tools/brv-curate-tool.test.ts used as regression net (30 tests covering BrvCurateInputSchema shape + handler paths).
  • Key scenario(s) covered:
    • Schema still accepts cwd as optional absolute path (validation unchanged).
    • BrvCurateInputSchema.shape.cwd and BrvQueryInputSchema.shape.cwd both reference the shared cwdField.
    • Handler project-mode, global-mode, associate-retry, transport-error, and fire-and-forget paths all pass unchanged.

User-visible changes

  • Updated descriptions on brv-curate / brv-query MCP tools (cwd field text, visible to coding agents via MCP schema introspection).
  • New server-level instructions string exposed to MCP clients on initialize response.
  • No changes to CLI commands, REPL output, config files, or any other user-facing surface.

Evidence

  • Failing test/log before + passing after
$ npm run typecheck
> tsc --noEmit
(no errors)

$ npx mocha --forbid-only "test/unit/infra/mcp/tools/brv-curate-tool.test.ts"
  30 passing (55ms)

$ npm run build
(success)

$ npm run lint
✖ 174 warnings, 0 errors
(pre-existing warnings only — no new violations)

Checklist

  • Tests added or updated and passing (npm test) — existing tests pass (description-only refactor, no new tests needed per CLAUDE.md TDD note on zero-behavior-change refactors)
  • Lint passes (npm run lint) — 0 errors
  • Type check passes (npm run typecheck)
  • Build succeeds (npm run build)
  • Commits follow Conventional Commits format
  • Documentation updated (if applicable) — N/A
  • No breaking changes (or clearly documented above)
  • Branch is up to date with main

Risks and mitigations

  • Risk: MCP clients that cache tool schemas may need to reconnect to pick up the new cwd descriptions.
    • Mitigation: Standard MCP reconnect on next session resolves this. No config or code change required on the client side.
  • Risk: Some MCP clients may not forward server-level instructions into their LLM system prompt (behavior varies by client).
    • Mitigation: The tool-level cwd description carries the full rule and is always injected with the tool schema; instructions is a short pointer, so clients that ignore it lose nothing substantive.

@bao-byterover bao-byterover requested a review from leehpham as a code owner April 15, 2026 15:08
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 15, 2026

Claude finished @bao-byterover's task in 2m 8s —— View job


PR Review: feat: [ENG-2124] Improve MCP tool descriptions

Overall Score: 4/5 — Clean, well-scoped refactor with genuine value. Single source of truth eliminates drift, and the capability-first description is a real improvement for LLM consumers. Two minor concerns worth addressing before merge.


Summary

The deduplication approach is sound: replacing two copies of an inline z.string().optional().describe(...) with a shared cwdField is the right call, and the re-export through tools/index.ts makes the abstraction available if more tools are added. The instructions pointer on the McpServer constructor is a lightweight addition that costs almost nothing but helps clients that surface it.


Issues

suggestion · src/server/infra/mcp/tools/shared-schema.ts:12-13

The new description tells the LLM: "If your runtime DOES expose an open workspace/project root to you (e.g., Cursor, Cline, Zed, Claude Code): you can OMIT this field — the MCP server was launched from that same project."

This claim is only valid in project mode. If brv mcp was started globally (no .brv/config.json in the working directory), getWorkingDirectory() returns undefined and the tool REQUIRES cwd — even from Cursor or Cline. The old description was actually more precise about this: it framed the rule around server mode (checkable via the server's behaviour), not client runtime (which is a correlated proxy but not equivalent).

Concrete failure scenario:

  1. User has brv mcp wired into Windsurf/Claude Desktop config via a global path (no --cwd flag → global mode)
  2. User later opens a project in Cursor
  3. Cursor (correctly following the new description) omits cwd
  4. Server is in global mode → resolveClientCwd sees undefined from getWorkingDirectory() → fails

Consider a blended framing: lead with the capability-first check (keeps it LLM-actionable), but add a fallback clause for when things go wrong:

// suggested rewrite (lines 7–16):
'When to provide:\n' +
'- If your runtime does NOT expose a workspace/project root ' +
  '(e.g., Claude Desktop, hosted MCP, global Windsurf): you MUST provide cwd. ' +
  'Use the path the user mentions, or ASK the user for the absolute path if unknown.\n' +
'- If your runtime exposes a workspace root AND ByteRover answers correctly without cwd ' +
  '(e.g., Cursor, Cline, Zed, Claude Code in project-mode MCP): you may OMIT this field.\n' +
'- If you omit cwd and get a "no project found" error, provide cwd explicitly.\n' +
'\n' +
'Never guess, never invent paths, never use relative paths.'

The key change is adding the error-recovery escape hatch so the LLM has a recovery path rather than being stuck in a dead end.

Fix this →


issue (non-blocking) · src/server/infra/mcp/tools/shared-schema.ts (missing tests)

Per CLAUDE.md TDD requirements, new implementation files need tests written first. shared-schema.ts has no corresponding test file. The existing curate/query tool tests serve as a regression net for the shape of the schema (optional string), but they don't assert on CWD_DESCRIPTION's content or on cwdField's Zod metadata (e.g., that .describe() is set, that it accepts undefined).

These would be trivial to add and would lock in the contract:

// suggested test additions in a new test/unit/infra/mcp/tools/shared-schema.test.ts
import { cwdField, CWD_DESCRIPTION } from '../../../../../src/server/infra/mcp/tools/shared-schema.js'
import { expect } from 'chai'

describe('shared-schema', () => {
  it('cwdField accepts undefined (optional)', () => {
    expect(cwdField.safeParse(undefined).success).to.be.true
  })
  it('cwdField accepts an absolute path string', () => {
    expect(cwdField.safeParse('/Users/me/project').success).to.be.true
  })
  it('cwdField description matches CWD_DESCRIPTION', () => {
    expect(cwdField.description).to.equal(CWD_DESCRIPTION)
  })
  it('CWD_DESCRIPTION contains the "Never guess" rule', () => {
    expect(CWD_DESCRIPTION).to.include('Never guess')
  })
})

Fix this →


Praise

praise · src/server/infra/mcp/tools/shared-schema.ts:5

String.raw for the Windows path example is a nice touch — keeps the template literal readable while ensuring \\ renders as \\ in the output (correct JSON-context guidance for LLMs constructing the payload).

praise · src/server/infra/mcp/mcp-server.ts:64-67

The instructions string is appropriately minimal — it's a pointer, not a duplicate. Clients that ignore it lose nothing (full rule is in the field description); clients that surface it get a useful hint. Good scoping.

praise · src/server/infra/mcp/tools/index.ts:4

Re-exporting CWD_DESCRIPTION and cwdField from the barrel is forward-thinking — if a third tool gets added, the import path is already stable.


Checklist

  • Read all 5 changed files
  • Analyzed code quality, correctness, and security
  • Posted review with inline comments and overall score

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 15, 2026

Claude finished @bao-byterover's task in 1m 24s —— View job


PR Review: feat: [ENG-2124] Improve MCP tool descriptions

Overall Score: 4/5 — The primary concern from the previous review is properly resolved. The blended description framing + error-recovery clause is the right approach. One minor gap remains in the new test file.


What Changed Since Last Review

The fix commit (e5f6551) addressed both previous issues:

  1. CWD_DESCRIPTION reframing — added the error-recovery escape hatch and restructured into two clear cases.
  2. Test file addedtest/unit/infra/mcp/tools/shared-schema.test.ts.

Assessment of Previous Issues

suggestion (resolved) · src/server/infra/mcp/tools/shared-schema.ts:7-18

The rework is correct. The description now leads with the "no runtime context" case (force MUST provide cwd) before the "has runtime context" case (may omit), which is the right precedence order for LLM instruction. The error-recovery clause at line 16 closes the failure loop:

If you omit cwd and the tool returns an error about the project not being resolved or cwd being required, retry with cwd explicitly set to the project root path.

This is exactly what was missing. The global-mode failure scenario is now recoverable.


suggestion (partially addressed) · test/unit/infra/mcp/tools/shared-schema.test.ts

The test file was added, which addresses the gap. However, two of the four suggested assertions are missing:

// Present ✓
it('accepts undefined (optional)', ...)
it('accepts an absolute path string', ...)

// Missing ✗
it('cwdField description matches CWD_DESCRIPTION', () => {
  expect(cwdField.description).to.equal(CWD_DESCRIPTION)
})
it('CWD_DESCRIPTION contains the "Never guess" rule', () => {
  expect(CWD_DESCRIPTION).to.include('Never guess')
})

Note that CWD_DESCRIPTION isn't even imported in the test file — the description-contract assertions (the most valuable ones, since they lock in that .describe() was wired up correctly) were skipped. There's also a minor style nit: cwdField.safeParse() without an explicit argument should be cwdField.safeParse(undefined) for clarity.

These are non-blocking — the Zod shape tests serve as a regression net — but the description assertions are the ones that would catch a future accidental cwdField = z.string().optional() (no .describe()) regression.

Fix this →


Unchanged Files — No Issues

  • brv-curate-tool.ts and brv-query-tool.ts: cwd: cwdField replacement is clean; no handler logic touched.
  • mcp-server.ts: instructions string is appropriately minimal and correctly placed on the McpServer constructor (lines 64–67).
  • tools/index.ts: Re-export of CWD_DESCRIPTION and cwdField is correct and forward-looking.

Praise

praise · src/server/infra/mcp/tools/shared-schema.ts:7-10

Leading with the "does NOT expose context → MUST provide" case is the right instruction order — it handles the more dangerous scenario first and gives the LLM an explicit recovery action ("ASK the user") rather than leaving it stuck.

praise · src/server/infra/mcp/tools/shared-schema.ts:16

The retry-on-error clause is exactly what was needed. The LLM now has a deterministic recovery path instead of a dead end.


Checklist

  • Read all 6 changed files
  • Assessed whether previous review concerns were addressed
  • Analyzed code quality, correctness, and test coverage
  • Posted updated review

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 15, 2026

Claude finished @bao-byterover's task in 1m 10s —— View job


PR Review: feat: [ENG-2124] Improve MCP tool descriptions

Overall Score: 5/5 — All previous review concerns fully resolved. Clean, complete refactor with no outstanding issues.


What Changed Since Last Review (commit 40bb0e6)

The fix commit addressed both remaining issues from the previous 4/5 review:

  1. CWD_DESCRIPTION import added — the import was missing in e5f6551's test file; 40bb0e6 adds it.
  2. Two missing assertions added:
    • cwdField.description equals CWD_DESCRIPTION — locks in that .describe() is wired correctly
    • CWD_DESCRIPTION includes 'Never guess' — locks in the anti-hallucination rule
  3. safeParse()safeParse(undefined) — with an appropriate eslint-disable comment explaining the Zod API requirement.

Assessment of All Previous Issues

suggestion (resolved) · src/server/infra/mcp/tools/shared-schema.ts:7-18

The blended framing is correct and complete:

  • Leads with the "no runtime context → MUST provide" case (dangerous path first, good instruction ordering)
  • Error-recovery clause at line 16 closes the global-mode failure loop
  • "Never guess" rule at line 18 prevents path invention

suggestion (resolved) · test/unit/infra/mcp/tools/shared-schema.test.ts

All four previously suggested assertions are now present:

// All four ✓
it('accepts undefined (optional)', ...)           // cwdField.safeParse(undefined)
it('accepts an absolute path string', ...)         // cwdField.safeParse('/Users/me/project')
it('has description wired to CWD_DESCRIPTION', ...)  // cwdField.description === CWD_DESCRIPTION
it('contains the "Never guess" anti-hallucination rule', ...)  // CWD_DESCRIPTION.includes('Never guess')

The safeParse(undefined) fix includes a well-placed eslint-disable comment explaining the Zod API requirement — good attention to detail.


Full File Review

src/server/infra/mcp/tools/shared-schema.ts — Clean. String.raw for Windows paths is correct, two-case structure is the right precedence order for LLM instructions, error-recovery clause is actionable.

test/unit/infra/mcp/tools/shared-schema.test.ts — Complete. All contract assertions present. Nested describe structure is clear. No issues.

src/server/infra/mcp/tools/brv-curate-tool.tscwd: cwdField replacement is clean; handler logic untouched.

src/server/infra/mcp/tools/brv-query-tool.ts — Same. No behavior change.

src/server/infra/mcp/tools/index.tsCWD_DESCRIPTION and cwdField re-exported correctly; stable import path for future tools.

src/server/infra/mcp/mcp-server.tsinstructions pointer at lines 64–67 is appropriately minimal and correctly placed on the McpServer constructor. Clients that surface it get a useful hint; clients that ignore it lose nothing.


Praise

praise · test/unit/infra/mcp/tools/shared-schema.test.ts:8

The // eslint-disable-next-line unicorn/no-useless-undefined -- Zod safeParse requires an argument comment is exactly right — it suppresses the lint rule while explaining why the otherwise-redundant undefined is intentional.

praise · src/server/infra/mcp/tools/shared-schema.ts:7-10

Leading with the "does NOT expose context → MUST provide cwd" case is the correct instruction order for LLM-targeted text. The more dangerous failure mode is addressed first.


Checklist

  • Read all 6 changed files
  • Reviewed all previous concerns
  • Verified test file completeness
  • Assessed code quality, correctness, and security

Copy link
Copy Markdown
Collaborator

@cuongdo-byterover cuongdo-byterover left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm.

@bao-byterover bao-byterover merged commit 56ed57f into main Apr 16, 2026
6 checks passed
@github-actions github-actions bot mentioned this pull request Apr 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants