Create a private MCP
Purpose
Section titled “Purpose”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.
Resulting architecture
Section titled “Resulting architecture”- 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/**/*.mdandsrc/content/docs/**/*.mdx. - A build step generates
src/generated/docs-index.json. - The MCP server exposes tools for
search_docs,get_doc, andlist_docs.
Repository changes
Section titled “Repository changes”Install the MCP runtime dependencies:
pnpm add agents @modelcontextprotocol/sdk zodAdd 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
.mdand.mdxfiles. - Parse basic frontmatter fields such as
titleanddescription. - 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
McpServerper request. - Register
search_docs,get_doc, andlist_docs. - Hand valid requests to
createMcpHandler()fromagents/mcp.
Use constant-time token comparison logic. Do not log the provided token or include it in responses.
Cloudflare Worker configuration
Section titled “Cloudflare Worker configuration”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-tokenSet the production secret with Wrangler:
pnpm exec wrangler secret put MCP_API_TOKENDeploy the Worker:
pnpm buildpnpm exec wrangler deployCloudflare Access setup
Section titled “Cloudflare Access setup”Use Cloudflare Access for the browser-facing documentation site.
- Open Cloudflare Zero Trust.
- Create an Access application.
- Choose a self-hosted application.
- Set the hostname to
sop.getquick.io. - Add an allow policy for the approved company identity provider, email domain, group, or explicit users.
- 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.
VS Code MCP configuration
Section titled “VS Code MCP configuration”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 } ]}Validation
Section titled “Validation”Run the production build:
pnpm buildRun the Worker locally:
pnpm exec wrangler dev --ip 127.0.0.1 --port 8787Test unauthenticated MCP access. Expected result: 401 Unauthorized.
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.
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/listshould showsearch_docs,get_doc, andlist_docs.tools/callwithsearch_docsshould return matching documentation snippets.tools/callwithget_docshould return one full normalized document.
Probe the deployed endpoint without a token. Expected result: 401 Unauthorized.
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());"Operational notes for agents
Section titled “Operational notes for agents”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.
Troubleshooting
Section titled “Troubleshooting”- If
/mcpreturns401, verify theAuthorizationheader usesBearer <token>and that the Worker hasMCP_API_TOKENconfigured. - If the MCP endpoint is unreachable, verify the
mcp.sop.getquick.iocustom domain route inwrangler.jsoncand redeploy. - If docs do not appear in search, run
pnpm docs:indexand confirm the file exists insrc/generated/docs-index.json. - If Cloudflare Access blocks browser access unexpectedly, review the Access application policy for
sop.getquick.io.