mcp

package
v0.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 10, 2026 License: MIT Imports: 17 Imported by: 0

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

View Source
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

func ParseToolFilter(value string) (selected map[string]bool, unknown []string)

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

func ResolveCompanions(value string, present bool) (companions map[string]bool, unknown []string)

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

func WarnToolFilterTo(w io.Writer, unknown []string, companions map[string]bool)

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

func WithSessionLog(w io.Writer) Option

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL