Wire the MCP server into a coding agent
You will register GuardLink’s MCP server with a coding agent, verify over the wire that it exposes 24 tools and 3 resources, and confirm the answers it returns are about your repository rather than whatever directory it started in.
An agent editing an annotated file otherwise infers security context from the source. The model already records what a human decided: which risks were accepted, which controls are load-bearing, and which asset a function belongs to. None of that is derivable from the code.
Before you start
Section titled “Before you start”- GuardLink installed. See Install GuardLink.
- A project with
guardlink initrun in it, and some annotations. The annotate guide builds one. - An MCP client. Claude Code discovers the config guardlink writes; others need a line of JSON.
Register the server
Section titled “Register the server”guardlink init already wrote .mcp.json at the project root:
{ "mcpServers": { "guardlink": { "command": "guardlink", "args": ["mcp"] } }}Claude Code picks that up and asks before using it:
claude mcp listguardlink: guardlink mcp - ⏸ Pending approval (run `claude` to approve)guardlink-mcp and guardlink mcp are the same server, with the same
startStdioServer(), the same tools. Use whichever your client’s config style
prefers. For a client that does not auto-discover .mcp.json, the whole
configuration is:
{"mcpServers": {"guardlink": {"command": "guardlink-mcp"}}}Confirm it starts
Section titled “Confirm it starts”Before involving an agent, check the server answers on its own. It speaks JSON-RPC on stdin and stdout, so you can drive it from a shell:
{ echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}' echo '{"jsonrpc":"2.0","method":"notifications/initialized"}' echo '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' sleep 2} | npx guardlink-mcpThe initialize response identifies the server:
{"serverInfo":{"name":"guardlink","version":"2.0.0"}}and tools/list returns 24:
guardlink_parse guardlink_status guardlink_validateguardlink_suggest guardlink_lookup guardlink_contextguardlink_graph guardlink_annotate_apply guardlink_reanchorguardlink_threat_report guardlink_annotate guardlink_reportguardlink_dashboard guardlink_sarif guardlink_diffguardlink_threat_reports guardlink_sync guardlink_clearguardlink_unannotated guardlink_review_list guardlink_review_acceptguardlink_entitlement_propose guardlink_entitlement_list guardlink_workspace_infoplus three resources: guardlink://model, guardlink://definitions, and
guardlink://unmitigated. Every one of them, with its input schema, is in the
MCP tool reference.
guardlink-mcp --help and --version print and exit before any transport is
connected, which is why they are safe on a server whose stdout is the protocol
channel. With no flag it serves and blocks on stdin, so an apparently hung process
is the server working.
Check it is reading the right project
Section titled “Check it is reading the right project”This is the failure that wastes the most time, and it is invisible unless you look for it.
The server resolves root per tool call. Omit it, and the root is the process’s
working directory, whatever your MCP client happened to launch the server in.
Every tool result carries a second content block naming what it used:
{ "guardlink": { "annotation_hash": "sha256-v2:102dc83796b48add30d28070c4aa654e36ea798f55df955fb339dd7d7a3473f5", "git_sha": "14c2bbb9a99244888e861b82ea05679d4bbc662a", "generated_at": "2026-08-13T08:12:00.768Z", "mode": "external", "root": "/path/to/legacy-api", "guardlink_version": "2.0.0" }}Started one directory too high, the same call still succeeds. It parses everything below that directory and reports a model assembled from unrelated projects:
{ "guardlink": { "annotation_hash": "sha256-v2:c5152b0bfb6a6861dae830dc4ba161cc9ed57b292ffc8d1746372346644ab7a7", "git_sha": null, "mode": "mixed", "root": "/path/to/the/parent/directory", "guardlink_version": "2.0.0" }}assets: 4 became assets: 14. Nothing errored. The three tells are root,
git_sha: null where you expected a commit, and mode: "mixed" where the
project has one annotation mode.
Ask for guardlink_status first in a new session and read the root line before
trusting anything else.
The tools worth knowing by name
Section titled “The tools worth knowing by name”Twenty-four is more than anyone memorises. These six carry most sessions.
| Tool | Ask it when |
|---|---|
guardlink_context(file) |
You opened or are about to edit a file. Everything known about that one file. |
guardlink_status() |
Cold start on an unfamiliar repository. Counts, plus the unmitigated list. |
guardlink_graph(from, depth, direction) |
You are about to change a shared component and need blast radius. |
guardlink_lookup(query) |
You have one specific question. |
guardlink_validate() |
Before finishing a change. |
guardlink_diff(ref) |
After a change: did this make the model worse? |
guardlink_context distinguishes kinds of empty
Section titled “guardlink_context distinguishes kinds of empty”{ "file": "test/users.test.js", "status": "not_scanned", "hint": "`test/users.test.js` exists but is not in the parser's scan set — it is under an excluded directory (node_modules, dist, build, test, tests, __tests__, vendor, target, .bravos, .bugb). Its annotations, if any, were never read."}annotated, scanned_without_annotations, not_scanned, not_found. The
second means genuinely clean; the third means nobody looked. An agent that treats
them alike will report a test directory as having no security concerns.
guardlink_graph answers blast radius
Section titled “guardlink_graph answers blast radius”{ "traversal": { "start": { "ref": "#api", "canonical": "api", "resolved": true, "matched_via": "exact" }, "nodes": [ { "key": "api", "depth": 0, "declared": true }, { "key": "tokens", "depth": 1, "declared": true }, { "key": "users", "depth": 1, "declared": true } ], "edges": [ { "kind": "flow", "from": "api", "to": "users", "directed": true, "label": "GET./api/users", "at": "src/routes.js:4" }, { "kind": "flow", "from": "api", "to": "tokens", "directed": true, "label": "POST./api/refresh", "at": "src/routes.js:8" } ], "depth_reached": 1, "depth_requested": 2, "completeness": "complete" }}depth_reached below depth_requested with completeness: "complete" means the
graph ran out, not that the traversal was cut short. The response also carries the
narrowed model beside the traversal.
guardlink_lookup refuses to guess
Section titled “guardlink_lookup refuses to guess”It understands a fixed set of named forms and returns the whole list rather than inventing an answer for anything else:
{ "query": "what is broken", "type": "no_match", "count": 0, "results": [ { "hint": "Unrecognised query form: `what is broken`. This is not a supported form, so no result was guessed.", "supported_forms": [ "unmitigated", "confirmed", "features", "asset <id>", "threats for <asset>", "flows into <asset>", "handles <classification>", "cwe:CWE-89 | owasp:A03 | CWE-89" ] } ]}Truncated. The real list runs to 26 forms. Send a deliberately bad query to see all of them.
Anything that resolves a reference reports matched_via: exact, alias, or
substring. A substring match is a suggestion, not an identification, and
ambiguous with a candidates list means several records tied.
Let the agent write annotations
Section titled “Let the agent write annotations”guardlink_annotate_apply writes a validated @source block into the sidecar
rather than having the agent edit .gal text. Pass the source file, the line
the block anchors to, and the raw annotation lines:
{ "file": "src/db.js", "line": 1, "symbol": "query", "dry_run": true, "annotations": ["@audit #dbstore -- \"All SQL passes through here\""]}{ "ok": true, "galPath": ".guardlink/annotations/src/db.js.gal", "status": "written", "diff": "--- .guardlink/annotations/src/db.js.gal\n+++ .guardlink/annotations/src/db.js.gal\n+@source file:src/db.js line:1 symbol:query\n+@audit #dbstore -- \"All SQL passes through here\"", "errors": [], "linesWritten": 2}Four things about that call.
- Do not pass
@sourceyourself. The header is synthesised fromfile,lineandsymbol. Passing one is an error. - Pass
symbol. It is what letsguardlink reanchorfind the block after a refactor moves the code. Omitting it also means “this is about the asset, not one function”. dry_run: truereturns the diff without writing.status: "written"in a dry run describes what would happen.- Every line is re-parsed before anything reaches disk, so a syntax error is
rejected with its reason. An undefined
#idis not, so runguardlink_validateafterwards.
guardlink_annotate_apply refuses @accepts and @entitles:
{ "ok": false, "galPath": ".guardlink/annotations/src/db.js.gal", "status": "rejected", "diff": "", "errors": [ "Line 1: refusing to write `@accepts`. Accepting a risk is a human governance decision. Record the risk with @exposes and flag it with @audit instead." ], "linesWritten": 0}Both are human governance decisions: accepting a risk, and confirming an actor is
legitimately entitled to a capability. There is deliberately no
guardlink_entitlement_accept. An agent can file a proposal with
guardlink_entitlement_propose, and only guardlink entitle at a terminal
records the acceptance, under the name of the human who made it.
Keep the instruction files current
Section titled “Keep the instruction files current”The MCP server is one of two channels. The other is the instruction files
init wrote, namely CLAUDE.md, AGENTS.md, .cursor/rules/guardlink.mdc,
.windsurfrules, .clinerules, .github/copilot-instructions.md,
and .gemini/GEMINI.md, which carry a live block of model context an agent reads
before it calls anything.
The two reach an agent differently, and that difference is the reason sync
exists at all.
flowchart TB
subgraph LIVE["over MCP, answered per call"]
direction LR
L1["the agent asks"] --> L2(["the server reads<br/>the model now"]) --> L3["always current"]
end
subgraph SNAP["in the instruction files, a copy"]
direction LR
S1["guardlink sync writes"] --> S2(["CLAUDE.md and<br/>seven more carry a block"]) --> S3["current only until<br/>the model changes"]
end
LIVE ~~~ SNAP
class L3 emphasis
An agent reads the instruction files before it calls anything, so that copy is what shapes its first move in a session. The MCP tools then answer from the model as it is. A stale block is not corrected by the tools being right, because the agent has already read it.
That block is a snapshot, and it goes stale:
npx guardlink sync .✓ Updated 8 agent instruction file(s):
.guardlink/README.md CLAUDE.md .cursor/rules/guardlink.mdc AGENTS.md .github/copilot-instructions.md .windsurfrules .clinerules .gemini/GEMINI.md
✓ All agent instruction files now include live threat model context. 3 assets, 2 threats, 2 controls, 2 exposures. Any coding agent (Cursor, Claude, Copilot, Windsurf, etc.) will see these IDs..guardlink/README.md gets the same treatment, and records the hash so a reader
can tell whether it is current:
Current model: 14 annotations · 3 assets · 2 threats · 2 controls · 2 exposures · 2 flowsContent hash: `sha256-v2:41f8aa906bbb22512a8cfabd2f45b4373593b3ecfabfa8a4dc92748f32c8f585` — identical hash means identical model.Run sync whenever the definitions change. validate --sync and status --sync
do it as a side effect; without the flag, neither writes anything.
What to change next
Section titled “What to change next”- Add
guardlink validate .to the agent’s finishing checklist. The instruction files already tell it to; a project-level hook makes it certain. - Commit
.mcp.json. It is configuration every contributor needs, and it is three lines. - Give the agent
guardlink_graphbefore a refactor, not after. It is the one tool whose answer is hard to reconstruct from reading files.
Related
Section titled “Related”- The threat model as an artifact
covers the freshness envelope, and what
annotation_hashidentifies. - Use guardlink as a library covers
createServerandstartStdioServer, for embedding this in another process. - MCP tool reference lists every tool and resource, with its full input schema.
guardlink-mcpdocuments the executable’s own flags.

