Skip to content

Commit cf883ae

Browse files
committed
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.
1 parent ac96b3c commit cf883ae

File tree

6 files changed

+474
-34
lines changed

6 files changed

+474
-34
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
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",
7878
"zod": "3.25.30"
7979
},

pnpm-lock.yaml

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

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

src/modules/logging.ts

Lines changed: 50 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,63 @@
1-
import { writeFileSync, appendFileSync, existsSync } from "fs";
2-
import { homedir } from "os";
3-
import { join } from "path";
4-
5-
// Safely determine log file path, handling environments where homedir() may return null
6-
let LOG_FILE: string | null = null;
7-
try {
8-
const home = homedir();
9-
if (home && home !== null && home !== undefined) {
10-
LOG_FILE = join(home, "mcpcat.log");
1+
// Lazy-loaded module references for Node.js file logging
2+
// These are loaded dynamically to support edge environments (Cloudflare Workers, etc.)
3+
let fsModule: typeof import("fs") | null = null;
4+
let logFilePath: string | null = null;
5+
let initAttempted = false;
6+
let useConsoleFallback = false;
7+
8+
/**
9+
* Attempts to initialize Node.js file logging.
10+
* Falls back to console.log in edge environments where fs/os modules are unavailable.
11+
*/
12+
function tryInitSync(): void {
13+
if (initAttempted) return;
14+
initAttempted = true;
15+
16+
try {
17+
// Use dynamic require for sync initialization
18+
// Works in Node.js, fails gracefully in Workers/edge environments
19+
const fs = require("fs");
20+
const os = require("os");
21+
const path = require("path");
22+
23+
const home = os.homedir?.();
24+
if (home) {
25+
fsModule = fs;
26+
logFilePath = path.join(home, "mcpcat.log");
27+
} else {
28+
// homedir() returned null/undefined - use console fallback
29+
useConsoleFallback = true;
30+
}
31+
} catch {
32+
// Module not available or homedir() not implemented - use console fallback
33+
useConsoleFallback = true;
34+
fsModule = null;
35+
logFilePath = null;
1136
}
12-
} catch {
13-
// If homedir() or join() fails, LOG_FILE remains null
14-
LOG_FILE = null;
1537
}
1638

1739
export function writeToLog(message: string): void {
18-
// Skip logging if we don't have a valid log file path
19-
if (!LOG_FILE) {
40+
tryInitSync();
41+
42+
const timestamp = new Date().toISOString();
43+
const logEntry = `[${timestamp}] ${message}`;
44+
45+
// Edge environment: use console.log as fallback
46+
if (useConsoleFallback) {
47+
console.log(`[mcpcat] ${logEntry}`);
2048
return;
2149
}
2250

23-
const timestamp = new Date().toISOString();
24-
const logEntry = `[${timestamp}] ${message}\n`;
51+
// Node.js environment: write to file
52+
if (!logFilePath || !fsModule) {
53+
return;
54+
}
2555

2656
try {
27-
if (!existsSync(LOG_FILE)) {
28-
writeFileSync(LOG_FILE, logEntry);
57+
if (!fsModule.existsSync(logFilePath)) {
58+
fsModule.writeFileSync(logFilePath, logEntry + "\n");
2959
} else {
30-
appendFileSync(LOG_FILE, logEntry);
60+
fsModule.appendFileSync(logFilePath, logEntry + "\n");
3161
}
3262
} catch {
3363
// Silently fail to avoid breaking the server

0 commit comments

Comments
 (0)