Documentation
¶
Overview ¶
Package mcpserver exposes the code graph over MCP so X-Ray's voice/chat layer and external coding agents can search and traverse code as their primary structural + semantic source.
Index ¶
- Constants
- func ForgetDirect(ctx context.Context, d Deps, qn string) (string, error)
- func FormatTTL(d time.Duration) string
- func GetNodeDirect(ctx context.Context, d Deps, qn string) (string, error)
- func ImpactDirect(ctx context.Context, d Deps, seeds []string, depth, maxNodes int) (string, error)
- func ImpactDirectGraph(g *graph.Graph, seeds []string, depth, maxNodes int) (string, error)
- func KindsForFilter(filter string) map[string]bool
- func New(d Deps) http.Handler
- func NewReadOnly(d Deps) http.Handler
- func NodeJSON(g *graph.Graph, qn string) (map[string]any, bool)
- func ParseTTL(s string) (time.Duration, error)
- func RecallDirect(ctx context.Context, d Deps, query string, limit int) (string, error)
- func RegisterReadTools(s *server.MCPServer, d Deps)
- func RememberDirect(ctx context.Context, d Deps, text, title string, attrs map[string]string, ...) (string, error)
- func SearchCodeDirect(ctx context.Context, d Deps, mapName, query string, limit int) (string, error)
- func SearchDirect(ctx context.Context, d Deps, mapName, query string, limit int, ...) (string, error)
- func SearchDirectIndex(ctx context.Context, ix *index.Index, query, repoFilter string, limit int, ...) (string, error)
- func SearchDocsDirect(ctx context.Context, d Deps, mapName, query string, limit int) (string, error)
- func StatsDirect(ctx context.Context, d Deps, mapName, query string) (string, error)
- type Deps
- type Forgotten
- type Note
- type Remembered
- type Rememberer
- type Resolved
- type Resolver
- type Updater
Constants ¶
const DefaultMemoryTTL = 7 * 24 * time.Hour
DefaultMemoryTTL is how long a note lives when neither the note nor the server says. A week: long enough that "what did we do last time" still works after a weekend and a few days off, short enough that a session's running notes do not pile up for months and rank beside this week's. A note worth longer says so (ttl=30d, ttl=never).
const TTLNever time.Duration = -1
TTLNever is Note.TTL's "keep forever" sentinel. Zero means "the server's default", so never needs a value of its own; any negative duration reads as never, and this is the one to write.
Variables ¶
This section is empty.
Functions ¶
func ForgetDirect ¶
ForgetDirect removes one note from the agent-notes map by qualified name. Direct-call twin of the forget tool.
func FormatTTL ¶
FormatTTL renders a lifetime the way ParseTTL reads it: "never" for the sentinel, whole days as "7d", anything else as a Go duration.
func GetNodeDirect ¶
GetNodeDirect returns the full node JSON (content + relationships) for a qn. Direct-call twin of get_graph_node.
func ImpactDirect ¶
ImpactDirect returns the blast radius of changing seeds. Direct-call twin of get_graph_impact.
func ImpactDirectGraph ¶
ImpactDirectGraph is ImpactDirect over a graph the caller already holds — `xray-memory impact` loads one from a snapshot with no Resolver. Keeping the defaults here means the CLI and get_graph_impact answer the same question.
func KindsForFilter ¶
KindsForFilter maps a UI-facing filter value onto the SAME codeKinds/docKinds groups search_graph/search_graph_docs already search over — the /search HTTP route's kind param (mirroring the browser UI's All/Docs/Code segmented control) reuses this rather than redefining the split. "code" and "docs" narrow to their respective group; anything else (including "" and "all") searches every kind, matching search_graph's own unfiltered default.
func New ¶
New returns an HTTP handler exposing the recall MCP tools over a single live graph (Mode A — local in-repo server, includes update_code_graph).
func NewReadOnly ¶
NewReadOnly returns an HTTP handler exposing the snapshot-routed tools (Mode B — online long-term memory / worked copy). Reads route per call through d.Resolver to the right per-project snapshot graph; update_code_graph edits the in-memory worked copy (the registry flushes dirty projects back to disk before eviction / on shutdown). The name is retained for back-compat; the server is a persistent worked copy, not strictly read-only.
func NodeJSON ¶
NodeJSON builds the get_graph_node reply for one qn against a specific graph: identity + full content (from memory) + capped relationships. Shared by the MCP get_graph_node tools and the browser /node endpoint so all three return the identical shape. Returns (nil,false) when the qn is not a node.
func ParseTTL ¶
ParseTTL reads a lifetime: a Go duration ("24h", "90m"), a day count with a d suffix ("7d", "0.5d" — Go's parser has no day unit), or "never". Empty means "server default" and returns 0. Anything else, including zero and negative durations, is an error: a note that expires at once is a mistake, not a request.
func RecallDirect ¶
RecallDirect reads the newest notes (or, with a query, the best-matching ones) from the agent-notes map. Direct-call twin of the recall tool.
func RegisterReadTools ¶
RegisterReadTools adds the five READ-ONLY code-graph tools (search_graph, search_graph_docs, get_graph_node, get_graph_impact, get_graph_stats) to an EXISTING MCP server — it does NOT register update_code_graph. This lets a host application serve the code graph from its own auth-gated /mcp endpoint alongside its own tools, sharing one session/bearer/OAuth gate, scope map, and rate limiter, without owning a second HTTP server.
It selects snapshot-routed (Resolver) or live (Graph/Index) handlers from the same flag the constructors use: a non-nil d.Resolver means snapshot mode. A static build runs snapshot mode (CGO-free); live mode is for the in-process recall CLI server.
func RememberDirect ¶
func RememberDirect(ctx context.Context, d Deps, text, title string, attrs map[string]string, ttl time.Duration) (string, error)
RememberDirect writes one note into the agent-notes map (Deps.MemoryMap). Direct-call twin of the remember tool; like it, there is no map to choose. ttl is as ParseTTL returns it: 0 for the server default, TTLNever for never.
func SearchCodeDirect ¶
func SearchCodeDirect(ctx context.Context, d Deps, mapName, query string, limit int) (string, error)
SearchCodeDirect runs a code search and returns the brief-hit JSON. project may be empty (semantic routing). It is the direct-call twin of the search_graph MCP tool.
func SearchDirect ¶
func SearchDirect(ctx context.Context, d Deps, mapName, query string, limit int, kinds map[string]bool, sel index.Select, cutoff bool) (string, error)
SearchDirect runs a search restricted to an explicit kind set (nil/empty means every kind — KindsForFilter("") returns nil for exactly this reason) against Deps.Resolver (Mode B / snapshot registry) and returns the brief-hit JSON. It is the generic direct-call entry point SearchCodeDirect/ SearchDocsDirect narrow with codeKinds/docKinds, and what the /search HTTP route (cmd/recall/servesnapshots.go) calls for the UI's "all" filter — going through the identical Resolve + Index.Search + RecordSearch path the MCP search tools use, so the search-metrics counters see UI searches too.
func SearchDirectIndex ¶
func SearchDirectIndex(ctx context.Context, ix *index.Index, query, repoFilter string, limit int, kinds map[string]bool, sel index.Select, cutoff bool) (string, error)
SearchDirectIndex runs a search directly against one fixed Index — Mode A (the live single-graph server, cmd/recall/serve.go), which has no per-project Resolver to route through. repoFilter narrows to one repo namespace when the server serves more than one (-repo a=...,b=...); empty searches every served repo. Returns the identical brief-hit JSON shape SearchDirect/the MCP search tools return (no "routedTo" key, matching registerSearchCode/registerSearchDocs's own Mode A reply), and goes through the same ix.Search call those tools make, so it records into the same SearchStats.
Types ¶
type Deps ¶
type Deps struct {
Graph *graph.Graph
Index *index.Index
Updater Updater
Resolver Resolver // set in read-only mode; nil in live mode
Logger zerolog.Logger
// SkillsMap names the loaded map (via Resolver) that backs list_skills /
// get_skill — a "Doc" node per SKILL.md. Empty disables both tools; only
// meaningful alongside a non-nil Resolver (Mode B).
SkillsMap string
// Memory backs the remember tool and MemoryMap is the agent-notes map every
// note goes into; either empty disables the tool. The caller cannot choose
// the map: that would let an agent write prose into a code or catalog map.
Memory Rememberer
MemoryMap string
// MemoryTTL is the lifetime a note gets when it names none; it is stated
// in the remember tool's description so the caller knows what silence
// means. Zero reads as DefaultMemoryTTL, negative as never.
MemoryTTL time.Duration
}
Deps are the graph + index + updater the tools operate on.
Mode A (local live server, New) sets Graph/Index/Updater to fixed instances. Mode B (read-only snapshot server, NewReadOnly) sets Resolver instead, and each tool resolves the right per-project graph/index per call.
type Note ¶
type Note struct {
Text string // required; the memory itself, embedded and previewed verbatim
Title string // optional short name
Attrs map[string]string // optional caller facets (session, topic); the server adds "at" and "expires"
TTL time.Duration // how long to keep it: 0 = the server default, TTLNever (negative) = forever
}
Note is one thing an agent asked the server to remember, and Remembered is where it landed. They are declared HERE rather than in engine because engine imports this package (its Registry implements Resolver), so the seam has to sit on this side; engine aliases them.
type Remembered ¶
type Remembered struct {
Map string
QN string
At time.Time
Expires time.Time // zero when the note never expires
Notes int // Memory nodes in the map after this one
}
Remembered reports where a note landed.
type Rememberer ¶
type Rememberer interface {
Remember(ctx context.Context, label string, n Note) (Remembered, error)
// Forget removes the note qn from the map label. It refuses anything that
// is not a Memory node in a notes map: forget is the undo of remember, not
// a general delete.
Forget(ctx context.Context, label, qn string) (Forgotten, error)
}
Rememberer writes one note into a memory map so a later session can find it, and removes one on request. Implemented by engine.Registry; a nil Deps.Memory leaves the remember and forget tools unregistered rather than registered-and-failing.
type Resolved ¶
type Resolved struct {
Label string
Graph *graph.Graph
Index *index.Index
// Summary and SummarySource are the map's router input and where it came
// from ("explicit"|"readme"|"stats") — carried here (not just on /status)
// so get_graph_stats/StatsDirect can surface them for diagnosis without a
// second round trip through the registry. See engine.SnapshotHeader.
Summary string
SummarySource string
// RouteScore is how well this map matched the route — 1 when the caller
// named it, else the cosine of its summary vector to the query. It is the
// weight a caller merging several maps' hits must apply, because a hit's
// own rank score is only meaningful WITHIN its map: every map's rank-1 hit
// carries the same score, so merging on it alone round-robins the maps and
// lifts an unrelated map's best hit above a good map's second. A resolve
// that names a single map leaves this 1.
RouteScore float64
}
Resolved is one map's graph + index, returned by a Resolver. It is the per-call substitute for Deps.Graph/Deps.Index in read-only (snapshot) mode.
type Resolver ¶
type Resolver interface {
Resolve(ctx context.Context, mapName, query string) ([]Resolved, error)
ResolveByQN(ctx context.Context, qn string) (Resolved, error)
UpdateByPaths(ctx context.Context, paths []string) (changed, removed, skipped int, unowned []string, err error)
RecordSearch(label string, hits int)
Metrics() map[string]any
}
Resolver routes a tool call to the right loaded map's graph. This interface is domain-neutral: a "map" is any loaded graph — code, a service dependency graph, or another domain — identified by its label. Search tools route by (mapName, query): an explicit map, else a semantic match on the query, and may fan out to several maps. Node-keyed tools route by the map prefix of a namespaced qualified name.
UpdateByPaths supports the writable worked-copy mode: a remote client edits the in-memory graph by file path; the registry routes each path to its owning map, re-parses + re-embeds only the changed nodes, and marks that map dirty so it is flushed back to its snapshot before eviction / on shutdown.
RecordSearch and Metrics back the search-quality metrics surface (docs/todo/search-metrics.md): RecordSearch attributes one resolved map's search + hit count to the registry's per-map counters — mcpserver is the only layer that knows both a search's resolved map label (from Resolve) and its returned hit count (from that map's Index.Search), so it is the one that must report the pairing back. Metrics returns a JSON-friendly snapshot of everything the registry tracks (routing outcomes, per-map totals, map loads/evictions, and the shared query-mechanics search stats), consumed by get_graph_stats and the /metrics HTTP endpoint.
type Updater ¶
type Updater interface {
Update(ctx context.Context, paths []string) (changed, removed, skipped int, unowned []string, err error)
}
Updater re-indexes changed files into the live graph. Implemented by the Indexer; lets the update_code_graph tool refresh without a hook.
skipped counts files this build cannot extract — code files in a CGO-free binary, whose Tree-sitter parser is a stub. They are left untouched in the graph. It is reported rather than folded into changed/removed so a caller can tell "your edit produced no nodes" from "this build cannot read your file".