Synaptic documentation
MCP Server
synaptic serve exposes a loaded graph to an AI assistant over the Model
Context Protocol. It speaks JSON-RPC 2.0 directly (no external MCP runtime
dependency). The server is read-only over the graph; the PR and working-changes
tools shell out to gh/git to read state but never write.
The graph is loaded once at startup from a graph.json and hot-reloads when that
file changes on disk. It also keeps itself current as you edit source. By default,
each query performs a cheap, debounced staleness check against the build manifest
and re-extracts changed files before answering. serve --watch (or
SYNAPTIC_SERVE_WATCH=1) embeds a filesystem watcher that makes this gate
event-driven: clean queries skip the manifest walk, while the first query after a
relevant event performs the same bounded catch-up. If the watcher cannot start,
serve falls back to the debounced check. See
Incremental-Updates → serve auto-freshen.
For a digest-pinned deployment, serve --immutable-graph --expected-graph-sha256 <HEX> verifies one owned byte buffer and parses that
same buffer before serving it. It disables hot reload, source catch-up, and
watching, closing both the initial-open and post-start replacement windows.
Every node
label, relation, and file path is sanitized before it reaches tool output (a
security boundary on names derived from source). get_source is the one tool
that returns raw file contents; it reads only files inside a configured source
root (a path-traversal jail), and the bytes it returns are the verbatim source
the agent asked for.
Protocol version
Synaptic is a dual-era server:
- MCP
2026-07-28uses the stateless, per-request connection model. Every request includes stringparams._meta["io.modelcontextprotocol/protocolVersion"]and objectparams._meta["io.modelcontextprotocol/clientCapabilities"].clientInfois recommended and, when present, must contain stringnameandversion. - MCP
2025-11-25,2025-06-18,2025-03-26, and2024-11-05retain the legacyinitialize/notifications/initializednegotiation and optional HTTP session.
Modern clients may call server/discover. The result advertises all supported
versions, capabilities, server info, instructions, and cache hints. Ordinary
modern results include resultType: "complete" and server identity in result
metadata. List and resource results also include ttlMs and cacheScope.
Requests for another modern version fail with -32022 and return the supported
versions in error.data.
{
"jsonrpc": "2.0",
"id": 1,
"method": "server/discover",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {},
"io.modelcontextprotocol/clientInfo": {
"name": "example-client",
"version": "1.0"
}
}
}
}
For HTTP, each modern request must also carry:
MCP-Protocol-Version: 2026-07-28Mcp-Method: <JSON-RPC method>Mcp-Name: <tool/resource/prompt name>fortools/call,resources/read, andprompts/get
The headers must agree with the JSON body. A mismatch returns JSON-RPC
-32020 with HTTP 400. Modern requests never use Mcp-Session-Id;
GET /mcp and DELETE /mcp are not part of the modern transport. Legacy
headers, version negotiation, and sessions remain unchanged for older clients.
Modern discovery advertises tools, resources, prompts, and completions. It does not advertise extensions or tasks because Synaptic currently makes no server-to-client requests and has no task-backed operations. Legacy discovery continues to advertise logging and HTTP resource subscriptions where those older protocols define them.
Running the server
synaptic serve [--graph <path>] [--http <addr>] [--api-key <key>] [--source-root <dir>] [--allow-exec] [--concise] [--watch] [--immutable-graph] [--expected-graph-sha256 <hex>] [--ready-file <path>]
--graph <path>selects thegraph.jsonto load. Default is the standard output location (synaptic-out/graph.json). If the file is missing, serve exits with an error pointing you to runsynaptic extractfirst.- No
--http: serve over stdio (the default transport). --http <addr>: serve over HTTP athost:port(for example127.0.0.1:8765).--api-key <key>: require this key on HTTP requests. May also be set via theSYNAPTIC_API_KEYenvironment variable (the flag takes precedence).--source-root <dir>: the trusted root for resolving a node's source file inget_source. Default is the directory abovesynaptic-out/(the repo root); it falls back to the current directory when that cannot be derived.--allow-exec: expose the command-runningspeculatetool (off by default). This makes the server no longer read-only —speculateexecutes the project's test/build commands in a throwaway worktree — so enable it only for trusted clients. Without it the tool is neither advertised nor runnable.--watch: embed the event-driven source watcher described above. Equivalent toSYNAPTIC_SERVE_WATCH=1; watcher startup failure falls back safely to the debounced per-query staleness check.--immutable-graph: pin the graph loaded at startup. This disables graph-file hot reload, source catch-up, and filesystem watching for verified read-only snapshot deployments.--expected-graph-sha256 <hex>: verify the exact bytes loaded and parsed against a 64-character SHA-256 digest. Requires--immutable-graphand intentionally serves the verified JSON instead of an auto-selected shard store.--ready-file <path>: after an HTTP listener is bound, atomically publish JSON containingaddressandmcp_url. This permits--http 127.0.0.1:0without a close-then-bind port race. The path is never overwritten, its parent must already exist and be operator-protected, and the file is mode0600on Unix. Requires--http.--concise: token-lean output. Lowers the default list/budget sizes so tool results return less to the model (query_graphtoken_budget800, list limits to 20,dynamic_hazardsto 20,get_communityto 40,top_nto 6,context_linesto 25). An explicit per-call argument always wins. Equivalent to setting theSYNAPTIC_CONCISEenvironment variable (see [Configuration]).
stdio transport
synaptic serve
Newline-delimited JSON-RPC 2.0 on stdin/stdout: one request per line, one
response line per request. Notifications (requests with no id) get no reply.
Blank lines are ignored; malformed JSON produces a JSON-RPC parse error
(-32700). A status line is printed to
stderr ([synaptic] MCP server ready on stdio) so it never pollutes the
JSON-RPC stream on stdout.
This is the mode an assistant launches as a subprocess. See Assistant-Integration for wiring it into a host.
Registering with Codex
synaptic install codex wires this stdio server into Codex automatically: a
[mcp_servers.synaptic] entry in the project .codex/config.toml (Codex CLI),
or a per-repo [mcp_servers.synaptic-<repo>] in the global ~/.codex/config.toml
with synaptic install codex --global (Codex desktop app, which only reads the
global config). synaptic must be on your PATH. See
Assistant-Integration.
HTTP transport
synaptic serve --http 127.0.0.1:8765 --api-key s3cret
Streamable-HTTP on the /mcp route:
POST /mcp-- one JSON-RPC request. Modern requests are stateless; ordinary responses are JSON. A modernsubscriptions/listenrequest returns an SSE stream whose first event acknowledges the subscription. A notification returns HTTP 202 with no body. Invalid JSON returns 400.GET /mcp-- legacy-only server-to-client SSE for a negotiated session. It emits heartbeats and resource updates. A modern-version GET returns 405.DELETE /mcp-- legacy-only session termination (204 if it existed, 404 if unknown, 400 if no session id header). A modern-version DELETE returns 405.
For a modern POST, an unknown JSON-RPC method maps to HTTP 404, while malformed parameters and routing headers map to HTTP 400. Successful non-streaming calls remain HTTP 200.
A startup line is printed to stderr: [synaptic] MCP server on http://<addr>/mcp.
Legacy sessions
MCP 2026-07-28 is stateless and does not create or consult a session. For
older protocols, an initialize POST mints an opaque 128-bit session id,
returned in the Mcp-Session-Id response header. Later legacy requests should
carry it in the Mcp-Session-Id request header:
- Unknown or expired id on a non-initialize request: 404 (the client should re-initialize).
- A missing id on a non-initialize request is tolerated, so simple request/response clients keep working.
A background reaper drops sessions idle longer than 1 hour (3600s).
Authentication
If --api-key (or SYNAPTIC_API_KEY) is set and non-blank, every request to
both /mcp and /api/* must supply it. A blank/absent key disables auth. The
key may be supplied as either:
X-API-Key: <key>, orAuthorization: Bearer <key>(theBearerscheme is case-insensitive).
A missing or wrong key returns 401. The comparison is constant-time.
If you serve on a wildcard address (0.0.0.0 / ::) with no API key, serve
prints a warning to stderr.
Host allowlist (DNS-rebinding protection)
When bound to a specific or loopback address, only requests whose Host header
is localhost, 127.0.0.1, [::1], or the bound IP (each with or without the
port) are accepted; others get 403 (forbidden host). Binding to a wildcard
address (0.0.0.0 / ::) disables this check, treated as an intentional public
exposure.
Serving a federated store (shard-aware)
extract and workspace build write the per-repo shard store by default
(--no-store opts out; synaptic migrate builds one from an existing
graph.json), and synaptic update keeps it fresh. When SYNAPTIC_STORE
selects it (redb, or unset with a store at least as fresh as graph.json),
serve runs shard-aware: the union of all members is never materialized.
Each repo's shard loads on demand behind a bounded LRU (SYNAPTIC_SHARD_LRU,
default 8 resident shards), so memory is the working set, not the federation
size.
- Aggregates are exact.
graph_stats,god_nodes,query_graph(ranking uses a global document-frequency index so scores are comparable across repos), communities,structural_search(per-shard evaluation withLIMITapplied after the merge),list_repos/repo_stats, anddynamic_hazardsstream the shards and return the same answer as running on the union. - Walks follow the cross-repo bridge automatically. When the store holds
bridge edges, callers/neighbors gain hits from other repos (annotated
[cross-repo]),affectedcrosses once and continues in the neighbor repo, andshortest_pathmay take one bridge hop -- no opt-in needed (graph_statsreports the traversal state). On a store with no bridge edges walks stay in the seed's repo, which is the same thing. SYNAPTIC_CROSS_REPO=0isolates per repo: every walk stops at the repo boundary; cross-repo edges still appear as annotated boundary evidence where they touch a result, and cross-repo questions answer honestly with the opt-out named. (=1forces traversal on, the pre-0.6 opt-in spelling.) Forecasts and renames always walk per-repo.- Hot reload is manifest-keyed: any store rewrite (a member re-extracted into its shard) is picked up on the next data request; shards rematerialize on demand from persisted indexes.
- Cold loads are coalesced per repo: concurrent requests that miss the same shard wait for one materialization/index load instead of rebuilding it once per request. A shared failure wakes every waiter and is then discarded so a later request can retry.
- Bridge work is indexed: endpoint-to-bridge incidence avoids full bridge scans for neighbor/degree/repo queries. A cross-shard shortest path builds one sorted-neighbor BFS tree in each participating shard, scores every candidate bridge from those trees, and preserves deterministic shortest-path ties.
MCP tools
tools/list reports 30 tools by default (31 with --allow-exec, which adds
speculate). Every tool documents its parameters in its input schema, and every
tool carries annotations so a host knows how safe it is to run:
"annotations": { "readOnlyHint": true, "destructiveHint": false, "idempotentHint": true, "openWorldHint": <bool> }
All 30 default tools are readOnlyHint: true. openWorldHint is true only for
the tools that reach outside the graph by shelling out (list_prs,
get_pr_impact, triage_prs, working_changes_impact, predict_impact,
affected_tests, and time_travel_diff); it is false for the rest, including
predict_edit (a pure in-memory graph query). plan_rename is plan-only and
never edits source.
The lone exception is the opt-in speculate tool, present only when the
server is started with --allow-exec. It runs the project's test/build commands
in a throwaway worktree (the empirical counterpart to predict_impact), so it is
annotated honestly as readOnlyHint: false, openWorldHint: true. The default
server never advertises or runs it, preserving the strictly read-only surface.
Each tool returns a text content block (the load-bearing, purpose-formatted
output). Sixteen tools additionally declare an outputSchema and return a typed
structuredContent object alongside the text (a 2025-06-18 feature) -- see
Structured output.
query_graph
Primary entry point. Retrieve a relevant subgraph for a natural-language question.
Results are ranked by relevance: symbol labels lead, lower-weight source-path
tokens distinguish parallel implementations across applications, packages,
languages, targets, and feature directories, and bounded extractor aliases make
resources such as flat or nested localization keys searchable. Expansion is
best-first (a priority frontier), high-fan-out hubs and repeated non-matching
labels are down-weighted so they do not flood the result, and natural-language
questions remove non-substantive scaffolding, add lower-weight graph-confirmed
inflections and bounded decision-intent aliases, reward multi-concept coverage,
preserve every selected seed under tight budgets, and diversify repeated
evidence signatures in both seeds and the terse prefix. An intent alias becomes
a seed only when the graph connects it directly to selected or otherwise scored
evidence covering at least two substantive query concepts; it can replace only
redundant evidence and remains lower-confidence than that supporting match.
Alias-only symbols cannot enter through the normal seed path, and high-degree
supporting hubs are penalized when choosing among intent candidates.
Symbol-shaped queries preserve all identifier tokens,
and full exact symbol-label matches are never diversity-penalized. Every node
carries a relevance score; nodes and edges come back sorted by it, so you can
focus on the top results and ignore the low-scored tail. See [Querying#query] for
the scoring details.
Parameters:
question(string, required) -- the natural-language question.mode(string,"bfs"or"dfs") -- traversal mode. Defaultbfs. Both expand best-first by relevance; the mode only breaks score ties (bfs favors shallower nodes, dfs deeper ones).full(boolean) -- return the whole subgraph (all budget-bounded nodes plus their edges) instead of the terse top-N node list. Default false. Set it true when you need the relationships, not just which symbols match.token_budget(integer) -- approximate output token budget for the full subgraph. Default 1200. It maps to a node cap (aboutbudget/40, clamped to 10..400); the terse default shows only the top ~15 of those. The rendered text is bounded to abouttoken_budgettokens: output withintoken_budget*4bytes is returned as-is (a fast path), and only larger output is truncated, using thecl100k_basetokenizer for an exact cut. Infullmode the edges are capped to about twice the node count.context_filter(array of strings) -- keep only nodes whose source file path contains one of these substrings.since(string) -- optional recency boost. Nodes whose file changed on the current branch since this baseline rank higher. The baseline is a git ref (main,HEAD~10), a date ("2 weeks ago"), orautoto detect the default branch. Scope ismerge-base(since, HEAD)..working-tree, so it includes uncommitted edits; the boost is churn-weighted. Silently ignored when the source is not a git repo or the ref does not resolve.recency_mode(string,"boost"or"seed") -- only withsince.boost(default) re-ranks query matches by recency.seedalso injects changed-file nodes as seeds, so the changed surface appears even when the question matches little -- use it to answer "what did this branch change".
By default the response is terse: a header (Traversal: <mode> | Start: [<seeds>] | <n> nodes found, plus a Recency: line when since is used) followed by the
top ranked NODE lines ([score], an optional (changed) marker, label, file
type, source file, and a copy-ready qualified: label@path marker when the label
is duplicated) and no edges, ending with a (terse: top N of M matched nodes ...) note when more matched. Pass full=true for the whole subgraph: all
budget-bounded nodes plus EDGE lines (source --relation--> target). Either way
a structuredContent mirror (see below) accompanies the text. Structured nodes
include changed only when since resolved successfully; an absent field means
change status was not evaluated, not that the file is unchanged.
When the SYNAPTIC_QUERY_LOG environment variable points to a path, each query
is appended to it as JSONL (disable with SYNAPTIC_QUERY_LOG_DISABLE=1).
Resolving names
Every tool that takes a label/symbol/source/target resolves it through the
same cascade: exact node id, then case-insensitive label, bare name, source file,
and finally a unique label substring. When a name is shared by several files you
can pin it to one with a name@file-substring qualifier (e.g.
announce@core/foo.ts) -- this works uniformly across get_node,
get_neighbors, get_source, find_callers, find_callees, find_references,
shortest_path, affected, and predict_edit. (plan_rename instead takes a dedicated file
parameter for the same purpose.) If the name is still ambiguous, the tool returns
the candidate list with each candidate's degree and a copy-ready qualified
reference (the label@file qualifier, or the node id when it has no file) that
resolves back to that exact node, so you can paste one back to disambiguate
without assembling it or making a follow-up get_node call.
get_node
Show a node's metadata and degree.
Parameters:
label(string, required) -- a label/keyword that resolves to a node.
Returns: node label, id, source file, type, community, and degree, plus kind,
visibility, and LOC when the node carries them (added by the enrichment pass). If
nothing resolves, returns No node matches '<label>'.
get_source
Return the actual source lines for a symbol, so an assistant can read a function or class body without opening the file itself.
Parameters:
label(string) -- resolves to a node. Omit when reading byfile.file(string) -- read this file directly instead of resolving a symbol: repo-relative, ortag/pathin a federated graph (thetagfromlist_repos). Use it to read a region that is not a single symbol -- a config block, or the lines around asearch_texthit.lines(string) -- withfile, the range to read:"108-140", or a single"108"(readscontext_linesfrom there). Ignored withoutfile.context_lines(integer) -- how many lines to return from the symbol's/line's start. Default 40, clamped to 1..400.
Resolves the node (or the raw file), reads it under the --source-root jail,
and returns a header (<label> [<type>] <source_file>:L<from>-L<to>, or
<file>:L<from>-L<to> for a raw range) followed by the numbered lines. For a
symbol the graph records a start line, so the window is context_lines from
there (or stops at the symbol's end when a span is recorded).
In a federated/global graph, a node's source_file is repo-prefixed
(<tag>/...) and the member repos live in sibling directories outside any single
--source-root. When the server is started on the global graph (a
global-manifest.json sits next to it), get_source registers each member's own
source root from the manifest and resolves a federated node under that root, so
sources read correctly across repos. A co-located workspace build (members are
subdirectories of one root) already resolves under the single --source-root.
When the source cannot be read, the message names the cause and the root it
tried, instead of a bare "not available": no source root configured; the file was
not found under <root> (with a hint that in a federated workspace it may live in
a sibling repo outside this root); or the path resolved outside the configured
--source-root and was refused.
get_neighbors
List a node's neighbours, optionally filtered by relation.
Parameters:
label(string, required).relation_filter(string) -- case-insensitive substring; keep only neighbours whose relation contains it.show_sites(boolean) -- under each neighbour, print the actual source lines for every retained call/reference site on that semantic edge (at file:line: <code>, read from the jail). Default false; enriches the text view only.limit(integer, default 50) -- max neighbours listed before a+N moresummary. Ignored whenverboseis true.verbose(boolean, default false) -- list every neighbour instead of the capped top-N (use after arelation_filteron a hub).
Returns one line per neighbour with a direction marker and the relation in
brackets, capped at limit with a +N more note on a hub. When a
relation_filter matches none of the node's edges, the result is (none with relation '<filter>'; this node has: <rel>(<count>), ...) -- naming the relations
the node does have, so an empty result is not mistaken for a missing node. A
structuredContent mirror carries { seed, neighbors, by_relation, total, truncated }, where by_relation tallies every edge on the node before any filter
and total is the full matched count (which may exceed the capped neighbors).
get_community
List the members of a community, one page at a time.
Parameters:
community_id(integer, required).offset(integer) -- members to skip before the page. Default 0.limit(integer) -- maximum members in this page. Default 100.
Returns Community <id> (showing <k> of <total>): and each member on the page
(label and source file). Unknown or empty community returns No community <id>.
god_nodes
The most-connected nodes (highest degree).
Parameters:
top_n(integer) -- how many to return. Default 10, capped at 200 per page (page further withoffset); each hub costs a reverse-impact walk to count its tests, so the page is bounded.offset(integer) -- hubs to skip before the page (absolute rank is preserved in the numbering). Default 0.
Returns a ranked list of label, connection count, and how many tests transitively
exercise each hub (N test(s)), plus a structuredContent mirror. A hub with
0 test(s) is an untested high-blast-radius symbol -- exactly what to flag.
degree is total connections -- every edge kind, including the method/contains
edges that link a class to its members. It therefore measures structural
centrality/size, not how many things depend on a symbol: a class can top this
list yet have very few incoming dependents (its members hold the coupling). For
"what depends on X / what breaks if I change it", use affected, not
degree.
graph_stats
Node, edge, and community counts plus the edge-confidence breakdown.
Parameters: none.
Returns Graph: <n> nodes, <n> edges, <n> communities and
Edges: <n> EXTRACTED, <n> INFERRED, <n> AMBIGUOUS, plus a structuredContent
mirror. A graph with cross-language coupling adds a Cross-language: <n> coupling edge(s) (HTTP/RPC/FFI/WebSocket/queue/SQL boundaries) line --
counted by relation, so a polyglot SINGLE repo reports its coupling too -- and
a federated (multi-repo) graph adds Cross-repo: <n> edge(s) span repositories. The structured output carries both counts (cross_language is
no longer a subset of cross_repo). When the graph has reflection /
dynamic-dispatch sites it adds a Dynamic-dispatch sites: <n> (<n> opaque, <n> evidence-linked) line, mirrored in the structured output as dynamic_sites /
dynamic_sites_opaque / dynamic_refs_linked (see
dynamic_hazards).
list_repos
Federated workspace members (repo tags) with node/edge counts. Edges are counted
under their source node's repo. Empty (single-repo) graphs return No federated repos (single-repo graph). plus a structuredContent mirror
({ repos: [{ repo, nodes, edges, source_hash? }] }, an empty array for a
single-repo graph). See Workspaces-and-Federation.
When a workspace-state.json sits next to the graph, each repo also carries a
source_hash -- a content fingerprint of that member's sources from the last
extraction -- shown as src <hash> in the text and source_hash in the structured
output. It makes per-repo staleness visible in a federation: a member whose code
changed since the graph was built keeps its old fingerprint until that member is
re-extracted.
Parameters: none.
repo_stats
Node and edge counts for one federated member.
Parameters:
repo(string, required) -- the repo tag.
Returns Repo <repo>: <n> nodes, <n> edges, or No nodes for repo <repo>.
shortest_path
Shortest undirected connection between two keyword-resolved nodes. Each node is
rendered as a copy-ready label@file identity and every arrow preserves the
stored edge direction. A backward arrow therefore means "connected through this
edge", not a forward call/control-flow chain.
Parameters:
source(string, required).target(string, required).max_hops(integer) -- hop limit. Default 8.
Returns the relation-annotated connection with the hop count. Reports when the
endpoints do not resolve, resolve to the same node, exceed max_hops, or have no
path.
affected
Reverse-impact: the nodes that transitively depend on a symbol (the blast radius of changing it), by walking impact edges backward.
Parameters:
label(string, required) -- the symbol to start from.depth(integer) -- maximum hops to walk backward. Default 3, clamped to 1..16.relations(array of strings) -- edge relations to follow. Defaults to the structural-impact set (calls,references,imports,imports_from,re_exports,inherits,extends,implements,uses,mixes_in,embeds,depends_on,reads_from) plus the cross-language relationsinvokes,binds_native,calls_service, andhandled_by, so the blast radius spans subprocess, FFI, and HTTP/gRPC boundaries (see Cross-Language-Edges).
Returns <n> nodes depend on <seed> (<= <depth> hops): and, per hit, the hop
count, the relation it was reached through, and the label, plus a
structuredContent mirror. This is the MCP form of the CLI affected command.
Class/type nodes fold in their members. A class's callers attach to its
methods, not the bare type symbol -- a reverse walk from the class node alone would
return ~0 and read as "safe to change". So when the target is a class / struct /
interface / enum / trait, affected seeds the walk from the type and its
members and prefixes the output with a note (<X> is a class with <N> members; impact is aggregated across the class and its members). The structuredContent
mirror carries aggregated_over_members: <N>. The same fold applies to
find_callers / find_callees (which report the
external callers/callees of the members) and to describe_node
(which lists the members). See also the Limitations on dynamic
dispatch.
When the name does not resolve to a single node, affected reports it rather than
silently picking one: the text lists the candidates, and the structured mirror sets
resolved: false (with ambiguous: true and a candidates array, or found: false) instead of a misleading total: 0.
find_callers
The nodes that call, use, or reference this symbol (incoming call-like edges
only). Answers "who calls X". The header carries the true total and a
per-relation breakdown; the list is capped on a hub with a +N more summary.
Parameters:
label(string, required).limit(integer, default 50) — max callers listed before a+N moresummary. Ignored whenverboseis true.verbose(boolean, default false) — emit the full, uncapped caller list.show_sites(boolean, default false) — under each caller, print the actual source line where the call happens (at file:line: <code>, read from the jail). Turns "who calls X" into "who calls X, and the exact line" with no secondget_source.
For a class/type, the external callers of its members are folded in and the output is labelled (a class's callers attach to its methods). Only static callers are seen -- see Limitations.
find_callees
The nodes this symbol calls, uses, or references (outgoing call-like edges only).
Answers "what does X call". Same capped, count-and-breakdown output as
find_callers. For a class/type, the callees of its members are folded in and
labelled (a class doesn't call; its methods do).
Parameters:
label(string, required).limit(integer, default 50) — max callees listed before a+N moresummary. Ignored whenverboseis true.verbose(boolean, default false) — emit the full, uncapped callee list.show_sites(boolean, default false) — under each callee, print the actual source line where this symbol calls it (at file:line: <code>), so "what does X call" also shows HOW it calls it.
find_references
Find-all-references: every place a symbol is used, not just where it is called.
find_callers reports incoming call/use/reference edges, so for a type or interface
it misses the imports, implements/inherits, and type-use edges that are the
whole point of "where is this type used". find_references returns every incoming
edge except structural ownership (contains/defines/has_*), so the result unions
calls, imports, inheritance/implements, type uses, cross-language coupling
(calls_service/handled_by/invokes/binds_native), and evidence-linked
dynamic_ref. The header carries the total and a per-relation breakdown.
It is the superset companion to find_callers (calls only) aimed at
types/interfaces/enums/constants. References are to the symbol itself — unlike
find_callers, a class's members are not folded in (it answers "where is Foo
referenced", not "where are Foo's methods called"). On a federated graph a
cross-repo use surfaces the same as a local one.
Parameters:
label(string, required).limit(integer, default 50) — max references listed before a+N moresummary. Ignored whenverboseis true.verbose(boolean, default false) — emit the full, uncapped reference list.show_sites(boolean, default false) — under each reference, print the actual source line where the use happens (at file:line: <code>).
list_prs
Open PRs with CI/review state targeting the base branch. Requires the gh CLI.
When gh is missing or unauthenticated, the tool reports
gh CLI not found or not authenticated (run: gh auth login). PR data is skipped; graph audit continues offline. -- the failure is scoped to the PR tools and the
rest of the graph stays usable. See PR-Dashboard.
Parameters:
base(string) -- base branch; defaults to the repo's detected default branch.repo(string) -- target repoowner/name; defaults to the current directory's repo.
get_pr_impact
One PR's detail plus its graph blast radius (communities and nodes touched).
Requires gh.
Parameters:
pr_number(integer, required).repo(string).
triage_prs
Actionable PRs ranked by status with blast radius, returned as structured data
with an instruction for the calling model to prioritize (the MCP host is itself
the LLM, so no LLM call is made here, unlike the CLI prs --triage). Requires
gh. Fetches each PR's changed files concurrently.
Parameters:
base(string).repo(string).
working_changes_impact
Graph blast radius of your branch's changes against a base branch (`git diff
<base>`, which covers committed plus uncommitted changes, the same set a PR would have): which graph nodes and communities they touch, before opening a PR. Uses `git` only, so it works offline and before any PR exists; no `gh` required.Default output lists the changed files plus node/community counts. Pass
verbose to also list the top touched nodes (ranked by connectivity) and the
touched communities with labels.
Parameters:
base(string) -- base branch to diff against; defaults to the detected default branch.verbose(boolean, default false) -- also list the top touched nodes and labeled communities, not just files.limit(integer, default 20) -- max touched nodes listed whenverbose.code_only(boolean, default false) -- count and list only code nodes, excluding non-code files (package.json, lockfiles,.mddocs) to sharpen the blast radius.
Returns Working changes vs <base>: <n> files, <n> graph nodes, <n> communities touched and the changed files. With code_only, the count reads <n> code graph nodes. The two empty outcomes are reported distinctly: a real clean tree gives
No changes vs <base>., while a missing/failed git or a directory that is not a
git repository (e.g. the top-level directory of a federated workspace) gives a
git unavailable or not a git repository ... Graph audit continues offline.
message, so the two are never conflated. git runs in the server's working
directory, so run the server from inside the repo whose diff you want.
predict_impact
Forecast the consequences of a change before editing: which graph nodes the
changed files define, the reverse-impact blast radius that depends on them, which
edited symbols are public API (callers outside the file or module may break), and
a verify checklist. Pure-graph and read-only; for new-cycle / removed-API
detection use time_travel_diff or the synaptic predict CLI (those build
worktrees). openWorldHint: true (shells out to git diff when files is
omitted).
Parameters:
files(array of strings) -- repo-relative changed files to forecast. Omit to use the working-tree diff againstbase.base(string) -- base branch to diff against whenfilesis omitted; defaults to the detected default branch.depth(integer) -- reverse-impact hop bound. Default 3, max 16.
Returns the forecast summary, a heuristic change-risk score (low/medium/high
with its drivers), the changed nodes, the public APIs at risk, the tests at risk,
the blast radius (one dependent per line: hop count, relation, label, file), and
the verify checklist, plus a structuredContent mirror carrying the full
ChangeForecast (not truncated by limit, which caps only the text).
affected_tests
Predictive test selection: the tests that exercise the code you are about to
change. Walks the reverse-impact set from the changed files and keeps the test
nodes (detected by path convention: a test/tests/__tests__/testing
directory, or a test_* / *_test / *_spec / *.test.* / *.spec.* /
*Test / conftest filename; a spec/ directory alone is not treated as a test
tree). The focused
"which tests should I run for this change" view. openWorldHint: true (shells out
to git diff when files is omitted).
Parameters:
files(array of strings) -- repo-relative changed files. Omit to use the working-tree diff againstbase.base(string) -- base branch to diff against whenfilesis omitted; defaults to the detected default branch.depth(integer) -- reverse-impact hop bound. Default 3, max 16.
Returns one test per line (hop count, relation, label, file), or a note when no
test in the graph reaches the change, plus a structuredContent mirror
({ tests, total }). Inline unit tests not under a test path (e.g. Rust
#[cfg(test)]) are not detected.
predict_edit
What breaks if you make a specific kind of edit to a symbol, classified into
"will break" and "to review". Complements plan_rename (which is rename-only).
Pure in-memory graph query; openWorldHint: false.
Parameters:
symbol(string, required) -- the symbol to edit (name, bare name, or node id).kind(string, required) --delete(every dependent breaks),signature(callers and type users break; bare imports go to review), orvisibility(references from other files break when narrowing to private).depth(integer) -- reverse-impact hop bound. Default 3, max 16.limit(integer, default 20) -- max entries shown per section (will break / review) before a+N moresummary. Ignored whenverbose.verbose(boolean, default false) -- emit the full, uncapped lists instead of the summarized top-N.
Returns the summary plus the "will break" and "review" dependents. Each section
header carries the total and a by-depth rollup (e.g. 1h: 274, 2h: 155); each
listed dependent shows its hop count, relation, label, file, and the reason. On a
hub the list is capped with a +N more note unless verbose. Returns a note if
the symbol or kind is not recognized.
structural_search
Structural search over the graph with SYNQL (a small Cypher-inspired query
language), a named architectural pattern, or a file outline. Not text search: it
matches on kind/visibility/loc/fan-in/out/etc. .name is the bare symbol (no
parentheses); use =~ for a regex/substring match.
Parameters:
query(string) -- a SYNQL query, e.g.MATCH (c:class) WHERE c.loc > 500 RETURN c. Omit when usingpatternorfile.pattern(string) -- a built-in pattern name instead of a query:singleton,factory,observer,service-locator,god-class.file(string) -- list every symbol defined in this file (a path substring), ordered by line: a file outline, no query needed. Used only whenqueryandpatternare omitted (precedence ispattern>query>file). The path matches literally; on a federated graph a bare path matches the file across every member, while atag/-qualified path scopes to one.limit(integer) -- max rows to return. Default 25.
Returns the matched rows (one node per line: label, kind/visibility, source
location), or the parse error if the query is malformed. It also returns a
structuredContent mirror: one object per matched node with its id, label,
kind, visibility, file, line, loc, and captured signature (params
with optional types, return type, and the raw header), so an agent can route on
a function's shape without reading source. Aggregate queries (count(...),
projections) return scalar groups instead.
search_text
The complement to structural_search. Where structural_search matches the
graph (kinds, loc, fan-in/out, symbol names) and cannot see file content,
search_text is a real content search over the source files -- for
everything text-shaped the graph does not model: string literals, config values,
log messages, a TODO's wording, error strings, magic numbers. It reads through
the same per-repo source roots and containment jail as get_source (so it needs
a --source-root, or a federated graph whose members register their own roots),
honoring each repo's .gitignore/.synapticignore and skipping Synaptic's own
generated output (any synaptic-out/ directory, plus any custom --out dir
identified by its graph.json + .manifest.json marker pair), so the graph
artifacts, exports, and graph.json.bak* backups never drown real source hits. A
genuine source file merely named graph.json (no sibling manifest) is still
searched.
Parameters:
pattern(string, required) -- a regex by default; a fixed string whenliteral=true.literal(boolean) -- treatpatternas a literal, not a regex. Default false.case_sensitive(boolean) -- force case sensitivity. Omit for smart case: case-insensitive unlesspatterncontains an uppercase letter, sotodostays broad whileTODO/FIXMEare precise (this sharply cuts false positives like a lowercase "todos" matchingTODO).trueis always sensitive,falsealways insensitive.repo(string) -- restrict to one federated member (a tag fromlist_repos). Works even when the graph is served over a single parent source root (the member is located under<source-root>/<tag>). Omit to search every member / the single repo.path_glob(string) -- only files matching this glob, e.g.**/*.tsorsrc/**, applied relative to each repo root.max_results(integer) -- hits to return before truncation is flagged. Default 100, max 1000.
The defining feature is graph attribution: every hit carries the node whose
body encloses it (innermost span wins), so a matched line is a pivot -- from a
fragile regex literal straight to affected/find_callers on the function that
contains it. Each text row is file:line:col <line> [enclosing-symbol kind];
the structuredContent mirror is
{ pattern, total, truncated, files_scanned, hits: [{ repo, file, line, col, match, line_text, node? }] },
where node is null only when the hit falls outside any captured span. On a
federated graph this is strictly more useful than a raw shell grep: grep does
not know where the member repos live, which ignore files apply, or which symbol a
line belongs to.
dynamic_hazards
Lists the reflection / dynamic-dispatch sites recorded in the graph. Static
analysis cannot follow a by-name member lookup, a dispatch table, eval, a
dynamic import(), or .NET / Python / JVM reflection, so a symbol reached only
that way has no static dependents -- and a bare "0 dependents" then reads as "safe
to change" when it is not. This tool is how you judge that risk.
Where it can, Synaptic resolves dynamic dispatch instead of just cataloging it:
event buses (EventEmitter, DOM CustomEvent, C# events) link a publisher and
subscriber through an event #<name> node, and a reflection site whose name is a
string literal is evidence-linked to its unique target with a low-confidence
dynamic_ref edge (so it shows up as a caveated dependent in affected /
find_callers). What remains -- computed names, fully-dynamic dispatch -- cannot
be linked, so it is listed here as the residual risk, and affected / get_node
/ describe_node attach a dynamic_caveat to a 0-dependent symbol in such a
scope.
Parameters:
repo(string) -- restrict to one federated member (a tag fromlist_repos).path_glob(string) -- only sites in files matching this glob, e.g.**/*.ts.kind(string) -- one ofreflection,dynamic_import,eval(the kinds the detectors emit; event buses are modeled as edges, not sites).target(string) -- only sites that could reach this symbol: sites whose literal key names it, plus opaque sites in a file that defines it.max_results(integer) -- sites to return before truncation. Default 30, max 1000. It is a scan-and-narrow tool: filter byrepo/path_glob/kind/targetrather than raising this.
Each text row is [repo] file:line <kind> <"key"|(opaque)> in <enclosing symbol>; the structuredContent mirror is
{ total, truncated, sites: [{ repo, file, line, kind, key, host }] }. graph_stats
reports the totals (dynamic_sites, dynamic_sites_opaque, dynamic_refs_linked),
and the CLI exposes the same listing as synaptic hazards.
describe_node
A compact "takes X, returns Y, calls Z" description of a symbol, composed from
its captured signature
and outgoing call edges. Graph-only (no source read), built for generating
tool/function descriptions or quickly grasping a function's shape. The "calls"
clause includes cross-language invokes and calls_service targets.
Parameters:
label(string, required) -- the symbol to describe (bare name, full label, node id, or source file).
Returns the one-line summary, then a Signature: line and a Calls (<n>): ...
line when present, plus a structuredContent mirror
({ found, id, label, kind, summary, callees, signature }). Returns
No node matches '<label>'. when nothing resolves.
time_travel_diff
How the code graph changed between two git revisions: added/removed module
dependencies, removed APIs, architectural drift, new dependency cycles, and
change hotspots. Builds each revision in a throwaway git worktree (slow on a cold
repo; cached per commit SHA afterwards). openWorldHint: true.
Parameters:
rev1(string, required) -- the base revision (e.g.HEAD~10, a branch, a SHA).rev2(string) -- the target revision. Defaults to the current working tree.top(integer) -- max rows per ranked section. Default 20.
Returns a summary (<n> new nodes, <n> new edges, ...), the added/removed
dependencies, removed APIs, drift, new cycles, and hotspots.
plan_rename
Plan-only: a confidence-scored rename plan for an agent to apply. Never edits
source. After applying the edits, run synaptic refactor verify on the CLI to
check the post-edit graph.
Parameters:
name(string, required) -- the symbol to rename (its name, or a node id).to(string, required) -- the new name.id(string) -- disambiguate by node id when the name matches several definitions.file(string) -- disambiguate by a file-path substring.limit(integer, default 50) -- max sites listed per section (Edits, Review) before a+N moresummary. Ignored whenverboseis true.verbose(boolean, default false) -- list every edit/review site instead of the summarized top-N.
Returns the Rename <old> -> <new> [<confidence>], <n> edit(s) across <n> file(s), <n> to review, <n> affected summary, followed by the actual edit sites
(file:line:col, old -> new, reason, confidence) under Edits (<n>): and the
lower-confidence ones under Review (<n>): — so an agent can apply the rename
without a second round-trip to the CLI's plan.md. Returns an error string if the
symbol is not found.
readiness_audit
Static port/readiness audit over the graph plus source and config metadata. It ranks likely blockers from framework sentinel returns, placeholders/stubs, generated-resource noise, and project metadata. It does not run a build.
Parameters:
profile(string) -- rule profile. Defaultauto; usegenericfor a language-neutral scan.repo(string) -- in a federated store, restrict the audit to one repo tag.severity(string) -- only return findings at least this severe (critical|high|medium|low|info).limit(integer, default 20) -- max findings before a+N moresummary. Ignored whenverboseis true.verbose(boolean, default false) -- list every finding and include detail, remediation, evidence, confidence, and impact.
Returns a summary grouped by severity and subsystem, then ranked findings. The
structuredContent mirror carries the full ReadinessReport. When the server
has no registered source root, source/config checks are skipped explicitly and
graph-only findings are still returned.
audit_sql
Audit the codebase's SQL for performance and security problems over the
SQL-aware graph: row-level-security gaps, over-broad grants, likely SQL
injection, missing indexes on filter/foreign-key columns, SELECT *,
non-sargable predicates, N+1 patterns, and missing primary keys.
Parameters:
severity(string) -- only return findings at least this severe (critical|high|medium|low|info).limit(integer, default 20) -- max findings before a+N moresummary. Ignored whenverboseis true.verbose(boolean, default false) -- list all findings AND each finding's full detail + fix, instead of the terse one-line-per-finding summary.
Returns a one-line report summary, then one line per finding by default
([severity] rule_id @ location (conf) title); verbose adds each finding's
detail and fix. A structuredContent mirror carries the full AuditReport. See
SQL Auditing.
advise_sql
Critique a single candidate query before it is written. Runs the same performance + security checks on the query text and cross-references the graph: whether the referenced tables exist, are behind row-level security, and have indexes on the columns you filter on.
Parameters:
query(string, required) -- the SQL to critique.dialect(string) -- optional dialect hint (postgres|mysql|mssql|sqlite).
Returns the findings as text + structuredContent.
Structured output
Sixteen tools declare an outputSchema and return a structuredContent object
beside the text content, so a client can parse the result instead of scraping the
formatted text:
| Tool | structuredContent shape |
|---|---|
graph_stats | { nodes, edges, communities, extracted, inferred, ambiguous, cross_repo, cross_language, dynamic_sites, dynamic_sites_opaque, dynamic_refs_linked } |
dynamic_hazards | { total, truncated, sites: [{ repo?, file, line, kind, key?, host }] } |
get_node | { found, id, label, source_file, file_type, degree, community?, kind?, visibility?, loc? } (on an ambiguous name: found:false with ambiguous+candidates, matching affected/describe_node) |
god_nodes | { god_nodes: [{ label, degree, id, test_count }] } (degree = total connections incl. members; centrality/size, not incoming-dependence) |
affected | { seed, seed_qualified, resolved, affected: [{ label, qualified, depth, via_relation }], total, truncated, by_depth, aggregated_over_members? } (on an unresolved name: resolved:false with ambiguous+candidates or found:false) |
query_graph | { nodes: [{ label, qualified?, file_type, source_file, score, changed? }], edges: [{ source, relation, target }] } (nodes sorted by score; qualified disambiguates duplicate result labels; changed is present only when since resolved, and true when the node's file changed; edges is empty unless full=true) |
structural_search | { columns, results: [[{ id, label, kind, visibility, file, line, loc, signature }]] } (or groups for aggregates) |
search_text | { pattern, total, truncated, files_scanned, hits: [{ repo, file, line, col, match, line_text, node? }] } (node is the enclosing symbol { id, label, kind, community }, or null when the hit is outside any captured span) |
describe_node | { found, id, label, kind, summary, callees, signature, members?, member_count? } (members listed for a class/type; on an ambiguous name: found:false with ambiguous+candidates) |
get_neighbors | { seed, seed_qualified, neighbors: [{ label, qualified, relation, direction }], by_relation: { <relation>: <count> }, total, truncated } (by_relation tallies every edge before any filter; total is the full matched count, while neighbors is capped to limit) |
list_repos | { repos: [{ repo, nodes, edges, source_hash? }] } (empty array for a single-repo graph; source_hash present when a workspace-state.json sibling exists) |
predict_impact | the full ChangeForecast: { summary, changed_files, changed_nodes, public_api_breaks, blast_radius, blast_radius_total, at_risk_tests, verify_checklist, risk } (not truncated by limit, which caps only the text) |
affected_tests | { tests: [{ id, label, file, depth, via_relation }], total } |
readiness_audit | { version, summary, counts_by_severity, groups, findings: [{ rule_id, severity, category, subsystem, title, detail, location, node_ids, evidence, remediation, confidence, impact }], skipped } |
audit_sql / advise_sql | { version, summary, findings: [{ rule_id, severity, category, title, detail, location, remediation, confidence }] } |
The other tools return text only. A tool whose structured mirror cannot resolve
its node (e.g. get_neighbors on an ambiguous label) omits structuredContent
rather than returning a null object; the text content still carries the
disambiguation message.
Tool error behavior
An unknown tool name is invalid tools/call parameters and returns JSON-RPC
-32602; tool execution failures after a valid dispatch stay in the tool-result
channel with isError: true. An unknown JSON-RPC method returns -32601, an
unknown resource returns MCP resource-not-found -32002, and an unknown prompt
returns -32602.
Limitations
- Static analysis only (with modelled exceptions). Edges are read from source
structure, so a call wired up at runtime is not always captured. Two common
cross-process mechanisms are modelled, so their handlers do show callers:
Electron IPC (
ipcMain.handle/on↔ipcRenderer.invoke/send/on,webContents.send) and WebSocket / socket.io message channels — a sender and its handler meet on a channel node (ipc #<ch>/ws #<cmd>), soaffected,find_callers, andshortest_pathcross the boundary. Still not traced: a handler reached only via a custom event bus, a DI container, a runtime-built dispatch table, or reflection can show 0 callers even though it runs. Read a surprising 0-caller result on a dispatched handler as "no static caller", not "dead code". - Class vs. method impact. Because callers attach to a class's methods, not the
bare type symbol,
affected/find_callers/find_callees/describe_nodefold a class's members in automatically and label the result (seeaffected).god_nodesdegreestill counts those member edges, so it ranks structural size/centrality, not incoming dependence -- useaffectedfor blast radius. - Inline unit tests. A test defined in the same file as the code under test may
not be linked as a separate test node, so
affected_testscan undercount in test-sparse or inline-test codebases. - Text content is not in the graph (but
search_textreaches it). The graph models structure, not the bytes of a line: string literals, config values, log messages, a TODO's wording, and magic numbers are not nodes. Usesearch_textfor those -- it searches the source directly and attributes each hit back to its enclosing symbol, so you still land on a graph node to pivot from. For reading logic,get_sourcereturns a symbol's body or an arbitraryfile+linesrange, andshow_sitesonfind_callers/find_callees/get_neighborsprints the exact call line for each edge -- so "A calls B" becomes "A calls B at this line" without leaving the graph. - Federated staleness. The graph is a snapshot. In a multi-repo workspace,
members drift between extractions;
list_repossurfaces a per-reposource_hash(when aworkspace-state.jsonsibling exists) so drift is at least visible, but the graph still reflects the last extraction until you re-run it.
MCP resources
resources/list reports six read-only resources, each fetched with
resources/read by uri:
synaptic://report(text/markdown) -- theGRAPH_REPORT.mdnext to the loaded graph, if present.synaptic://stats(text/plain) -- the same content asgraph_stats.synaptic://god-nodes(text/plain) -- the top 10 god nodes.synaptic://surprises(text/plain) -- surprising cross-community connections.synaptic://audit(text/plain) -- the edge-confidence breakdown.synaptic://questions(text/plain) -- suggested questions to ask the graph.
See Analysis-and-Reports and Semantic-Analysis for what these surface.
Resource templates
resources/templates/list reports two templates, so any node or community is
addressable as a resource by URI:
synaptic://node/{label}(text/plain) -- metadata for one node by label, id, or bare name (the same content asget_node).synaptic://community/{id}(text/plain) -- members of one community by id.
resources/read resolves these templated URIs directly (for example
synaptic://node/AuthService).
Resource subscriptions
For MCP 2026-07-28, subscriptions/listen replaces both
resources/subscribe / resources/unsubscribe and the transport-level GET
stream. A listen request supplies resource URI filters and receives an
acknowledgement before any updates. HTTP keeps the POST response open as SSE;
stdio keeps the request active and writes subsequent JSON-RPC notifications.
When the graph hot-reloads, matching listeners receive
notifications/resources/updated with the subscription id in metadata. A
cancelled request stops the listener.
Legacy HTTP clients keep the older flow: call resources/subscribe, then open
the session's GET /mcp SSE stream. resources/unsubscribe removes the URI.
Legacy stdio does not advertise resource subscription support.
MCP prompts
prompts/list reports four guided workflows; prompts/get returns a single
user-role message that tells the host model how to drive the tools. Arguments are
sanitized before interpolation.
| Prompt | Arguments | What it asks for |
|---|---|---|
onboard | none | Orient in the codebase via graph_stats, god_nodes, and synaptic://questions. |
explain_subsystem | topic (required) | Explain a subsystem using query_graph, get_source, and find_callers/find_callees. |
assess_pr | pr_number (required) | Assess a PR's risk via get_pr_impact and affected. |
trace_flow | from, to (required) | Trace a path with shortest_path and get_source. |
An unknown prompt name returns -32602.
Completions
completion/complete provides argument autocomplete. It keys off the argument's
name and prefix-matches values, returning { completion: { values, total, hasMore } } capped at 100 values:
label,source,target-- node labels. The match also sees past leading punctuation, so a prefix liketool_getmatches a method node labeled.tool_get_node().repo-- federated repo tags.community_id-- community ids.
Logging
MCP 2026-07-28 removed logging/setLevel; Synaptic therefore does not
advertise modern logging and returns method-not-found if a modern client calls
it. Modern clients can send the standard per-request log-level metadata, though
the server currently emits no log records. Legacy discovery still advertises
logging, accepts logging/setLevel, and acknowledges it with an empty result.
REST API
A read-only JSON surface mirrors the engine calls behind the tools (for non-MCP
clients). Every route honors the same API-key and Host-allowlist guard as
/mcp. Each returns { "text": <output> } with the tool's formatted text passed
through verbatim.
| Method and route | Query params | Returns |
|---|---|---|
GET /api/stats | none | graph stats text |
GET /api/god-nodes | top_n (default 10) | god-node list |
GET /api/node/:label | label in the path | node metadata |
GET /api/query | q (required), token_budget (default 1200) | subgraph text; BFS traversal, no context filter |
GET /api/repos | repo (optional) | one member's stats if repo is given, else the member list |
GET /api/query without ?q= returns 400.
Example:
curl -H "X-API-Key: s3cret" "http://127.0.0.1:8765/api/query?q=authentication&token_budget=800"
Example: stateless MCP 2026-07-28 over stdio
Each line below is a complete JSON-RPC request. The connection metadata is repeated on every call; there is no initialize notification.
{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/connection":{"protocolVersion":"2026-07-28","clientCapabilities":{},"clientInfo":{"name":"example-client","version":"1.0"}}}}}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"query_graph","arguments":{"question":"how does login work","mode":"bfs"},"_meta":{"io.modelcontextprotocol/connection":{"protocolVersion":"2026-07-28","clientCapabilities":{},"clientInfo":{"name":"example-client","version":"1.0"}}}}}
Example: legacy JSON-RPC session over stdio
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"example-client","version":"1.0"}}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"query_graph","arguments":{"question":"how does login work","mode":"bfs"}}}
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_source","arguments":{"label":"login_user"}}}
{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"affected","arguments":{"label":"login_user"}}}
See also
- Assistant-Integration -- wire the server into an
assistant with
synaptic install. - Querying -- the query semantics shared with the CLI.
- PR-Dashboard -- the PR tools in detail.
- Commands -- the full CLI reference.