{"id":"732fd898-d0be-4271-9afd-83dcc416a3d9","slug":"version-aware-code-mcp","kind":"mcp","name":"version-aware-code-mcp","summary":"Version-aware code intelligence for AI coding agents. Combines branch-aware code search with structural code graph analysis through MCP.","owner":"tc3oliver","repoUrl":"https://github.com/tc3oliver/version-aware-code-mcp","license":"Apache-2.0","licenseTier":"permissive","trust":"community","stars":1,"match":null,"fewStars":true,"format":"mcp","createdAt":"2026-08-11T15:39:54.000Z","addedAt":"2026-10-21T06:34:02.180Z","logoUrl":"https://avatars.githubusercontent.com/u/57586652?u=3b231703177b185a78fd87bea7be6443056a18d2&v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["developer-tools","search","memory-knowledge"],"tags":["ai-agents","call-graph","code-graph","code-intelligence","code-search","developer-tools","golang","zoekt"],"runtime":"go","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-21T06:34:23.343Z","sample":false,"url":"https://conduits.dev/tools/version-aware-code-mcp","language":"Go","transports":["stdio"],"install":{},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"# version-aware-code-mcp\n\nVersion-aware code intelligence for AI coding agents. Combines branch-aware code\nsearch with structural code graph analysis through MCP.\n\n## Why\n\nAgents search the right code in the wrong version.\n\nAsk an agent to fix a bug in release 2.x and it will happily grep its way into a\nfile from `main`, or into a checkout that stopped tracking the branch three\nmerges ago. The snippet looks plausible. The function name matches. Nothing in\nthe result says which version it came from, so the mistake is invisible until it\nships.\n\nThis server makes the version explicit and enforced. Every request names a\ncontext — a repository, a branch, and a revision — and every result is confined\nto it:\n\n- Search is restricted to that context's branch.\n- The call graph queried is the one built for that context's revision.\n- Source is read at that revision, and a mismatch fails the request instead of\n  quietly returning the wrong file.\n- Every result carries the repository, branch, revision, file, and line it came\n  from, so the answer can be checked rather than trusted.\n\nThe server does not answer questions. It supplies search, graph, source,\nversion, and evidence. Your agent does the reasoning.\n\n## Architecture\n\nvacmcp sits between your agent and the engines that already index your code. It\nadds one thing to them: a *context*, an id that names one repository, one\nbranch, one revision and one call graph. Every tool call takes a context id —\ntwo, when it compares two versions — and nothing it returns comes from outside\nthat scope.\n\n```text\n  your agent\n      │   MCP 2026-07-28, over STDIO or Streamable HTTP\n      ▼\n   vacmcp ──── list_contexts   search_code    trace_calls   get_code\n      │        compare_code    compare_calls\n      │\n      │   resolve the context id against the configuration:\n      │   repository, branch, revision, graph reference\n      │\n      ├── SearchProvider ──▶ Zoekt   (local web server, queried over HTTP)\n      ├── GraphProvider  ──▶ CBM     (codebase-memory-mcp, local subprocess)\n      └── SourceProvider ──▶ git     (local clone, read at the revision)\n```\n\nNeither engine is vendored or forked. Zoekt indexes several branches into one\nindex and picks the version at query time; codebase-memory-mcp holds one graph\nper project, so each version gets a project of its own. vacmcp resolves the\ncontext, hands that scope to the provider, and puts the context and the evidence\nback on every answer.\n\n## Features\n\n- **Version-scoped search.** A query is confined to the repository and branch of\n  the context it names; it cannot widen its own scope.\n- **Version-scoped call graph.** A trace runs in the graph built for that\n  context's revision, never in another version's.\n- **Source read at a revision.** Lines come out of the commit the context\n  declares, not out of whatever the working tree is on.\n- **Comparison between two versions.** One file or one symbol's call graph, read\n  in two contexts and reported as two sides, so neither version's evidence is\n  mixed into the other's.\n- **Evidence on every result.** Repository, branch, revision, file and line, so\n  an answer can be checked instead of trusted.\n- **Local-first.** Local Zoekt, local codebase-memory-mcp, local git. No LLM API\n  key, no embeddings. See [Security](#security).\n\n## Quick Start\n\n### With Docker\n\nNeeds Docker and nothing else. The stack brings up a Zoekt web server, builds a\ngraph per release and serves vacmcp against a demo repository baked into the\nimages: `release/v1`'s `Process()` calls `LegacyHandler()`, `release/v2`'s calls\n`NewHandler()`, and the two contexts `demo-v1` and `demo-v2` are configured over\nthem.\n\n```bash\ndocker compose -f docker/compose.yaml up -d --build\ndocker compose -f docker/compose.yaml exec vacmcp \\\n        vacmcp doctor --config /etc/vacmcp/config.yaml\n```\n\nvacmcp then serves Streamable HTTP on `http://127.0.0.1:8080`, with the contexts\n`demo-v1` and `demo-v2`. The tool call at the end of the next section works\nagainst it with `demo-v2` as the context. Both published ports are bound to\nloopback. Tear it down with:\n\n```bash\ndocker compose -f docker/compose.yaml down -v\n```\n\n### With your own repository\n\nYou need Go 1.26, git, and the two engines vacmcp queries. Neither engine ships\nwith this project:\n\n```bash\ngo install github.com/sourcegraph/zoekt/cmd/zoekt-git-index@latest\ngo install github.com/sourcegraph/zoekt/cmd/zoekt-webserver@latest\ngo install github.com/sourcegraph/zoekt/cmd/zoekt@latest          # optional, to inspect the index\n```\n\nFor the graph, download\n[codebase-memory-mcp](https://github.com/DeusData/codebase-memory-mcp/releases)\n0.11.0 or newer for your platform and put the binary on your `PATH`.\n\nInstall vacmcp. Every release publishes an archive per platform — linux, macOS\nand Windows, amd64 and arm64 — next to a `SHA256SUMS` file covering all of\nthem, on the [releases page](https://github.com/tc3oliver/version-aware-code-mcp/releases):\n\n```bash\nsha256sum -c SHA256SUMS --ignore-missing\ntar -xzf vacmcp_v0.8.0_linux_amd64.tar.gz\n./vacmcp version          # v0.8.0, the release this binary was built from\n```\n\nOr build it from source, which reports `0.0.0-dev` because no release built it:\n\n```bash\ngit clone https://github.com/tc3oliver/version-aware-code-mcp\ncd version-aware-code-mcp\nmake build\n```\n\nIndex the branches you want to search. One index holds them all:\n\n```bash\nzoekt-git-index -index ~/.vacmcp/zoekt-index \\\n        -branches release/1.x,release/2.x /path/to/backend\n```\n\nBuild one graph per version. codebase-memory-mcp indexes a directory rather than\na revision, so each version needs a checkout of its own — a worktree does:\n\n```bash\ngit -C /path/to/backend worktree add --detach /tmp/backend-v1 release/1.x\ngit -C /path/to/backend worktree add --detach /tmp/backend-v2 release/2.x\ncodebase-memory-mcp cli index_repository --repo-path /tmp/backend-v1 --name backend-v1\ncodebase-memory-mcp cli index_repository --repo-path /tmp/backend-v2 --name backend-v2\n```\n\nWrite a configuration naming both — `config/example.yaml` is a working starting\npoint, and [Context Configuration](#context-configuration) explains the fields:\n\n```yaml\nproviders:\n  zoekt:\n    url: http://127.0.0.1:6070\n  cbm:\n    command: codebase-memory-mcp\n\nrepositories:\n  backend:\n    path: /path/to/backend\n\ncontexts:\n  backend-v1:\n    repository: backend\n    branch: release/1.x\n    revision: 8af31e2\n    graph_ref: backend-v1\n  backend-v2:\n    repository: backend\n    branch: release/2.x\n    revision: 94cb821\n    graph_ref: backend-v2\n```\n\n`revision` is a commit: `git -C /path/to/backend rev-parse release/1.x` prints\nthe one to write down.\n\nThe `repositories` key has to be the name Zoekt indexed the repository under,\nwhich it derives from the clone's remote and falls back to the directory name\nfor. Get it wrong and searches come back empty rather than failing, so check it\nagainst the index:\n\n```bash\nzoekt -index_dir ~/.vacmcp/zoekt-index -r -l . | head -1   # prints repository/path\n```\n\nStart Zoekt, check the setup, and serve:\n\n```bash\nzoekt-webserver -index ~/.vacmcp/zoekt-index -listen 127.0.0.1:6070 -rpc &\n./bin/vacmcp doctor --config vacmcp.yaml\n./bin/vacmcp serve --config vacmcp.yaml\n```\n\n`doctor` reports one row per dependency and one per context, and exits non-zero\nif any of them did not pass:\n\n```text\nMCP SDK       OK    go-sdk v1.7.0, 6 tools\nConfig        OK    vacmcp.yaml: 1 repositories, 2 contexts\nZoekt         OK    http://127.0.0.1:6070\nCBM           OK    0.11.0\nGit repo      OK    backend\n\nContexts\nbackend-v1    OK    backend release/1.x 68c19fdb264bad80cf220f672f89dc90478a9d66\nbackend-v2    OK    backend release/2.x 7b05c6aeb7544cc7fd2b69c56c89c9bcfd46a1a2\n```\n\nPoint your MCP client at the server as below. To check it answers without one,\nStreamable HTTP takes a JSON-RPC call over plain HTTP:\n\n```bash\ncurl -sS http://127.0.0.1:8080 \\\n  -H 'Content-Type: application/json' \\\n  -H 'Accept: application/json, text/event-stream' \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"search_code\",\n       \"arguments\":{\"context\":\"backend-v2\",\"query\":\"NewHandler\"}}}'\n```\n\n### Managed Mode\n\nEverything above is the static mode: you index, you check out, you write the\nconfiguration, and `vacmcp serve --config FILE` serves what you wrote. Managed\nmode does that part for you. `vacmcp repo add` clones a repository into a data\ndirectory, `vacmcp context create` pins a ref to a commit and builds that\ncommit's index and graph, and `vacmcp serve --managed` serves the contexts that\ncame out ready. Same engines, same seven tools, same isolation — no configuration\nfile and no `zoekt-git-index`, `git worktree` or `index_repository` by hand.\n\n```bash\nvacmcp repo add backend --url git@git.example.com:team/backend.git\nvacmcp context create backend-v1 --repo backend --ref release/1.x\nvacmcp context create backend-v2 --repo backend --ref release/2.x\n```\n\n`context create` resolves the ref once and pins the full commit SHA it found.\nThat pin never moves: `vacmcp repo sync` fetches new commits, and the contexts\nyou already have keep answering out of the revisions they were created with. To\nserve a newer commit, create another context — a context is immutable, so a\nsecond version is a second name, never an edit of the first.\n\nA context can name several repositories, one `--repo`/`--ref` pair each, and\neach of them is pinned to its own commit:\n\n```bash\nvacmcp context create stack-v1 --repo backend --ref release/1.x \\\n                               --repo frontend --ref release/3.x\n```\n\nThe nth `--ref` belongs to the nth `--repo`. Such a context is built and removed\nas one thing: it reaches `READY` only when every repository in it has been\nchecked out, indexed, given its graph and verified, and one that fails anywhere\nis `FAILED` whole rather than half servable.\n\nThe query tools answer in such a context by taking a `repository` argument, one\nof the ones `list_contexts` reports for it. `search_code` covers every repository\nof the context without it and one of them with it; the other four answer in\nexactly one repository, so they require it and refuse the call with\n`INVALID_ARGUMENT` naming the selectable repositories rather than answering out\nof the first one and leaving the rest silently outside the scope. A `repository`\nthe context does not name is refused too — it selects one of the versions the\nconfiguration granted, it never reaches past them.\n\nEverything lives under `~/.vacmcp` unless `--data-dir` says otherwise, including\nthe search index. Start Zoekt over it, check the installation, and serve:\n\n```bash\nzoekt-webserver -index ~/.vacmcp/zoekt -listen 127.0.0.1:6070 -rpc &\nvacmcp doctor --managed\nvacmcp serve --managed\n```\n\n```text\n$ vacmcp context list\nbackend-v1    backend     8af31e2c8d0a4f1b6e5d3c2a90b7f4e1d6c8a3b5    READY\nbackend-v2    backend     94cb821f7a6e5d4c3b2a1908f7e6d5c4b3a29187    READY\nstack-v1      backend     8af31e2c8d0a4f1b6e5d3c2a90b7f4e1d6c8a3b5    READY\nstack-v1      frontend    1d0c9b8a7f6e5d4c3b2a19087f6e5d4c3b2a1908    READY\n```\n\nOne row per repository a context names, so a context over one reads as it always\nhas and one over several says which commit each of its repositories is pinned to.\n\nThe commands, in full:\n\n| Command | What it does |\n| --- | --- |\n| `vacmcp repo add NAME --url URL` | clone a repository into the data directory |\n| `vacmcp repo list` / `status NAME` | report the managed repositories |\n| `vacmcp repo sync NAME` / `--all` | fetch remote refs, moving no pinned revision |\n| `vacmcp repo remove NAME` | forget a repository and delete its clone |\n| `vacmcp context create NAME --repo REPO --ref REF [--repo REPO --ref REF ...]` | pin one ref per repository to a commit and build its index and graph |\n| `vacmcp context list` / `status NAME` | report the managed contexts and their state |\n| `vacmcp context verify NAME` | re-run the readiness checks, changing nothing |\n| `vacmcp context retry NAME` | rebuild a context that did not reach `READY` |\n| `vacmcp context remove NAME` | forget a context, its worktree and its graph |\n| `vacmcp serve --managed [--data-dir DIR] [--zoekt-url URL] [--cbm-command CMD]` | serve the ready contexts |\n| `vacmcp doctor --managed [--data-dir DIR]` | check the managed repositories and contexts |\n\nOnly these commands reach a network, and only from your shell: creating,\nsyncing and removing repositories and contexts is not exposed as an MCP tool, so\nan agent connected to the server cannot make it clone or delete anything.\nvacmcp holds no credential either — `git` authenticates the clone the way it\nalways does, through your SSH agent, `~/.ssh/config` or a credential helper, and\na URL with a secret embedded in it is refused rather than stored.\n\nA running `vacmcp serve --managed` serves the contexts it read when it started,\nand nothing changes them underneath it. `context create`, `context retry`,\n`context remove` and `repo remove` are refused while one is running rather than\ndone behind its back — a server left answering out of a worktree, an index or a\ngraph that has been deleted is exactly what this project must never do. To put a\nchange into service, stop the server, run the command, and start it again:\n\n```bash\n# stop the running `vacmcp serve --managed`\nvacmcp context create backend-v3 --repo backend --ref release/3.x\nvacmcp serve --managed\n```\n\n`vacmcp repo sync` is not refused and needs no restart: it fetches, and a fetch\nmoves no pinned revision, so a running server keeps answering exactly as it did.\nThe commits it brings in reach a server through the next `context create` and the\nrestart that follows. Reading is never refused either — `context list`, `status`,\n`verify`, `repo list`, `status` and `doctor --managed` all run while a server\ndoes, so nothing stops you diagnosing one.\n\nOn Unix-like systems both of these hold across processes: repository lifecycle\noperations take turns, and a management command really is refused while a\nmanaged server runs.\n\nOn Windows, Managed Mode currently provides in-process locking only, and that\napplies to the server as much as to the commands: `vacmcp serve --managed`\nstarts without holding a cross-process lock on its data directory, so the\nrefusal above does not happen there — a `context` or `repo` command run against\nthe directory it is serving is allowed through and can change the artifacts\nunderneath it. Do not run vacmcp management commands against a data directory\nthat another vacmcp management command, or a running `vacmcp serve --managed`,\nis using. All three commands say so themselves on Windows, on stderr, at the\nmoment they start.\n\nStatic Mode is unaffected.\n\n**Already using `vacmcp serve --config FILE`? Nothing changes for you.** Managed\nmode is additive and entirely optional: static configuration files are still\nsupported exactly as before, there is no migration to do, and the two modes are\nchosen per server with `--config` or `--managed`.\n\n## MCP Configuration\n\nvacmcp speaks MCP `2026-07-28` through the official Go SDK. Streamable HTTP runs\nin stateless mode, which is what the protocol version needs: there is no session\nand no `initialize` handshake, and a client discovers the server with\n`server/discover`. The server advertises tools and nothing else — it sends no log\nnotifications, so it does not claim a capability it does not have.\n\nSTDIO, for a client that runs vacmcp as a subprocess. The shape below is the\ncommon one; use whatever your client's configuration file wants:\n\n```json\n{\n  \"mcpServers\": {\n    \"vacmcp\": {\n      \"command\": \"/path/to/bin/vacmcp\",\n      \"args\": [\"serve\", \"--stdio\", \"--config\", \"/path/to/vacmcp.yaml\"]\n    }\n  }\n}\n```\n\nStreamable HTTP, for a server shared by several clients or run in a container:\n\n```bash\nvacmcp serve --config vacmcp.yaml --address 127.0.0.1:8080\n```\n\nThe listen address is `--address` if you pass it, otherwise `server.address`\nfrom the configuration file, otherwise `127.0.0.1:8080`. The default is loopback\non purpose: this server reads your source, so reaching it from the network is\nsomething you have to ask for.\n\nThe other commands take the same configuration:\n\n```text\nvacmcp validate --config FILE    load and check the configuration\nvacmcp contexts --config FILE    list the configured contexts\nvacmcp doctor   --config FILE    check every dependency and context\nvacmcp version                   print the version\n```\n\n## Context Configuration\n\nA context is the unit of version isolation. It names:\n\n| Field | What it decides |\n| --- | --- |\n| `repository` | which repository, by its key under `repositories` |\n| `branch` | the branch `search_code` is confined to |\n| `revision` | the commit `get_code` reads at, and the version the context claims |\n| `graph_ref` | the codebase-memory-mcp project `trace_calls` runs in |\n\nTwo contexts over one repository is the normal case, and the reason the server\nexists: the same file name means different code in `release/1.x` and\n`release/2.x`, and every call has to say which one it means. `revision` may be\nany revision git resolves, but a commit SHA is what actually pins a version — a\nbranch name moves.\n\n`graph_ref` is a codebase-memory-mcp project, and a project is one indexed\ngraph. Two contexts at different revisions may not share one: that would trace\ncalls against the other version's graph, so the configuration is rejected rather\nthan loaded. The graph reference is internal — it never appears in tool output.\n\nParsing is strict. An unknown field, a duplicate key, a context pointing at a\nrepository that is not declared: each is an error naming the context and the\nfield, not a setting silently ignored. There are no LLM settings of any kind in\nthis file, by design.\n\nCheck a configuration before serving with it:\n\n```bash\nvacmcp validate --config vacmcp.yaml    # vacmcp.yaml: ok, 2 contexts\nvacmcp contexts --config vacmcp.yaml    # id, repository, branch, revision\n```\n\n## Multi-Repo Context\n\nA context can name one repository or several. One naming several is a\n*workspace*: each repository it lists — a *member* — is pinned to its own\nrevision, exactly as a single-repository context pins one. A context over one\nrepository is a workspace with exactly one member, and answers exactly as it\ndid before this feature existed: same arguments, same wire shape, same error\ncodes. Nothing about that case changes; multi-repo is what a context does once\nit names a second repository.\n\n```yaml\ncontexts:\n  stack-v1:\n    members:\n      - repository: example/backend\n        branch: release/1.x\n        revision: 8af31e2\n        graph_ref: backend-v1\n      - repository: example/frontend\n        branch: release/3.x\n        revision: 1a2b3c4\n        graph_ref: frontend-v3\n```\n\n`config/example.yaml` has this alongside the single-repository contexts above\nit, and `vacmcp validate` loads both the same way. Managed Mode builds the same\nshape from the CLI, with one `--repo`/`--ref` pair per member:\n\n```bash\nvacmcp context create stack-v1 --repo backend --ref release/1.x \\\n                               --repo frontend --ref release/3.x\n```\n\n`list_contexts` reports `stack-v1` as `{\"id\": \"stack-v1\", \"members\": [...]}`\ninstead of the flat `repository`/`branch`/`revision` a single-member context\ncarries — the wire shape follows the member count, one member emitting exactly\nwhat v0.4.0 did and several emitting the array instead, never both at once.\nEvery result's `context` block and every citation follow the same rule: a\nsingle-member answer's evidence carries no `repository` or `revision` of its\nown, as before, and a several-member answer's evidence — and `search_code`'s\nmatches — each carry the repository and revision they were found in. Only\n`search_code` and `list_contexts` ever emit the several-member shape; the other\nfour narrow to one member before they answer, so their `context` block is always\nthe flat one, and so is every example in [Tools](#tools) below.\n\n`repository` is the one argument a request may use to say which member it\nmeans, and it only narrows what the context already names — a repository the\ncontext does not have is refused, never reached:\n\n| Tool | One member | Several members |\n| --- | --- | --- |\n| `search_code` | unchanged | searches every member unless `repository` narrows it to one |\n| `get_code` | unchanged | `repository` required — two members could both have the path, so nothing else says which one was meant |\n| `trace_calls` | unchanged | `repository` required — a call graph is one repository's own, so there is no walk without it |\n| `compare_code` / `compare_calls` | unchanged | `repository` required on both sides, and it must name a repository both contexts have |\n| `search_history` | unchanged | searches every member unless `repository` narrows it to one, exactly as `search_code` does |\n\nNothing is inferred even when only one member happens to have a match today:\nthe same request would silently change meaning the day a second member\nacquired the same path or symbol, which is the ambiguity this server always\nrefuses rather than guesses at.\n\n### Limitations\n\nv0.5.0 does not:\n\n- **Build, infer or claim a call edge between two repositories.** `trace_calls`\n  walks the one member's graph `repository` names and nothing else — there is\n  no attempt to connect a caller in one repository to a callee in another.\n- **Compare a whole workspace to another.** `compare_code` and `compare_calls`\n  compare one repository at a time, picked out by `repository` on both sides;\n  there is no member-added/member-removed report and no workspace-level diff.\n- **Deduplicate artifacts two workspaces share.** Two Managed Mode workspaces\n  naming the same repository at the same revision each get their own index and\n  graph.\n- **Detect a rename or move.** Unchanged since v0.4.0: a comparison matches\n  paths literally, in a multi-repo workspace exactly as in a single-repository\n  one.\n- **Resolve symbol identity semantically.** Unchanged since v0.4.0: a\n  comparison matches the symbol name that was asked for, textually, one\n  repository at a time.\n\n## Tools\n\n| Tool | Arguments | Returns |\n| --- | --- | --- |\n| `list_contexts` | none | every configured context: id, and either repository, branch and revision, or a `members` array of them |\n| `search_code` | `context`, `query`, `repository`? | matches as path, line and snippet, from that context's branch only |\n| `trace_calls` | `context`, `symbol`, `direction`, `depth`, `repository`? | call edges as caller, callee, path and line, from that context's graph only |\n| `get_code` | `context`, `path`, `start_line`, `end_line`, `repository`? | those lines as they are at that context's revision |\n| `compare_code` | `from_context`, `to_context`, `path`, `repository`? | what happened to that file between the two revisions — `ADDED`, `REMOVED`, `MODIFIED` or `UNCHANGED` — with the changed regions as hunks |\n| `compare_calls` | `from_context`, `to_context`, `symbol`, `direction`, `depth`, `repository`? | which versions had the symbol, and the call relations added, removed and unchanged between them |\n| `search_history` | `context`, `query`?, `symbol`?, `path`?, `limit`?, `repository`? | the commits reachable from that context's pinned revision, one entry per commit-path occurrence, with author, timestamp and message |\n\n`repository` is marked `?` because it is optional to the schema, not because it\nis always optional: a context naming one repository never needs it, and a context\nnaming several requires it in every tool but `search_code`, which searches all of\nthem without it.\n\n`direction` is `callers` or `callees`; `depth` is 1 to 5. A query is wrapped in\nthe context's `repo:` and `branch:` filters and otherwise passed to Zoekt as\nwritten, so `file:` and `lang:` work as they do there — and `sym:` does too, if\nthe index was built on a machine with universal-ctags, which is what puts\nsymbols into it.\n\nEvery successful result carries the context it was answered in and the evidence\nbacking it:\n\n```json\n{\n  \"content\": \"func Process(req string) string {\\n\\treturn NewHandler(req)\\n}\\n\",\n  \"context\": {\n    \"id\": \"backend-v2\",\n    \"repository\": \"backend\",\n    \"branch\": \"release/2.x\",\n    \"revision\": \"7b05c6aeb7544cc7fd2b69c56c89c9bcfd46a1a2\"\n  },\n  \"end_line\": 6,\n  \"evidence\": [\n    { \"location\": { \"path\": \"processor.go\", \"start_line\": 4, \"end_line\": 6 } }\n  ],\n  \"path\": \"processor.go\",\n  \"start_line\": 4\n}\n```\n\nThat is `get_code` on `processor.go`. The same three lines in `backend-v1` read\n`return LegacyHandler(req)`, with that context's revision on the answer.\n\n`list_contexts` is the exception: its payload is the set of contexts, each\nalready carrying those four fields, and there is no single version to scope it\nto. An unconfigured server answers it with an empty list rather than an error.\n\nA context naming several repositories carries them as a `members` array instead\nof those four fields, in `list_contexts` and in the `context` block of a result\nalike — one entry per repository, each with its own `branch` and `revision`, and\nno `id` of its own, since the context's id names the whole set. Each citation in\n`evidence` then carries the `repository` and `revision` it was found in, and so\ndoes each `search_code` match. The shape follows the number of repositories: a\ncontext naming one answers exactly as it does above, so a client written against\nthat shape keeps reading it.\n\nThe two comparison tools take two contexts instead of one and answer in both, so\nthey carry two of those blocks rather than one merged pair. `from` and `to` each\nhold the version that side was answered in and the evidence backing it, and the\nside of a version that does not have the file or the symbol is `null`. There is\nno combined context or evidence beside them: a citation only means anything at\nthe revision it was read at, so one flattened list could not say which version\nan entry came from.\n\n```json\n{\n  \"from\": {\n    \"context\": {\n      \"id\": \"backend-v1\",\n      \"repository\": \"backend\",\n      \"branch\": \"release/1.x\",\n      \"revision\": \"68c19fdb264bad80cf220f672f89dc90478a9d66\"\n    },\n    \"evidence\": []\n  },\n  \"to\": {\n    \"context\": {\n      \"id\": \"backend-v2\",\n      \"repository\": \"backend\",\n      \"branch\": \"release/2.x\",\n      \"revision\": \"7b05c6aeb7544cc7fd2b69c56c89c9bcfd46a1a2\"\n    },\n    \"evidence\": []\n  },\n  \"change\": \"MODIFIED\",\n  \"path\": \"processor.go\",\n  \"binary\": false,\n  \"hunks\": [\n    {\n      \"old_start\": 2,\n      \"old_lines\": 5,\n      \"new_start\": 2,\n      \"new_lines\": 5,\n      \"lines\": [\n        { \"kind\": \"CONTEXT\", \"content\": \"\" },\n        { \"kind\": \"CONTEXT\", \"content\": \"// Process handles one request.\" },\n        { \"kind\": \"CONTEXT\", \"content\": \"func Process(req string) string {\" },\n        { \"kind\": \"REMOVED\", \"content\": \"\\treturn LegacyHandler(req)\" },\n        { \"kind\": \"ADDED\", \"content\": \"\\treturn NewHandler(req)\" },\n        { \"kind\": \"CONTEXT\", \"content\": \"}\" }\n      ]\n    }\n  ]\n}\n```\n\nThat is `compare_code` on the file `get_code` read above, across the two\ncontexts: the one line that differs between the versions, as one hunk, with the\nlines it covers in each of them. A binary file is marked `binary` and has no\nhunks, which is what tells it apart from a file with nothing to show.\n`compare_calls` answers in the same two sides, with `presence` — `BOTH`,\n`FROM_ONLY` or `TO_ONLY` — saying which versions had the symbol at all, and the\nrelations around it as `added`, `removed` and `unchanged`, each carrying its\ncall sites in each version separately. A relation is caller, callee and path\nwithout the line, so code moving down a file is not reported as one call\ndisappearing and another appearing.\n\n**What a comparison does not do.** Paths and symbol names are matched literally.\nThere is no rename or move detection: a file renamed between the two revisions\nis one path removed and another added rather than one path that moved, and a\nsymbol renamed between versions is not recognised as the same symbol. Each side\nresolves the name that was asked for on its own — `requested_symbol` is what was\nasked for, `from_resolved_symbol` and `to_resolved_symbol` are what each version\nmatched it to — and a version with no such symbol is the absent side rather than\na failure, while a symbol neither version has is `SYMBOL_NOT_FOUND`. Identity\nhere is textual, not semantic or AST-aware, so a comparison is not an impact\nanalysis. Comparison also stays inside one repository: two repositories have no\nshared history, and their call graphs are not two versions of one, so two\ncontexts naming different repositories are `INVALID_ARGUMENT`. In a multi-repo\nworkspace this is `repository`'s job to prevent — it picks one repository on\nboth sides of a comparison, so two repositories are never compared against\neach other there either. `search_code` is the one tool that does span every\nmember of a workspace; see [Multi-Repo Context](#multi-repo-context) and its\n[Limitations](#limitations).\n\nA failure is one shape too, with a code to branch on:\n\n```json\n{ \"error\": { \"code\": \"CONTEXT_NOT_FOUND\", \"message\": \"...\", \"details\": {} } }\n```\n\n| Code | Raised when |\n| --- | --- |\n| `CONTEXT_NOT_FOUND` | the context id is not configured; the server never guesses one |\n| `CONTEXT_AMBIGUOUS` | reserved; contexts are keyed by a unique id, so nothing produces it yet |\n| `REPOSITORY_NOT_FOUND` | the repository a context names cannot be read here |\n| `REVISION_NOT_FOUND` | that repository does not have the revision |\n| `SYMBOL_NOT_FOUND` | the graph has no such symbol |\n| `SYMBOL_AMBIGUOUS` | several symbols match; the candidates come back instead of a guess |\n| `SEARCH_PROVIDER_UNAVAILABLE` | Zoekt cannot be reached or will not serve the query |\n| `GRAPH_PROVIDER_UNAVAILABLE` | codebase-memory-mcp cannot be reached, or the graph is not indexed |\n| `SOURCE_MISMATCH` | the source is not the revision the context declares |\n| `INVALID_ARGUMENT` | an argument is missing or outside its range |\n| `SOURCE_DIFF_UNAVAILABLE` | the source this server has reads one version at a time and cannot compare two |\n\n## How It Works\n\n1. **Resolve.** The context id is looked up in the configuration and its\n   repository and revision are checked against git. An unknown id stops here as\n   `CONTEXT_NOT_FOUND`: no fuzzy match, no default, no \"the only configured one\".\n2. **Scope.** The resolved context — not the caller's arguments — decides where\n   the work happens. Search is sent to Zoekt wrapped in a `repo:` and a `branch:`\n   filter, and a match Zoekt reports under another repository or branch is\n   dropped on the way back. A trace runs against the context's graph, which is\n   the only project name the graph adapter ever passes. Source is read out of\n   git's object database at the declared commit, so no working tree, and no\n   `git checkout`, comes into it.\n3. **Fail closed.** Where content cannot be served from the declared revision,\n   the call fails with `SOURCE_MISMATCH` carrying both revisions. There is no\n   warning-level variant and no way to continue past it.\n4. **Cite.** The context and the evidence are attached to the result by the type\n   that builds it, so a bare answer is not representable.\n\nSearch and graph degrade separately. `search_code` never touches\ncodebase-memory-mcp, so an absent or unindexed graph engine leaves search\nworking.\n\ncodebase-memory-mcp is started once and kept as a child process: the server\nholds an MCP session on it instead of running the binary again for every query,\nwhich is the difference between about 8.6 seconds and about 50 milliseconds per\n`trace_calls`. The graph project still travels with every single query, so no\nscope is inherited from the connection. Where that process cannot be started —\nan older build, a sandbox that will not allow it — queries fall back to\n`codebase-memory-mcp cli`, one process per call: slower, and answering exactly\nthe same.\n\n## Version Correctness\n\nThe test suite generates a demo repository whose `release/v1` `Process()` calls\n`LegacyHandler()` and whose `release/v2` `Process()` calls `NewHandler()`, and\nruns four checks against it through the real engines:\n\n| Check | Expected |\n| --- | --- |\n| `search_code` `NewHandler` in v1 | 0 matches |\n| `search_code` `NewHandler` in v2 | at least 1 match |\n| `trace_calls` `Process` in v1 | reaches `LegacyHandler` |\n| `trace_calls` `Process` in v2 | reaches `NewHandler` |\n\nThey run in CI on every pull request, on every push to `main`, and again before\nany release is published (`integration/release_gate_test.go`). Any cross-version\ncontamination fails the build; this is a release blocker rather than a test that\ncan be marked flaky.\n\n## Embedding Guide\n\nThe seven tools are a thin MCP layer over a Go package you can call directly.\n`engine.Engine` answers all seven queries with no server, no transport and no\nwire schema in the way, so a gateway of your own holds the same version\nisolation this server does — and can be tested without a server in front of it.\n\n```go\ncfg, err := config.Load(\"vacmcp.yaml\")\nif err != nil {\n    return err\n}\n\neng := engine.New(resolver.New(cfg), zoekt.New(cfg), cbm.New(cfg), git.New(cfg))\n\nresult, err := eng.SearchCode(ctx, engine.SearchCodeRequest{\n    Context: \"backend-v2\",\n    Query:   \"NewHandler\",\n})\nif err != nil {\n    return errors.Join(err, eng.Close())\n}\n\nresult.Context()   // the vacctx.Workspace it was answered in — one member here\nresult.Evidence()  // where the answer can be checked, one list per member\nresult.Matches()   // the payload\n```\n\n`engine.New(contexts, search, graph, source)` starts nothing, so it cannot fail.\nAny of the three providers may be nil: the query needing an absent one fails and\nthe others are unaffected, so an engine built with a search provider and no\ngraph still searches. `SearchCode` and `TraceCalls` name what is missing —\n`SEARCH_PROVIDER_UNAVAILABLE` and `GRAPH_PROVIDER_UNAVAILABLE`. `GetCode` with\nno source provider fails with `REPOSITORY_NOT_FOUND` instead: the code set has\nno source equivalent, and adding one would change the public tool API.\n`CompareCalls` names an absent graph provider exactly as `TraceCalls` does, and\n`CompareCode` with no source provider fails exactly as `GetCode` does. A source\nprovider that is there but cannot compare two versions is a different answer\nagain, `SOURCE_DIFF_UNAVAILABLE`, and the next section is where that capability\nis implemented. `SearchHistory` follows the same pattern once more: a source\nprovider that cannot walk history fails with `SOURCE_HISTORY_UNAVAILABLE`.\n\n### Commit history\n\n`SearchHistory` answers what changed in a version, and the version is the whole\nscope of it:\n\n```go\nresult, err := eng.SearchHistory(ctx, engine.SearchHistoryRequest{\n    Context: \"backend-v1\",\n    Query:   \"timeout\",           // commit message contains this\n    Symbol:  \"ResetWatchdog\",     // git pickaxe: this string's count changed\n    Path:    \"src/watchdog.c\",    // only commits touching this path\n    Limit:   20,\n})\nfor _, c := range result.Commits() {\n    fmt.Println(c.Repository, c.Commit, c.Path, c.Timestamp, c.Message)\n}\n```\n\n**Version scope.** The walk starts at the commit the context pins, not at HEAD\nand not at the default branch. A context pinned to an older commit does not see\nthe commits made after it, whatever the worktree is currently checked out to.\nThis is what makes the answer version-aware rather than merely repository-aware,\nand it is why the same query against two contexts gives two answers.\n\n**Multi-repo.** History is search-like: with no `Repository` it spans every\nrepository the context names, and each result carries the repository it came\nfrom. With one, it answers in that repository alone; a repository the context\ndoes not name is `INVALID_ARGUMENT` rather than a guess. If any searched member\ncannot answer, the whole request fails — a history that quietly omitted one\nrepository would look like a complete answer for the context.\n\n**Filters.** `Query` is a literal, case-insensitive substring test of the commit\nmessage: no regular expression, no ranking, no relevance model. `Symbol` is\ngit's `-S` pickaxe, which selects the commits where the number of occurrences of\nthat exact string changed — it is string history, not symbol resolution, so it\ndoes not parse the language, does not know two spellings name one function, and\ndoes not follow a rename. It is deliberately not `-G`: a commit that edits the\nline a symbol sits on without adding or removing an occurrence of it has not\nchanged the count and is not reported. `Path` restricts both the walk and the\nreported entries to that path. All three combine with AND, and a filter matching\nnothing yields an empty result rather than being dropped.\n\n**Results.** A `Commit` is one commit-path occurrence. A commit that touched\nseveral paths produces one entry per path, so a multi-file commit keeps every\nfile's provenance rather than being attributed to an arbitrary one; asking for a\n`Path` reports only that path. Entries are ordered by member, then git's own\nnewest-first order, then the paths of a commit as git lists them, and `Limit` is\napplied to that order so a capped answer is the prefix of the uncapped one. A\n`Limit` of 0 means the provider's own bound rather than \"everything\"; a negative\none is refused. Commit ids are always the full 40 characters, and timestamps are\nRFC3339 in UTC.\n\n**Read-only.** History never fetches, syncs, re-indexes, changes a context or\nchecks a revision out. It reads the local object database.\n\nA request names its scope with context ids: no request type has a branch or\nrevision field, and the `Repository` field some of them have —\n`SearchCodeRequest.Repository`, and its equivalent on `TraceCallsRequest`,\n`GetCodeRequest`, `CompareCodeRequest`, `CompareCallsRequest` and\n`SearchHistoryRequest` — narrows a\nquery to one of the repositories its context already names rather than\nreaching outside them; a context naming one repository never needs it, and a\ncontext naming several has no query without it except `SearchCode` and\n`SearchHistory`, which span every member unless narrowed. Every successful result carries the\nversion it was answered in together with its evidence, because the result\ntypes have no exported fields and only a method on the engine builds one. The\nversion guarantee is inherited by embedding rather than re-implemented.\n\n**Lifecycle.** `Close` releases the dependencies that can be released: one\nimplementing `io.Closer` is closed, one that does not is left untouched, and\nevery dependency gets its turn whether or not an earlier one failed. That\nfeature detection *is* the ownership contract — handing over a provider that can\nbe closed is what says \"close this one for me\". A provider you keep owning — one\nCBM session shared between two engines, say — is handed over without a `Close`\nmethod, or wrapped in a type that does not promote one:\n\n```go\ntype keepOpen struct{ provider.GraphProvider }\n```\n\nBefore `New` returns, cleanup is the caller's: a program that starts a CBM\nsubprocess and then fails while building the next provider must close what it\nalready started, because there is no Engine yet to do it. From then until\n`Close`, it is the Engine's. `engine.Engine.Close`'s doc comment is the\nauthority on both.\n\n`examples/embed` is a complete program doing all of the above. It runs with no\nZoekt, no codebase-memory-mcp and no checkout — its providers are stand-ins — so\n`go run ./examples/embed` prints two versions' answers straight away.\n\n**Supported embedding API.** `engine`, `provider`, `vacctx`, `vacerr`,\n`evidence`, `config`, `resolver`, `adapters/*` and `managed` are supported for\nembedding: documented, tested, and meant to be called from outside this\nrepository. Supported is not a permanent backward-compatibility guarantee — that\nis v1.0.0's, in the [Roadmap](#roadmap) below. Until then a breaking change to\nthese packages is possible and arrives with a release note, not silently.\nAnything under `internal/` is not part of this and may change in any release.\n\n## Custom Provider Guide\n\nFour interfaces are the whole required extension surface. Implement them and the\nengine answers out of your backend — an SCIP index, another graph service,\nsomething in-house — with no fork of anything here. `SearchProvider`,\n`GraphProvider` and `SourceProvider` are each handed one member's version scope\n(`vacctx.CodeContext`) alongside a cancellation `context.Context`, whether the\ncontext it came from names one repository or several: an implementation that\nignores the scope answers from the wrong version, which is the one thing this\nproject is for. `ContextSource` is where those scopes come from rather than\nsomething handed one, and answers with a `vacctx.Workspace` — the set of\nrepositories a context id names, one member for a single-repository context and\nseveral for a multi-repo one, with no separate method or type for either.\n`Contexts(ctx)` lists the workspaces that exist, and `Resolve(ctx, id)` turns\nan id into the one it names; both take a `context.Context` now, so an\nimplementation reaching a backend of its own to answer either has somewhere to\ncarry cancellation and a deadline.\n\n| Interface | Package | Responsible for |\n| --- | --- | --- |\n| `ContextSource` | `engine` | which workspaces exist, and resolving an id to one |\n| `SearchProvider` | `provider` | matches confined to one member's repository and branch |\n| `GraphProvider` | `provider` | a traversal of the graph one member's `graph_ref` names, and no other |\n| `SourceProvider` | `provider` | the lines of a file as they are at one member's revision |\n\n```go\ntype ContextSource interface {\n    Contexts(ctx context.Context) ([]vacctx.Workspace, error)\n    Resolve(ctx context.Context, id string) (vacctx.Workspace, error)\n}\ntype SearchProvider interface {\n    Search(ctx context.Context, codeCtx vacctx.CodeContext, query SearchQuery) ([]SearchResult, error)\n}\ntype GraphProvider interface {\n    TraceCalls(ctx context.Context, codeCtx vacctx.CodeContext, req TraceRequest) (*CallGraph, error)\n}\ntype SourceProvider interface {\n    Read(ctx context.Context, codeCtx vacctx.CodeContext, path string, start, end int) (*SourceContent, error)\n}\n```\n\n`ContextSource` is a compile-breaking change from the interface v0.4.0 shipped:\nboth methods returned `vacctx.CodeContext` then, and `Contexts` took no\nargument. A single-repository implementation needs no new logic to catch up —\nwrap each existing `vacctx.CodeContext` in a one-member\n`vacctx.Workspace{ID: id, Members: []vacctx.CodeContext{c}}` and add the\n`context.Context` parameter — but every implementation has to be touched\nbefore it compiles again, which is deliberate: the alternative, adding a\n`Members` field to `CodeContext` instead of changing the return type, would\nlet an unfixed implementation keep compiling while its `Repository` field\nsilently stayed empty — a wrong version served rather than a build caught\nbefore it ships.\n\n**Optional: comparing two versions.** A `SourceProvider` may additionally\nimplement `provider.SourceDiffer`, and that is what `compare_code` is answered\nby. It is a capability of its own rather than a fifth method on `SourceProvider`\nbecause not every source backend has a second revision to compare against, and a\nbackend that cannot compare should be a type assertion that fails rather than a\n`Read`-shaped promise returning an apology:\n\n```go\ntype SourceDiffer interface {\n    Diff(ctx context.Context, from, to vacctx.CodeContext, req SourceDiffRequest) (*SourceDiff, error)\n}\n```\n\nThe two contexts are the entire scope of the comparison, exactly as the single\none is for `Read`, and the answer is structured — added, removed, modified or\nunchanged, with the changed regions as hunks — never diff text for someone else\nto parse. A `SourceProvider` that does not implement it is not a broken one:\nevery other query is unaffected, `compare_calls` included, since that walks each\nversion's graph through the `GraphProvider` you already wrote. `compare_code` is\nthen the only thing that fails, with `SOURCE_DIFF_UNAVAILABLE` — a fact about\nthis server's capability rather than about the file, the revisions or the\nrepository, so the tool is still there and still says exactly why it cannot\nanswer. `adapters/git` implements it, over the same repository it reads.\n\nTwo rules are yours to hold rather than the engine's to enforce. A\n`SourceProvider` must fail closed: content that is not the revision the context\ndeclares is a `vacerr` `SOURCE_MISMATCH` and never a warning or a best effort.\nAnd a `ContextSource` refuses an id it does not hold with `CONTEXT_NOT_FOUND` —\nthe engine re-checks that a resolved context has all four of its fields, but it\ncannot check that the version you returned is the version that was asked for.\n\nValidating what a caller sent is yours as well. Those four fields of the\nresolved context are the whole of what the engine checks: the `path` handed to\n`SourceProvider.Read` and the query handed to `SearchProvider.Search` reach you\nexactly as the caller wrote them. A provider serving files from a checkout is\ntherefore the one that has to refuse a path traversing out of it, and a provider\nbuilding a backend query out of one is the one that has to make it safe.\n\n`engine/extension_test.go`'s\n`TestEngineRunsOnImplementationsOfNothingButItsInterfaces` is a tested\nimplementation of all four from scratch: a map, and three types returning canned\nanswers, reaching all four queries with no Zoekt, no codebase-memory-mcp and no\ngit anywhere in it.\n\nv0.1.0's own implementations are these same interfaces — `adapters/` for Zoekt,\ncodebase-memory-mcp and git, `resolver/` for the configuration file — so another\nengine is a fourth adapter, not a fork.\n\n## Security\n\n**This project does not upload source code to an LLM.** It does no reasoning of\nits own: it serves search, graph, source, version and evidence to whichever\nagent your MCP client runs.\n\n- **No LLM API key.** There is no model configuration in this project, and the\n  configuration file rejects one.\n- **No embeddings.** Nothing is vectorised, and there is no vector store.\n- **No automatic upload.** vacmcp does not send your code anywhere on its own.\n- **Local engines.** Zoekt is a web server you run, codebase-memory-mcp is a\n  binary vacmcp runs as a subprocess, and git repositories are read from local\n  paths. The default listen address is loopback.\n- **Read only.** Repositories are read, never written. Source is read out of the\n  object database, so no branch is checked out and no working tree is touched.\n- **Your MCP client is yours.** You choose it, you trust it, and it is outside\n  this project's control.\n\nThat last point is the boundary of what this project can promise. Where your\ncode goes after a tool answers is decided by the client you connected, and by\nwhere you chose to run this server: point it at a remote client and the results\ntravel there. What is stated here is what vacmcp itself does and does not do.\n\nVulnerability reports go to the address in [SECURITY.md](SECURITY.md).\n\n## Roadmap\n\nv0.1.0 is the first four tools above, one context per call, with the version\nisolation and evidence they are built around. v0.2.0 is the managed lifecycle\nthat puts a repository behind them without anyone assembling one by hand. v0.3.0\nis the same query plane as a Go package, so a program of your own can call it\ndirectly instead of running this server and talking MCP to it. v0.4.0 is the\nother two tools, the first queries that name two versions and answer in both.\nv0.5.0 is multi-repo contexts: one context id can name several repositories, a\nsearch spans all of them, and the graph and the comparisons stay scoped to one\nrepository at a time — see [Multi-Repo Context](#multi-repo-context). v0.6.0 is\nversion-scoped commit history: `search_history` walks from the commit a context\npins rather than from HEAD or the default branch, so a context pinned to an older\ncommit does not see the commits made after it. v0.7.0 is operational\ncorrectness: every query-plane operation carries a budget it reports as\n`OPERATION_TIMEOUT`, shutdown and cancellation are honoured throughout, and the\nsupported codebase-memory-mcp floor moves to 0.11.0. v0.8.0 makes those budgets\nadjustable by a Go embedder, because a fixture cannot know how large somebody\nelse's history is. Ahead of those:\n\n| Version | Direction |\n| --- | --- |\n| v0.1.0 | version-aware query plane: the four tools, context isolation, evidence — done |\n| v0.2.0 | managed repository and context lifecycle: `repo` and `context` commands, automatic Zoekt and graph provisioning, readiness verification — done |\n| v0.3.0 | embeddable core / extension API: a supported, documented Go API to embed this core in your own gateway, not yet under a compatibility guarantee — done |\n| v0.4.0 | version intelligence: `compare_code`, `compare_calls`, revision and graph diff within one repository — done |\n| v0.5.0 | multi-repo contexts: a workspace of several repositories under one context id, search spanning every member, and multi-repo graph query — each member keeps its own revision-scoped graph, so there is no cross-repository call edge — done |\n| v0.6.0 | version-scoped commit history: `search_history` over the revision a context pins, one entry per commit-path occurrence, spanning a workspace or narrowed to one member — done |\n| v0.7.0 | operational correctness: per-operation `OPERATION_TIMEOUT` budgets on the query plane, signal-honouring `serve`, `repo` and `context`, HTTP drain on shutdown, and codebase-memory-mcp 0.11.0 as the supported floor — done |\n| v0.8.0 | embedder-adjustable operation budgets: variadic options on the git, Zoekt, codebase-memory-mcp and resolver constructors, defaults unchanged — done |\n| unscheduled | operations: metrics, OpenTelemetry, garbage collection, scheduled sync primitives |\n| v1.0.0 | stable public API and compatibility contract |\n\nThe operations row carries no version number on purpose. It was written here as\nv0.6.0 and that number went to commit history instead; which release it lands in\nis not decided, and guessing one again would only make this table wrong a second\ntime. v1.0.0 keeps its number because what it names is a semver commitment rather\nthan a scheduling one.\n\nDeliberately out of scope, at every version: chat UI, RAG, vector databases,\nembeddings, issue trackers, documentation search, forge-specific APIs, agent\nmemory, code modification, PR review. This server supplies evidence; the\nreasoning belongs to your agent.\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md).\n\n## License\n\nApache License 2.0. See [LICENSE](LICENSE) and\n[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).\n"}