Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mcpcat",
"version": "0.1.8",
"version": "0.1.9",
"description": "Analytics tool for MCP (Model Context Protocol) servers - tracks tool usage patterns and provides insights",
"type": "module",
"main": "dist/index.js",
Expand Down Expand Up @@ -54,7 +54,7 @@
"packageManager": "pnpm@10.11.0",
"devDependencies": {
"@changesets/cli": "^2.29.4",
"@modelcontextprotocol/sdk": "1.11",
"@modelcontextprotocol/sdk": "~1.23.0",
"@types/node": "^22.15.21",
"@typescript-eslint/eslint-plugin": "^8.32.1",
"@typescript-eslint/parser": "^8.32.1",
Expand All @@ -73,9 +73,9 @@
},
"dependencies": {
"@opentelemetry/otlp-transformer": "^0.203.0",
"mcpcat-api": "0.1.3",
"mcpcat-api": "0.1.6",
"redact-pii": "3.4.0",
"zod": "3.25.30"
"zod": "^3.25 || ^4.0"
},
"lint-staged": {
"*.{ts,js}": [
Expand Down
55 changes: 39 additions & 16 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

89 changes: 30 additions & 59 deletions src/modules/context-parameters.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,12 @@
import { RegisteredTool } from "../types";
import { z } from "zod";
import { DEFAULT_CONTEXT_PARAMETER_DESCRIPTION } from "./constants";

// Detect if something is a Zod schema (has _def and parse methods)
function isZodSchema(schema: any): boolean {
return (
schema &&
typeof schema === "object" &&
"_def" in schema &&
typeof schema.parse === "function"
);
}

// Detect if it's shorthand Zod syntax (object with z.* values)
function isShorthandZodSyntax(schema: any): boolean {
if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
return false;
}

// Check if any value is a Zod schema
return Object.values(schema).some((value) => isZodSchema(value));
}
import {
isZodSchema,
isShorthandZodSyntax,
schemaHasProperty,
extendObjectSchema,
} from "./zod-compat";

export function addContextParameterToTool(
tool: RegisteredTool,
Expand All @@ -43,36 +29,25 @@ export function addContextParameterToTool(
return modifiedTool;
}

// Handle Zod z.object() schemas
const contextDescription =
customContextDescription || DEFAULT_CONTEXT_PARAMETER_DESCRIPTION;

// Handle Zod z.object() schemas (both v3 and v4)
if (isZodSchema(modifiedTool.inputSchema)) {
// Check if context already exists in Zod schema shape
if (
modifiedTool.inputSchema.shape &&
"context" in modifiedTool.inputSchema.shape
) {
if (schemaHasProperty(modifiedTool.inputSchema, "context")) {
return modifiedTool;
}
// It's a Zod schema, augment it with context
const contextSchema = z.object({
context: z
.string()
.describe(
customContextDescription || DEFAULT_CONTEXT_PARAMETER_DESCRIPTION,
),
});

// Use extend to add context to the schema
if (typeof modifiedTool.inputSchema.extend === "function") {
modifiedTool.inputSchema = modifiedTool.inputSchema.extend(
contextSchema.shape,
);
} else if (typeof modifiedTool.inputSchema.augment === "function") {
modifiedTool.inputSchema =
modifiedTool.inputSchema.augment(contextSchema);
} else {
// Fallback: merge with new z.object
modifiedTool.inputSchema = contextSchema.merge(modifiedTool.inputSchema);
}

// Extend the schema with context using our compat layer
const contextShape = {
context: z.string().describe(contextDescription),
};

modifiedTool.inputSchema = extendObjectSchema(
modifiedTool.inputSchema,
contextShape,
);

return modifiedTool;
}
Expand All @@ -84,18 +59,15 @@ export function addContextParameterToTool(
return modifiedTool;
}

// Create a new Zod schema with context
const contextField = z
.string()
.describe(
customContextDescription || DEFAULT_CONTEXT_PARAMETER_DESCRIPTION,
);
// Extend using our compat layer (handles both v3 and v4)
const contextShape = {
context: z.string().describe(contextDescription),
};

// Create new z.object with context and all original fields
modifiedTool.inputSchema = z.object({
context: contextField,
...modifiedTool.inputSchema,
});
modifiedTool.inputSchema = extendObjectSchema(
modifiedTool.inputSchema,
contextShape,
);

return modifiedTool;
}
Expand All @@ -115,8 +87,7 @@ export function addContextParameterToTool(

modifiedTool.inputSchema.properties.context = {
type: "string",
description:
customContextDescription || DEFAULT_CONTEXT_PARAMETER_DESCRIPTION,
description: contextDescription,
};

// Add context to required array if it exists
Expand Down
15 changes: 12 additions & 3 deletions src/modules/eventQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,18 @@ class EventQueue {
}

export const eventQueue = new EventQueue();
process.once("SIGINT", () => eventQueue.destroy());
process.once("SIGTERM", () => eventQueue.destroy());
process.once("beforeExit", () => eventQueue.destroy());

// Register graceful shutdown handlers if available (Node.js only)
// Edge environments (Cloudflare Workers, etc.) don't have process signals
try {
if (typeof process !== "undefined" && typeof process.once === "function") {
process.once("SIGINT", () => eventQueue.destroy());
process.once("SIGTERM", () => eventQueue.destroy());
process.once("beforeExit", () => eventQueue.destroy());
}
} catch {
// process.once not available in this environment - graceful shutdown handlers not registered
}

export function setTelemetryManager(telemetryManager: TelemetryManager): void {
eventQueue.setTelemetryManager(telemetryManager);
Expand Down
41 changes: 36 additions & 5 deletions src/modules/exceptions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
import { ErrorData, StackFrame, ChainedErrorData } from "../types.js";
import { readFileSync } from "fs";

// Lazy-loaded fs module for context_line extraction (Node.js only)
// Edge environments don't have filesystem access
let fsModule: typeof import("fs") | null = null;
let fsInitAttempted = false;

function getFsSync(): typeof import("fs") | null {
if (!fsInitAttempted) {
fsInitAttempted = true;
try {
fsModule = require("fs");
} catch {
fsModule = null;
}
}
return fsModule;
}

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

// Get fs module lazily - returns null in edge environments
const fs = getFsSync();
if (!fs) {
return frame; // File reading not available in this environment
}

try {
const source = readFileSync(frame.abs_path, "utf8");
const source = fs.readFileSync(frame.abs_path, "utf8");
const lines = source.split("\n");
const lineIndex = frame.lineno - 1; // Convert to 0-based index

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

// Step 8: Strip current working directory
const cwd = process.cwd();
if (result.startsWith(cwd)) {
// Step 8: Strip current working directory (if available)
// process.cwd() may not be available in edge environments
let cwd: string | null = null;
try {
if (typeof process !== "undefined" && typeof process.cwd === "function") {
cwd = process.cwd();
}
} catch {
// process.cwd() not available in this environment
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe add a writeToLog here?

}

if (cwd && result.startsWith(cwd)) {
result = result.substring(cwd.length + 1); // +1 to remove leading /
}

Expand Down
Loading