MCP setup
MCP setup
Section titled “MCP setup”Use this guide to connect handover’s MCP server to Claude Desktop, Cursor, or VS Code.
Quickstart
Section titled “Quickstart”- Generate docs and search index in your project:
handover generatehandover reindex- Confirm the MCP server starts cleanly:
handover serveExpected stderr output includes MCP server listening on stdio.
-
Add one of the client configs below.
-
Restart the client and run the verification checklist at the end of this document.
Claude Desktop
Section titled “Claude Desktop”Add this server under mcpServers in Claude Desktop config.
{ "mcpServers": { "handover": { "command": "handover", "args": ["serve"], "cwd": "/absolute/path/to/your/project" } }}- macOS config path:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows config path:
%APPDATA%\\Claude\\claude_desktop_config.json
Cursor
Section titled “Cursor”Add this server in Cursor MCP settings.
{ "mcpServers": { "handover": { "command": "handover", "args": ["serve"], "cwd": "/absolute/path/to/your/project" } }}If your environment does not include global npm binaries, use npx:
{ "mcpServers": { "handover": { "command": "npx", "args": ["-y", "handover-cli", "serve"], "cwd": "/absolute/path/to/your/project" } }}VS Code
Section titled “VS Code”Add this to VS Code MCP server configuration.
{ "servers": { "handover": { "type": "stdio", "command": "handover", "args": ["serve"], "cwd": "/absolute/path/to/your/project" } }}If needed, swap command/args to the same npx variant shown in the Cursor section.
Remote regeneration workflow
Section titled “Remote regeneration workflow”Use these MCP tool calls to trigger and monitor regeneration jobs from remote clients.
1) Trigger regeneration with regenerate_docs
Section titled “1) Trigger regeneration with regenerate_docs”Default behavior (no target) regenerates full project docs and search index.
{ "name": "regenerate_docs", "arguments": {}}Named target examples:
{ "name": "regenerate_docs", "arguments": { "target": "docs" }}{ "name": "regenerate_docs", "arguments": { "target": "search-index" }}Supported targets:
full-project(default): rungenerate, thenreindexdocs: rungenerateonlysearch-index: runreindexonly
Successful trigger shape:
{ "ok": true, "jobId": "81f5c6d5-4de1-4b72-96be-5c3e5ae22d6b", "state": "queued", "target": { "key": "full-project", "requested": "full-project", "canonical": "full-project" }, "createdAt": "2026-02-24T19:00:00.000Z", "dedupe": { "joined": false, "key": "full-project", "reason": "none" }, "next": { "tool": "regenerate_docs_status", "message": "Poll regenerate_docs_status with this job ID until the job reaches a terminal state.", "pollAfterMs": 750 }}If a second request hits the same in-flight target, you get the same jobId with dedupe join signaling:
{ "ok": true, "jobId": "81f5c6d5-4de1-4b72-96be-5c3e5ae22d6b", "dedupe": { "joined": true, "key": "full-project", "reason": "in_flight_target" }}2) Poll status with regenerate_docs_status
Section titled “2) Poll status with regenerate_docs_status”{ "name": "regenerate_docs_status", "arguments": { "jobId": "81f5c6d5-4de1-4b72-96be-5c3e5ae22d6b" }}Status response includes deterministic lifecycle state and progress summary:
{ "ok": true, "jobId": "81f5c6d5-4de1-4b72-96be-5c3e5ae22d6b", "state": "running", "lifecycle": { "stage": "running", "progressPercent": 50, "summary": "Regeneration is actively running for the requested target." }, "next": { "tool": "regenerate_docs_status", "message": "Poll regenerate_docs_status with this job ID until the job reaches a terminal state.", "pollAfterMs": 750 }}Keep polling until terminal state completed or failed.
3) Remediation examples
Section titled “3) Remediation examples”Unknown target:
{ "ok": false, "error": { "code": "REGENERATION_TARGET_UNKNOWN", "message": "Unknown regeneration target: everything", "action": "Use one of: full-project, docs, search-index." }}Failed job:
{ "ok": false, "error": { "code": "REGENERATION_GENERATE_FAILED", "message": "Regeneration subcommand failed: generate", "action": "Resolve the generate failure and retry regenerate_docs." }}HTTP transport mode
Section titled “HTTP transport mode”handover serve uses stdio by default, but you can run Streamable HTTP when you want to connect over a networked endpoint.
Start HTTP mode:
# Start with HTTP transporthandover serve --transport http
# Customize port and hosthandover serve --transport http --port 8080handover serve --transport http --host 0.0.0.0 --port 8080Or configure .handover.yml:
serve: transport: http http: port: 3000 host: 127.0.0.1 path: /mcpWhen HTTP mode starts, stderr includes endpoint discovery details:
MCP server listening over HTTP.Transport: httpBase URL: http://127.0.0.1:3000MCP path: /mcpEndpoint: http://127.0.0.1:3000/mcpReady: POST/GET/DELETE requests accepted at MCP endpoint.Use any MCP client that supports Streamable HTTP and point it to http://127.0.0.1:3000/mcp (or your configured host/port/path).
HTTP and stdio expose the same tools, resources, and prompts.
For the current run, CLI flags override config values from .handover.yml (for example, --transport and --port).
HTTP security configuration
Section titled “HTTP security configuration”Configure these controls when running handover serve --transport http.
CORS origin policy
Section titled “CORS origin policy”Cross-origin requests are denied by default. Requests without an Origin header (such as same-origin or non-browser requests) continue normally.
Set an explicit allowlist in .handover.yml:
serve: transport: http http: allowedOrigins: - https://example.com - https://app.example.comFor local development, you can override allowed origins for the current run:
handover serve --transport http --allow-origin http://localhost:5173--allow-originis repeatable and replaces (does not merge with)serve.http.allowedOriginsfor that run.- Wildcard mode is development-only and logs a startup warning:
serve.http.allowedOrigins: ['*'].
Disallowed origins receive this shape:
{ "ok": false, "error": { "code": "MCP_HTTP_ORIGIN_REJECTED", "message": "Cross-origin request from 'https://evil.example' is not allowed.", "action": "Add 'https://evil.example' to serve.http.allowedOrigins in .handover.yml, or set serve.http.allowedOrigins: ['*'] for development." }}Authentication
Section titled “Authentication”Enable bearer token auth with either HANDOVER_AUTH_TOKEN or serve.http.auth.token in config.
Environment variable example:
HANDOVER_AUTH_TOKEN=mysecret handover serve --transport httpConfig file example:
serve: transport: http http: auth: token: mysecretHANDOVER_AUTH_TOKENtakes precedence overserve.http.auth.token.- Auth is required when binding to non-loopback addresses.
- Auth is optional (but still recommended) for localhost.
Missing or invalid tokens receive this shape:
{ "ok": false, "error": { "code": "MCP_HTTP_UNAUTHORIZED", "message": "Missing Authorization header.", "action": "Include an Authorization: Bearer <token> header. Set the token via HANDOVER_AUTH_TOKEN env var or serve.http.auth.token in .handover.yml." }}Bind safety
Section titled “Bind safety”The default bind host is 127.0.0.1 (localhost-only access).
When you bind to 0.0.0.0 or another non-loopback host, auth must be configured. Startup is refused without auth:
{ "ok": false, "error": { "code": "MCP_HTTP_AUTH_REQUIRED", "message": "HTTP server cannot start on '0.0.0.0' without authentication configured.", "action": "Set the HANDOVER_AUTH_TOKEN environment variable, or add 'serve.http.auth.token' to .handover.yml." }}With auth configured on a non-loopback host, startup continues and stderr includes:
Warning: HTTP endpoint is network-accessible (binding to 0.0.0.0).Warning: Ensure HANDOVER_AUTH_TOKEN and serve.http.allowedOrigins are configured.Troubleshooting
Section titled “Troubleshooting”| Symptom | Likely cause | Fix |
|---|---|---|
| Client cannot connect to server | handover command not on PATH | Use absolute command path or npx -y handover-cli serve |
| Server exits with missing docs/index error | handover generate or handover reindex not run in this project | Run both commands in project root, then reconnect |
| MCP protocol error or malformed JSON | Non-MCP stdout output from wrappers/scripts | Run handover serve directly; do not wrap with shell scripts that print to stdout |
semantic_search returns error with code SEARCH_INVALID_INPUT | Invalid tool args (empty query, non-numeric limit, invalid type list) | Send query as non-empty string, limit as integer 1-50, types as string array |
semantic_search returns SEARCH_INDEX_MISSING or SEARCH_INDEX_EMPTY | Search index database missing or empty | Run handover reindex and retry |
regenerate_docs returns REGENERATION_TARGET_UNKNOWN | Unknown target passed to tool call | Retry with one of full-project, docs, or search-index |
regenerate_docs_status returns JOB_NOT_FOUND | Unknown or expired job reference | Trigger a new run with regenerate_docs and poll the returned jobId |
Regeneration status reaches failed | Generate/reindex subcommand failed in executor | Follow error.action, fix root issue, then call regenerate_docs again |
Origin rejected (403 MCP_HTTP_ORIGIN_REJECTED) | Request origin is not allowlisted | Add origin to serve.http.allowedOrigins or use --allow-origin for that run |
Auth failed (401 MCP_HTTP_UNAUTHORIZED) | Missing/invalid bearer token | Check HANDOVER_AUTH_TOKEN or serve.http.auth.token |
Server refuses to start (MCP_HTTP_AUTH_REQUIRED) | Non-loopback bind without configured auth | Set auth token before binding to non-loopback host |
| CORS preflight fails in browser | Browser origin not included in allowlist | Ensure the browser origin is in serve.http.allowedOrigins |
Verification checklist
Section titled “Verification checklist”- Client shows
handoverserver as connected. - Resource listing includes
handover://docs/*andhandover://analysis/*entries. - Run
semantic_searchwith{ "query": "architecture" }and confirm a successful response shape. - Confirm each result includes
relevance,source,section, andsnippet. - Run a no-match query and confirm success with
results: [](not a tool failure). - Run
regenerate_docsand verify response includesjobId,state, anddedupefields. - Poll
regenerate_docs_statuswith thatjobIduntilstateiscompletedorfailed.