search-mcp
Provides a search tool for agents and a CORS-enabled query endpoint for browsers, both backed by Cloudflare AI Search, allowing semantic search over corpora stored in R2 buckets.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@search-mcpsearch for 'rate limit' in the repository"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
search-mcp
Open-source toolkit for Cloudflare AI Search:
MCP Worker (
src/mcp.ts) -- bearer-gated Streamable-HTTP MCP with asearchtool for agents.Query Worker (
src/index.ts) -- CORS + Turnstile + rate-limitedPOST /askthat streams answers for a browser widget.Corpus sync (
scripts/sync.mjs,scripts/sync-runner.mjs) -- git-tracked sources to R2, with extension remapping so TypeScript, Dockerfiles, and other text AI Search would otherwise skip get indexed.
git repos -> sync.mjs -> R2 bucket -> AI Search instance -> /ask + /mcpInstall (npm)
The corpus sync CLIs and ask-widget assets ship on npm as @skyphusion/search-mcp (the unscoped name search-mcp is taken by another project).
npm install @skyphusion/search-mcp
# or run without installing:
npx --package=@skyphusion/search-mcp search-mcp-sync corpus --dry-runCommand | Role |
| Upload git-tracked corpus files to R2 for one target |
| Clone/fetch repos, sync all targets, optional reindex |
Put targets.json in your project root (copy from node_modules/@skyphusion/search-mcp/scripts/targets.json.example) or set SEARCH_MCP_TARGETS. Clone roots default to the current working directory; override with SYNC_REPO_ROOT.
Widget assets after install:
cp node_modules/@skyphusion/search-mcp/public/ask-widget.{js,css} ./docs/Workers (src/) deploy from a git clone; see docs/DEPLOY.md.
Related MCP server: github-mcp
Quick start (from source)
npm install
cp wrangler.toml.example wrangler.toml
cp wrangler.mcp.toml.example wrangler.mcp.toml
cp scripts/targets.json.example scripts/targets.json
# edit the three files for your account, instance, bucket, and repos
npm run typecheck
npm testProvision R2 + AI Search, sync your corpus, deploy both Workers. Step-by-step: docs/DEPLOY.md.
Skyphusion production
This repo is the production home for Skyphusion AI Search (search.vivijure.com,
search-internal.vivijure.com). Config is materialized from GitHub Actions secrets at deploy/sync
time; do not commit wrangler.toml, wrangler.mcp.toml, or scripts/targets.json.
Operator runbook -- secrets, topology, bootstrap
Cutover record (2026-07-08) -- migration history, failure log, archive steps
Workers
Worker | Entry | Endpoint | Auth |
Query |
|
| Turnstile (optional) + CORS allowlist |
MCP |
|
|
|
Deploy separately so browser traffic and agent traffic can bind different AI Search instances if you want.
npm run deploy # query Worker
npm run deploy:mcp # MCP Worker
wrangler secret put MCP_TOKEN -c wrangler.mcp.toml
wrangler secret put TURNSTILE_SECRET # optional; skips verification when unsetMCP client wiring
{
"mcpServers": {
"search-mcp": {
"type": "http",
"url": "https://YOUR_MCP_HOST/mcp",
"headers": { "Authorization": "Bearer YOUR_TOKEN" }
}
}
}MCP_TOKEN accepts a single token or comma-separated name=token pairs for per-consumer attribution in logs.
Corpus sync
export R2_ACCESS_KEY_ID=... R2_SECRET_ACCESS_KEY=... CLOUDFLARE_ACCOUNT_ID=...
export CORPUS_GIT_ORG=your-org GITHUB_TOKEN=... # for sync-runner clone auth
npm run sync:dry # plan upload for the default `corpus` target
npm run sync # upload + prune
npm run sync:run # isolated clone root, sync all targets, optional reindexThe sync remaps non-native extensions (.ts, .tsx, extensionless Dockerfile, .service, etc.) to .txt keys so AI Search indexes them. See scripts/sync-ingest.mjs.
Bounding a corpus: includePaths vs excludePaths
excludePaths is a denylist and is fail-open: add a new top-level file to a
repo and it silently joins the corpus. That is fine for a docs site. It is wrong
whenever the corpus boundary actually matters, because "we forgot to exclude it"
becomes a real incident.
includePaths is an allowlist and is fail-closed: when a repo has an entry,
only paths under those prefixes are eligible and everything else is refused.
{
"includePaths": { "my-repo": ["docs/", "_corpus/"] },
"excludePaths": { "my-repo": ["docs/internal/"] }
}The two compose subtractively: includePaths decides what is eligible, then
excludePaths subtracts from that. A denylist entry can never add a path back.
Omit a repo from includePaths to keep the previous behaviour. An entry that is
present but empty ([]) is a configuration error, not a way to say "index
nothing": those are different states and collapsing them into the permissive one
is how a whole repo joins a corpus by accident.
A present allowlist must earn its keep
An allowlist that quietly matches nothing is worse than no allowlist. The sync plans zero objects, the mirror prune deletes the corpus that was there, the reindex succeeds over nothing, and the answer surface returns a confident nothing with every status light green. So each of these refuses the run (exit 2) before anything uploads or is pruned:
Refusal | Meaning |
| An entry matched zero git-tracked files (a typo, or a directory that moved) |
| The entry is |
| An entry cannot match a git path (absolute, |
| Wrong config shape |
| The entry names a repo no target lists, so nothing reads it |
| An allowlisted repo is missing from the clone root |
|
|
| Nothing the allowlist selected survived the ingest filters |
Shape and repo-name checks run for every target on every sync, not just the
one being synced, so a rule that has rotted is caught by the next run of any
target. A stale excludePaths repo name warns rather than refuses: denylist rot
grows a corpus, allowlist rot empties one.
Verify what a target will actually upload before you trust it:
node scripts/sync.mjs my-target --dry-runSize cap
Objects over SYNC_MAX_BYTES (default 4 MB) are skipped. Skips are summarised at
the end of the run, not only warned inline, because a file silently missing from
the corpus looks exactly like a file the corpus does not contain -- the worst
failure mode for something that answers questions. Pass --fail-on-skip to turn
an incomplete corpus into a failed run.
SYNC_MAX_BYTES=$((16 * 1024 * 1024)) node scripts/sync.mjs corpus --fail-on-skipCorpus manifest (optional)
A corpus producer can publish a manifest.json next to its objects so the widget
renders citations instead of raw R2 keys. Entries need a key; title, url,
page, and total_pages are used when present:
{ "pages": [
{ "key": "my-doc/p003.txt", "title": "Deploy guide", "url": "/docs/deploy/", "page": 3, "total_pages": 12 }
] }Keys are matched exactly first, then by suffix, because the sync namespaces
every object under its repo name (<repo>/<path>) while a producer naturally
writes its manifest in terms of its own paths.
Reindex dispatch
AI Search rejects a new reindex job for two distinct reasons, and sync-runner clears both
before dispatching:
A job is in flight. Firing anyway does not queue behind it; Cloudflare ends the running job with
end_reason: "new_job_has_started"and restarts. So we wait forended_at.The post-job cooldown. Even once a job ends, a new one is refused for a cooldown window with
sync_in_cooldown [code: 7020]. Waiting for the job to end is necessary but not sufficient, so we retry until it clears.
Waiting (rather than skipping) means the job we start always lands strictly after our own upload, so it sees every object this run wrote. Merge bursts still coalesce: a waiting run holds the workflow concurrency group, and GitHub keeps only the newest queued run, so the runs behind it collapse instead of each firing their own reindex.
Each wait has its own budget (10 min in-flight, 10 min cooldown) rather than one shared deadline, since the two are additive on a perfectly healthy path: a run can wait minutes for an in-flight reindex and then still owe a cooldown wait.
The measured cooldown is short (rejected at 10s after a job ends, accepted at 32s), so the budgets are far larger than they need to be today. That is deliberate. The measurement is an observation, not a contract, and a budget sized to it would turn ordinary upstream variance into red builds. If a budget is exhausted the run fails loudly and says what it means: the R2 corpus uploaded fine, nothing is lost, the index lags until the next sync or the daily backstop.
Ask widget
Copy public/ask-widget.js and public/ask-widget.css to your docs site:
<div id="docs-ask"></div>
<script defer src="/ask-widget.js"
data-endpoint="https://search.example.com/ask"
data-target="#docs-ask"
data-label="Ask the docs"
data-manifest="/corpus-manifest.json"
data-empty-text="Nothing in the indexed corpus addresses that."
data-sitekey="YOUR_TURNSTILE_SITEKEY"></script>data-manifest is optional; without it sources render as raw object keys. A
missing or malformed manifest degrades to raw keys rather than breaking answers.
data-empty-text is shown when a query returns no retrieved sources, so an
unsourced answer is never left on screen looking authoritative.
Per-site system prompts
One deployment can serve several sites. ORIGIN_PROFILES is a JSON object in
[vars] mapping an exact request Origin to that site's system prompt:
ORIGIN_PROFILES = '{"https://docs.example.com":"You are the docs assistant...","https://other.example":"You are..."}'Precedence is ORIGIN_PROFILES, then the legacy blog special-case, then
ASSISTANT_SYSTEM_PROMPT. A malformed value is logged and ignored so a bad var
cannot take /ask down.
Who this is for
Operators building documentation search, agent tooling, or internal knowledge bases on Cloudflare AI Search with MCP and a browser widget.
Links
Deploy guide: docs/DEPLOY.md
Skyphusion Labs: https://skyphusion.org · Org: https://github.com/skyphusion-labs
License
AGPL-3.0-only. See LICENSE.
Community
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- Alicense-qualityBmaintenanceAgent-safe code retrieval MCP server that indexes repositories and provides semantic search, file navigation, call graph analysis, and bounded file reading tools for coding agents.Last updated3,814,6963MIT
- Flicense-qualityCmaintenanceA read-only MCP server that exposes GitHub user profiles, repository info, and search via tools for AI assistants like Claude.Last updated
- Alicense-qualityBmaintenanceSemantic + lexical code search as an MCP server. Agents query in natural language and get back ranked file:line ranges to read precisely.Last updated12MIT
- Alicense-qualityCmaintenanceMCP server that enables AI agents to search code, find files, and read files in a codebase at lightning speed using ripgrep.Last updatedMIT
Related MCP Connectors
An MCP server that gives your AI access to the source code and docs of all public github repos
Agent-native MCP server over the public saagarpatel.dev corpus. Read-only, stateless.
MCP server for AI agents to plan, verify, and deploy Cloudflare-native apps.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/skyphusion-labs/search-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server