Skip to content

Commit 89f829d

Browse files
jimaekeyepokes
andauthored
Add MCPcat (#26)
* remove migration * add mcpcat * order fix * fix mcpcat in tests * Revert "fix mcpcat in tests" This reverts commit b432c6e. * update mcpcat * recommendations * fix inputs * chore: update wrangler to version 4.51.0 refactor: formatting code fix: use Number.isNaN for loss check in formatMeasurementSummary * beta3 * beta4 * beta5 * feat: add isOAuth property to Props and update authentication logic, fix for incorrect user type detection --------- Co-authored-by: eyepokes <[email protected]>
1 parent 9c158c5 commit 89f829d

File tree

12 files changed

+1150
-171
lines changed

12 files changed

+1150
-171
lines changed

package-lock.json

Lines changed: 1064 additions & 157 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,15 @@
2727
"patch-package": "^8.0.1",
2828
"typescript": "^5.9.3",
2929
"vitest": "^3.2.4",
30-
"wrangler": "^4.42.0"
30+
"wrangler": "^4.51.0"
3131
},
3232
"dependencies": {
3333
"@cloudflare/workers-oauth-provider": "^0.1.0",
3434
"@modelcontextprotocol/sdk": "^1.20.2",
3535
"agents": "^0.2.21",
3636
"globalping": "^0.2.0",
3737
"hono": "^4.10.6",
38+
"mcpcat": "^0.1.9-beta.5",
3839
"zod": "^3.25.76"
3940
}
4041
}

src/app.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ app.get("/auth/callback", async (c) => {
261261
state,
262262
userName: userData.username,
263263
isAuthenticated: true,
264+
isOAuth: true,
264265
},
265266
});
266267

src/config/constants.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,11 @@ export const CORS_CONFIG = {
133133
EXPOSE_HEADERS: "Mcp-Session-Id",
134134
MAX_AGE: 86400, // 24 hours
135135
};
136+
137+
/**
138+
* MCPcat configuration for analytics and telemetry
139+
*/
140+
export const MCPCAT_CONFIG = {
141+
ENABLED: true, // Set to false to disable MCPcat in dev environments
142+
PROJECT_ID_ENV_VAR: "MCPCAT_PROJECT_ID",
143+
} as const;

src/index.ts

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import { McpAgent } from "agents/mcp";
22
import OAuthProvider from "@cloudflare/workers-oauth-provider";
33
import { isAPITokenRequest, isValidAPIToken } from "./auth";
44
import app from "./app";
5-
import { MCP_CONFIG, OAUTH_CONFIG } from "./config";
5+
import { MCP_CONFIG, OAUTH_CONFIG, MCPCAT_CONFIG } from "./config";
66
import type { GlobalpingEnv, Props, State } from "./types";
77
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
88
import { z } from "zod";
99
import { registerGlobalpingTools } from "./mcp";
1010
import { sanitizeToken } from "./auth";
1111
import { validateOrigin, validateHost, getCorsOptionsForRequest } from "./lib";
12+
import * as mcpcat from "mcpcat";
1213

1314
export class GlobalpingMCP extends McpAgent<GlobalpingEnv, State, Props> {
1415
server = new McpServer({
@@ -51,6 +52,24 @@ Key guidelines:
5152
async init() {
5253
console.log("Initializing Globalping MCP...");
5354

55+
// Initialize MCPcat tracking if project ID is configured
56+
if (this.env.MCPCAT_PROJECT_ID && MCPCAT_CONFIG.ENABLED) {
57+
try {
58+
mcpcat.track(this.server, this.env.MCPCAT_PROJECT_ID, {
59+
// Identify users with generic labels
60+
identify: async () => {
61+
return this.getUserIdentification();
62+
},
63+
});
64+
65+
console.log("✓ MCPcat tracking initialized");
66+
} catch (error) {
67+
console.warn("⚠ MCPcat tracking initialization failed (non-fatal):", error);
68+
}
69+
} else {
70+
console.log("✗ MCPcat tracking disabled (no project ID or disabled in config)");
71+
}
72+
5473
// Register all the Globalping tools
5574
registerGlobalpingTools(this, () => {
5675
const raw = this.getToken() ?? "";
@@ -114,7 +133,6 @@ Key guidelines:
114133
annotations: {
115134
readOnlyHint: true,
116135
},
117-
inputSchema: {},
118136
outputSchema: {
119137
guide: z.string(),
120138
},
@@ -174,7 +192,6 @@ This approach allows for direct side-by-side comparisons of different targets us
174192
annotations: {
175193
readOnlyHint: true,
176194
},
177-
inputSchema: {},
178195
outputSchema: {
179196
helpText: z.string(),
180197
},
@@ -277,7 +294,6 @@ For more information, visit: https://www.globalping.io
277294
annotations: {
278295
readOnlyHint: true,
279296
},
280-
inputSchema: {},
281297
outputSchema: {
282298
authenticated: z.boolean(),
283299
status: z.string(),
@@ -359,6 +375,50 @@ For more information, visit: https://www.globalping.io
359375
return this.props?.accessToken;
360376
}
361377

378+
/**
379+
* Returns generic user identification for MCPcat analytics
380+
* Does not expose PII - uses generic labels only
381+
*/
382+
private getUserIdentification(): {
383+
userId: string;
384+
userName: string;
385+
userData: Record<string, any>;
386+
} {
387+
const isAuth = this.props?.isAuthenticated;
388+
const hasAPIToken = !this.props?.isOAuth;
389+
390+
// Check API token first (most specific) to prevent misclassification
391+
// as OAuth when API token flow sets isAuthenticated and userName
392+
if (hasAPIToken) {
393+
return {
394+
userId: "api_token_user",
395+
userName: "API Token User",
396+
userData: {
397+
authMethod: "api_token",
398+
},
399+
};
400+
}
401+
402+
if (isAuth && this.props?.userName) {
403+
return {
404+
userId: "oauth_user",
405+
userName: "OAuth User",
406+
userData: {
407+
authMethod: "oauth",
408+
clientId: this.props.clientId || "unknown",
409+
},
410+
};
411+
}
412+
413+
return {
414+
userId: "anonymous_user",
415+
userName: "Anonymous User",
416+
userData: {
417+
authMethod: "none",
418+
},
419+
};
420+
}
421+
362422
setIsAuthenticated(isAuthenticated: boolean): void {
363423
if (!this.props) return;
364424
this.props.isAuthenticated = isAuthenticated;
@@ -490,6 +550,7 @@ async function handleAPITokenRequest<
490550
userName: "API Token User",
491551
clientId: "",
492552
isAuthenticated: true,
553+
isOAuth: false,
493554
} satisfies Props;
494555

495556
if (pathname === MCP_CONFIG.ROUTES.SSE || pathname === MCP_CONFIG.ROUTES.SSE_MESSAGE) {

src/mcp/helpers.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,8 @@ export function formatMeasurementSummary(measurement: any): string {
136136
const avg = testResult.stats.avg !== undefined ? testResult.stats.avg : "N/A";
137137
const max = testResult.stats.max !== undefined ? testResult.stats.max : "N/A";
138138
const loss =
139-
typeof testResult.stats.loss === "number" && !isNaN(testResult.stats.loss)
139+
typeof testResult.stats.loss === "number" &&
140+
!Number.isNaN(testResult.stats.loss)
140141
? testResult.stats.loss.toFixed(2)
141142
: "N/A";
142143

src/mcp/tools.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,7 @@ import { maskToken } from "../auth";
1010
/**
1111
* Helper to wrap tool execution with error handling
1212
*/
13-
async function handleToolExecution(
14-
operation: () => Promise<any>,
15-
errorMessagePrefix: string
16-
) {
13+
async function handleToolExecution(operation: () => Promise<any>, errorMessagePrefix: string) {
1714
try {
1815
return await operation();
1916
} catch (error: any) {
@@ -561,7 +558,6 @@ export function registerGlobalpingTools(agent: GlobalpingMCP, getToken: () => st
561558
annotations: {
562559
readOnlyHint: true,
563560
},
564-
inputSchema: {},
565561
outputSchema: {
566562
totalProbes: z.number(),
567563
continents: z.array(
@@ -658,7 +654,6 @@ export function registerGlobalpingTools(agent: GlobalpingMCP, getToken: () => st
658654
annotations: {
659655
readOnlyHint: true,
660656
},
661-
inputSchema: {},
662657
outputSchema: {
663658
authenticated: z.boolean(),
664659
rateLimit: z.object({

src/types/app.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export type Props = {
55
userName: string;
66
clientId: string;
77
isAuthenticated: boolean;
8+
isOAuth: boolean;
89
};
910

1011
// Define custom state for storing previous measurements

src/types/globalping.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export interface GlobalpingEnv {
2424
GLOBALPING_CLIENT_ID: string;
2525
globalping_mcp_object: DurableObjectNamespace;
2626
OAUTH_KV: KVNamespace;
27+
MCPCAT_PROJECT_ID?: string;
2728
}
2829

2930
/**

vitest.workers.config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@ export default defineWorkersConfig({
1818
main: "./src/index.ts",
1919
miniflare: {
2020
compatibilityDate: "2025-03-10",
21-
compatibilityFlags: ["nodejs_compat"],
21+
compatibilityFlags: ["nodejs_compat_v2"],
2222
kvNamespaces: ["OAUTH_KV"],
2323
bindings: {
2424
GLOBALPING_CLIENT_ID: "test-client-id",
25+
// Disable MCPcat during tests - it adds a required 'context' parameter to all tools for agents to understand their use-cases
26+
MCPCAT_PROJECT_ID: "",
2527
},
2628
},
2729
isolatedStorage: false,

0 commit comments

Comments
 (0)