Skip to content

Commit 1e95431

Browse files
authored
fix: add edge runtime compatibility for Cloudflare Workers (#17)
* fix: add edge runtime compatibility for Cloudflare Workers Make the SDK compatible with edge runtimes (Cloudflare Workers, Vercel Edge, Deno Deploy) by gracefully handling missing Node.js APIs: - Lazy-load fs module for context_line extraction in exceptions.ts - Guard process.once signal handlers in eventQueue.ts - Lazy-load fs/os/path modules in logging.ts with console fallback - Guard process.cwd() calls which may not exist in edge environments - Update mcpcat-api dependency to 0.1.6 Add comprehensive test suite for edge runtime compatibility scenarios. * chore: bump version to 0.1.9 * fix: add Zod v3/v4 compatibility for MCP SDK 1.23.0 MCP SDK 1.23.0 now supports both Zod v3 and v4, which have different internal structures. This caused context parameter injection to fail. Changes: - Add zod-compat.ts with version-agnostic detection and utilities - Update context-parameters.ts to use zod-compat utilities - Update tracingV2.ts to use zod-compat for schema shape access - Update Zod dependency to support both v3 and v4 - Add comprehensive tests for zod-compat utilities Tested with MCP SDK versions 1.21.2 and 1.23.0.
1 parent ac96b3c commit 1e95431

File tree

10 files changed

+1005
-108
lines changed

10 files changed

+1005
-108
lines changed

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "mcpcat",
3-
"version": "0.1.8",
3+
"version": "0.1.9",
44
"description": "Analytics tool for MCP (Model Context Protocol) servers - tracks tool usage patterns and provides insights",
55
"type": "module",
66
"main": "dist/index.js",
@@ -54,7 +54,7 @@
5454
"packageManager": "pnpm@10.11.0",
5555
"devDependencies": {
5656
"@changesets/cli": "^2.29.4",
57-
"@modelcontextprotocol/sdk": "1.11",
57+
"@modelcontextprotocol/sdk": "~1.23.0",
5858
"@types/node": "^22.15.21",
5959
"@typescript-eslint/eslint-plugin": "^8.32.1",
6060
"@typescript-eslint/parser": "^8.32.1",
@@ -73,9 +73,9 @@
7373
},
7474
"dependencies": {
7575
"@opentelemetry/otlp-transformer": "^0.203.0",
76-
"mcpcat-api": "0.1.3",
76+
"mcpcat-api": "0.1.6",
7777
"redact-pii": "3.4.0",
78-
"zod": "3.25.30"
78+
"zod": "^3.25 || ^4.0"
7979
},
8080
"lint-staged": {
8181
"*.{ts,js}": [

pnpm-lock.yaml

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

src/modules/context-parameters.ts

Lines changed: 30 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,12 @@
11
import { RegisteredTool } from "../types";
22
import { z } from "zod";
33
import { DEFAULT_CONTEXT_PARAMETER_DESCRIPTION } from "./constants";
4-
5-
// Detect if something is a Zod schema (has _def and parse methods)
6-
function isZodSchema(schema: any): boolean {
7-
return (
8-
schema &&
9-
typeof schema === "object" &&
10-
"_def" in schema &&
11-
typeof schema.parse === "function"
12-
);
13-
}
14-
15-
// Detect if it's shorthand Zod syntax (object with z.* values)
16-
function isShorthandZodSyntax(schema: any): boolean {
17-
if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
18-
return false;
19-
}
20-
21-
// Check if any value is a Zod schema
22-
return Object.values(schema).some((value) => isZodSchema(value));
23-
}
4+
import {
5+
isZodSchema,
6+
isShorthandZodSyntax,
7+
schemaHasProperty,
8+
extendObjectSchema,
9+
} from "./zod-compat";
2410

2511
export function addContextParameterToTool(
2612
tool: RegisteredTool,
@@ -43,36 +29,25 @@ export function addContextParameterToTool(
4329
return modifiedTool;
4430
}
4531

46-
// Handle Zod z.object() schemas
32+
const contextDescription =
33+
customContextDescription || DEFAULT_CONTEXT_PARAMETER_DESCRIPTION;
34+
35+
// Handle Zod z.object() schemas (both v3 and v4)
4736
if (isZodSchema(modifiedTool.inputSchema)) {
4837
// Check if context already exists in Zod schema shape
49-
if (
50-
modifiedTool.inputSchema.shape &&
51-
"context" in modifiedTool.inputSchema.shape
52-
) {
38+
if (schemaHasProperty(modifiedTool.inputSchema, "context")) {
5339
return modifiedTool;
5440
}
55-
// It's a Zod schema, augment it with context
56-
const contextSchema = z.object({
57-
context: z
58-
.string()
59-
.describe(
60-
customContextDescription || DEFAULT_CONTEXT_PARAMETER_DESCRIPTION,
61-
),
62-
});
63-
64-
// Use extend to add context to the schema
65-
if (typeof modifiedTool.inputSchema.extend === "function") {
66-
modifiedTool.inputSchema = modifiedTool.inputSchema.extend(
67-
contextSchema.shape,
68-
);
69-
} else if (typeof modifiedTool.inputSchema.augment === "function") {
70-
modifiedTool.inputSchema =
71-
modifiedTool.inputSchema.augment(contextSchema);
72-
} else {
73-
// Fallback: merge with new z.object
74-
modifiedTool.inputSchema = contextSchema.merge(modifiedTool.inputSchema);
75-
}
41+
42+
// Extend the schema with context using our compat layer
43+
const contextShape = {
44+
context: z.string().describe(contextDescription),
45+
};
46+
47+
modifiedTool.inputSchema = extendObjectSchema(
48+
modifiedTool.inputSchema,
49+
contextShape,
50+
);
7651

7752
return modifiedTool;
7853
}
@@ -84,18 +59,15 @@ export function addContextParameterToTool(
8459
return modifiedTool;
8560
}
8661

87-
// Create a new Zod schema with context
88-
const contextField = z
89-
.string()
90-
.describe(
91-
customContextDescription || DEFAULT_CONTEXT_PARAMETER_DESCRIPTION,
92-
);
62+
// Extend using our compat layer (handles both v3 and v4)
63+
const contextShape = {
64+
context: z.string().describe(contextDescription),
65+
};
9366

94-
// Create new z.object with context and all original fields
95-
modifiedTool.inputSchema = z.object({
96-
context: contextField,
97-
...modifiedTool.inputSchema,
98-
});
67+
modifiedTool.inputSchema = extendObjectSchema(
68+
modifiedTool.inputSchema,
69+
contextShape,
70+
);
9971

10072
return modifiedTool;
10173
}
@@ -115,8 +87,7 @@ export function addContextParameterToTool(
11587

11688
modifiedTool.inputSchema.properties.context = {
11789
type: "string",
118-
description:
119-
customContextDescription || DEFAULT_CONTEXT_PARAMETER_DESCRIPTION,
90+
description: contextDescription,
12091
};
12192

12293
// Add context to required array if it exists

src/modules/eventQueue.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,9 +189,18 @@ class EventQueue {
189189
}
190190

191191
export const eventQueue = new EventQueue();
192-
process.once("SIGINT", () => eventQueue.destroy());
193-
process.once("SIGTERM", () => eventQueue.destroy());
194-
process.once("beforeExit", () => eventQueue.destroy());
192+
193+
// Register graceful shutdown handlers if available (Node.js only)
194+
// Edge environments (Cloudflare Workers, etc.) don't have process signals
195+
try {
196+
if (typeof process !== "undefined" && typeof process.once === "function") {
197+
process.once("SIGINT", () => eventQueue.destroy());
198+
process.once("SIGTERM", () => eventQueue.destroy());
199+
process.once("beforeExit", () => eventQueue.destroy());
200+
}
201+
} catch {
202+
// process.once not available in this environment - graceful shutdown handlers not registered
203+
}
195204

196205
export function setTelemetryManager(telemetryManager: TelemetryManager): void {
197206
eventQueue.setTelemetryManager(telemetryManager);

src/modules/exceptions.ts

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
import { ErrorData, StackFrame, ChainedErrorData } from "../types.js";
2-
import { readFileSync } from "fs";
2+
3+
// Lazy-loaded fs module for context_line extraction (Node.js only)
4+
// Edge environments don't have filesystem access
5+
let fsModule: typeof import("fs") | null = null;
6+
let fsInitAttempted = false;
7+
8+
function getFsSync(): typeof import("fs") | null {
9+
if (!fsInitAttempted) {
10+
fsInitAttempted = true;
11+
try {
12+
fsModule = require("fs");
13+
} catch {
14+
fsModule = null;
15+
}
16+
}
17+
return fsModule;
18+
}
319

420
// Maximum number of exceptions to capture in a cause chain
521
const MAX_EXCEPTION_CHAIN_DEPTH = 10;
@@ -120,8 +136,14 @@ function addContextToFrame(frame: StackFrame): StackFrame {
120136
return frame;
121137
}
122138

139+
// Get fs module lazily - returns null in edge environments
140+
const fs = getFsSync();
141+
if (!fs) {
142+
return frame; // File reading not available in this environment
143+
}
144+
123145
try {
124-
const source = readFileSync(frame.abs_path, "utf8");
146+
const source = fs.readFileSync(frame.abs_path, "utf8");
125147
const lines = source.split("\n");
126148
const lineIndex = frame.lineno - 1; // Convert to 0-based index
127149

@@ -635,9 +657,18 @@ function makeRelativePath(filename: string): string {
635657
// Step 7: Strip deployment-specific paths
636658
result = stripDeploymentPaths(result);
637659

638-
// Step 8: Strip current working directory
639-
const cwd = process.cwd();
640-
if (result.startsWith(cwd)) {
660+
// Step 8: Strip current working directory (if available)
661+
// process.cwd() may not be available in edge environments
662+
let cwd: string | null = null;
663+
try {
664+
if (typeof process !== "undefined" && typeof process.cwd === "function") {
665+
cwd = process.cwd();
666+
}
667+
} catch {
668+
// process.cwd() not available in this environment
669+
}
670+
671+
if (cwd && result.startsWith(cwd)) {
641672
result = result.substring(cwd.length + 1); // +1 to remove leading /
642673
}
643674

0 commit comments

Comments
 (0)