Skip to content

Commit e2fc0f3

Browse files
committed
WIP copy workers-observability to workers-builds
1 parent 599bfcf commit e2fc0f3

File tree

13 files changed

+6353
-0
lines changed

13 files changed

+6353
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
CLOUDFLARE_CLIENT_ID=
2+
CLOUDFLARE_CLIENT_SECRET=
3+
DEV_DISABLE_OAUTH=
4+
DEV_CLOUDFLARE_API_TOKEN=
5+
DEV_CLOUDFLARE_EMAIL=

apps/workers-builds/.eslintrc.cjs

Lines changed: 5 additions & 0 deletions
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+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Setup
2+
3+
If you'd like to iterate and test your MCP server, you can do so in local development.
4+
5+
## Local Development
6+
7+
1. Create a `.dev.vars` file in your project root:
8+
9+
If you're a Cloudflare employee:
10+
11+
```
12+
CLOUDFLARE_CLIENT_ID=your_development_cloudflare_client_id
13+
CLOUDFLARE_CLIENT_SECRET=your_development_cloudflare_client_secret
14+
```
15+
16+
If you're an external contributor, you can provide a development API token:
17+
18+
```
19+
DEV_DISABLE_OAUTH=true
20+
DEV_CLOUDFLARE_EMAIL=your_cloudflare_email
21+
# This is your global api token
22+
DEV_CLOUDFLARE_API_TOKEN=your_development_api_token
23+
```
24+
25+
2. Start the local development server:
26+
27+
```bash
28+
npx wrangler dev
29+
```
30+
31+
3. To test locally, open Inspector, and connect to `http://localhost:8976/sse`.
32+
Once you follow the prompts, you'll be able to "List Tools". You can also connect with any MCP client.
33+
34+
## Deploying the Worker ( Cloudflare employees only )
35+
36+
Set secrets via Wrangler:
37+
38+
```bash
39+
npx wrangler secret put CLOUDFLARE_CLIENT_ID -e <ENVIRONMENT>
40+
npx wrangler secret put CLOUDFLARE_CLIENT_SECRET -e <ENVIRONMENT>
41+
```
42+
43+
## Set up a KV namespace
44+
45+
Create the KV namespace:
46+
47+
```bash
48+
npx wrangler kv namespace create "OAUTH_KV"
49+
```
50+
51+
Then, update the Wrangler file with the generated KV namespace ID.
52+
53+
## Deploy & Test
54+
55+
Deploy the MCP server to make it available on your workers.dev domain:
56+
57+
```bash
58+
npx wrangler deploy -e <ENVIRONMENT>
59+
```
60+
61+
Test the remote server using [Inspector](https://modelcontextprotocol.io/docs/tools/inspector):
62+
63+
```bash
64+
npx @modelcontextprotocol/inspector@latest
65+
```

apps/workers-builds/README.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Workers Observability 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 [Workers Observability](https://developers.cloudflare.com/workers/observability/) to provide global
7+
Internet traffic insights, trends and other utilities.
8+
9+
## 🔨 Available Tools
10+
11+
Currently available tools:
12+
13+
| **Category** | **Tool** | **Description** |
14+
| --------------------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
15+
| **Workers Analytics** | `query_worker_observability` | Queries Workers Observability API to analyze logs and metrics from your Cloudflare Workers. Supports listing events, calculating metrics, and finding specific invocations |
16+
| **Schema Discovery** | `observability_keys` | Discovers available data fields in your Workers logs including metadata fields, worker-specific fields, and custom logged fields |
17+
| **Value Exploration** | `observability_values` | Finds available values for specific fields in Workers logs to help build precise filters for analytics queries |
18+
19+
This MCP server is still a work in progress, and we plan to add more tools in the future.
20+
21+
### Prompt Examples
22+
23+
- `Can you tell me about any potential issues on this particular worker 'my-worker-name'?`
24+
- `Show me the CPU time usage for my worker 'api-gateway' over the last 24 hours`
25+
- `What were the top 5 countries by request count for my worker yesterday?`
26+
- `How many requests were made to my worker 'my-app' broken down by HTTP status code?`
27+
- `Compare the error rates between my production and staging workers`
28+
29+
## Access the remote MCP server from from any MCP Client
30+
31+
If your MCP client has first class support for remote MCP servers, the client will provide a way to accept the server URL (`https://observability.mcp.cloudflare.com`) directly within its interface (for example in [Cloudflare AI Playground](https://playground.ai.cloudflare.com/)).
32+
33+
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.
34+
35+
Replace the content with the following configuration:
36+
37+
```json
38+
{
39+
"mcpServers": {
40+
"cloudflare": {
41+
"command": "npx",
42+
"args": ["mcp-remote", "https://observability.mcp.cloudflare.com/sse"]
43+
}
44+
}
45+
}
46+
```
47+
48+
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.
49+
50+
Interested in contributing, and running this server locally? See [CONTRIBUTING.md](CONTRIBUTING.md) to get started.

apps/workers-builds/package.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"name": "workers-builds",
3+
"version": "0.0.1",
4+
"private": true,
5+
"scripts": {
6+
"check:lint": "run-eslint-workers",
7+
"check:types": "run-tsc",
8+
"deploy": "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.5",
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/workers-builds/src/context.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { UserDetails } from '@repo/mcp-common/src/durable-objects/user_details'
2+
import type { ObservabilityMCP } 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<ObservabilityMCP>
12+
USER_DETAILS: DurableObjectNamespace<UserDetails>
13+
MCP_METRICS: AnalyticsEngineDataset
14+
SENTRY_ACCESS_CLIENT_ID: string
15+
SENTRY_ACCESS_CLIENT_SECRET: string
16+
GIT_HASH: string
17+
SENTRY_DSN: string
18+
DEV_DISABLE_OAUTH: string
19+
DEV_CLOUDFLARE_API_TOKEN: string
20+
DEV_CLOUDFLARE_EMAIL: string
21+
}

apps/workers-builds/src/index.ts

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import OAuthProvider from '@cloudflare/workers-oauth-provider'
2+
import { McpAgent } from 'agents/mcp'
3+
4+
import {
5+
createAuthHandlers,
6+
handleTokenExchangeCallback,
7+
} from '@repo/mcp-common/src/cloudflare-oauth-handler'
8+
import { handleDevMode } from '@repo/mcp-common/src/dev-mode'
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 { initSentryWithUser } from '@repo/mcp-common/src/sentry'
13+
import { CloudflareMCPServer } from '@repo/mcp-common/src/server'
14+
import { registerAccountTools } from '@repo/mcp-common/src/tools/account'
15+
import { registerWorkersTools } from '@repo/mcp-common/src/tools/worker'
16+
17+
import { MetricsTracker } from '../../../packages/mcp-observability/src'
18+
import { registerObservabilityTools } from './tools/observability'
19+
20+
import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler'
21+
import type { Env } from './context'
22+
23+
export { UserDetails }
24+
25+
const env = getEnv<Env>()
26+
27+
const metrics = new MetricsTracker(env.MCP_METRICS, {
28+
name: env.MCP_SERVER_NAME,
29+
version: env.MCP_SERVER_VERSION,
30+
})
31+
32+
// Context from the auth process, encrypted & stored in the auth token
33+
// and provided to the DurableMCP as this.props
34+
type Props = AuthProps
35+
36+
type State = { activeAccountId: string | null }
37+
38+
export class ObservabilityMCP extends McpAgent<Env, State, Props> {
39+
_server: CloudflareMCPServer | undefined
40+
set server(server: CloudflareMCPServer) {
41+
this._server = server
42+
}
43+
get server(): CloudflareMCPServer {
44+
if (!this._server) {
45+
throw new Error('Tried to access server before it was initialized')
46+
}
47+
48+
return this._server
49+
}
50+
51+
async init() {
52+
this.server = new CloudflareMCPServer({
53+
userId: this.props.user.id,
54+
wae: this.env.MCP_METRICS,
55+
serverInfo: {
56+
name: this.env.MCP_SERVER_NAME,
57+
version: this.env.MCP_SERVER_VERSION,
58+
},
59+
sentry: initSentryWithUser(env, this.ctx, this.props.user.id),
60+
options: {
61+
instructions: `# Cloudflare Workers Observability Tool
62+
* A cloudflare worker is a serverless function
63+
* Workers Observability is the tool to inspect the logs for your cloudflare Worker
64+
* Each log is a structured JSON payload with keys and values
65+
66+
67+
This server allows you to analyze your Cloudflare Workers logs and metrics.
68+
`,
69+
},
70+
})
71+
72+
registerAccountTools(this)
73+
74+
// Register Cloudflare Workers tools
75+
registerWorkersTools(this)
76+
77+
// Register Cloudflare Workers logs tools
78+
registerObservabilityTools(this)
79+
}
80+
81+
async getActiveAccountId() {
82+
try {
83+
// Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it
84+
// we do this so we can persist activeAccountId across sessions
85+
const userDetails = getUserDetails(env, this.props.user.id)
86+
return await userDetails.getActiveAccountId()
87+
} catch (e) {
88+
this.server.recordError(e)
89+
return null
90+
}
91+
}
92+
93+
async setActiveAccountId(accountId: string) {
94+
try {
95+
const userDetails = getUserDetails(env, this.props.user.id)
96+
await userDetails.setActiveAccountId(accountId)
97+
} catch (e) {
98+
this.server.recordError(e)
99+
}
100+
}
101+
}
102+
103+
const ObservabilityScopes = {
104+
...RequiredScopes,
105+
'account:read': 'See your account info such as account details, analytics, and memberships.',
106+
'workers:write':
107+
'See and change Cloudflare Workers data such as zones, KV storage, namespaces, scripts, and routes.',
108+
'workers_observability:read': 'See observability logs for your account',
109+
} as const
110+
111+
export default {
112+
fetch: async (req: Request, env: Env, ctx: ExecutionContext) => {
113+
if (env.ENVIRONMENT === 'development' && env.DEV_DISABLE_OAUTH === 'true') {
114+
return await handleDevMode(ObservabilityMCP, req, env, ctx)
115+
}
116+
117+
return new OAuthProvider({
118+
apiHandlers: {
119+
'/mcp': ObservabilityMCP.serve('/mcp'),
120+
'/sse': ObservabilityMCP.serveSSE('/sse'),
121+
},
122+
// @ts-ignore
123+
defaultHandler: createAuthHandlers({ scopes: ObservabilityScopes, metrics }),
124+
authorizeEndpoint: '/oauth/authorize',
125+
tokenEndpoint: '/token',
126+
tokenExchangeCallback: (options) =>
127+
handleTokenExchangeCallback(
128+
options,
129+
env.CLOUDFLARE_CLIENT_ID,
130+
env.CLOUDFLARE_CLIENT_SECRET
131+
),
132+
// Cloudflare access token TTL
133+
accessTokenTTL: 3600,
134+
clientRegistrationEndpoint: '/register',
135+
}).fetch(req, env, ctx)
136+
},
137+
}

0 commit comments

Comments
 (0)