Skip to content

Commit ee85fee

Browse files
committed
fix WAE metrics
1 parent 8f5bf9b commit ee85fee

File tree

9 files changed

+196
-136
lines changed

9 files changed

+196
-136
lines changed

apps/docs-autorag/wrangler.jsonc

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,15 @@
6262
"ENVIRONMENT": "staging",
6363
"AUTORAG_NAME": "cloudflare-docs-autorag"
6464
},
65-
6665
"ai": {
6766
"binding": "AI"
68-
}
67+
},
68+
"analytics_engine_datasets": [
69+
{
70+
"binding": "MCP_METRICS",
71+
"dataset": "mcp-metrics-staging"
72+
}
73+
],
6974
},
7075
"production": {
7176
"name": "mcp-cloudflare-docs-autorag-production",
@@ -85,7 +90,13 @@
8590
},
8691
"ai": {
8792
"binding": "AI"
88-
}
93+
},
94+
"analytics_engine_datasets": [
95+
{
96+
"binding": "MCP_METRICS",
97+
"dataset": "mcp-metrics-production"
98+
}
99+
],
89100
}
90101
}
91102
}

apps/workers-bindings/wrangler.jsonc

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,13 @@
7070
],
7171
"vars": {
7272
"ENVIRONMENT": "staging"
73-
}
73+
},
74+
"analytics_engine_datasets": [
75+
{
76+
"binding": "MCP_METRICS",
77+
"dataset": "mcp-metrics-staging"
78+
}
79+
],
7480
},
7581
"production": {
7682
"name": "mcp-cloudflare-workers-bindings-production",
@@ -92,7 +98,13 @@
9298
],
9399
"vars": {
94100
"ENVIRONMENT": "production"
95-
}
101+
},
102+
"analytics_engine_datasets": [
103+
{
104+
"binding": "MCP_METRICS",
105+
"dataset": "mcp-metrics-production"
106+
}
107+
],
96108
}
97109
}
98110
}

apps/workers-observability/wrangler.jsonc

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,13 @@
7171
],
7272
"vars": {
7373
"ENVIRONMENT": "staging"
74-
}
74+
},
75+
"analytics_engine_datasets": [
76+
{
77+
"binding": "MCP_METRICS",
78+
"dataset": "mcp-metrics-staging"
79+
}
80+
],
7581
},
7682
"production": {
7783
"name": "mcp-cloudflare-workers-observability-production",
@@ -93,7 +99,13 @@
9399
],
94100
"vars": {
95101
"ENVIRONMENT": "production"
96-
}
102+
},
103+
"analytics_engine_datasets": [
104+
{
105+
"binding": "MCP_METRICS",
106+
"dataset": "mcp-metrics-production"
107+
}
108+
],
97109
}
98110
}
99111
}

packages/mcp-common/src/server.ts

Lines changed: 51 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import { McpServer, ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js'
2+
import { InitializedNotificationSchema, ClientCapabilities } from "@modelcontextprotocol/sdk/types.js"
23
import { type ServerOptions } from "@modelcontextprotocol/sdk/server/index.js"
34
import { MetricsTracker, SessionStart, ToolCall } from '@repo/mcp-observability';
4-
import { ZodRawShape } from 'zod';
5+
import { ZodRawShape, ZodType } from 'zod';
56
import { McpError } from './mcp-error';
7+
import { isPromise } from 'node:util/types'
68

79
export class CloudflareMCPServer extends McpServer {
810
private metrics;
911

1012
constructor(
11-
userId: string | undefined,
13+
private userId: string | undefined,
1214
wae: AnalyticsEngineDataset,
1315
serverInfo: {
1416
[x: string]: unknown
@@ -21,47 +23,67 @@ export class CloudflareMCPServer extends McpServer {
2123
this.metrics = new MetricsTracker(wae, serverInfo)
2224

2325
this.server.oninitialized = () => {
24-
const sessionId = this.server.transport?.sessionId
2526
const clientInfo = this.server.getClientVersion()
27+
const clientCapabilities = this.server.getClientCapabilities()
2628
this.metrics.logEvent(new SessionStart({
2729
userId,
28-
sessionId,
29-
clientInfo
30+
clientInfo,
31+
clientCapabilities
3032
}))
3133
}
3234

3335
const _tool = this.tool.bind(this);
3436
this.tool = (name: string, ...rest: unknown[]): ReturnType<typeof this.tool> => {
35-
const baseToolCallback = rest[rest.length - 1] as ToolCallback<ZodRawShape | undefined>
36-
rest[rest.length - 1] = (args: Parameters<ToolCallback<ZodRawShape | undefined>>) => {
37-
const sessionId = args.length === 2 ? args[1].sessionId : args[0].sessionId
38-
// @ts-ignore there's a weird typescript issue where it uses | instead of &
39-
return baseToolCallback(...args)
40-
.then(() => {
37+
const toolCb = rest[rest.length - 1] as ToolCallback<ZodRawShape | undefined>
38+
const replacementToolCb: ToolCallback<ZodRawShape | undefined> = (arg1, arg2) => {
39+
const toolCall = toolCb(arg1 as { [x: string]: any; } & { signal: AbortSignal }, arg2)
40+
// There are 4 cases to track:
41+
try {
42+
if (isPromise(toolCall)) {
43+
return toolCall
44+
.then((r) => {
45+
// promise succeeds
46+
this.metrics.logEvent(new ToolCall({
47+
userId,
48+
toolName: name
49+
}))
50+
return r
51+
})
52+
.catch((e) => {
53+
// promise throws
54+
this.trackToolCallError(e, name)
55+
throw e
56+
})
57+
} else {
58+
// non-promise succeeds
4159
this.metrics.logEvent(new ToolCall({
4260
userId,
43-
sessionId,
4461
toolName: name
4562
}))
46-
})
47-
.catch((e: any) => {
48-
let errorCode = -1
49-
if (e instanceof McpError) {
50-
errorCode = e.code
51-
}
52-
this.metrics.logEvent(new ToolCall({
53-
userId,
54-
sessionId,
55-
toolName: name,
56-
errorCode: errorCode
57-
}))
58-
59-
throw e
60-
})
63+
return toolCall
64+
}
65+
} catch (e) {
66+
// non-promise throws
67+
this.trackToolCallError(e, name)
68+
throw e
69+
}
6170
}
71+
rest[rest.length - 1] = replacementToolCb
6272

63-
// @ts-ignore
73+
// @ts-ignore
6474
return _tool(name, ...rest)
6575
}
6676
}
67-
}
77+
78+
private trackToolCallError(e: any, toolName: string) {
79+
let errorCode = -1
80+
if (e instanceof McpError) {
81+
errorCode = e.code
82+
}
83+
this.metrics.logEvent(new ToolCall({
84+
toolName,
85+
userId: this.userId,
86+
errorCode: errorCode
87+
}))
88+
}
89+
}

packages/mcp-observabiility/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"bin": "bin"
1010
},
1111
"dependencies": {
12+
"@modelcontextprotocol/sdk": "1.9.0",
1213
"wrangler": "^4.10.0",
1314
"zod": "^3.24.2"
1415
},

packages/mcp-observabiility/src/analytics-engine.ts

Lines changed: 69 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,8 @@ export class MetricsTracker {
1717

1818
logEvent(event: MetricsEvent): void {
1919
try {
20+
event.serverInfo = this.mcpServerInfo
2021
let dataPoint = event.toDataPoint()
21-
if (
22-
dataPoint.blobs &&
23-
dataPoint.blobs.length >= 2 &&
24-
dataPoint.blobs[0] === null &&
25-
dataPoint.blobs[1] === null
26-
) {
27-
dataPoint.blobs[0] = this.mcpServerInfo.name
28-
dataPoint.blobs[1] = this.mcpServerInfo.version
29-
} else {
30-
// we should never hit this because of type definitions and our mapBlobs utility function
31-
throw new MetricsError('Unexpected result from toDataPoint, could not add MCP server info. Was the `mapBlobs` utility function used?')
32-
}
3322
this.wae.writeDataPoint(dataPoint)
3423
} catch (e) {
3524
console.error(`Failed to log metrics event, ${e}`)
@@ -43,13 +32,77 @@ export class MetricsTracker {
4332
* Each event type is stored with a different indexId and has an associated class which
4433
* maps a more ergonomic event object to a ReadyAnalyticsEvent
4534
*/
46-
export interface MetricsEvent {
35+
export abstract class MetricsEvent {
36+
public _serverInfo: { name: string, version: string } | undefined
37+
set serverInfo(serverInfo: { name: string, version: string }) {
38+
this._serverInfo = serverInfo
39+
}
40+
41+
get serverInfo(): { name: string, version: string } {
42+
if (!this._serverInfo) {
43+
throw new Error("Server info not set")
44+
}
45+
return this._serverInfo
46+
}
47+
4748
/**
4849
* Output a valid AnalyticsEngineDataPoint. Use `mapBlobs` and `mapDoubles` to write well defined
4950
* analytics engine datapoints. The first and second blob entries are reserved for the MCP server name and
5051
* MCP server version.
5152
*/
52-
toDataPoint(): AnalyticsEngineDataPoint
53+
abstract toDataPoint(): AnalyticsEngineDataPoint
54+
55+
mapBlobs(blobs: Blobs): Array<string | null> {
56+
if (blobs.blob1 || blobs.blob2) {
57+
throw new MetricsError('Failed to map blobs, blob1 and blob2 are reserved for MCP server info')
58+
}
59+
// add placeholder blobs, filled in by the MetricsTracker later
60+
blobs.blob1 = this.serverInfo.name
61+
blobs.blob2 = this.serverInfo.version
62+
const blobsArray = new Array(Object.keys(blobs).length)
63+
for (const [key, value] of Object.entries(blobs)) {
64+
const match = key.match(/^blob(\d+)$/)
65+
if (match === null || match.length < 2) {
66+
// we should never hit this because of the typedefinitions above,
67+
// but this error is for safety
68+
throw new MetricsError('Failed to map blobs, invalid key')
69+
}
70+
const index = parseInt(match[1], 10)
71+
if (isNaN(index)) {
72+
// we should never hit this because of the typedefinitions above,
73+
// but this esrror is for safety
74+
throw new MetricsError('Failed to map blobs, invalid index')
75+
}
76+
if (index - 1 >= blobsArray.length) {
77+
throw new MetricsError('Failed to map blobs, missing blob')
78+
}
79+
blobsArray[index - 1] = value
80+
}
81+
return blobsArray
82+
}
83+
84+
mapDoubles(doubles: Doubles): number[] {
85+
const doublesArray = new Array(Object.keys(doubles).length)
86+
for (const [key, value] of Object.entries(doubles)) {
87+
const match = key.match(/^double(\d+)$/)
88+
if (match === null || match.length < 2) {
89+
// we should never hit this because of the typedefinitions above,
90+
// but this error is for safety
91+
throw new MetricsError(': Failed to map doubles, invalid key')
92+
}
93+
const index = parseInt(match[1], 10)
94+
if (isNaN(index)) {
95+
// we should never hit this because of the typedefinitions above,
96+
// but this error is for safety
97+
throw new MetricsError('Failed to map doubles, invalid index')
98+
}
99+
if (index - 1 >= doublesArray.length) {
100+
throw new MetricsError('Failed to map doubles, missing blob')
101+
}
102+
doublesArray[index - 1] = value
103+
}
104+
return doublesArray
105+
}
53106
}
54107

55108
export enum MetricsEventIndexIds {
@@ -92,7 +145,7 @@ type Range1To20 =
92145
// blob1 and blob2 are reserved for server name and version
93146
type Blobs = {
94147
[key in `blob${Range1To20}`]?: string | null
95-
} & { blob1?: never; blob2?: never }
148+
}
96149

97150
type Doubles = {
98151
[key in `double${Range1To20}`]?: number
@@ -103,69 +156,4 @@ export class MetricsError extends Error {
103156
super(message)
104157
this.name = 'MetricsError'
105158
}
106-
}
107-
108-
/**
109-
*
110-
* @param blobs Named blobs to map to an array. blob1 and blob2 are reserved.
111-
* @returns Array of blobs
112-
*/
113-
export function mapBlobs(blobs: Blobs): Array<string | null> {
114-
if (blobs.blob1 || blobs.blob2) {
115-
throw new MetricsError('Failed to map blobs, blob1 and blob2 are reserved for MCP server info')
116-
}
117-
// add placeholder blobs, filled in by the MetricsTracker later
118-
blobs = {
119-
...blobs,
120-
blob1: undefined,
121-
blob2: undefined
122-
}
123-
const blobsArray = new Array(Object.keys(blobs).length)
124-
for (const [key, value] of Object.entries(blobs)) {
125-
const match = key.match(/^blob(\d+)$/)
126-
if (match === null || match.length < 2) {
127-
// we should never hit this because of the typedefinitions above,
128-
// but this error is for safety
129-
throw new MetricsError('Failed to map blobs, invalid key')
130-
}
131-
const index = parseInt(match[1], 10)
132-
if (isNaN(index)) {
133-
// we should never hit this because of the typedefinitions above,
134-
// but this error is for safety
135-
throw new MetricsError('Failed to map blobs, invalid index')
136-
}
137-
if (index - 1 >= blobsArray.length) {
138-
throw new MetricsError('Failed to map blobs, missing blob')
139-
}
140-
blobsArray[index - 1] = value
141-
}
142-
return blobsArray
143-
}
144-
145-
/**
146-
*
147-
* @param doubles List of named doubles
148-
* @returns Doubles mapped to a valid WAE doubles array
149-
*/
150-
export function mapDoubles(doubles: Doubles): number[] {
151-
const doublesArray = new Array(Object.keys(doubles).length)
152-
for (const [key, value] of Object.entries(doubles)) {
153-
const match = key.match(/^double(\d+)$/)
154-
if (match === null || match.length < 2) {
155-
// we should never hit this because of the typedefinitions above,
156-
// but this error is for safety
157-
throw new MetricsError(': Failed to map doubles, invalid key')
158-
}
159-
const index = parseInt(match[1], 10)
160-
if (isNaN(index)) {
161-
// we should never hit this because of the typedefinitions above,
162-
// but this error is for safety
163-
throw new MetricsError('Failed to map doubles, invalid index')
164-
}
165-
if (index - 1 >= doublesArray.length) {
166-
throw new MetricsError('Failed to map doubles, missing blob')
167-
}
168-
doublesArray[index - 1] = value
169-
}
170-
return doublesArray
171-
}
159+
}

0 commit comments

Comments
 (0)