@upstash/context7-mcp 4.0.7 → 4.1.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.
package/README.md CHANGED
@@ -1490,6 +1490,133 @@ CONTEXT7_API_KEY=your_api_key_here
1490
1490
  }
1491
1491
  ```
1492
1492
 
1493
+ ### OpenTelemetry observability
1494
+
1495
+ Context7 instruments individual MCP requests and notifications at the SDK transport boundary,
1496
+ including messages inside a valid batch and MCP v2 `subscriptions/listen` operations handled by the
1497
+ SDK entry layer. Requests rejected by the SDK's HTTP envelope and protocol-version validation before
1498
+ dispatch remain visible in normal HTTP/gateway telemetry, but are not reported as MCP operations.
1499
+ Observed operations follow the
1500
+ development-status [OpenTelemetry MCP semantic conventions](https://github.com/open-telemetry/semantic-conventions-genai/blob/main/docs/gen-ai/mcp.md)
1501
+ for server metrics and spans. Trace context is extracted from the `traceparent`, `tracestate`, and
1502
+ `baggage` fields in MCP `params._meta` as defined by
1503
+ [SEP-414](https://modelcontextprotocol.io/seps/414-request-meta).
1504
+
1505
+ The HTTP transport exposes metrics in Prometheus format on a dedicated listener at
1506
+ `127.0.0.1:9464/metrics` by default. The production Docker image explicitly binds that listener to
1507
+ `0.0.0.0` so an internal Prometheus pod scraper or `PodMonitor` can reach it. The stdio transport
1508
+ does not open a telemetry port. Keeping this listener separate from the public MCP port prevents
1509
+ the metrics endpoint from being routed through a catch-all gateway rule. On SIGTERM, SIGINT, or
1510
+ SIGHUP, both transports use a bounded shutdown path that stops serving, closes active MCP
1511
+ connections and subscriptions, and best-effort flushes externally installed SDK metric and trace
1512
+ providers before exit. Stdio EOF triggers the same path.
1513
+
1514
+ The exporter uses the standard OpenTelemetry Prometheus settings:
1515
+
1516
+ - `OTEL_EXPORTER_PROMETHEUS_HOST` changes the bind address (default `127.0.0.1`; the Docker image
1517
+ sets `0.0.0.0`).
1518
+ - `OTEL_EXPORTER_PROMETHEUS_PORT` changes the port (default `9464`).
1519
+ - `OTEL_METRICS_EXPORTER=none` or `OTEL_SDK_DISABLED=true` disables the embedded exporter.
1520
+
1521
+ `OTEL_SDK_DISABLED=true` is the hard-off switch: provider modules are not loaded and MCP
1522
+ transports and handlers are not wrapped, preserving the baseline request path. In contrast,
1523
+ `OTEL_METRICS_EXPORTER=none` disables only the embedded Prometheus bootstrap, so a provider
1524
+ installed by a Node preload can still receive the MCP signals.
1525
+
1526
+ Exporter bind or configuration failures are logged but do not prevent the MCP endpoint from
1527
+ starting. If a Node preload has already registered global OpenTelemetry providers, they take
1528
+ precedence. The embedded Prometheus listener is not started when a global `MeterProvider` exists,
1529
+ and MCP spans are exported through the preload's `TracerProvider`. This supports an OpenTelemetry
1530
+ Node SDK or Kubernetes auto-instrumentation without creating a second provider in the application.
1531
+ When an external SDK owns the provider, configure its Node runtime instrumentation there as well;
1532
+ the application does not register a duplicate collector.
1533
+
1534
+ It reports bounded-cardinality counters, histograms, and in-flight gauges for MCP methods,
1535
+ subscriptions, tool outcomes, authentication outcomes, Context7 upstream requests, and Node runtime
1536
+ saturation.
1537
+ Prometheus receives these metric families:
1538
+
1539
+ - `mcp_server_operation_duration` (its `_count` series is the MCP operation count, and tool-call
1540
+ series include the `context7_mcp_tool_outcome` label)
1541
+ - `mcp_server_session_duration` for real stateful stdio sessions (stateless HTTP request transports
1542
+ are intentionally excluded)
1543
+ - `context7_mcp_operations_active`
1544
+ - `context7_mcp_subscriptions_active` and `context7_mcp_subscription_duration`
1545
+ - `context7_mcp_upstream_requests_total` and `context7_mcp_upstream_request_duration`
1546
+ - `context7_mcp_authentication_attempts_total` and `context7_mcp_authentication_duration`
1547
+ - `context7_mcp_upstream_requests_active` and `context7_mcp_authentication_active`
1548
+ - `nodejs_eventloop_*`, `v8js_gc_duration`, `v8js_memory_heap_*`, and
1549
+ `v8js_resource_active` from the official OpenTelemetry Node runtime instrumentation
1550
+
1551
+ Tool outcomes on the standard MCP operation metric distinguish `success`, `not_found`, and
1552
+ `error`. An acknowledged `subscriptions/listen` operation is timed through its acknowledgement;
1553
+ the separate subscription metrics track the active stream and its bounded terminal outcome.
1554
+ Upstream outcomes distinguish
1555
+ HTTP, response-decoding, network, timeout, and cancellation failures and include both the bounded
1556
+ status-code class and the exact numeric HTTP status. Authentication reports accepted, missing,
1557
+ invalid, and unexpected-error outcomes. The OAuth authorization-server metadata proxy caps its
1558
+ upstream fetch at 10 seconds and returns `502` if that dependency times out.
1559
+
1560
+ The labels intentionally exclude API keys, client IPs, queries, library IDs, session IDs, and raw
1561
+ error text. Expose port `9464` only to your Prometheus scraper or `ServiceMonitor`, not through the
1562
+ public MCP ingress.
1563
+
1564
+ #### Signal ownership with an Envoy gateway
1565
+
1566
+ Do not treat `mcp_server_operation_duration_count` as another HTTP request counter. An Envoy
1567
+ Gateway observes HTTP envelopes, while this metric observes JSON-RPC requests and notifications
1568
+ after SDK dispatch. A valid batch is one HTTP request but several MCP operations, and HTTP requests
1569
+ rejected before MCP dispatch never increment the MCP metric.
1570
+
1571
+ Context7 deliberately does **not** register generic inbound HTTP server metrics. Keep the following
1572
+ signals in the existing Envoy scrape instead of collecting them again from the application:
1573
+
1574
+ - downstream HTTP request/response totals, status classes, duration, active requests, connections,
1575
+ resets, and gateway timeouts (`envoy_http_*_downstream_*`)
1576
+ - Envoy-to-MCP backend request totals, status codes, duration, active/pending requests, connection
1577
+ failures, retries, resets, timeouts, and circuit-breaker overflows (`envoy_cluster_upstream_*`)
1578
+ - Envoy process health and resource metrics
1579
+
1580
+ The application exporter owns only signals the ingress gateway cannot provide: MCP method and
1581
+ protocol semantics (including batches, notifications, and active MCP v2 subscriptions), tool and
1582
+ authentication outcomes, MCP-to-Context7 API calls, and Node event-loop/V8 health. In the
1583
+ Kubernetes deployment Envoy is a
1584
+ Gateway API proxy rather than a sidecar in the MCP pod, so `context7_mcp_upstream_*` describes the
1585
+ MCP server's outbound Context7 API dependency, not Envoy's inbound MCP backend cluster. Pod and
1586
+ container CPU, memory, network, and restart metrics should continue to come from the Kubernetes
1587
+ monitoring stack.
1588
+
1589
+ See the [Envoy HTTP connection manager statistics](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/stats)
1590
+ and [upstream cluster statistics](https://www.envoyproxy.io/docs/envoy/latest/configuration/upstream/cluster_manager/cluster_stats.html)
1591
+ for the proxy-owned metric families.
1592
+
1593
+ For a replicated Kubernetes deployment, discover and scrape every MCP pod directly. Do not use one
1594
+ static, load-balanced Service target: successive scrapes can reach different replicas and produce
1595
+ incomplete per-process counters and runtime series. For an annotation-based `kubernetes-pods`
1596
+ scrape job, add the following fields to the MCP workload's pod template:
1597
+
1598
+ ```yaml
1599
+ spec:
1600
+ template:
1601
+ metadata:
1602
+ annotations:
1603
+ prometheus.io/scrape: "true"
1604
+ prometheus.io/port: "9464"
1605
+ prometheus.io/path: /metrics
1606
+ spec:
1607
+ containers:
1608
+ - name: mcp
1609
+ ports:
1610
+ - name: metrics
1611
+ containerPort: 9464
1612
+ protocol: TCP
1613
+ ```
1614
+
1615
+ Prometheus will then scrape `http://<mcp-pod-ip>:9464/metrics` for each replica. Declaring
1616
+ `EXPOSE 9464` in the image does not add the Kubernetes `containerPort` metadata. The scrape interval
1617
+ is controlled by Prometheus; the exporter does not impose one. If the monitoring stack uses the
1618
+ Prometheus Operator instead, configure the equivalent per-pod endpoint with a `PodMonitor`.
1619
+
1493
1620
  <details>
1494
1621
  <summary><b>Local Configuration Example</b></summary>
1495
1622
 
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { toNodeHandler } from "@modelcontextprotocol/node";
3
- import { serveStdio } from "@modelcontextprotocol/server/stdio";
4
- import { McpServer, createMcpHandler } from "@modelcontextprotocol/server";
3
+ import { StdioServerTransport, serveStdio } from "@modelcontextprotocol/server/stdio";
4
+ import { McpServer, createMcpHandler, } from "@modelcontextprotocol/server";
5
5
  import { z } from "zod";
6
6
  import { searchLibraries, fetchLibraryContext } from "./lib/api.js";
7
7
  import { formatSearchResults, extractClientInfoFromUserAgent, envelopeClientInfo, } from "./lib/utils.js";
@@ -12,11 +12,16 @@ import { AsyncLocalStorage } from "async_hooks";
12
12
  import { randomUUID } from "node:crypto";
13
13
  import { SERVER_VERSION, RESOURCE_URL, OAUTH_AUTH_SERVER_URL, EMA_ISSUER, OPENAI_APPS_CHALLENGE_TOKEN, } from "./lib/constants.js";
14
14
  import { maybeElicitAuthSignIn } from "./lib/auth/auth-prompt.js";
15
+ import { QUERY_DOCS_TOOL, RESOLVE_LIBRARY_ID_TOOL } from "./lib/tool-names.js";
16
+ import { installProcessShutdown } from "./lib/process-shutdown.js";
15
17
  import { getMaxSubscriptions } from "./lib/subscriptions.js";
18
+ import { forceFlushTelemetry, initializeTelemetry, observeAuthentication, observeUpstreamRequest, recordToolCallOutcome, } from "./lib/telemetry-runtime.js";
16
19
  import { mcpBodyErrorHandler } from "./lib/mcp-body-error-handler.js";
17
20
  /** Default HTTP server port */
18
21
  const DEFAULT_PORT = 3000;
22
+ const OAUTH_METADATA_TIMEOUT_MS = 10_000;
19
23
  const CLAUDE_CODE_PLUGIN = "claude-code-plugin";
24
+ let mcpInstrumentation;
20
25
  function getPluginFromRequest(req) {
21
26
  return req.query.client === CLAUDE_CODE_PLUGIN ? CLAUDE_CODE_PLUGIN : undefined;
22
27
  }
@@ -116,8 +121,8 @@ function aliasArgs(aliases) {
116
121
  return args;
117
122
  };
118
123
  }
119
- function createMcpServer() {
120
- const server = new McpServer({
124
+ function createMcpServer(mcpContext) {
125
+ const serverInfo = {
121
126
  name: "Context7",
122
127
  version: SERVER_VERSION,
123
128
  websiteUrl: "https://context7.com",
@@ -128,17 +133,26 @@ function createMcpServer() {
128
133
  mimeType: "image/png",
129
134
  },
130
135
  ],
131
- }, {
132
- // Declaring the capabilities makes the SDK install prompts/list,
133
- // resources/list, and resources/templates/list handlers that answer
134
- // with the registered (i.e. empty) collections, for clients that
135
- // request them unconditionally.
136
- capabilities: { prompts: {}, resources: {} },
136
+ };
137
+ const serverOptions = {
138
+ // The collections are static for the process lifetime. Explicitly disabling
139
+ // change notifications prevents clients from opening subscriptions/listen
140
+ // streams for events this server never publishes. Declaring prompts and
141
+ // resources still installs their empty list handlers for clients that call
142
+ // them unconditionally.
143
+ capabilities: {
144
+ tools: { listChanged: false },
145
+ prompts: { listChanged: false },
146
+ resources: { listChanged: false, subscribe: false },
147
+ },
137
148
  instructions: `Use this server to fetch current documentation whenever the user asks about a library, framework, SDK, API, CLI tool, or cloud service — even well-known ones like React, Next.js, Prisma, Express, Tailwind, Django, or Spring Boot. This includes API syntax, configuration, version migration, library-specific debugging, setup instructions, and CLI tool usage. Use even when you think you know the answer — your training data may not reflect recent changes. Prefer this over web search for library docs.
138
149
 
139
150
  Do not use for: refactoring, writing scripts from scratch, debugging business logic, code review, or general programming concepts.`,
140
- });
141
- server.registerTool("resolve-library-id", {
151
+ };
152
+ const server = mcpInstrumentation
153
+ ? mcpInstrumentation.createServer(serverInfo, serverOptions, mcpContext)
154
+ : new McpServer(serverInfo, serverOptions);
155
+ server.registerTool(RESOLVE_LIBRARY_ID_TOOL, {
142
156
  title: "Resolve Context7 Library ID",
143
157
  description: `Resolves a package/product name to a Context7-compatible library ID and returns matching libraries.
144
158
 
@@ -193,6 +207,7 @@ IMPORTANT: Do not call this tool more than 3 times per question. If you cannot f
193
207
  if (!searchResponse.results || searchResponse.results.length === 0) {
194
208
  const text = searchResponse.error ?? "No libraries found matching the provided name.";
195
209
  maybeElicitAuthSignIn(server, ctx);
210
+ recordToolCallOutcome(searchResponse.error ? "error" : "not_found");
196
211
  return {
197
212
  content: [
198
213
  {
@@ -205,6 +220,7 @@ IMPORTANT: Do not call this tool more than 3 times per question. If you cannot f
205
220
  const resultsText = formatSearchResults(searchResponse);
206
221
  const responseText = `Available Libraries:\n\n${resultsText}`;
207
222
  maybeElicitAuthSignIn(server, ctx);
223
+ recordToolCallOutcome("success");
208
224
  return {
209
225
  content: [
210
226
  {
@@ -214,7 +230,7 @@ IMPORTANT: Do not call this tool more than 3 times per question. If you cannot f
214
230
  ],
215
231
  };
216
232
  });
217
- server.registerTool("query-docs", {
233
+ server.registerTool(QUERY_DOCS_TOOL, {
218
234
  title: "Query Documentation",
219
235
  description: `Retrieves and queries up-to-date documentation and code examples from Context7 for any programming library or framework.
220
236
 
@@ -239,6 +255,7 @@ Do not call this tool more than 3 times per question.`,
239
255
  const ctx = getClientContext(toolCtx);
240
256
  const response = await fetchLibraryContext({ query, libraryId }, ctx);
241
257
  maybeElicitAuthSignIn(server, ctx);
258
+ recordToolCallOutcome(response.outcome);
242
259
  return {
243
260
  content: [
244
261
  {
@@ -251,6 +268,10 @@ Do not call this tool more than 3 times per question.`,
251
268
  return server;
252
269
  }
253
270
  async function main() {
271
+ mcpInstrumentation = await initializeTelemetry({
272
+ allowEmbeddedPrometheus: TRANSPORT_TYPE === "http",
273
+ serviceVersion: SERVER_VERSION,
274
+ });
254
275
  if (TRANSPORT_TYPE === "http") {
255
276
  const initialPort = CLI_PORT ?? DEFAULT_PORT;
256
277
  const app = express();
@@ -309,11 +330,14 @@ async function main() {
309
330
  // then never closes the stream, and with heartbeats it survived until the
310
331
  // gateway's 1200s hard cap (the 2026-08-11 outage). Silent hangs instead
311
332
  // go idle and the gateway reaps them at streamIdleTimeout (300s).
312
- const mcpHandler = createMcpHandler(() => createMcpServer(), {
333
+ const rawMcpHandler = createMcpHandler((mcpContext) => createMcpServer(mcpContext), {
313
334
  keepAliveMs: 0,
314
335
  maxSubscriptions: getMaxSubscriptions(),
315
336
  onerror: (error) => console.error("MCP handler error:", error),
316
337
  });
338
+ const mcpHandler = mcpInstrumentation
339
+ ? mcpInstrumentation.instrumentHttpHandler(rawMcpHandler)
340
+ : rawMcpHandler;
317
341
  // Without onerror, request-conversion / handler.fetch throws are answered
318
342
  // with a bare 500 inside the adapter and never reach our express handler.
319
343
  const nodeHandler = toNodeHandler(mcpHandler, {
@@ -325,34 +349,42 @@ async function main() {
325
349
  const apiKey = extractApiKey(req);
326
350
  const baseUrl = new URL(RESOURCE_URL).origin;
327
351
  // OAuth discovery info header, used by MCP clients to discover the authorization server
328
- // TODO: @modelcontextprotocol/server now ships canonical OAuth helpers
329
- // (bearerAuthChallengeResponse, buildOAuthProtectedResourceMetadata,
330
- // oauthMetadataResponse) — replace this hand-rolled header and the
331
- // /.well-known/oauth-protected-resource route with them.
332
352
  res.set("WWW-Authenticate", `Bearer resource_metadata="${baseUrl}/.well-known/oauth-protected-resource"`);
333
353
  if (requiresAuthentication(req, plugin)) {
334
- if (!apiKey) {
335
- return res.status(401).json({
354
+ const authentication = await observeAuthentication(async () => {
355
+ if (!apiKey) {
356
+ return {
357
+ outcome: "missing",
358
+ value: {
359
+ accepted: false,
360
+ error: "Authentication required. Please authenticate to use this MCP server.",
361
+ },
362
+ };
363
+ }
364
+ if (isJWT(apiKey)) {
365
+ const validationResult = await validateJWT(apiKey);
366
+ if (!validationResult.valid) {
367
+ return {
368
+ outcome: "invalid",
369
+ value: {
370
+ accepted: false,
371
+ error: validationResult.error || "Invalid token. Please re-authenticate.",
372
+ },
373
+ };
374
+ }
375
+ }
376
+ return { outcome: "accepted", value: { accepted: true } };
377
+ });
378
+ if (!authentication.accepted) {
379
+ res.status(401).json({
336
380
  jsonrpc: "2.0",
337
381
  error: {
338
382
  code: -32001,
339
- message: "Authentication required. Please authenticate to use this MCP server.",
383
+ message: authentication.error,
340
384
  },
341
385
  id: null,
342
386
  });
343
- }
344
- if (isJWT(apiKey)) {
345
- const validationResult = await validateJWT(apiKey);
346
- if (!validationResult.valid) {
347
- return res.status(401).json({
348
- jsonrpc: "2.0",
349
- error: {
350
- code: -32001,
351
- message: validationResult.error || "Invalid token. Please re-authenticate.",
352
- },
353
- id: null,
354
- });
355
- }
387
+ return;
356
388
  }
357
389
  }
358
390
  const context = {
@@ -406,16 +438,22 @@ async function main() {
406
438
  app.get("/.well-known/oauth-authorization-server", async (_req, res) => {
407
439
  const authServerUrl = OAUTH_AUTH_SERVER_URL;
408
440
  try {
409
- const response = await fetch(`${authServerUrl}/.well-known/oauth-authorization-server`);
410
- if (!response.ok) {
411
- console.error("[OAuth] Upstream error:", response.status);
412
- return res.status(response.status).json({
441
+ const abortSignal = AbortSignal.timeout(OAUTH_METADATA_TIMEOUT_MS);
442
+ const upstream = await observeUpstreamRequest("oauth_metadata", () => fetch(`${authServerUrl}/.well-known/oauth-authorization-server`, {
443
+ signal: abortSignal,
444
+ }), async (response) => {
445
+ if (!response.ok)
446
+ return { ok: false, status: response.status };
447
+ return { ok: true, metadata: await response.json() };
448
+ }, { abortSignal });
449
+ if (!upstream.ok) {
450
+ console.error("[OAuth] Upstream error:", upstream.status);
451
+ return res.status(upstream.status).json({
413
452
  error: "upstream_error",
414
453
  message: "Failed to fetch authorization server metadata",
415
454
  });
416
455
  }
417
- const metadata = await response.json();
418
- res.json(metadata);
456
+ res.json(upstream.metadata);
419
457
  }
420
458
  catch (error) {
421
459
  console.error("[OAuth] Error fetching OAuth metadata:", error);
@@ -442,8 +480,36 @@ async function main() {
442
480
  message: "Endpoint not found. Use /mcp for MCP protocol communication.",
443
481
  });
444
482
  });
483
+ let activeHttpServer;
484
+ installProcessShutdown({
485
+ close: async () => {
486
+ const server = activeHttpServer;
487
+ const operations = [mcpHandler.close()];
488
+ if (server) {
489
+ operations.unshift(new Promise((resolve, reject) => {
490
+ server.close((error) => {
491
+ if (error)
492
+ reject(error);
493
+ else
494
+ resolve();
495
+ });
496
+ }));
497
+ }
498
+ const results = await Promise.allSettled(operations);
499
+ const failures = results
500
+ .filter((result) => result.status === "rejected")
501
+ .map((result) => result.reason);
502
+ if (failures.length > 0) {
503
+ throw new AggregateError(failures, "MCP HTTP server failed to close cleanly");
504
+ }
505
+ },
506
+ }, {
507
+ flush: forceFlushTelemetry,
508
+ onerror: (error) => console.error("Failed to close MCP HTTP server:", error),
509
+ });
445
510
  const startServer = (port, maxAttempts = 10) => {
446
511
  const httpServer = app.listen(port);
512
+ activeHttpServer = httpServer;
447
513
  httpServer.once("error", (err) => {
448
514
  if (err.code === "EADDRINUSE" && port < initialPort + maxAttempts) {
449
515
  console.warn(`Port ${port} is in use, trying port ${port + 1}...`);
@@ -463,11 +529,12 @@ async function main() {
463
529
  else {
464
530
  stdioApiKey = cliOptions.apiKey || process.env.CONTEXT7_API_KEY;
465
531
  stdioSessionId = randomUUID();
466
- process.stdin.on("end", () => process.exit(0));
467
- process.stdin.on("close", () => process.exit(0));
468
- process.on("SIGHUP", () => process.exit(0));
469
- serveStdio(() => {
470
- const server = createMcpServer();
532
+ const rawStdioTransport = new StdioServerTransport();
533
+ const stdioTransport = mcpInstrumentation
534
+ ? mcpInstrumentation.instrumentStdioTransport(rawStdioTransport)
535
+ : rawStdioTransport;
536
+ const stdioHandle = serveStdio((mcpContext) => {
537
+ const server = createMcpServer(mcpContext);
471
538
  // Capture client info from MCP initialize handshake (stdio only — HTTP
472
539
  // mode plumbs client info through requestContext per request).
473
540
  server.server.oninitialized = () => {
@@ -481,8 +548,15 @@ async function main() {
481
548
  };
482
549
  return server;
483
550
  }, {
551
+ transport: stdioTransport,
552
+ maxSubscriptions: getMaxSubscriptions(),
484
553
  onerror: (error) => console.error("MCP stdio error:", error),
485
554
  });
555
+ installProcessShutdown(stdioHandle, {
556
+ flush: forceFlushTelemetry,
557
+ input: process.stdin,
558
+ onerror: (error) => console.error("Failed to close MCP stdio server:", error),
559
+ });
486
560
  console.error(`Context7 Documentation MCP Server v${SERVER_VERSION} running on stdio`);
487
561
  }
488
562
  }
package/dist/lib/api.js CHANGED
@@ -3,6 +3,7 @@ import { Agent, ProxyAgent, setGlobalDispatcher } from "undici";
3
3
  import { CONTEXT7_API_BASE_URL } from "./constants.js";
4
4
  import { readFileSync } from "fs";
5
5
  import tls from "tls";
6
+ import { observeUpstreamRequest } from "./telemetry-runtime.js";
6
7
  /**
7
8
  * Ceiling on a single Context7 API call. Without a signal a stalled backend
8
9
  * call rides undici's ~300s default before failing. 60s is generous for these
@@ -106,15 +107,17 @@ export async function searchLibraries(query, libraryName, context = {}) {
106
107
  url.searchParams.set("query", query);
107
108
  url.searchParams.set("libraryName", libraryName);
108
109
  const headers = generateHeaders(context);
109
- const response = await fetch(url, { headers, signal: AbortSignal.timeout(API_TIMEOUT_MS) });
110
- readPromptSignal(response, context);
111
- if (!response.ok) {
112
- const errorMessage = await parseErrorResponse(response, context.apiKey);
113
- console.error(errorMessage);
114
- return { results: [], error: errorMessage };
115
- }
116
- const searchData = await response.json();
117
- return searchData;
110
+ const abortSignal = AbortSignal.timeout(API_TIMEOUT_MS);
111
+ return await observeUpstreamRequest("search_libraries", () => fetch(url, { headers, signal: abortSignal }), async (response) => {
112
+ readPromptSignal(response, context);
113
+ if (!response.ok) {
114
+ const errorMessage = await parseErrorResponse(response, context.apiKey);
115
+ console.error(errorMessage);
116
+ return { results: [], error: errorMessage };
117
+ }
118
+ const searchData = await response.json();
119
+ return searchData;
120
+ }, { abortSignal });
118
121
  }
119
122
  catch (error) {
120
123
  const errorMessage = `Error searching libraries: ${error}`;
@@ -134,24 +137,27 @@ export async function fetchLibraryContext(request, context = {}) {
134
137
  url.searchParams.set("query", request.query);
135
138
  url.searchParams.set("libraryId", request.libraryId);
136
139
  const headers = generateHeaders(context);
137
- const response = await fetch(url, { headers, signal: AbortSignal.timeout(API_TIMEOUT_MS) });
138
- readPromptSignal(response, context);
139
- if (!response.ok) {
140
- const errorMessage = await parseErrorResponse(response, context.apiKey);
141
- console.error(errorMessage);
142
- return { data: errorMessage };
143
- }
144
- const text = await response.text();
145
- if (!text) {
146
- return {
147
- data: "Documentation not found or not finalized for this library. This might have happened because you used an invalid Context7-compatible library ID. To get a valid Context7-compatible library ID, use the 'resolve-library-id' with the package name you wish to retrieve documentation for.",
148
- };
149
- }
150
- return { data: text };
140
+ const abortSignal = AbortSignal.timeout(API_TIMEOUT_MS);
141
+ return await observeUpstreamRequest("fetch_context", () => fetch(url, { headers, signal: abortSignal }), async (response) => {
142
+ readPromptSignal(response, context);
143
+ if (!response.ok) {
144
+ const errorMessage = await parseErrorResponse(response, context.apiKey);
145
+ console.error(errorMessage);
146
+ return { data: errorMessage, outcome: "error" };
147
+ }
148
+ const text = await response.text();
149
+ if (!text) {
150
+ return {
151
+ data: "Documentation not found or not finalized for this library. This might have happened because you used an invalid Context7-compatible library ID. To get a valid Context7-compatible library ID, use the 'resolve-library-id' with the package name you wish to retrieve documentation for.",
152
+ outcome: "not_found",
153
+ };
154
+ }
155
+ return { data: text, outcome: "success" };
156
+ }, { abortSignal });
151
157
  }
152
158
  catch (error) {
153
159
  const errorMessage = `Error fetching library context. Please try again later. ${error}`;
154
160
  console.error(errorMessage);
155
- return { data: errorMessage };
161
+ return { data: errorMessage, outcome: "error" };
156
162
  }
157
163
  }
@@ -0,0 +1,15 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ const operationScope = new AsyncLocalStorage();
3
+ export function runInMcpOperationScope(target, callback) {
4
+ return operationScope.run(target, callback);
5
+ }
6
+ export function markCurrentMcpOperationError(errorType = "tool_error") {
7
+ const target = operationScope.getStore();
8
+ if (target)
9
+ target.errorType = errorType;
10
+ }
11
+ export function markCurrentMcpToolOutcome(outcome) {
12
+ const target = operationScope.getStore();
13
+ if (target)
14
+ target.toolOutcome = outcome;
15
+ }