-
Notifications
You must be signed in to change notification settings - Fork 141
Add a docs server based on the AI Assistant Vectorize index #90
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
7b6badf
Add a docs server based on the AI Assistant Vectorize index
mhart f15584b
Move docs2 -> docs
mhart b6df99a
Add streamable HTTP support to docs server at /mcp
mhart 6fae91f
Don't need ts-ignore for serve/serveSSE anymore
mhart 59cea7a
Use the newer createApiHandler to get sse+mcp endpoints
mhart File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
/** @type {import("eslint").Linter.Config} */ | ||
module.exports = { | ||
root: true, | ||
extends: ['@repo/eslint-config/default.cjs'], | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# Model Context Protocol (MCP) Server + Cloudflare Documentation (via Autorag) | ||
|
||
This is a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) server that supports remote MCP connections. It connects to a Vectorize DB (in this case, indexed w/ the Cloudflare docs) | ||
|
||
The Cloudflare account this worker is deployed on already has this Vectorize DB setup and indexed. | ||
|
||
## Running locally | ||
|
||
``` | ||
pnpm run start | ||
``` | ||
|
||
Then connect to the server via remote MCP at `http://localhost:8976/sse` | ||
|
||
## Deploying | ||
|
||
``` | ||
pnpm run deploy --env [ENVIRONMENT] | ||
``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
{ | ||
"name": "docs-vectorize", | ||
"version": "0.0.1", | ||
"private": true, | ||
"scripts": { | ||
"check:lint": "run-eslint-workers", | ||
"check:types": "run-tsc", | ||
"deploy": "run-wrangler-deploy", | ||
"dev": "wrangler dev --experimental-vectorize-bind-to-prod", | ||
"start": "npm run dev", | ||
"types": "wrangler types --include-env=false", | ||
"test": "vitest run" | ||
}, | ||
"dependencies": { | ||
"@cloudflare/workers-oauth-provider": "0.0.3", | ||
"@hono/zod-validator": "0.4.3", | ||
"@modelcontextprotocol/sdk": "1.10.2", | ||
"@repo/mcp-common": "workspace:*", | ||
"@repo/mcp-observability": "workspace:*", | ||
"agents": "0.0.67", | ||
"cloudflare": "4.2.0", | ||
"hono": "4.7.6", | ||
"mime": "4.0.6", | ||
"zod": "3.24.2" | ||
}, | ||
"devDependencies": { | ||
"@cloudflare/vitest-pool-workers": "0.8.14", | ||
"@types/node": "22.14.1", | ||
"prettier": "3.5.3", | ||
"typescript": "5.5.4", | ||
"vitest": "3.0.9", | ||
"wrangler": "4.10.0" | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import type { CloudflareDocumentationMCP } from './index' | ||
|
||
export interface Env { | ||
ENVIRONMENT: 'development' | 'staging' | 'production' | ||
MCP_SERVER_NAME: string | ||
MCP_SERVER_VERSION: string | ||
MCP_OBJECT: DurableObjectNamespace<CloudflareDocumentationMCP> | ||
MCP_METRICS: AnalyticsEngineDataset | ||
AI: Ai | ||
VECTORIZE: VectorizeIndex | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import { McpAgent } from 'agents/mcp' | ||
|
||
import { createApiHandler } from '@repo/mcp-common/src/api-handler' | ||
import { getEnv } from '@repo/mcp-common/src/env' | ||
import { CloudflareMCPServer } from '@repo/mcp-common/src/server' | ||
|
||
import { registerDocsTools } from './tools/docs' | ||
|
||
import type { Env } from './context' | ||
|
||
const env = getEnv<Env>() | ||
|
||
// The docs MCP server isn't stateful, so we don't have state/props | ||
export type Props = never | ||
|
||
export type State = never | ||
|
||
export class CloudflareDocumentationMCP extends McpAgent<Env, State, Props> { | ||
server = new CloudflareMCPServer({ | ||
wae: env.MCP_METRICS, | ||
serverInfo: { | ||
name: env.MCP_SERVER_NAME, | ||
version: env.MCP_SERVER_VERSION, | ||
}, | ||
}) | ||
|
||
constructor( | ||
public ctx: DurableObjectState, | ||
public env: Env | ||
) { | ||
super(ctx, env) | ||
} | ||
|
||
async init() { | ||
registerDocsTools(this) | ||
} | ||
} | ||
|
||
export default createApiHandler(CloudflareDocumentationMCP) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import { z } from 'zod' | ||
|
||
import type { CloudflareDocumentationMCP } from '../index' | ||
|
||
// Always return 10 results for simplicity, don't make it configurable | ||
const TOP_K = 10 | ||
|
||
/** | ||
* Registers the docs search tool with the MCP server | ||
* @param agent The MCP server instance | ||
*/ | ||
export function registerDocsTools(agent: CloudflareDocumentationMCP) { | ||
// Register the worker logs analysis tool by worker name | ||
agent.server.tool( | ||
'search_cloudflare_documentation', | ||
`Search the Cloudflare documentation. | ||
|
||
This tool should be used to answer any question about Cloudflare products or features, including: | ||
- Workers, Pages, R2, Images, Stream, D1, Durable Objects, KV, Workflows, Hyperdrive, Queues | ||
- AutoRAG, Workers AI, Vectorize, AI Gateway, Browser Rendering | ||
- Zero Trust, Access, Tunnel, Gateway, Browser Isolation, WARP, DDOS, Magic Transit, Magic WAN | ||
- CDN, Cache, DNS, Zaraz, Argo, Rulesets, Terraform, Account and Billing | ||
|
||
Results are returned as semantically similar chunks to the query. | ||
`, | ||
{ | ||
query: z.string(), | ||
}, | ||
async ({ query }) => { | ||
const results = await queryVectorize(agent.env.AI, agent.env.VECTORIZE, query, TOP_K) | ||
const resultsAsXml = results | ||
.map((result) => { | ||
return `<result> | ||
<url>${result.url}</url> | ||
<text> | ||
${result.text} | ||
</text> | ||
</result>` | ||
}) | ||
.join('\n') | ||
return { | ||
content: [{ type: 'text', text: resultsAsXml }], | ||
} | ||
} | ||
) | ||
} | ||
|
||
async function queryVectorize(ai: Ai, vectorizeIndex: VectorizeIndex, query: string, topK: number) { | ||
// Recommendation from: https://huggingface.co/BAAI/bge-base-en-v1.5#model-list | ||
const [queryEmbedding] = await getEmbeddings(ai, [ | ||
'Represent this sentence for searching relevant passages: ' + query, | ||
]) | ||
|
||
const { matches } = await vectorizeIndex.query(queryEmbedding, { | ||
topK, | ||
returnMetadata: 'all', | ||
returnValues: false, | ||
}) | ||
|
||
return matches.map((match, _i) => ({ | ||
similarity: Math.min(match.score, 1), | ||
id: match.id, | ||
url: sourceToUrl(String(match.metadata?.filePath ?? '')), | ||
text: String(match.metadata?.text ?? ''), | ||
})) | ||
} | ||
|
||
const TOP_DIR = 'src/content/docs' | ||
function sourceToUrl(path: string) { | ||
const prefix = `${TOP_DIR}/` | ||
return ( | ||
'https://developers.cloudflare.com/' + | ||
(path.startsWith(prefix) ? path.slice(prefix.length) : path) | ||
.replace(/index\.mdx$/, '') | ||
.replace(/\.mdx$/, '') | ||
) | ||
} | ||
|
||
async function getEmbeddings(ai: Ai, strings: string[]) { | ||
const response = await doWithRetries(() => | ||
ai.run('@cf/baai/bge-base-en-v1.5', { | ||
text: strings, | ||
// @ts-expect-error pooling not in types yet | ||
pooling: 'cls', | ||
}) | ||
) | ||
|
||
return response.data | ||
} | ||
|
||
/** | ||
* @template T | ||
* @param {() => Promise<T>} action | ||
*/ | ||
async function doWithRetries<T>(action: () => Promise<T>) { | ||
const NUM_RETRIES = 10 | ||
const INIT_RETRY_MS = 50 | ||
for (let i = 0; i <= NUM_RETRIES; i++) { | ||
try { | ||
return await action() | ||
} catch (e) { | ||
// TODO: distinguish between user errors (4xx) and system errors (5xx) | ||
console.error(e) | ||
if (i === NUM_RETRIES) { | ||
throw e | ||
} | ||
// Exponential backoff with full jitter | ||
await scheduler.wait(Math.random() * INIT_RETRY_MS * Math.pow(2, i)) | ||
} | ||
} | ||
// Should never reach here – last loop iteration should return | ||
throw new Error('An unknown error occurred') | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"extends": "@repo/typescript-config/workers.json", | ||
"include": ["*/**.ts", "./vitest.config.ts"] | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config' | ||
|
||
import type { Env } from './src/context' | ||
|
||
export interface TestEnv extends Env { | ||
CLOUDFLARE_MOCK_ACCOUNT_ID: string | ||
CLOUDFLARE_MOCK_API_TOKEN: string | ||
} | ||
|
||
export default defineWorkersConfig({ | ||
test: { | ||
poolOptions: { | ||
workers: { | ||
wrangler: { configPath: `${__dirname}/wrangler.jsonc` }, | ||
miniflare: { | ||
bindings: { | ||
CLOUDFLARE_MOCK_ACCOUNT_ID: 'mock-account-id', | ||
CLOUDFLARE_MOCK_API_TOKEN: 'mock-api-token', | ||
} satisfies Partial<TestEnv>, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a mixture of different response types or is it just chunks of markdown? Curious to see how this works vs the standard embedded resource setup. Cool with merging this as is
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah interested to see too. It's just chunks of markdown. I think resources is ultimately the right way – but I'm not sure that all MCP clients would work as well with it (also I highly doubt any models have actually been trained specifically on it yet, so I suspect resources are just thrown into the LLM context as a JSON blob).
Probably so subtle it'd be hard to come up with a scenario to test, but would be cool if we could.