toad-eye 2.7.0 → 2.8.0

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.
@@ -2,8 +2,13 @@ export async function postGrafanaAnnotation(grafanaUrl, alert, apiKey) {
2
2
  const headers = {
3
3
  "Content-Type": "application/json",
4
4
  };
5
- if (apiKey)
5
+ if (apiKey) {
6
6
  headers["Authorization"] = `Bearer ${apiKey}`;
7
+ }
8
+ else {
9
+ // Fall back to Basic Auth with default Grafana credentials
10
+ headers["Authorization"] = `Basic ${btoa("admin:admin")}`;
11
+ }
7
12
  const res = await fetch(`${grafanaUrl}/api/annotations`, {
8
13
  method: "POST",
9
14
  headers,
@@ -8,6 +8,9 @@ import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
8
8
  import { initMetrics, resetMetrics } from "./metrics.js";
9
9
  import { resetCustomPricing } from "./pricing.js";
10
10
  import { enableAll, disableAll } from "../instrumentations/registry.js";
11
+ import { enableMcpInstrumentation, disableMcpInstrumentation, } from "../instrumentations/mcp.js";
12
+ import { enableMcpClientInstrumentation, disableMcpClientInstrumentation, } from "../mcp/client.js";
13
+ import { resetMcpMetrics } from "../mcp/metrics.js";
11
14
  import { BudgetTracker } from "../budget/index.js";
12
15
  import { ToadEyeAISpanProcessor } from "../vercel.js";
13
16
  const DEFAULT_ENDPOINT = "http://localhost:4318";
@@ -122,8 +125,22 @@ export function initObservability(config) {
122
125
  budgetTracker = new BudgetTracker(config.budgets, config.onBudgetExceeded ?? "warn", config.downgradeCallback);
123
126
  }
124
127
  if (config.instrument?.length) {
125
- // Filter out 'ai' — it uses SpanProcessor, not monkey-patching
126
- const patchProviders = config.instrument.filter((i) => i !== "ai");
128
+ // Handle MCP server auto-instrumentation
129
+ if (config.instrument.includes("mcp")) {
130
+ const patched = enableMcpInstrumentation();
131
+ if (!patched) {
132
+ console.warn(`toad-eye: "@modelcontextprotocol/sdk" not found — install it to enable MCP server auto-instrumentation`);
133
+ }
134
+ }
135
+ // Handle MCP client auto-instrumentation
136
+ if (config.instrument.includes("mcp-client")) {
137
+ const patched = enableMcpClientInstrumentation();
138
+ if (!patched) {
139
+ console.warn(`toad-eye: "@modelcontextprotocol/sdk" not found — install it to enable MCP client auto-instrumentation`);
140
+ }
141
+ }
142
+ // Filter out special targets — they use separate mechanisms
143
+ const patchProviders = config.instrument.filter((i) => i !== "ai" && i !== "mcp" && i !== "mcp-client");
127
144
  if (patchProviders.length > 0) {
128
145
  // enableAll is async (lazy-loads provider modules on first call).
129
146
  // Fire-and-forget: patching completes before any async SDK call.
@@ -137,10 +154,13 @@ export async function shutdown() {
137
154
  if (!sdk)
138
155
  return;
139
156
  disableAll();
157
+ disableMcpInstrumentation();
158
+ disableMcpClientInstrumentation();
140
159
  await sdk.shutdown();
141
160
  sdk = null;
142
161
  currentConfig = null;
143
162
  budgetTracker = null;
144
163
  resetMetrics();
164
+ resetMcpMetrics();
145
165
  resetCustomPricing();
146
166
  }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Auto-instrumentation for MCP SDK.
3
+ *
4
+ * Patches McpServer.prototype so that any server instance created after
5
+ * enableMcpInstrumentation() is automatically instrumented — no need
6
+ * to call toadEyeMiddleware(server) manually.
7
+ *
8
+ * Uses __toad_eye_mcp_patched flag to prevent double-instrumentation
9
+ * when both auto and manual approaches are used.
10
+ */
11
+ export declare function enableMcpInstrumentation(): boolean;
12
+ export declare function disableMcpInstrumentation(): void;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Auto-instrumentation for MCP SDK.
3
+ *
4
+ * Patches McpServer.prototype so that any server instance created after
5
+ * enableMcpInstrumentation() is automatically instrumented — no need
6
+ * to call toadEyeMiddleware(server) manually.
7
+ *
8
+ * Uses __toad_eye_mcp_patched flag to prevent double-instrumentation
9
+ * when both auto and manual approaches are used.
10
+ */
11
+ import { createRequire } from "node:module";
12
+ import { toadEyeMiddleware } from "../mcp/middleware.js";
13
+ const require = createRequire(import.meta.url);
14
+ const PATCHED_FLAG = "__toad_eye_mcp_patched";
15
+ const MODULE_NAME = "@modelcontextprotocol/sdk/server/mcp.js";
16
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
+ let mcpServerProto = null;
18
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
+ let originals = null;
20
+ export function enableMcpInstrumentation() {
21
+ try {
22
+ require.resolve(MODULE_NAME);
23
+ }
24
+ catch {
25
+ return false;
26
+ }
27
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
28
+ const sdk = require(MODULE_NAME);
29
+ const McpServer = sdk.McpServer ?? sdk.default?.McpServer;
30
+ if (!McpServer?.prototype)
31
+ return false;
32
+ if (McpServer.prototype[PATCHED_FLAG])
33
+ return true;
34
+ const proto = McpServer.prototype;
35
+ mcpServerProto = proto;
36
+ originals = {
37
+ tool: proto.tool,
38
+ resource: proto.resource,
39
+ prompt: proto.prompt,
40
+ };
41
+ // WeakSet tracks which instances have been instrumented
42
+ const instrumented = new WeakSet();
43
+ function ensureInstrumented(instance) {
44
+ if (instrumented.has(instance))
45
+ return;
46
+ instrumented.add(instance);
47
+ // Restore original methods so middleware wraps the real ones
48
+ instance.tool = originals.tool.bind(instance);
49
+ if (originals.resource)
50
+ instance.resource = originals.resource.bind(instance);
51
+ if (originals.prompt)
52
+ instance.prompt = originals.prompt.bind(instance);
53
+ toadEyeMiddleware(instance);
54
+ }
55
+ proto.tool = function patchedTool(...args) {
56
+ ensureInstrumented(this);
57
+ return this.tool(...args);
58
+ };
59
+ if (originals.resource) {
60
+ proto.resource = function patchedResource(...args) {
61
+ ensureInstrumented(this);
62
+ return this.resource(...args);
63
+ };
64
+ }
65
+ if (originals.prompt) {
66
+ proto.prompt = function patchedPrompt(...args) {
67
+ ensureInstrumented(this);
68
+ return this.prompt(...args);
69
+ };
70
+ }
71
+ proto[PATCHED_FLAG] = true;
72
+ return true;
73
+ }
74
+ export function disableMcpInstrumentation() {
75
+ if (!mcpServerProto || !originals)
76
+ return;
77
+ mcpServerProto.tool = originals.tool;
78
+ if (originals.resource)
79
+ mcpServerProto.resource = originals.resource;
80
+ if (originals.prompt)
81
+ mcpServerProto.prompt = originals.prompt;
82
+ delete mcpServerProto[PATCHED_FLAG];
83
+ mcpServerProto = null;
84
+ originals = null;
85
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * MCP Client instrumentation — patches Client.prototype to create
3
+ * SpanKind.CLIENT spans for outgoing tool calls, resource reads, and prompt gets.
4
+ *
5
+ * Injects W3C traceparent into _meta field so server-side middleware
6
+ * can link client and server spans into one distributed trace.
7
+ */
8
+ export declare function enableMcpClientInstrumentation(): boolean;
9
+ export declare function disableMcpClientInstrumentation(): void;
@@ -0,0 +1,137 @@
1
+ /**
2
+ * MCP Client instrumentation — patches Client.prototype to create
3
+ * SpanKind.CLIENT spans for outgoing tool calls, resource reads, and prompt gets.
4
+ *
5
+ * Injects W3C traceparent into _meta field so server-side middleware
6
+ * can link client and server spans into one distributed trace.
7
+ */
8
+ import { createRequire } from "node:module";
9
+ import { trace, context, propagation, SpanKind } from "@opentelemetry/api";
10
+ import { MCP_METHODS, endSpanSuccess, endSpanError } from "./spans.js";
11
+ const require = createRequire(import.meta.url);
12
+ const TRACER_NAME = "toad-eye-mcp-client";
13
+ const PATCHED_FLAG = "__toad_eye_mcp_client_patched";
14
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
+ let clientProto = null;
16
+ let originals = null;
17
+ /**
18
+ * Inject W3C traceparent into MCP _meta field.
19
+ * This allows server-side middleware to extract parent context.
20
+ */
21
+ function injectTraceContext(params) {
22
+ const meta = params._meta ?? {};
23
+ const carrier = {};
24
+ propagation.inject(context.active(), carrier);
25
+ if (carrier["traceparent"]) {
26
+ return {
27
+ ...params,
28
+ _meta: {
29
+ ...meta,
30
+ traceparent: carrier["traceparent"],
31
+ ...(carrier["tracestate"] ? { tracestate: carrier["tracestate"] } : {}),
32
+ },
33
+ };
34
+ }
35
+ return params;
36
+ }
37
+ export function enableMcpClientInstrumentation() {
38
+ try {
39
+ require.resolve("@modelcontextprotocol/sdk/client/index.js");
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
45
+ const sdk = require("@modelcontextprotocol/sdk/client/index.js");
46
+ const Client = sdk.Client ?? sdk.default?.Client;
47
+ if (!Client?.prototype)
48
+ return false;
49
+ if (Client.prototype[PATCHED_FLAG])
50
+ return true;
51
+ const proto = Client.prototype;
52
+ clientProto = proto;
53
+ originals = {
54
+ callTool: proto.callTool,
55
+ readResource: proto.readResource,
56
+ getPrompt: proto.getPrompt,
57
+ };
58
+ const tracer = trace.getTracer(TRACER_NAME);
59
+ // --- Patch callTool ---
60
+ proto.callTool = async function patchedCallTool(params, ...rest) {
61
+ const toolName = params.name ?? "unknown";
62
+ const span = tracer.startSpan(`${MCP_METHODS.TOOLS_CALL} ${toolName}`, {
63
+ kind: SpanKind.CLIENT,
64
+ attributes: {
65
+ "gen_ai.operation.name": MCP_METHODS.TOOLS_CALL,
66
+ "mcp.method.name": MCP_METHODS.TOOLS_CALL,
67
+ "gen_ai.tool.name": toolName,
68
+ },
69
+ });
70
+ const enrichedParams = injectTraceContext(params);
71
+ try {
72
+ const result = await context.with(trace.setSpan(context.active(), span), () => originals.callTool.call(this, enrichedParams, ...rest));
73
+ endSpanSuccess(span);
74
+ return result;
75
+ }
76
+ catch (error) {
77
+ endSpanError(span, error);
78
+ throw error;
79
+ }
80
+ };
81
+ // --- Patch readResource ---
82
+ proto.readResource = async function patchedReadResource(params, ...rest) {
83
+ const uri = params.uri ?? "unknown";
84
+ const span = tracer.startSpan(`${MCP_METHODS.RESOURCES_READ} ${uri}`, {
85
+ kind: SpanKind.CLIENT,
86
+ attributes: {
87
+ "gen_ai.operation.name": MCP_METHODS.RESOURCES_READ,
88
+ "mcp.method.name": MCP_METHODS.RESOURCES_READ,
89
+ "gen_ai.data_source.id": uri,
90
+ },
91
+ });
92
+ const enrichedParams = injectTraceContext(params);
93
+ try {
94
+ const result = await context.with(trace.setSpan(context.active(), span), () => originals.readResource.call(this, enrichedParams, ...rest));
95
+ endSpanSuccess(span);
96
+ return result;
97
+ }
98
+ catch (error) {
99
+ endSpanError(span, error);
100
+ throw error;
101
+ }
102
+ };
103
+ // --- Patch getPrompt ---
104
+ proto.getPrompt = async function patchedGetPrompt(params, ...rest) {
105
+ const promptName = params.name ?? "unknown";
106
+ const span = tracer.startSpan(`${MCP_METHODS.PROMPTS_GET} ${promptName}`, {
107
+ kind: SpanKind.CLIENT,
108
+ attributes: {
109
+ "gen_ai.operation.name": MCP_METHODS.PROMPTS_GET,
110
+ "mcp.method.name": MCP_METHODS.PROMPTS_GET,
111
+ "gen_ai.prompt.name": promptName,
112
+ },
113
+ });
114
+ const enrichedParams = injectTraceContext(params);
115
+ try {
116
+ const result = await context.with(trace.setSpan(context.active(), span), () => originals.getPrompt.call(this, enrichedParams, ...rest));
117
+ endSpanSuccess(span);
118
+ return result;
119
+ }
120
+ catch (error) {
121
+ endSpanError(span, error);
122
+ throw error;
123
+ }
124
+ };
125
+ proto[PATCHED_FLAG] = true;
126
+ return true;
127
+ }
128
+ export function disableMcpClientInstrumentation() {
129
+ if (!clientProto || !originals)
130
+ return;
131
+ clientProto.callTool = originals.callTool;
132
+ clientProto.readResource = originals.readResource;
133
+ clientProto.getPrompt = originals.getPrompt;
134
+ delete clientProto[PATCHED_FLAG];
135
+ clientProto = null;
136
+ originals = null;
137
+ }
@@ -1,4 +1,5 @@
1
1
  export { toadEyeMiddleware, traceSampling } from "./middleware.js";
2
- export type { TraceSamplingOptions } from "./middleware.js";
3
- export type { ToadMcpOptions, McpSpanAttributes } from "./types.js";
2
+ export type { TraceSamplingOptions, ToadEyeMiddlewareDispose, } from "./middleware.js";
3
+ export type { ToadMcpOptions } from "./types.js";
4
4
  export { MCP_METHODS } from "./spans.js";
5
+ export { enableMcpClientInstrumentation, disableMcpClientInstrumentation, } from "./client.js";
package/dist/mcp/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export { toadEyeMiddleware, traceSampling } from "./middleware.js";
2
2
  export { MCP_METHODS } from "./spans.js";
3
+ export { enableMcpClientInstrumentation, disableMcpClientInstrumentation, } from "./client.js";
@@ -6,8 +6,10 @@
6
6
  *
7
7
  * All metrics include `mcp.method.name` dimension per OTel MCP semconv.
8
8
  */
9
- export declare function recordMcpToolCall(toolName: string, durationMs: number, status: "success" | "error"): void;
9
+ export declare function recordMcpToolCall(toolName: string, durationMs: number, status: "success" | "error", sessionId?: string): void;
10
10
  export declare function recordMcpToolError(toolName: string, errorType: string): void;
11
11
  export declare function recordMcpResourceRead(uri: string): void;
12
12
  export declare function recordMcpSessionStart(): void;
13
13
  export declare function recordMcpSessionEnd(): void;
14
+ /** Reset MCP metrics state — must be called from shutdown() alongside resetMetrics(). */
15
+ export declare function resetMcpMetrics(): void;
@@ -15,6 +15,7 @@ let toolCalls;
15
15
  let toolErrors;
16
16
  let resourceReads;
17
17
  let sessionActive;
18
+ let toolCallers;
18
19
  let initialized = false;
19
20
  function ensureInit() {
20
21
  if (initialized)
@@ -37,8 +38,11 @@ function ensureInit() {
37
38
  sessionActive = meter.createUpDownCounter(GEN_AI_METRICS.MCP_SESSION_ACTIVE, {
38
39
  description: "Number of active MCP sessions",
39
40
  });
41
+ toolCallers = meter.createCounter(GEN_AI_METRICS.MCP_TOOL_CALLERS, {
42
+ description: "MCP tool calls by session — use count(distinct) for unique callers",
43
+ });
40
44
  }
41
- export function recordMcpToolCall(toolName, durationMs, status) {
45
+ export function recordMcpToolCall(toolName, durationMs, status, sessionId) {
42
46
  ensureInit();
43
47
  toolCalls.add(1, {
44
48
  "gen_ai.tool.name": toolName,
@@ -49,6 +53,12 @@ export function recordMcpToolCall(toolName, durationMs, status) {
49
53
  "gen_ai.tool.name": toolName,
50
54
  "mcp.method.name": MCP_METHODS.TOOLS_CALL,
51
55
  });
56
+ if (sessionId) {
57
+ toolCallers.add(1, {
58
+ "gen_ai.tool.name": toolName,
59
+ "mcp.session.id": sessionId,
60
+ });
61
+ }
52
62
  }
53
63
  export function recordMcpToolError(toolName, errorType) {
54
64
  ensureInit();
@@ -73,3 +83,7 @@ export function recordMcpSessionEnd() {
73
83
  ensureInit();
74
84
  sessionActive.add(-1);
75
85
  }
86
+ /** Reset MCP metrics state — must be called from shutdown() alongside resetMetrics(). */
87
+ export function resetMcpMetrics() {
88
+ initialized = false;
89
+ }
@@ -9,7 +9,11 @@
9
9
  * and strict TypeScript. Wrapping the public API is the stable path.
10
10
  */
11
11
  import type { ToadMcpOptions } from "./types.js";
12
- export declare function toadEyeMiddleware(server: any, options?: ToadMcpOptions): void;
12
+ export interface ToadEyeMiddlewareDispose {
13
+ /** Call when the MCP server is shutting down to decrement the active session counter. */
14
+ (): void;
15
+ }
16
+ export declare function toadEyeMiddleware(server: any, options?: ToadMcpOptions): ToadEyeMiddlewareDispose;
13
17
  /**
14
18
  * Trace a sampling/createMessage call (server → client LLM request).
15
19
  *
@@ -8,11 +8,11 @@
8
8
  * Why not monkey-patching: MCP SDK uses private class members (_requestHandlers)
9
9
  * and strict TypeScript. Wrapping the public API is the stable path.
10
10
  */
11
- import { context, diag, DiagConsoleLogger, DiagLogLevel, } from "@opentelemetry/api";
11
+ import { context, trace, diag, DiagConsoleLogger, DiagLogLevel, } from "@opentelemetry/api";
12
12
  import { randomUUID } from "node:crypto";
13
13
  import { extractContextFromMeta } from "./context.js";
14
14
  import { startToolSpan, startResourceSpan, startPromptSpan, startSamplingSpan, endSpanSuccess, endSpanError, } from "./spans.js";
15
- import { recordMcpToolCall, recordMcpToolError, recordMcpResourceRead, recordMcpSessionStart, } from "./metrics.js";
15
+ import { recordMcpToolCall, recordMcpToolError, recordMcpResourceRead, recordMcpSessionStart, recordMcpSessionEnd, } from "./metrics.js";
16
16
  const DEFAULT_MAX_PAYLOAD_SIZE = 4096;
17
17
  /** Detect MCP transport type from server internals. */
18
18
  function detectTransport(server) {
@@ -38,7 +38,15 @@ function redactObject(obj, keys) {
38
38
  return {};
39
39
  const result = {};
40
40
  for (const [k, v] of Object.entries(obj)) {
41
- result[k] = keys.includes(k) ? "[REDACTED]" : v;
41
+ if (keys.includes(k)) {
42
+ result[k] = "[REDACTED]";
43
+ }
44
+ else if (typeof v === "object" && v !== null && !Array.isArray(v)) {
45
+ result[k] = redactObject(v, keys);
46
+ }
47
+ else {
48
+ result[k] = v;
49
+ }
42
50
  }
43
51
  return result;
44
52
  }
@@ -137,7 +145,7 @@ server, options = {}) {
137
145
  }
138
146
  const start = performance.now();
139
147
  try {
140
- const result = await context.with(context.active(), () => originalHandler(...handlerArgs));
148
+ const result = await context.with(trace.setSpan(parentCtx, span), () => originalHandler(...handlerArgs));
141
149
  const durationMs = performance.now() - start;
142
150
  // Record outputs if enabled
143
151
  if (recordOutputs && result) {
@@ -145,14 +153,14 @@ server, options = {}) {
145
153
  span.setAttribute("gen_ai.tool.call.result", serialized);
146
154
  }
147
155
  endSpanSuccess(span);
148
- recordMcpToolCall(name, durationMs, "success");
156
+ recordMcpToolCall(name, durationMs, "success", sessionId);
149
157
  return result;
150
158
  }
151
159
  catch (error) {
152
160
  const durationMs = performance.now() - start;
153
161
  const errorType = error instanceof Error ? error.constructor.name : "UnknownError";
154
162
  endSpanError(span, error);
155
- recordMcpToolCall(name, durationMs, "error");
163
+ recordMcpToolCall(name, durationMs, "error", sessionId);
156
164
  recordMcpToolError(name, errorType);
157
165
  throw error;
158
166
  }
@@ -181,7 +189,7 @@ server, options = {}) {
181
189
  options.onResourceRead(span, uri);
182
190
  }
183
191
  try {
184
- const result = await originalHandler(...handlerArgs);
192
+ const result = await context.with(trace.setSpan(parentCtx, span), () => originalHandler(...handlerArgs));
185
193
  endSpanSuccess(span);
186
194
  recordMcpResourceRead(uri);
187
195
  return result;
@@ -212,7 +220,7 @@ server, options = {}) {
212
220
  parentContext: parentCtx,
213
221
  });
214
222
  try {
215
- const result = await originalHandler(...handlerArgs);
223
+ const result = await context.with(trace.setSpan(parentCtx, span), () => originalHandler(...handlerArgs));
216
224
  endSpanSuccess(span);
217
225
  return result;
218
226
  }
@@ -224,6 +232,13 @@ server, options = {}) {
224
232
  return originalPrompt(name, ...rest);
225
233
  };
226
234
  }
235
+ let disposed = false;
236
+ return () => {
237
+ if (disposed)
238
+ return;
239
+ disposed = true;
240
+ recordMcpSessionEnd();
241
+ };
227
242
  }
228
243
  /**
229
244
  * Trace a sampling/createMessage call (server → client LLM request).
package/dist/mcp/spans.js CHANGED
@@ -35,7 +35,7 @@ function baseAttrs(method, options) {
35
35
  }
36
36
  export function startToolSpan(toolName, options) {
37
37
  return tracer.startSpan(`${MCP_METHODS.TOOLS_CALL} ${toolName}`, {
38
- kind: SpanKind.INTERNAL,
38
+ kind: SpanKind.SERVER,
39
39
  attributes: {
40
40
  ...baseAttrs(MCP_METHODS.TOOLS_CALL, options),
41
41
  "gen_ai.tool.name": toolName,
@@ -44,7 +44,7 @@ export function startToolSpan(toolName, options) {
44
44
  }
45
45
  export function startResourceSpan(uri, options) {
46
46
  return tracer.startSpan(`${MCP_METHODS.RESOURCES_READ} ${uri}`, {
47
- kind: SpanKind.INTERNAL,
47
+ kind: SpanKind.SERVER,
48
48
  attributes: {
49
49
  ...baseAttrs(MCP_METHODS.RESOURCES_READ, options),
50
50
  "gen_ai.data_source.id": uri,
@@ -53,7 +53,7 @@ export function startResourceSpan(uri, options) {
53
53
  }
54
54
  export function startPromptSpan(promptName, options) {
55
55
  return tracer.startSpan(`${MCP_METHODS.PROMPTS_GET} ${promptName}`, {
56
- kind: SpanKind.INTERNAL,
56
+ kind: SpanKind.SERVER,
57
57
  attributes: {
58
58
  ...baseAttrs(MCP_METHODS.PROMPTS_GET, options),
59
59
  "gen_ai.prompt.name": promptName,
@@ -1,17 +1,13 @@
1
1
  import type { Span } from "@opentelemetry/api";
2
2
  /** Configuration for toadEyeMiddleware(). */
3
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
4
  /** Record tool call arguments in spans. Default: false (privacy). */
9
5
  readonly recordInputs?: boolean | undefined;
10
6
  /** Record tool call results in spans. Default: false (privacy). */
11
7
  readonly recordOutputs?: boolean | undefined;
12
- /** Keys to redact from recorded arguments. */
8
+ /** Keys to redact from recorded arguments (recursive). */
13
9
  readonly redactKeys?: readonly string[] | undefined;
14
- /** Max payload size in bytes before truncation. Default: 4096. */
10
+ /** Max payload size in characters before truncation. Default: 4096. */
15
11
  readonly maxPayloadSize?: number | undefined;
16
12
  /** Explicit session ID. Auto-generated if not provided. */
17
13
  readonly sessionId?: string | undefined;
@@ -22,18 +18,3 @@ export interface ToadMcpOptions {
22
18
  /** Hook called on each resource read span. */
23
19
  readonly onResourceRead?: ((span: Span, uri: string) => void) | undefined;
24
20
  }
25
- /** OTel attributes set on MCP spans. */
26
- export interface McpSpanAttributes {
27
- readonly "gen_ai.operation.name": string;
28
- readonly "mcp.method.name": string;
29
- readonly "gen_ai.tool.name"?: string;
30
- readonly "gen_ai.tool.call.id"?: string;
31
- readonly "gen_ai.data_source.id"?: string;
32
- readonly "gen_ai.prompt.name"?: string;
33
- readonly "mcp.server.name": string;
34
- readonly "mcp.server.version": string;
35
- readonly "mcp.session.id"?: string;
36
- readonly "mcp.protocol.version"?: string;
37
- readonly "network.transport"?: "pipe" | "tcp";
38
- readonly "rpc.response.status_code"?: number;
39
- }
@@ -31,6 +31,7 @@ export declare const GEN_AI_METRICS: {
31
31
  readonly MCP_TOOL_ERRORS: "gen_ai.mcp.tool.errors";
32
32
  readonly MCP_RESOURCE_READS: "gen_ai.mcp.resource.reads";
33
33
  readonly MCP_SESSION_ACTIVE: "gen_ai.mcp.session.active";
34
+ readonly MCP_TOOL_CALLERS: "gen_ai.mcp.tool.callers";
34
35
  };
35
36
  /** @deprecated Use GEN_AI_METRICS instead. Kept for backward compatibility. */
36
37
  export declare const LLM_METRICS: {
@@ -39,6 +39,7 @@ export const GEN_AI_METRICS = {
39
39
  MCP_TOOL_ERRORS: "gen_ai.mcp.tool.errors",
40
40
  MCP_RESOURCE_READS: "gen_ai.mcp.resource.reads",
41
41
  MCP_SESSION_ACTIVE: "gen_ai.mcp.session.active",
42
+ MCP_TOOL_CALLERS: "gen_ai.mcp.tool.callers",
42
43
  };
43
44
  /** @deprecated Use GEN_AI_METRICS instead. Kept for backward compatibility. */
44
45
  export const LLM_METRICS = {
@@ -6,5 +6,7 @@ export type LLMProvider = "anthropic" | "gemini" | "openai" | (string & {});
6
6
  /**
7
7
  * All instrumentable targets — includes LLM providers + framework SDKs.
8
8
  * 'ai' = Vercel AI SDK (uses SpanProcessor, not monkey-patching).
9
+ * 'mcp' = MCP SDK McpServer (patches prototype for auto-instrumentation).
10
+ * 'mcp-client' = MCP SDK Client (patches callTool/readResource/getPrompt for distributed tracing).
9
11
  */
10
- export type InstrumentTarget = LLMProvider | "ai";
12
+ export type InstrumentTarget = LLMProvider | "ai" | "mcp" | "mcp-client";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toad-eye",
3
- "version": "2.7.0",
3
+ "version": "2.8.0",
4
4
  "description": "Observability for MCP servers and LLM applications — traces, metrics, dashboards. One-line instrumentation for OpenAI, Anthropic, Gemini, and MCP. Self-hosted with Grafana + Prometheus + Jaeger.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -52,3 +52,24 @@ alerts:
52
52
  condition: ratio_5m > 0.2
53
53
  channels: [slack]
54
54
  cooldown: 15
55
+
56
+ # --- MCP Server alerts ---
57
+
58
+ # MCP tool error rate: >10% over 5 minutes
59
+ - name: mcp_tool_error_rate
60
+ metric: gen_ai.mcp.tool.errors
61
+ condition: ratio_5m > 0.1
62
+ channels: [slack]
63
+
64
+ # MCP tool latency spike: avg > 2 seconds over 5 minutes
65
+ - name: mcp_tool_latency
66
+ metric: gen_ai.mcp.tool.duration
67
+ condition: avg_5m > 2000
68
+ channels: [slack]
69
+
70
+ # MCP tool call spike: >100 calls in 5 minutes
71
+ - name: mcp_tool_call_spike
72
+ metric: gen_ai.mcp.tool.calls
73
+ condition: sum_5m > 100
74
+ channels: [slack]
75
+ cooldown: 15