Self-host
Deploy search-mcp on your Cloudflare account
You run every part of search-mcp on your own account: the R2 bucket, the AI Search instance, and both Workers. Replace every example.com and my-* name below with your own.
1. Before you start
- A Cloudflare account with Workers, R2 and AI Search turned on.
wrangler, logged in:npx wrangler login.- A clone of search-mcp at the release tag. See Install from source.
- The git repos you want to index, cloned locally, or reachable by the sync runner.
2. R2 bucket
npx wrangler r2 bucket create my-search-corpus
Create an R2 API token with Object Read and Write on this bucket only. Export it for the sync:
export R2_ACCESS_KEY_ID=...
export R2_SECRET_ACCESS_KEY=...
export CLOUDFLARE_ACCOUNT_ID=...
3. AI Search instance
npx wrangler ai-search create my-ai-search-instance --type r2 --source my-search-corpus
Keep the instance name. It goes in both wrangler files and in targets.json.
4. Sync the corpus
Edit scripts/targets.json: set repos, bucket and instance. See targets.json.
By hand, from local clones
Clone the repos next to search-mcp, or set SYNC_REPO_ROOT. Then:
npm run sync:dry
npm run sync
npx wrangler ai-search jobs create my-ai-search-instance
Automated, in CI or on a schedule
The sync runner uses its own clone directory (CORPUS_ROOT). It clones or fetches each repo, syncs, then starts a reindex:
export CORPUS_GIT_ORG=your-github-org
export GITHUB_TOKEN=... # read access to the source repos
npm run sync:run
To sync when a source repo merges, copy docs/notify-corpus-sync.snippet.yml into that repo's CI. It sends a repository_dispatch event.
5. Query Worker
cp wrangler.toml.example wrangler.toml
Set instance_name, ALLOWED_ORIGINS and, if you want, ASSISTANT_SYSTEM_PROMPT. Add a route, or use the workers.dev URL. All settings are on the configuration page.
npm run deploy
curl "https://YOUR_QUERY_HOST/health"
For a public /ask, turn on Turnstile.
6. MCP Worker
cp wrangler.mcp.toml.example wrangler.mcp.toml
umask 077 && openssl rand -hex 32 > /tmp/mcp-token
wrangler secret put MCP_TOKEN -c wrangler.mcp.toml < /tmp/mcp-token
rm /tmp/mcp-token
npm run deploy:mcp
Bind the same R2 bucket the sync uploads to as CORPUS, so that get_file and corpus_status work. Then connect a client.
7. Separate instances
You can run the query Worker and the MCP Worker against different AI Search instances, so that browser answers and agent retrieval use different corpora. Use two R2 buckets, two instances, and a different instance_name in each wrangler file.
To add another public query Worker, share src/index.ts with its own wrangler file:
- Create a bucket and an AI Search instance.
- Add a target in
targets.json. Use a nestedincludePathsif the corpus boundary matters. - Copy a wrangler file as a template, and set
instance_name, the origins and the route. - Deploy it with
wrangler deploy -c your.toml.
Service binding RPC
POST /ask is the browser route. Since v0.7.0, another Worker on the same account can get retrieval chunks without that route. It binds to the named entrypoint SearchEntrypoint, which the query Worker exports from src/index.ts. The MCP Worker does not export it.
The entrypoint is not a URL: internet requests still reach only the default export. The caller selects the class in its own wrangler file:
[[services]]
binding = "SEARCH_RPC"
service = "your-query-worker-name"
entrypoint = "SearchEntrypoint"
await env.SEARCH_RPC.search(question) returns { chunks } from the same SEARCH binding that /ask uses, with the same retrieval options (hybrid, 6 results, query rewrite on). It generates no text. The question must be a non-empty string of at most 2000 characters after trim.
A bad call throws an Error with one of these messages: empty_question, question_too_long, rate_limited, search_unavailable. ASK_LIMITER still applies, with the key ask:rpc for every binding caller. There is no origin check and no Turnstile check, because only a Worker whose config names this entrypoint gets a stub.
Troubleshooting
| Symptom | Check |
|---|---|
| Empty search results | Are the R2 objects there? Run npx wrangler ai-search stats INSTANCE. |
The sync exits 2 with include_paths_* | An allowlist matched nothing, named an unknown repo, or was emptied by excludePaths. The message names the repo and the prefix. Fix targets.json and run again. See refusals. |
| TypeScript is not indexed | The sync renames it to .txt. Run the sync and a reindex job again. |
| The reindex fails after a good R2 upload | Cooldown 7020 or connect error 7017. The sync runner retries both. Wait, or run the reindex again. |
| MCP answers 401 | Is MCP_TOKEN set? Does the bearer header match it exactly? |
/ask answers 403 for the origin | ALLOWED_ORIGINS must include the page origin. |
/ask answers 403 for Turnstile | TURNSTILE_SECRET must be set, and the widget site key must match it. |
The source for this page is docs/DEPLOY.md at search-mcp v0.7.0. Report a problem in the issue tracker.