Documentation
¶
Overview ¶
Package mcp implements the codegraph stdio MCP server (D-08): a startup-time conditional tool registration surface. It imports only internal/query (the read-only engine + formatters) — never internal/graphstore's Pebble implementation directly — so the internal/graphstore/archtest boundary holds (D-08b). Every tool handler in tools.go delegates to the same internal/query.Engine methods and formatters the CLI uses, taking a fresh query.OpenAt snapshot per call (D-02/D-08b, RESEARCH Pitfall 2) — one engine, two front-ends, so MCP output shapes stay byte-identical to the CLI's without a second rendering path.
Phase 2 (SDK-01) migrated this package's backend from github.com/mark3labs/mcp-go to github.com/modelcontextprotocol/go-sdk — the official MCP org's SDK — behind the Server seam SDK-02 built.
Index ¶
- Constants
- func BuildServer(hasIndex bool, companions map[string]bool, repoPath, startPath string, ...) *mcp.Server
- func ParseToolFilter(value string) (selected map[string]bool, unknown []string)
- func ResolveCompanions(value string, present bool) (companions map[string]bool, unknown []string)
- func WarnToolFilterTo(w io.Writer, unknown []string, companions map[string]bool)
- type CalleesArgs
- type CallersArgs
- type ExploreArgs
- type FilesArgs
- type ImpactArgs
- type NodeArgs
- type Option
- type SearchArgs
- type Server
- type StatusArgs
Constants ¶
const ProtocolVersion = "2025-11-25"
ProtocolVersion is the MCP protocol revision this server declares.
This is the revision today's server actually negotiates. It is owned by this repository rather than read from an SDK, so a dependency bump can never move it silently (VRFY-02).
Mechanism honesty (Phase 2, corrected from Phase 1's prediction): this literal is an asserted compatibility pin, not the source of the negotiated value, and that remains true under github.com/modelcontextprotocol/go-sdk v1.7.0 too. The negotiated revision is decided by the unexported package-level negotiatedVersion function in the SDK's own mcp package (mcp/shared.go), whose version- value constants (latestProtocolVersion, protocolVersion20260728, etc.) are all unexported and therefore structurally unreferenceable from internal/mcp — Go's own visibility rules make it impossible, not merely discouraged. ServerOptions exposes no ProtocolVersion field among its 17 fields (it does expose Instructions and Capabilities); the sole reachable mechanism, AddReceivingMiddleware, only ever READS the SDK's own already- computed *InitializeResult after negotiation has happened (see server.go's session-line middleware) — it is not a setter. What this constant buys is the same alarm Phase 1 built: it currently equals the value go-sdk negotiates for this literal, so the moment a dependency bump moves that value, the wire oracle's spec anchor (test/wireoracle) turns red — the "a dependency bump must never move wire behavior silently" property REQUIREMENTS.md VRFY-02 asks for ("asserted against a repo-owned literal"). VRFY-02's stricter "reads from" phrasing is confirmed undeliverable against this SDK too (D-04, D-06) — not deferred again, but proven false by exhaustive source enumeration (02-RESEARCH.md Q1) rather than assumed. Do not mistake this pin for an injection point.
This file deliberately references no SDK identifier.
Variables ¶
This section is empty.
Functions ¶
func BuildServer ¶
func BuildServer(hasIndex bool, companions map[string]bool, repoPath, startPath string, opts ...Option) *mcp.Server
BuildServer constructs the stdio MCP server with startup-time conditional tool registration (D-08a, Pattern 3): hasIndex gates whether ANY tool is registered at all (MCP-03 — zero tools when no .codegraph/ resolves, though MCP init still completes successfully), companions names which of the 7 companion tools register beyond the always-visible codegraph_explore.
companions is a RESOLVED set, never a raw environment value: this function has no opinion about defaults, and in particular does not treat an empty map as "the caller didn't say, so give them everything." ResolveCompanions owns that decision, and an empty map here means exactly what it says — register codegraph_explore and nothing else.
repoPath and startPath are DELIBERATELY DISTINCT (CR-01, the Phase-1 CR-02 recurrence this parameter split fixes): repoPath is the confinement root — the RESOLVED index root every handler's confineToRepoRoot check anchors against, rejecting any client-supplied "path" argument that resolves outside it (CR-02/tools.go's trust boundary) — while startPath is the CALLER'S actual starting directory (serve.go's `start`, before ResolveCodegraphDir's upward walk), the value every handler falls back to when the caller omits "path" and the value that must reach query.OpenAt for WorktreeMismatch to have anything to compare. Because repoPath is always startPath itself or an ANCESTOR of it (it is ResolveCodegraphDir(startPath)'s own return value), confining the default startPath to repoPath always succeeds structurally — only an explicit, client-supplied "path" redirecting elsewhere can ever be rejected.
opts is variadic (SDK-02/VRFY-03) specifically so every pre-existing positional call site (17 of them, all in tests, as of this change) keeps compiling unchanged — only NewStdioServer, this file's one production caller, passes WithSessionLog.
func ParseToolFilter ¶ added in v0.6.0
ParseToolFilter splits a SET CODEGRAPH_MCP_TOOLS value on commas, trims whitespace around each entry, and classifies each non-empty name against companionNames. Recognized names are returned in selected (selected[name] == true); unrecognized names are returned in unknown, in the order they were seen, for the caller to warn about via WarnToolFilterTo — ParseToolFilter itself never writes output or aborts, so an unknown name can never fail startup (MCP-02: "unknown names ignored with a stderr warning").
Callers should not reach for this directly: ResolveCompanions is the seam that knows an UNSET variable means something entirely different from an empty one. This function only ever sees the set case.
func ResolveCompanions ¶ added in v0.6.0
ResolveCompanions decides which of the 7 companion tools register, from the raw CODEGRAPH_MCP_TOOLS value and whether that variable was SET AT ALL. It is the ONE place the default lives; BuildServer knows only "here is the companion set", never how it was derived.
present is load-bearing and MUST come from os.LookupEnv — never from a `value != ""` test. Under narrowing semantics "unset" and "set to the empty string" are two DIFFERENT answers (all 7 companions vs none), and os.Getenv collapses them into the same empty string. That collapse is the single most fragile point of this inversion, which is why the distinction is a parameter rather than an inference.
Semantics (superseding the pre-inversion opt-in allowlist, which made codegraph_explore the only default-visible tool and required the operator to name every companion they wanted):
- unset => all 7 companions register (8 tools total)
- "node,status" => only those 2 register (3 tools total)
- "" => no companion registers (explore only) — the pre-inversion default, still reachable, now by explicit operator action rather than by accident
An index must still resolve for ANY tool to register; nothing here overrides MCP-03's zero-tools-without-.codegraph/ rule, which BuildServer enforces via hasIndex.
func WarnToolFilterTo ¶ added in v0.6.0
WarnToolFilterTo writes one stderr-style warning line per unrecognized filter name to w, followed by ONE line stating the resulting surface. Diagnostics never go to stdout — stdout is reserved for the MCP JSON-RPC transport (T-03-07-Leak) — so callers must pass os.Stderr, never os.Stdout, in production.
The consequence line is not decoration. Under the pre-inversion opt-in allowlist a typo cost exactly the mistyped tool: the operator was ADDING, so the loss showed up as "the thing I asked for is missing", blast radius one. Under narrowing semantics a value whose names are ALL typos narrows the surface to codegraph_explore alone — blast radius seven, presenting as the exact "the MCP server is only showing one tool" symptom this contract was rewritten to eliminate. Naming what was ignored without naming what survived leaves the operator to work that out themselves.
Deliberately NOT fatal: MCP-02 requires unknown names be ignored with a warning, and failing startup over one typo would take down a working server. Equally deliberately NOT a silent fallback to the default set — that would make a fully-typo'd value indistinguishable from a considered choice to narrow.
Types ¶
type CalleesArgs ¶
type CalleesArgs struct {
Limit int `json:"limit,omitempty" jsonschema:"Cap on results returned"`
Path string `json:"path,omitempty" jsonschema:"Repo path (default: server cwd)"`
Symbol string `json:"symbol" jsonschema:"Symbol name"`
}
CalleesArgs is codegraph_callees' input schema (D-07). Fields declared alphabetically by JSON name (limit, path, symbol). Symbol carries no omitempty — it is the only required field.
type CallersArgs ¶
type CallersArgs struct {
Limit int `json:"limit,omitempty" jsonschema:"Cap on results returned"`
Path string `json:"path,omitempty" jsonschema:"Repo path (default: server cwd)"`
Symbol string `json:"symbol" jsonschema:"Symbol name"`
}
CallersArgs is codegraph_callers' input schema (D-07). Fields declared alphabetically by JSON name (limit, path, symbol). Symbol carries no omitempty — it is the only required field.
type ExploreArgs ¶
type ExploreArgs struct {
MaxFiles int `json:"max_files,omitempty" jsonschema:"Cap on distinct files returned (default 5)"`
Path string `json:"path,omitempty" jsonschema:"Repo path (default: server cwd)"`
Query string `json:"query" jsonschema:"Natural-language or symbol/file query"`
}
ExploreArgs is codegraph_explore's input schema (D-07), inferred by mcp.AddTool from these struct tags. Fields are declared in alphabetical order of their JSON name (max_files, path, query) so jsonschema-go's Go-struct-field-declaration-order property emission matches mark3labs' alphabetical order, minimizing plan 02-05's diff review surface. Query carries no omitempty — it is the only required field.
type FilesArgs ¶
type FilesArgs struct {
Depth int `json:"depth,omitempty" jsonschema:"Directory-nesting cap (0 = unlimited)"`
Filter string `json:"filter,omitempty" jsonschema:"Restrict to one language"`
Format string `json:"format,omitempty" jsonschema:"\"flat\" (default) or \"tree\""`
Path string `json:"path,omitempty" jsonschema:"Repo path (default: server cwd)"`
Pattern string `json:"pattern,omitempty" jsonschema:"Shell glob narrowing the result set"`
}
FilesArgs is codegraph_files' input schema (D-07). Fields declared alphabetically by JSON name (depth, filter, format, path, pattern). No field is required.
type ImpactArgs ¶
type ImpactArgs struct {
Depth int `json:"depth,omitempty" jsonschema:"BFS depth (default 2, max 50)"`
Path string `json:"path,omitempty" jsonschema:"Repo path (default: server cwd)"`
Symbol string `json:"symbol" jsonschema:"Symbol name"`
}
ImpactArgs is codegraph_impact's input schema (D-07). Fields declared alphabetically by JSON name (depth, path, symbol). Symbol carries no omitempty — it is the only required field.
type NodeArgs ¶
type NodeArgs struct {
File string `json:"file,omitempty" jsonschema:"File path — disambiguates symbol, or selects file-mode when symbol is omitted"`
Line int `` /* 141-byte string literal not displayed */
Path string `json:"path,omitempty" jsonschema:"Repo path (default: server cwd)"`
Symbol string `json:"symbol,omitempty" jsonschema:"Symbol name to look up (omit for a file-mode read)"`
}
NodeArgs is codegraph_node's input schema (D-07). Fields declared alphabetically by JSON name (file, line, path, symbol). Symbol carries omitempty — node's file-mode read means symbol is optional, matching today's schema (never marked required).
type Option ¶
type Option func(*buildConfig)
Option configures BuildServer via the functional-options pattern.
func WithSessionLog ¶
WithSessionLog sets the writer VRFY-03's always-on session line is written to. Passing a nil writer here is equivalent to omitting the option (no session line is emitted) — NewStdioServer is the seam that makes "always on" a construction guarantee rather than a convention; see its doc comment.
type SearchArgs ¶
type SearchArgs struct {
Kind string `json:"kind,omitempty" jsonschema:"Restrict to one node kind"`
Limit int `json:"limit,omitempty" jsonschema:"Cap on results returned"`
Path string `json:"path,omitempty" jsonschema:"Repo path (default: server cwd)"`
Query string `json:"query" jsonschema:"Search term"`
}
SearchArgs is codegraph_search's input schema (D-07). Fields declared alphabetically by JSON name (kind, limit, path, query). Query carries no omitempty — it is the only required field.
type Server ¶
type Server interface {
ServeStdio() error
}
Server is SDK-02's narrow seam: the entire surface internal/cli needs to bootstrap and run the stdio MCP server, with no SDK type anywhere in its signature. internal/cli/serve.go depends only on this interface (and NewStdioServer, below) — it never imports modelcontextprotocol/go-sdk/mcp.
func NewStdioServer ¶
func NewStdioServer(hasIndex bool, companions map[string]bool, repoPath, startPath string, sessionLog io.Writer) Server
NewStdioServer is internal/cli/serve.go's sole entrypoint into this package (SDK-02): it builds the server via BuildServer and returns it as the SDK-agnostic Server interface.
sessionLog must not be nil, and NewStdioServer panics if it is. VRFY-03's always-on negotiated-version stderr line is the milestone's only mitigation for a spec-sanctioned silent version mismatch (Legacy mark3labs servers silently coerce an unrecognized protocolVersion rather than rejecting it) — so silently disabling that line by passing a nil writer through some future call site must be structurally impossible, not merely unlikely. Callers that genuinely want the line suppressed (there is no such production caller today) must pass io.Discard explicitly — a deliberate, greppable opt-out, never a nil default.
type StatusArgs ¶
type StatusArgs struct {
Path string `json:"path,omitempty" jsonschema:"Repo path (default: server cwd)"`
}
StatusArgs is codegraph_status's input schema (D-07). No field is required.