Skip to content

Commit 47a7e65

Browse files
committed
Add AutoRAG mcp server
1 parent d0866b1 commit 47a7e65

14 files changed

+6279
-1
lines changed

apps/autorag/.dev.vars.example

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
CLOUDFLARE_CLIENT_ID=
2+
CLOUDFLARE_CLIENT_SECRET=

apps/autorag/.eslintrc.cjs

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/** @type {import("eslint").Linter.Config} */
2+
module.exports = {
3+
root: true,
4+
extends: ['@repo/eslint-config/default.cjs'],
5+
}

apps/autorag/README.md

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Cloudflare AutoRAG MCP Server 📡
2+
3+
This is a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) server that supports remote MCP
4+
connections, with Cloudflare OAuth built-in.
5+
6+
It integrates tools powered by the [Cloudflare AutoRAG API](https://developers.cloudflare.com/autorag/) to provide global
7+
Internet traffic insights, trends and other utilities.
8+
9+
## Access the remote MCP server from from any MCP Client
10+
11+
If your MCP client has first class support for remote MCP servers, the client will provide a way to accept the server URL (`https://autorag.mcp.cloudflare.com`) directly within its interface (for example in[Cloudflare AI Playground](https://playground.ai.cloudflare.com/)).
12+
13+
If your client does not yet support remote MCP servers, you will need to set up its resepective configuration file using mcp-remote (https://www.npmjs.com/package/mcp-remote) to specify which servers your client can access.
14+
15+
Replace the content with the following configuration:
16+
17+
```json
18+
{
19+
"mcpServers": {
20+
"cloudflare": {
21+
"command": "npx",
22+
"args": ["mcp-remote", "https://autorag.mcp.cloudflare.com/sse"]
23+
}
24+
}
25+
}
26+
```
27+
28+
Once you've set up your configuration file, restart MCP client and a browser window will open showing your OAuth login page. Proceed through the authentication flow to grant the client access to your MCP server. After you grant access, the tools will become available for you to use.
29+
30+
Interested in contributing, and running this server locally? See [CONTRIBUTING.md](CONTRIBUTING.md) to get started.

apps/autorag/package.json

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"name": "cloudflare-autorag-mcp-server",
3+
"version": "0.0.1",
4+
"private": true,
5+
"scripts": {
6+
"check:lint": "run-eslint-workers",
7+
"check:types": "run-tsc",
8+
"deploy": "run-wrangler-deploy",
9+
"dev": "wrangler dev",
10+
"start": "wrangler dev",
11+
"types": "wrangler types --include-env=false",
12+
"test": "vitest run"
13+
},
14+
"dependencies": {
15+
"@cloudflare/workers-oauth-provider": "0.0.3",
16+
"@hono/zod-validator": "0.4.3",
17+
"@modelcontextprotocol/sdk": "1.10.2",
18+
"@repo/mcp-common": "workspace:*",
19+
"@repo/mcp-observability": "workspace:*",
20+
"agents": "0.0.67",
21+
"cloudflare": "4.2.0",
22+
"hono": "4.7.6",
23+
"zod": "3.24.2"
24+
},
25+
"devDependencies": {
26+
"@cloudflare/vitest-pool-workers": "0.8.14",
27+
"@types/node": "22.14.1",
28+
"prettier": "3.5.3",
29+
"typescript": "5.5.4",
30+
"vitest": "3.0.9",
31+
"wrangler": "4.10.0"
32+
}
33+
}

apps/autorag/src/context.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { UserDetails } from '@repo/mcp-common/src/durable-objects/user_details'
2+
import type { AutoRAGMCP } from './index'
3+
4+
export interface Env {
5+
OAUTH_KV: KVNamespace
6+
ENVIRONMENT: 'development' | 'staging' | 'production'
7+
MCP_SERVER_NAME: string
8+
MCP_SERVER_VERSION: string
9+
CLOUDFLARE_CLIENT_ID: string
10+
CLOUDFLARE_CLIENT_SECRET: string
11+
MCP_OBJECT: DurableObjectNamespace<AutoRAGMCP>
12+
USER_DETAILS: DurableObjectNamespace<UserDetails>
13+
MCP_METRICS: AnalyticsEngineDataset
14+
}

apps/autorag/src/index.ts

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import OAuthProvider from '@cloudflare/workers-oauth-provider'
2+
import { McpAgent } from 'agents/mcp'
3+
4+
import { createApiHandler } from '@repo/mcp-common/src/api-handler'
5+
import {
6+
createAuthHandlers,
7+
handleTokenExchangeCallback,
8+
} from '@repo/mcp-common/src/cloudflare-oauth-handler'
9+
import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details'
10+
import { getEnv } from '@repo/mcp-common/src/env'
11+
import { RequiredScopes } from '@repo/mcp-common/src/scopes'
12+
import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
13+
import { registerAccountTools } from '@repo/mcp-common/src/tools/account'
14+
15+
import { MetricsTracker } from '../../../packages/mcp-observability/src'
16+
import { registerAutoRAGTools } from './tools/autorag'
17+
18+
import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler'
19+
import type { Env } from './context'
20+
21+
const env = getEnv<Env>()
22+
23+
export { UserDetails }
24+
25+
const metrics = new MetricsTracker(env.MCP_METRICS, {
26+
name: env.MCP_SERVER_NAME,
27+
version: env.MCP_SERVER_VERSION,
28+
})
29+
30+
// Context from the auth process, encrypted & stored in the auth token
31+
// and provided to the DurableMCP as this.props
32+
type Props = AuthProps
33+
type State = { activeAccountId: string | null }
34+
35+
export class AutoRAGMCP extends McpAgent<Env, State, Props> {
36+
_server: CloudflareMCPServer | undefined
37+
set server(server: CloudflareMCPServer) {
38+
this._server = server
39+
}
40+
get server(): CloudflareMCPServer {
41+
if (!this._server) {
42+
throw new Error('Tried to access server before it was initialized')
43+
}
44+
45+
return this._server
46+
}
47+
48+
constructor(ctx: DurableObjectState, env: Env) {
49+
super(ctx, env)
50+
}
51+
52+
async init() {
53+
this.server = new CloudflareMCPServer({
54+
userId: this.props.user.id,
55+
wae: this.env.MCP_METRICS,
56+
serverInfo: {
57+
name: this.env.MCP_SERVER_NAME,
58+
version: this.env.MCP_SERVER_VERSION,
59+
},
60+
})
61+
62+
registerAccountTools(this)
63+
64+
// Register Cloudflare Log Push tools
65+
registerAutoRAGTools(this)
66+
}
67+
68+
async getActiveAccountId() {
69+
try {
70+
// Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it
71+
// we do this so we can persist activeAccountId across sessions
72+
const userDetails = getUserDetails(env, this.props.user.id)
73+
return await userDetails.getActiveAccountId()
74+
} catch (e) {
75+
this.server.recordError(e)
76+
return null
77+
}
78+
}
79+
80+
async setActiveAccountId(accountId: string) {
81+
try {
82+
const userDetails = getUserDetails(env, this.props.user.id)
83+
await userDetails.setActiveAccountId(accountId)
84+
} catch (e) {
85+
this.server.recordError(e)
86+
}
87+
}
88+
}
89+
90+
const LogPushScopes = {
91+
...RequiredScopes,
92+
'account:read': 'See your account info such as account details, analytics, and memberships.',
93+
'rag:write': 'Grants write level access to AutoRag.',
94+
} as const
95+
96+
export default {
97+
fetch: async (req: Request, env: Env, ctx: ExecutionContext) => {
98+
return new OAuthProvider({
99+
apiRoute: ['/mcp', '/sse'],
100+
apiHandler: createApiHandler(AutoRAGMCP),
101+
// @ts-ignore
102+
defaultHandler: createAuthHandlers({ scopes: LogPushScopes, metrics }),
103+
authorizeEndpoint: '/oauth/authorize',
104+
tokenEndpoint: '/token',
105+
tokenExchangeCallback: (options) =>
106+
handleTokenExchangeCallback(
107+
options,
108+
env.CLOUDFLARE_CLIENT_ID,
109+
env.CLOUDFLARE_CLIENT_SECRET
110+
),
111+
// Cloudflare access token TTL
112+
accessTokenTTL: 3600,
113+
clientRegistrationEndpoint: '/register',
114+
}).fetch(req, env, ctx)
115+
},
116+
}

apps/autorag/src/tools/autorag.ts

+171
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import Cloudflare from 'cloudflare'
2+
import { z } from 'zod'
3+
4+
import { getCloudflareClient } from '@repo/mcp-common/src/cloudflare-api'
5+
6+
import { pageParam, perPageParam } from '../types'
7+
8+
import type { AutoRAGMCP } from '../index'
9+
10+
import V4PagePaginationArray = Cloudflare.V4PagePaginationArray
11+
12+
export function registerAutoRAGTools(agent: AutoRAGMCP) {
13+
agent.server.tool(
14+
'list_rags',
15+
'List Rags (vector stores)',
16+
{
17+
page: pageParam,
18+
per_page: perPageParam,
19+
},
20+
async (params) => {
21+
const accountId = await agent.getActiveAccountId()
22+
if (!accountId) {
23+
return {
24+
content: [
25+
{
26+
type: 'text',
27+
text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)',
28+
},
29+
],
30+
}
31+
}
32+
try {
33+
const client = getCloudflareClient(agent.props.accessToken)
34+
const r = (await client.getAPIList(
35+
`/accounts/${accountId}/autorag/rags`,
36+
V4PagePaginationArray,
37+
{ query: { page: params.page, per_page: params.per_page } }
38+
)) as { result: unknown; result_info: unknown }
39+
40+
return {
41+
content: [
42+
{
43+
type: 'text',
44+
text: JSON.stringify({
45+
result: r.result,
46+
result_info: r.result_info,
47+
}),
48+
},
49+
],
50+
}
51+
} catch (error) {
52+
return {
53+
content: [
54+
{
55+
type: 'text',
56+
text: `Error listing rags: ${error instanceof Error && error.message}`,
57+
},
58+
],
59+
}
60+
}
61+
}
62+
)
63+
64+
agent.server.tool(
65+
'search',
66+
'Search Rag (vector store)',
67+
{
68+
rag_id: z.string(),
69+
query: z.string(),
70+
},
71+
async (params) => {
72+
try {
73+
const accountId = await agent.getActiveAccountId()
74+
if (!accountId) {
75+
return {
76+
content: [
77+
{
78+
type: 'text',
79+
text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)',
80+
},
81+
],
82+
}
83+
}
84+
85+
const client = getCloudflareClient(agent.props.accessToken)
86+
const r = (await client.post(
87+
`/accounts/${accountId}/autorag/rags/${params.rag_id}/search`,
88+
{
89+
body: {
90+
query: params.query,
91+
},
92+
}
93+
)) as { result: unknown }
94+
95+
return {
96+
content: [
97+
{
98+
type: 'text',
99+
text: JSON.stringify({
100+
result: r.result,
101+
}),
102+
},
103+
],
104+
}
105+
} catch (error) {
106+
return {
107+
content: [
108+
{
109+
type: 'text',
110+
text: `Error searching rag: ${error instanceof Error && error.message}`,
111+
},
112+
],
113+
}
114+
}
115+
}
116+
)
117+
118+
agent.server.tool(
119+
'ai_search',
120+
'AI Search Rag (vector store)',
121+
{
122+
rag_id: z.string(),
123+
query: z.string(),
124+
},
125+
async (params) => {
126+
try {
127+
const accountId = await agent.getActiveAccountId()
128+
if (!accountId) {
129+
return {
130+
content: [
131+
{
132+
type: 'text',
133+
text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)',
134+
},
135+
],
136+
}
137+
}
138+
139+
const client = getCloudflareClient(agent.props.accessToken)
140+
const r = (await client.post(
141+
`/accounts/${accountId}/autorag/rags/${params.rag_id}/ai-search`,
142+
{
143+
body: {
144+
query: params.query,
145+
},
146+
}
147+
)) as { result: unknown }
148+
149+
return {
150+
content: [
151+
{
152+
type: 'text',
153+
text: JSON.stringify({
154+
result: r.result,
155+
}),
156+
},
157+
],
158+
}
159+
} catch (error) {
160+
return {
161+
content: [
162+
{
163+
type: 'text',
164+
text: `Error searching rag: ${error instanceof Error && error.message}`,
165+
},
166+
],
167+
}
168+
}
169+
}
170+
)
171+
}

apps/autorag/src/types.ts

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { z } from 'zod'
2+
3+
export const pageParam = z.number().int().min(1).optional().default(1)
4+
export const perPageParam = z.number().int().min(1).max(50).optional().default(20)

apps/autorag/tsconfig.json

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"extends": "@repo/typescript-config/workers.json",
3+
"include": ["*/**.ts", "./vitest.config.ts", "./types.d.ts"]
4+
}

apps/autorag/types.d.ts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import type { TestEnv } from './vitest.config'
2+
3+
declare module 'cloudflare:test' {
4+
interface ProvidedEnv extends TestEnv {}
5+
}

0 commit comments

Comments
 (0)