@upstash/context7-mcp 4.0.7 → 4.1.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.
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,7 +133,8 @@ function createMcpServer() {
128
133
  mimeType: "image/png",
129
134
  },
130
135
  ],
131
- }, {
136
+ };
137
+ const serverOptions = {
132
138
  // Declaring the capabilities makes the SDK install prompts/list,
133
139
  // resources/list, and resources/templates/list handlers that answer
134
140
  // with the registered (i.e. empty) collections, for clients that
@@ -137,8 +143,11 @@ function createMcpServer() {
137
143
  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
144
 
139
145
  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", {
146
+ };
147
+ const server = mcpInstrumentation
148
+ ? mcpInstrumentation.createServer(serverInfo, serverOptions, mcpContext)
149
+ : new McpServer(serverInfo, serverOptions);
150
+ server.registerTool(RESOLVE_LIBRARY_ID_TOOL, {
142
151
  title: "Resolve Context7 Library ID",
143
152
  description: `Resolves a package/product name to a Context7-compatible library ID and returns matching libraries.
144
153
 
@@ -193,6 +202,7 @@ IMPORTANT: Do not call this tool more than 3 times per question. If you cannot f
193
202
  if (!searchResponse.results || searchResponse.results.length === 0) {
194
203
  const text = searchResponse.error ?? "No libraries found matching the provided name.";
195
204
  maybeElicitAuthSignIn(server, ctx);
205
+ recordToolCallOutcome(searchResponse.error ? "error" : "not_found");
196
206
  return {
197
207
  content: [
198
208
  {
@@ -205,6 +215,7 @@ IMPORTANT: Do not call this tool more than 3 times per question. If you cannot f
205
215
  const resultsText = formatSearchResults(searchResponse);
206
216
  const responseText = `Available Libraries:\n\n${resultsText}`;
207
217
  maybeElicitAuthSignIn(server, ctx);
218
+ recordToolCallOutcome("success");
208
219
  return {
209
220
  content: [
210
221
  {
@@ -214,7 +225,7 @@ IMPORTANT: Do not call this tool more than 3 times per question. If you cannot f
214
225
  ],
215
226
  };
216
227
  });
217
- server.registerTool("query-docs", {
228
+ server.registerTool(QUERY_DOCS_TOOL, {
218
229
  title: "Query Documentation",
219
230
  description: `Retrieves and queries up-to-date documentation and code examples from Context7 for any programming library or framework.
220
231
 
@@ -239,6 +250,7 @@ Do not call this tool more than 3 times per question.`,
239
250
  const ctx = getClientContext(toolCtx);
240
251
  const response = await fetchLibraryContext({ query, libraryId }, ctx);
241
252
  maybeElicitAuthSignIn(server, ctx);
253
+ recordToolCallOutcome(response.outcome);
242
254
  return {
243
255
  content: [
244
256
  {
@@ -251,6 +263,10 @@ Do not call this tool more than 3 times per question.`,
251
263
  return server;
252
264
  }
253
265
  async function main() {
266
+ mcpInstrumentation = await initializeTelemetry({
267
+ allowEmbeddedPrometheus: TRANSPORT_TYPE === "http",
268
+ serviceVersion: SERVER_VERSION,
269
+ });
254
270
  if (TRANSPORT_TYPE === "http") {
255
271
  const initialPort = CLI_PORT ?? DEFAULT_PORT;
256
272
  const app = express();
@@ -309,11 +325,14 @@ async function main() {
309
325
  // then never closes the stream, and with heartbeats it survived until the
310
326
  // gateway's 1200s hard cap (the 2026-08-11 outage). Silent hangs instead
311
327
  // go idle and the gateway reaps them at streamIdleTimeout (300s).
312
- const mcpHandler = createMcpHandler(() => createMcpServer(), {
328
+ const rawMcpHandler = createMcpHandler((mcpContext) => createMcpServer(mcpContext), {
313
329
  keepAliveMs: 0,
314
330
  maxSubscriptions: getMaxSubscriptions(),
315
331
  onerror: (error) => console.error("MCP handler error:", error),
316
332
  });
333
+ const mcpHandler = mcpInstrumentation
334
+ ? mcpInstrumentation.instrumentHttpHandler(rawMcpHandler)
335
+ : rawMcpHandler;
317
336
  // Without onerror, request-conversion / handler.fetch throws are answered
318
337
  // with a bare 500 inside the adapter and never reach our express handler.
319
338
  const nodeHandler = toNodeHandler(mcpHandler, {
@@ -325,34 +344,42 @@ async function main() {
325
344
  const apiKey = extractApiKey(req);
326
345
  const baseUrl = new URL(RESOURCE_URL).origin;
327
346
  // 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
347
  res.set("WWW-Authenticate", `Bearer resource_metadata="${baseUrl}/.well-known/oauth-protected-resource"`);
333
348
  if (requiresAuthentication(req, plugin)) {
334
- if (!apiKey) {
335
- return res.status(401).json({
349
+ const authentication = await observeAuthentication(async () => {
350
+ if (!apiKey) {
351
+ return {
352
+ outcome: "missing",
353
+ value: {
354
+ accepted: false,
355
+ error: "Authentication required. Please authenticate to use this MCP server.",
356
+ },
357
+ };
358
+ }
359
+ if (isJWT(apiKey)) {
360
+ const validationResult = await validateJWT(apiKey);
361
+ if (!validationResult.valid) {
362
+ return {
363
+ outcome: "invalid",
364
+ value: {
365
+ accepted: false,
366
+ error: validationResult.error || "Invalid token. Please re-authenticate.",
367
+ },
368
+ };
369
+ }
370
+ }
371
+ return { outcome: "accepted", value: { accepted: true } };
372
+ });
373
+ if (!authentication.accepted) {
374
+ res.status(401).json({
336
375
  jsonrpc: "2.0",
337
376
  error: {
338
377
  code: -32001,
339
- message: "Authentication required. Please authenticate to use this MCP server.",
378
+ message: authentication.error,
340
379
  },
341
380
  id: null,
342
381
  });
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
- }
382
+ return;
356
383
  }
357
384
  }
358
385
  const context = {
@@ -406,16 +433,22 @@ async function main() {
406
433
  app.get("/.well-known/oauth-authorization-server", async (_req, res) => {
407
434
  const authServerUrl = OAUTH_AUTH_SERVER_URL;
408
435
  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({
436
+ const abortSignal = AbortSignal.timeout(OAUTH_METADATA_TIMEOUT_MS);
437
+ const upstream = await observeUpstreamRequest("oauth_metadata", () => fetch(`${authServerUrl}/.well-known/oauth-authorization-server`, {
438
+ signal: abortSignal,
439
+ }), async (response) => {
440
+ if (!response.ok)
441
+ return { ok: false, status: response.status };
442
+ return { ok: true, metadata: await response.json() };
443
+ }, { abortSignal });
444
+ if (!upstream.ok) {
445
+ console.error("[OAuth] Upstream error:", upstream.status);
446
+ return res.status(upstream.status).json({
413
447
  error: "upstream_error",
414
448
  message: "Failed to fetch authorization server metadata",
415
449
  });
416
450
  }
417
- const metadata = await response.json();
418
- res.json(metadata);
451
+ res.json(upstream.metadata);
419
452
  }
420
453
  catch (error) {
421
454
  console.error("[OAuth] Error fetching OAuth metadata:", error);
@@ -442,8 +475,36 @@ async function main() {
442
475
  message: "Endpoint not found. Use /mcp for MCP protocol communication.",
443
476
  });
444
477
  });
478
+ let activeHttpServer;
479
+ installProcessShutdown({
480
+ close: async () => {
481
+ const server = activeHttpServer;
482
+ const operations = [mcpHandler.close()];
483
+ if (server) {
484
+ operations.unshift(new Promise((resolve, reject) => {
485
+ server.close((error) => {
486
+ if (error)
487
+ reject(error);
488
+ else
489
+ resolve();
490
+ });
491
+ }));
492
+ }
493
+ const results = await Promise.allSettled(operations);
494
+ const failures = results
495
+ .filter((result) => result.status === "rejected")
496
+ .map((result) => result.reason);
497
+ if (failures.length > 0) {
498
+ throw new AggregateError(failures, "MCP HTTP server failed to close cleanly");
499
+ }
500
+ },
501
+ }, {
502
+ flush: forceFlushTelemetry,
503
+ onerror: (error) => console.error("Failed to close MCP HTTP server:", error),
504
+ });
445
505
  const startServer = (port, maxAttempts = 10) => {
446
506
  const httpServer = app.listen(port);
507
+ activeHttpServer = httpServer;
447
508
  httpServer.once("error", (err) => {
448
509
  if (err.code === "EADDRINUSE" && port < initialPort + maxAttempts) {
449
510
  console.warn(`Port ${port} is in use, trying port ${port + 1}...`);
@@ -463,11 +524,12 @@ async function main() {
463
524
  else {
464
525
  stdioApiKey = cliOptions.apiKey || process.env.CONTEXT7_API_KEY;
465
526
  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();
527
+ const rawStdioTransport = new StdioServerTransport();
528
+ const stdioTransport = mcpInstrumentation
529
+ ? mcpInstrumentation.instrumentStdioTransport(rawStdioTransport)
530
+ : rawStdioTransport;
531
+ const stdioHandle = serveStdio((mcpContext) => {
532
+ const server = createMcpServer(mcpContext);
471
533
  // Capture client info from MCP initialize handshake (stdio only — HTTP
472
534
  // mode plumbs client info through requestContext per request).
473
535
  server.server.oninitialized = () => {
@@ -481,8 +543,14 @@ async function main() {
481
543
  };
482
544
  return server;
483
545
  }, {
546
+ transport: stdioTransport,
484
547
  onerror: (error) => console.error("MCP stdio error:", error),
485
548
  });
549
+ installProcessShutdown(stdioHandle, {
550
+ flush: forceFlushTelemetry,
551
+ input: process.stdin,
552
+ onerror: (error) => console.error("Failed to close MCP stdio server:", error),
553
+ });
486
554
  console.error(`Context7 Documentation MCP Server v${SERVER_VERSION} running on stdio`);
487
555
  }
488
556
  }
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
+ }