toad-eye 2.5.0 → 2.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,48 @@
1
+ /**
2
+ * MCP Server Middleware — wraps McpServer handler registration
3
+ * to automatically instrument tool calls, resource reads, and prompt gets.
4
+ *
5
+ * Uses Wrapper Pattern: intercepts .tool(), .resource(), .prompt() methods
6
+ * on the McpServer instance. Each handler is wrapped with OTel span creation.
7
+ *
8
+ * Why not monkey-patching: MCP SDK uses private class members (_requestHandlers)
9
+ * and strict TypeScript. Wrapping the public API is the stable path.
10
+ */
11
+ import type { ToadMcpOptions } from "./types.js";
12
+ export declare function toadEyeMiddleware(server: any, options?: ToadMcpOptions): void;
13
+ /**
14
+ * Trace a sampling/createMessage call (server → client LLM request).
15
+ *
16
+ * Sampling is an outgoing request from the MCP server to the client,
17
+ * asking the client's LLM to generate a response. This cannot be
18
+ * auto-intercepted via wrapper pattern, so wrap manually.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * import { traceSampling } from "toad-eye/mcp";
23
+ *
24
+ * server.tool("summarize", { text: z.string() }, async ({ text }, ctx) => {
25
+ * const response = await traceSampling(
26
+ * () => ctx.mcpReq.requestSampling({
27
+ * messages: [{ role: "user", content: { type: "text", text } }],
28
+ * maxTokens: 500,
29
+ * }),
30
+ * { model: "gpt-4" }
31
+ * );
32
+ * return { content: [{ type: "text", text: response.content.text }] };
33
+ * });
34
+ * ```
35
+ */
36
+ export declare function traceSampling<T>(fn: () => Promise<T>, options: TraceSamplingOptions): Promise<T>;
37
+ export interface TraceSamplingOptions {
38
+ /** The model being requested for sampling. */
39
+ readonly model?: string | undefined;
40
+ /** Max tokens requested. */
41
+ readonly maxTokens?: number | undefined;
42
+ /** MCP server name for span attributes. */
43
+ readonly serverName?: string | undefined;
44
+ /** MCP server version for span attributes. */
45
+ readonly serverVersion?: string | undefined;
46
+ /** Parent OTel context. */
47
+ readonly parentContext?: import("@opentelemetry/api").Context | undefined;
48
+ }
@@ -0,0 +1,253 @@
1
+ /**
2
+ * MCP Server Middleware — wraps McpServer handler registration
3
+ * to automatically instrument tool calls, resource reads, and prompt gets.
4
+ *
5
+ * Uses Wrapper Pattern: intercepts .tool(), .resource(), .prompt() methods
6
+ * on the McpServer instance. Each handler is wrapped with OTel span creation.
7
+ *
8
+ * Why not monkey-patching: MCP SDK uses private class members (_requestHandlers)
9
+ * and strict TypeScript. Wrapping the public API is the stable path.
10
+ */
11
+ import { context, diag, DiagConsoleLogger, DiagLogLevel, } from "@opentelemetry/api";
12
+ import { extractContextFromMeta } from "./context.js";
13
+ import { startToolSpan, startResourceSpan, startPromptSpan, startSamplingSpan, endSpanSuccess, endSpanError, } from "./spans.js";
14
+ import { recordMcpToolCall, recordMcpToolError, recordMcpResourceRead, recordMcpSessionStart, } from "./metrics.js";
15
+ const DEFAULT_MAX_PAYLOAD_SIZE = 4096;
16
+ function truncate(value, maxBytes) {
17
+ if (value.length <= maxBytes)
18
+ return value;
19
+ return value.slice(0, maxBytes) + "...[truncated]";
20
+ }
21
+ function redactObject(obj, keys) {
22
+ if (typeof obj !== "object" || obj === null)
23
+ return {};
24
+ const result = {};
25
+ for (const [k, v] of Object.entries(obj)) {
26
+ result[k] = keys.includes(k) ? "[REDACTED]" : v;
27
+ }
28
+ return result;
29
+ }
30
+ /**
31
+ * Instrument an MCP server with OpenTelemetry tracing.
32
+ *
33
+ * Wraps .tool(), .resource(), .prompt() registration methods so every
34
+ * handler execution produces an OTel span with correct GenAI semconv attributes.
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * import { McpServer } from "@modelcontextprotocol/sdk/server/index.js";
39
+ * import { toadEyeMiddleware } from "toad-eye/mcp";
40
+ *
41
+ * const server = new McpServer({ name: "my-server", version: "1.0.0" });
42
+ * toadEyeMiddleware(server);
43
+ *
44
+ * server.tool("calculate", { expression: z.string() }, async ({ expression }) => {
45
+ * return { content: [{ type: "text", text: String(eval(expression)) }] };
46
+ * });
47
+ * ```
48
+ */
49
+ /**
50
+ * Redirect OTel diagnostics to stderr to avoid polluting stdout.
51
+ * In stdio MCP transport, stdout IS the JSON-RPC channel —
52
+ * any stray console.log or OTel diagnostic would crash the connection.
53
+ */
54
+ function ensureStdioSafe() {
55
+ const stderrLogger = new DiagConsoleLogger();
56
+ // Override the logger to use stderr-only methods
57
+ const safeLogger = {
58
+ verbose: (...args) => stderrLogger.verbose(String(args[0])),
59
+ debug: (...args) => stderrLogger.debug(String(args[0])),
60
+ info: (...args) => stderrLogger.info(String(args[0])),
61
+ warn: (...args) => stderrLogger.warn(String(args[0])),
62
+ error: (...args) => stderrLogger.error(String(args[0])),
63
+ };
64
+ diag.setLogger(safeLogger, DiagLogLevel.WARN);
65
+ }
66
+ export function toadEyeMiddleware(
67
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
68
+ server, options = {}) {
69
+ ensureStdioSafe();
70
+ recordMcpSessionStart();
71
+ const serverName = server.name ?? server._name ?? "mcp-server";
72
+ const serverVersion = server.version ?? server._version ?? "unknown";
73
+ const recordInputs = options.recordInputs ?? false;
74
+ const recordOutputs = options.recordOutputs ?? false;
75
+ const redactKeys = options.redactKeys ?? [];
76
+ const maxPayload = options.maxPayloadSize ?? DEFAULT_MAX_PAYLOAD_SIZE;
77
+ const propagate = options.propagateContext ?? true;
78
+ const spanOpts = { serverName, serverVersion };
79
+ // --- Wrap .tool() ---
80
+ const originalTool = server.tool.bind(server);
81
+ server.tool = function wrappedTool(name, ...rest) {
82
+ // Find the handler — last function argument
83
+ const handlerIndex = rest.findIndex((arg) => typeof arg === "function");
84
+ if (handlerIndex === -1) {
85
+ return originalTool(name, ...rest);
86
+ }
87
+ const originalHandler = rest[handlerIndex];
88
+ rest[handlerIndex] = async function wrappedHandler(...handlerArgs) {
89
+ // Extract _meta for context propagation
90
+ const firstArg = handlerArgs[0];
91
+ const meta = propagate
92
+ ? firstArg?._meta
93
+ : undefined;
94
+ const parentCtx = propagate
95
+ ? extractContextFromMeta(meta)
96
+ : context.active();
97
+ const span = startToolSpan(name, {
98
+ ...spanOpts,
99
+ parentContext: parentCtx,
100
+ });
101
+ // Record inputs if enabled
102
+ if (recordInputs && firstArg) {
103
+ const sanitized = redactKeys.length > 0 ? redactObject(firstArg, redactKeys) : firstArg;
104
+ const serialized = truncate(JSON.stringify(sanitized), maxPayload);
105
+ span.setAttribute("gen_ai.tool.call.arguments", serialized);
106
+ }
107
+ // Custom hook
108
+ if (options.onToolCall) {
109
+ options.onToolCall(span, name, firstArg);
110
+ }
111
+ const start = performance.now();
112
+ try {
113
+ const result = await context.with(context.active(), () => originalHandler(...handlerArgs));
114
+ const durationMs = performance.now() - start;
115
+ // Record outputs if enabled
116
+ if (recordOutputs && result) {
117
+ const serialized = truncate(JSON.stringify(result), maxPayload);
118
+ span.setAttribute("gen_ai.tool.call.result", serialized);
119
+ }
120
+ endSpanSuccess(span);
121
+ recordMcpToolCall(name, durationMs, "success");
122
+ return result;
123
+ }
124
+ catch (error) {
125
+ const durationMs = performance.now() - start;
126
+ const errorType = error instanceof Error ? error.constructor.name : "UnknownError";
127
+ endSpanError(span, error);
128
+ recordMcpToolCall(name, durationMs, "error");
129
+ recordMcpToolError(name, errorType);
130
+ throw error;
131
+ }
132
+ };
133
+ return originalTool(name, ...rest);
134
+ };
135
+ // --- Wrap .resource() ---
136
+ const originalResource = server.resource?.bind(server);
137
+ if (originalResource) {
138
+ server.resource = function wrappedResource(name, ...rest) {
139
+ const handlerIndex = rest.findIndex((arg) => typeof arg === "function");
140
+ if (handlerIndex === -1) {
141
+ return originalResource(name, ...rest);
142
+ }
143
+ const originalHandler = rest[handlerIndex];
144
+ rest[handlerIndex] = async function wrappedHandler(...handlerArgs) {
145
+ const uri = handlerArgs[0]?.uri ?? name;
146
+ const parentCtx = propagate
147
+ ? extractContextFromMeta(handlerArgs[0]?._meta)
148
+ : context.active();
149
+ const span = startResourceSpan(uri, {
150
+ ...spanOpts,
151
+ parentContext: parentCtx,
152
+ });
153
+ if (options.onResourceRead) {
154
+ options.onResourceRead(span, uri);
155
+ }
156
+ try {
157
+ const result = await originalHandler(...handlerArgs);
158
+ endSpanSuccess(span);
159
+ recordMcpResourceRead(uri);
160
+ return result;
161
+ }
162
+ catch (error) {
163
+ endSpanError(span, error);
164
+ throw error;
165
+ }
166
+ };
167
+ return originalResource(name, ...rest);
168
+ };
169
+ }
170
+ // --- Wrap .prompt() ---
171
+ const originalPrompt = server.prompt?.bind(server);
172
+ if (originalPrompt) {
173
+ server.prompt = function wrappedPrompt(name, ...rest) {
174
+ const handlerIndex = rest.findIndex((arg) => typeof arg === "function");
175
+ if (handlerIndex === -1) {
176
+ return originalPrompt(name, ...rest);
177
+ }
178
+ const originalHandler = rest[handlerIndex];
179
+ rest[handlerIndex] = async function wrappedHandler(...handlerArgs) {
180
+ const parentCtx = propagate
181
+ ? extractContextFromMeta(handlerArgs[0]?._meta)
182
+ : context.active();
183
+ const span = startPromptSpan(name, {
184
+ ...spanOpts,
185
+ parentContext: parentCtx,
186
+ });
187
+ try {
188
+ const result = await originalHandler(...handlerArgs);
189
+ endSpanSuccess(span);
190
+ return result;
191
+ }
192
+ catch (error) {
193
+ endSpanError(span, error);
194
+ throw error;
195
+ }
196
+ };
197
+ return originalPrompt(name, ...rest);
198
+ };
199
+ }
200
+ }
201
+ /**
202
+ * Trace a sampling/createMessage call (server → client LLM request).
203
+ *
204
+ * Sampling is an outgoing request from the MCP server to the client,
205
+ * asking the client's LLM to generate a response. This cannot be
206
+ * auto-intercepted via wrapper pattern, so wrap manually.
207
+ *
208
+ * @example
209
+ * ```ts
210
+ * import { traceSampling } from "toad-eye/mcp";
211
+ *
212
+ * server.tool("summarize", { text: z.string() }, async ({ text }, ctx) => {
213
+ * const response = await traceSampling(
214
+ * () => ctx.mcpReq.requestSampling({
215
+ * messages: [{ role: "user", content: { type: "text", text } }],
216
+ * maxTokens: 500,
217
+ * }),
218
+ * { model: "gpt-4" }
219
+ * );
220
+ * return { content: [{ type: "text", text: response.content.text }] };
221
+ * });
222
+ * ```
223
+ */
224
+ export async function traceSampling(fn, options) {
225
+ const model = options.model ?? "unknown";
226
+ const span = startSamplingSpan(model, {
227
+ serverName: options.serverName ?? "mcp-server",
228
+ serverVersion: options.serverVersion ?? "unknown",
229
+ parentContext: options.parentContext,
230
+ });
231
+ if (options.maxTokens !== undefined) {
232
+ span.setAttribute("gen_ai.request.max_tokens", options.maxTokens);
233
+ }
234
+ const start = performance.now();
235
+ try {
236
+ const result = await fn();
237
+ const durationMs = performance.now() - start;
238
+ // Extract token usage from response if available
239
+ const response = result;
240
+ if (response && typeof response === "object") {
241
+ if (response["model"]) {
242
+ span.setAttribute("gen_ai.response.model", String(response["model"]));
243
+ }
244
+ }
245
+ span.setAttribute("gen_ai.mcp.sampling.duration_ms", durationMs);
246
+ endSpanSuccess(span);
247
+ return result;
248
+ }
249
+ catch (error) {
250
+ endSpanError(span, error);
251
+ throw error;
252
+ }
253
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * OTel span creation for MCP operations.
3
+ *
4
+ * Maps MCP methods to OTel GenAI semantic conventions:
5
+ * - tools/call → execute_tool {tool_name}
6
+ * - resources/read → retrieval {uri}
7
+ * - prompts/get → prompt {name}
8
+ * - sampling/createMessage → chat {model}
9
+ */
10
+ import { type Context } from "@opentelemetry/api";
11
+ declare const tracer: import("@opentelemetry/api").Tracer;
12
+ export interface SpanOptions {
13
+ readonly serverName: string;
14
+ readonly serverVersion: string;
15
+ readonly parentContext: Context;
16
+ }
17
+ export declare function startToolSpan(toolName: string, options: SpanOptions): import("@opentelemetry/api").Span;
18
+ export declare function startResourceSpan(uri: string, options: SpanOptions): import("@opentelemetry/api").Span;
19
+ export declare function startPromptSpan(promptName: string, options: SpanOptions): import("@opentelemetry/api").Span;
20
+ export interface SamplingSpanOptions {
21
+ readonly serverName: string;
22
+ readonly serverVersion: string;
23
+ readonly parentContext?: Context | undefined;
24
+ }
25
+ export declare function startSamplingSpan(model: string, options: SamplingSpanOptions): import("@opentelemetry/api").Span;
26
+ export declare function endSpanSuccess(span: ReturnType<typeof tracer.startSpan>): void;
27
+ export declare function endSpanError(span: ReturnType<typeof tracer.startSpan>, error: unknown): void;
28
+ export {};
@@ -0,0 +1,67 @@
1
+ /**
2
+ * OTel span creation for MCP operations.
3
+ *
4
+ * Maps MCP methods to OTel GenAI semantic conventions:
5
+ * - tools/call → execute_tool {tool_name}
6
+ * - resources/read → retrieval {uri}
7
+ * - prompts/get → prompt {name}
8
+ * - sampling/createMessage → chat {model}
9
+ */
10
+ import { trace, SpanStatusCode, SpanKind, } from "@opentelemetry/api";
11
+ const TRACER_NAME = "toad-eye-mcp";
12
+ const tracer = trace.getTracer(TRACER_NAME);
13
+ export function startToolSpan(toolName, options) {
14
+ return tracer.startSpan(`execute_tool ${toolName}`, {
15
+ kind: SpanKind.INTERNAL,
16
+ attributes: {
17
+ "gen_ai.operation.name": "execute_tool",
18
+ "gen_ai.tool.name": toolName,
19
+ "mcp.server.name": options.serverName,
20
+ "mcp.server.version": options.serverVersion,
21
+ },
22
+ }, options.parentContext);
23
+ }
24
+ export function startResourceSpan(uri, options) {
25
+ return tracer.startSpan(`retrieval ${uri}`, {
26
+ kind: SpanKind.INTERNAL,
27
+ attributes: {
28
+ "gen_ai.operation.name": "retrieval",
29
+ "gen_ai.data_source.id": uri,
30
+ "mcp.server.name": options.serverName,
31
+ "mcp.server.version": options.serverVersion,
32
+ },
33
+ }, options.parentContext);
34
+ }
35
+ export function startPromptSpan(promptName, options) {
36
+ return tracer.startSpan(`prompt ${promptName}`, {
37
+ kind: SpanKind.INTERNAL,
38
+ attributes: {
39
+ "gen_ai.operation.name": "prompt",
40
+ "gen_ai.prompt.name": promptName,
41
+ "mcp.server.name": options.serverName,
42
+ "mcp.server.version": options.serverVersion,
43
+ },
44
+ }, options.parentContext);
45
+ }
46
+ export function startSamplingSpan(model, options) {
47
+ return tracer.startSpan(`chat ${model}`, {
48
+ kind: SpanKind.CLIENT,
49
+ attributes: {
50
+ "gen_ai.operation.name": "chat",
51
+ "gen_ai.request.model": model,
52
+ "mcp.server.name": options.serverName,
53
+ "mcp.server.version": options.serverVersion,
54
+ },
55
+ }, options.parentContext);
56
+ }
57
+ export function endSpanSuccess(span) {
58
+ span.setStatus({ code: SpanStatusCode.OK });
59
+ span.end();
60
+ }
61
+ export function endSpanError(span, error) {
62
+ const message = error instanceof Error ? error.message : String(error);
63
+ const errorType = error instanceof Error ? error.constructor.name : "UnknownError";
64
+ span.setStatus({ code: SpanStatusCode.ERROR, message });
65
+ span.setAttribute("error.type", errorType);
66
+ span.end();
67
+ }
@@ -0,0 +1,34 @@
1
+ import type { Span } from "@opentelemetry/api";
2
+ /** Configuration for toadEyeMiddleware(). */
3
+ export interface ToadMcpOptions {
4
+ /** Service name for OTel resource. Defaults to MCP server name. */
5
+ readonly serviceName?: string | undefined;
6
+ /** OTLP endpoint. Uses existing OTel config if initObservability() was called. */
7
+ readonly endpoint?: string | undefined;
8
+ /** Record tool call arguments in spans. Default: false (privacy). */
9
+ readonly recordInputs?: boolean | undefined;
10
+ /** Record tool call results in spans. Default: false (privacy). */
11
+ readonly recordOutputs?: boolean | undefined;
12
+ /** Keys to redact from recorded arguments. */
13
+ readonly redactKeys?: readonly string[] | undefined;
14
+ /** Max payload size in bytes before truncation. Default: 4096. */
15
+ readonly maxPayloadSize?: number | undefined;
16
+ /** Extract W3C traceparent from _meta field. Default: true. */
17
+ readonly propagateContext?: boolean | undefined;
18
+ /** Hook called on each tool call span. Add custom attributes here. */
19
+ readonly onToolCall?: ((span: Span, toolName: string, args: unknown) => void) | undefined;
20
+ /** Hook called on each resource read span. */
21
+ readonly onResourceRead?: ((span: Span, uri: string) => void) | undefined;
22
+ }
23
+ /** OTel attributes set on MCP spans. */
24
+ export interface McpSpanAttributes {
25
+ readonly "gen_ai.operation.name": string;
26
+ readonly "gen_ai.tool.name"?: string;
27
+ readonly "gen_ai.tool.call.id"?: string;
28
+ readonly "gen_ai.data_source.id"?: string;
29
+ readonly "gen_ai.prompt.name"?: string;
30
+ readonly "mcp.server.name": string;
31
+ readonly "mcp.server.version": string;
32
+ readonly "mcp.transport"?: "stdio" | "sse" | "streamable-http";
33
+ readonly "mcp.session.id"?: string;
34
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -38,6 +38,8 @@ export interface ToadEyeConfig {
38
38
  readonly redactDefaults?: boolean | undefined;
39
39
  /** Log what was masked to console (for debugging redaction config). No data is sent externally. */
40
40
  readonly auditMasking?: boolean | undefined;
41
+ /** Record content for only this fraction of calls (0.0–1.0). E.g., 0.01 = 1%. Enables debugging without full storage/privacy cost. */
42
+ readonly contentSamplingRate?: number | undefined;
41
43
  /** Configure trace sampling. Tail sampling happens in OTel Collector, not SDK. */
42
44
  readonly sampling?: SamplingConfig | undefined;
43
45
  readonly instrument?: readonly InstrumentTarget[] | undefined;
@@ -47,12 +49,12 @@ export interface ToadEyeConfig {
47
49
  readonly onBudgetExceeded?: BudgetExceededMode | undefined;
48
50
  /** Callback for 'downgrade' mode — receives original provider/model, returns replacement. */
49
51
  readonly downgradeCallback?: DowngradeCallback | undefined;
50
- /** Warn or block when context window utilization exceeds thresholds. */
52
+ /** Warn when context window utilization exceeds thresholds (post-call). */
51
53
  readonly contextGuard?: {
52
54
  /** Warn when utilization exceeds this ratio (0.0–1.0). Default: none. */
53
55
  readonly warnAt?: number | undefined;
54
- /** Block (throw ToadContextExceededError) when utilization exceeds this ratio. Default: none. */
55
- readonly blockAt?: number | undefined;
56
+ /** Alert (span event + metric + warning) when utilization exceeds this ratio. Default: none. */
57
+ readonly alertAt?: number | undefined;
56
58
  } | undefined;
57
59
  /** Static session ID — all spans will carry this value as `session.id`. */
58
60
  readonly sessionId?: string | undefined;
@@ -26,6 +26,11 @@ export declare const GEN_AI_METRICS: {
26
26
  readonly RESPONSE_LATENCY_PER_TOKEN: "gen_ai.toad_eye.response.latency_per_token";
27
27
  readonly CONTEXT_UTILIZATION: "gen_ai.toad_eye.context_utilization";
28
28
  readonly CONTEXT_BLOCKED: "gen_ai.toad_eye.context.blocked";
29
+ readonly MCP_TOOL_DURATION: "gen_ai.mcp.tool.duration";
30
+ readonly MCP_TOOL_CALLS: "gen_ai.mcp.tool.calls";
31
+ readonly MCP_TOOL_ERRORS: "gen_ai.mcp.tool.errors";
32
+ readonly MCP_RESOURCE_READS: "gen_ai.mcp.resource.reads";
33
+ readonly MCP_SESSION_ACTIVE: "gen_ai.mcp.session.active";
29
34
  };
30
35
  /** @deprecated Use GEN_AI_METRICS instead. Kept for backward compatibility. */
31
36
  export declare const LLM_METRICS: {
@@ -33,6 +33,12 @@ export const GEN_AI_METRICS = {
33
33
  // Context utilization
34
34
  CONTEXT_UTILIZATION: "gen_ai.toad_eye.context_utilization",
35
35
  CONTEXT_BLOCKED: "gen_ai.toad_eye.context.blocked",
36
+ // MCP metrics
37
+ MCP_TOOL_DURATION: "gen_ai.mcp.tool.duration",
38
+ MCP_TOOL_CALLS: "gen_ai.mcp.tool.calls",
39
+ MCP_TOOL_ERRORS: "gen_ai.mcp.tool.errors",
40
+ MCP_RESOURCE_READS: "gen_ai.mcp.resource.reads",
41
+ MCP_SESSION_ACTIVE: "gen_ai.mcp.session.active",
36
42
  };
37
43
  /** @deprecated Use GEN_AI_METRICS instead. Kept for backward compatibility. */
38
44
  export const LLM_METRICS = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toad-eye",
3
- "version": "2.5.0",
3
+ "version": "2.6.1",
4
4
  "description": "Auto-instrumentation, cost tracking, budget guards, and observability for LLM services",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -30,6 +30,11 @@
30
30
  "types": "./dist/vercel.d.ts",
31
31
  "import": "./dist/vercel.js",
32
32
  "default": "./dist/vercel.js"
33
+ },
34
+ "./mcp": {
35
+ "types": "./dist/mcp/index.d.ts",
36
+ "import": "./dist/mcp/index.js",
37
+ "default": "./dist/mcp/index.js"
33
38
  }
34
39
  },
35
40
  "bin": {
@@ -87,7 +92,8 @@
87
92
  "@anthropic-ai/sdk": ">=0.30.0",
88
93
  "@google/generative-ai": ">=0.20.0",
89
94
  "nodemailer": ">=8.0.0",
90
- "openai": ">=4.0.0"
95
+ "openai": ">=4.0.0",
96
+ "@modelcontextprotocol/sdk": ">=1.0.0"
91
97
  },
92
98
  "peerDependenciesMeta": {
93
99
  "openai": {
@@ -101,6 +107,9 @@
101
107
  },
102
108
  "nodemailer": {
103
109
  "optional": true
110
+ },
111
+ "@modelcontextprotocol/sdk": {
112
+ "optional": true
104
113
  }
105
114
  },
106
115
  "devDependencies": {
@@ -1,6 +1,6 @@
1
1
  services:
2
2
  collector:
3
- image: otel/opentelemetry-collector-contrib:latest
3
+ image: otel/opentelemetry-collector-contrib:0.102.0
4
4
  volumes:
5
5
  - ./otel-collector.yml:/etc/otelcol-contrib/config.yaml
6
6
  ports:
@@ -10,14 +10,14 @@ services:
10
10
  - prometheus
11
11
 
12
12
  prometheus:
13
- image: prom/prometheus:latest
13
+ image: prom/prometheus:v2.53.0
14
14
  volumes:
15
15
  - ./prometheus.yml:/etc/prometheus/prometheus.yml
16
16
  ports:
17
17
  - "9090:9090"
18
18
 
19
19
  jaeger:
20
- image: jaegertracing/all-in-one:latest
20
+ image: jaegertracing/all-in-one:1.58
21
21
  environment:
22
22
  - COLLECTOR_OTLP_ENABLED=true
23
23
  ports:
@@ -25,7 +25,7 @@ services:
25
25
  - "4317:4317"
26
26
 
27
27
  grafana:
28
- image: grafana/grafana:latest
28
+ image: grafana/grafana:11.1.0
29
29
  volumes:
30
30
  - ./grafana/datasources.yml:/etc/grafana/provisioning/datasources/datasources.yml
31
31
  - ./grafana/dashboards:/etc/grafana/provisioning/dashboards