Skip to content

Create a private MCP

Create a private documentation website and a private MCP endpoint that lets approved AI agents search and read the same documentation.

This SOP is safe for agent consumption. It describes the architecture, files, commands, and validation steps without exposing production tokens, account IDs, or other sensitive values.

  • The human documentation site runs at https://sop.getquick.io.
  • Cloudflare Access protects the human website login.
  • The MCP endpoint runs at https://mcp.sop.getquick.io/mcp.
  • MCP requests authenticate with Authorization: Bearer <MCP_API_TOKEN>.
  • Documentation source files live under src/content/docs/**/*.md and src/content/docs/**/*.mdx.
  • A build step generates src/generated/docs-index.json.
  • The MCP server exposes tools for search_docs, get_doc, and list_docs.

Install the MCP runtime dependencies:

Terminal window
pnpm add agents @modelcontextprotocol/sdk zod

Add a docs index script to package.json:

{
"scripts": {
"docs:index": "node scripts/generate-docs-index.mjs",
"predev": "pnpm docs:index",
"prestart": "pnpm docs:index",
"prebuild": "pnpm docs:index"
}
}

Create scripts/generate-docs-index.mjs to:

  • Walk src/content/docs.
  • Read .md and .mdx files.
  • Parse basic frontmatter fields such as title and description.
  • Strip Markdown, MDX imports, components, and links into normalized searchable text.
  • Write src/generated/docs-index.json.

Create src/lib/docs-search.ts to:

  • Load src/generated/docs-index.json.
  • Normalize slugs and URL paths.
  • Return all docs through listDocs().
  • Return one doc through getDoc(slug).
  • Score keyword matches through searchDocs(query, limit).

Create src/pages/mcp.ts to:

  • Disable prerendering for the MCP route.
  • Reject missing or invalid bearer tokens with 401.
  • Create a fresh stateless McpServer per request.
  • Register search_docs, get_doc, and list_docs.
  • Hand valid requests to createMcpHandler() from agents/mcp.

Use constant-time token comparison logic. Do not log the provided token or include it in responses.

Add both public hostnames to wrangler.jsonc:

{
"routes": [
{ "pattern": "sop.getquick.io", "custom_domain": true },
{ "pattern": "mcp.sop.getquick.io", "custom_domain": true }
]
}

Keep secrets out of source control. Ignore .dev.vars and commit only .dev.vars.example:

MCP_API_TOKEN=replace-with-a-local-development-token

Set the production secret with Wrangler:

Terminal window
pnpm exec wrangler secret put MCP_API_TOKEN

Deploy the Worker:

Terminal window
pnpm build
pnpm exec wrangler deploy

Use Cloudflare Access for the browser-facing documentation site.

  1. Open Cloudflare Zero Trust.
  2. Create an Access application.
  3. Choose a self-hosted application.
  4. Set the hostname to sop.getquick.io.
  5. Add an allow policy for the approved company identity provider, email domain, group, or explicit users.
  6. Leave the app deny-by-default for everyone else.

Do not place the browser Access app in front of mcp.sop.getquick.io unless every MCP client is also configured to send Cloudflare Access service credentials. The MCP endpoint already requires the bearer API token.

Create .vscode/mcp.json so VS Code can connect to the remote MCP endpoint without committing the token:

{
"servers": {
"getquick-sop-docs": {
"type": "http",
"url": "https://mcp.sop.getquick.io/mcp",
"headers": {
"Authorization": "Bearer ${input:getquick-sop-mcp-token}"
}
}
},
"inputs": [
{
"id": "getquick-sop-mcp-token",
"type": "promptString",
"description": "GETQUICK SOP MCP API token",
"password": true
}
]
}

Run the production build:

Terminal window
pnpm build

Run the Worker locally:

Terminal window
pnpm exec wrangler dev --ip 127.0.0.1 --port 8787

Test unauthenticated MCP access. Expected result: 401 Unauthorized.

Terminal window
node -e "const body={jsonrpc:'2.0',id:1,method:'initialize',params:{protocolVersion:'2025-06-18',capabilities:{},clientInfo:{name:'smoke-test',version:'1.0.0'}}}; const res=await fetch('http://127.0.0.1:8787/mcp',{method:'POST',headers:{'content-type':'application/json','accept':'application/json, text/event-stream'},body:JSON.stringify(body)}); console.log(res.status, await res.text());"

Test authenticated MCP initialize. Expected result: 200 and server info for getquick-sop-docs.

Terminal window
node -e "const token=process.env.MCP_API_TOKEN; const body={jsonrpc:'2.0',id:1,method:'initialize',params:{protocolVersion:'2025-06-18',capabilities:{},clientInfo:{name:'smoke-test',version:'1.0.0'}}}; const res=await fetch('http://127.0.0.1:8787/mcp',{method:'POST',headers:{'content-type':'application/json','accept':'application/json, text/event-stream',authorization:'Bearer '+token},body:JSON.stringify(body)}); console.log(res.status, await res.text());"

Test MCP tools:

  • tools/list should show search_docs, get_doc, and list_docs.
  • tools/call with search_docs should return matching documentation snippets.
  • tools/call with get_doc should return one full normalized document.

Probe the deployed endpoint without a token. Expected result: 401 Unauthorized.

Terminal window
node -e "const body={jsonrpc:'2.0',id:1,method:'initialize',params:{protocolVersion:'2025-06-18',capabilities:{},clientInfo:{name:'remote-smoke-test',version:'1.0.0'}}}; const res=await fetch('https://mcp.sop.getquick.io/mcp',{method:'POST',headers:{'content-type':'application/json','accept':'application/json, text/event-stream'},body:JSON.stringify(body)}); console.log(res.status, await res.text());"

When an AI agent needs SOP context, call search_docs first with the user task as the query. Use get_doc on the best matching slug before giving procedural instructions. Use list_docs only when discovery or broad navigation is needed.

Never expose MCP bearer tokens in chat, logs, source control, screenshots, or documentation. Rotate the Cloudflare Worker secret if a token is disclosed.

  • If /mcp returns 401, verify the Authorization header uses Bearer <token> and that the Worker has MCP_API_TOKEN configured.
  • If the MCP endpoint is unreachable, verify the mcp.sop.getquick.io custom domain route in wrangler.jsonc and redeploy.
  • If docs do not appear in search, run pnpm docs:index and confirm the file exists in src/generated/docs-index.json.
  • If Cloudflare Access blocks browser access unexpectedly, review the Access application policy for sop.getquick.io.