@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 +127 -0
- package/dist/index.js +108 -40
- package/dist/lib/api.js +30 -24
- package/dist/lib/mcp-operation-scope.js +15 -0
- package/dist/lib/mcp-subscription-telemetry.js +466 -0
- package/dist/lib/mcp-telemetry.js +627 -0
- package/dist/lib/process-shutdown.js +63 -0
- package/dist/lib/telemetry-config.js +14 -0
- package/dist/lib/telemetry-contracts.js +1 -0
- package/dist/lib/telemetry-provider.js +72 -0
- package/dist/lib/telemetry-runtime.js +67 -0
- package/dist/lib/telemetry.js +189 -0
- package/dist/lib/tool-names.js +3 -0
- package/package.json +9 -1
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
335
|
-
|
|
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:
|
|
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
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
+
}
|