@upstash/context7-mcp 4.0.1 → 4.0.3

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/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import express from "express";
10
10
  import { Command } from "commander";
11
11
  import { AsyncLocalStorage } from "async_hooks";
12
12
  import { randomUUID } from "node:crypto";
13
- import { SERVER_VERSION, RESOURCE_URL, AUTH_SERVER_URL, OPENAI_APPS_CHALLENGE_TOKEN, } from "./lib/constants.js";
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
15
  import { getClientIp } from "./lib/client-ip.js";
16
16
  /** Default HTTP server port */
@@ -282,7 +282,17 @@ async function main() {
282
282
  // no session store. The handler serves modern (2026-07-28) traffic natively
283
283
  // and 2025-era traffic through its stateless legacy fallback, which answers
284
284
  // GET/DELETE (session operations) with 405.
285
+ // keepAliveMs: 0 disables SSE keepalive heartbeats. Every tool here is a
286
+ // millisecond vector query (p100 ~28s), so no legitimate exchange needs a
287
+ // heartbeat to stay alive — but a hung exchange kept "alive" by heartbeats
288
+ // can never be reaped by the gateway's stream idle timeout. A batch
289
+ // carrying a request plus its own notifications/cancelled produces exactly
290
+ // that: per spec the cancelled request gets no response, the SDK transport
291
+ // then never closes the stream, and with heartbeats it survived until the
292
+ // gateway's 1200s hard cap (the 2026-08-11 outage). Silent hangs instead
293
+ // go idle and the gateway reaps them at streamIdleTimeout (300s).
285
294
  const mcpHandler = createMcpHandler(() => createMcpServer(), {
295
+ keepAliveMs: 0,
286
296
  onerror: (error) => console.error("MCP handler error:", error),
287
297
  });
288
298
  // Without onerror, request-conversion / handler.fetch throws are answered
@@ -362,13 +372,16 @@ async function main() {
362
372
  app.get("/.well-known/oauth-protected-resource", (_req, res) => {
363
373
  res.json({
364
374
  resource: RESOURCE_URL,
365
- authorization_servers: [AUTH_SERVER_URL],
375
+ // Each entry is an independent authorization server. Clerk handles
376
+ // regular authorization-code flows; Context7 handles only the
377
+ // enterprise-managed id-jag exchange.
378
+ authorization_servers: Array.from(new Set([OAUTH_AUTH_SERVER_URL, EMA_ISSUER])),
366
379
  scopes_supported: ["profile", "email"],
367
380
  bearer_methods_supported: ["header"],
368
381
  });
369
382
  });
370
383
  app.get("/.well-known/oauth-authorization-server", async (_req, res) => {
371
- const authServerUrl = AUTH_SERVER_URL;
384
+ const authServerUrl = OAUTH_AUTH_SERVER_URL;
372
385
  try {
373
386
  const response = await fetch(`${authServerUrl}/.well-known/oauth-authorization-server`);
374
387
  if (!response.ok) {
package/dist/lib/api.js CHANGED
@@ -3,6 +3,13 @@ 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
+ /**
7
+ * Ceiling on a single Context7 API call. Without a signal a stalled backend
8
+ * call rides undici's ~300s default before failing. 60s is generous for these
9
+ * vector queries: p99.9 is ~3.2s, and no request exceeded 30s across a full
10
+ * day of production traffic.
11
+ */
12
+ const API_TIMEOUT_MS = 60_000;
6
13
  /**
7
14
  * Parses error response from the Context7 API
8
15
  * Extracts the server's error message, falling back to status-based messages if parsing fails
@@ -99,7 +106,7 @@ export async function searchLibraries(query, libraryName, context = {}) {
99
106
  url.searchParams.set("query", query);
100
107
  url.searchParams.set("libraryName", libraryName);
101
108
  const headers = generateHeaders(context);
102
- const response = await fetch(url, { headers });
109
+ const response = await fetch(url, { headers, signal: AbortSignal.timeout(API_TIMEOUT_MS) });
103
110
  readPromptSignal(response, context);
104
111
  if (!response.ok) {
105
112
  const errorMessage = await parseErrorResponse(response, context.apiKey);
@@ -127,7 +134,7 @@ export async function fetchLibraryContext(request, context = {}) {
127
134
  url.searchParams.set("query", request.query);
128
135
  url.searchParams.set("libraryId", request.libraryId);
129
136
  const headers = generateHeaders(context);
130
- const response = await fetch(url, { headers });
137
+ const response = await fetch(url, { headers, signal: AbortSignal.timeout(API_TIMEOUT_MS) });
131
138
  readPromptSignal(response, context);
132
139
  if (!response.ok) {
133
140
  const errorMessage = await parseErrorResponse(response, context.apiKey);
@@ -6,12 +6,20 @@ const pkg = JSON.parse(readFileSync(join(__dirname, "../../package.json"), "utf-
6
6
  export const SERVER_VERSION = pkg.version;
7
7
  const CONTEXT7_BASE_URL = "https://context7.com";
8
8
  const MCP_RESOURCE_URL = "https://mcp.context7.com";
9
- export const CLERK_DOMAIN = "clerk.context7.com";
9
+ const DEFAULT_OAUTH_AUTH_SERVER_URL = "https://clerk.context7.com";
10
10
  export const CONTEXT7_API_BASE_URL = process.env.CONTEXT7_API_URL || `${CONTEXT7_BASE_URL}/api`;
11
11
  export const RESOURCE_URL = process.env.RESOURCE_URL || MCP_RESOURCE_URL;
12
- export const AUTH_SERVER_URL = process.env.AUTH_SERVER_URL || CONTEXT7_BASE_URL;
12
+ // Clerk owns the interactive OAuth flow and is the issuer returned in the
13
+ // authorization response. Advertising Clerk directly keeps RFC 8414 discovery
14
+ // and RFC 9207 response-issuer validation on the same authorization-server
15
+ // identity.
16
+ export const OAUTH_AUTH_SERVER_URL = (process.env.OAUTH_AUTH_SERVER_URL || DEFAULT_OAUTH_AUTH_SERVER_URL).replace(/\/+$/, "");
17
+ export const OAUTH_JWKS_URL = process.env.OAUTH_JWKS_URL || `${OAUTH_AUTH_SERVER_URL}/.well-known/jwks.json`;
13
18
  // Enterprise-Managed Auth (id-jag): access tokens minted by the Context7
14
19
  // authorization server, validated against its public JWKS.
15
- export const EMA_ISSUER = AUTH_SERVER_URL;
20
+ // AUTH_SERVER_URL remains a backwards-compatible alias for local EMA setups;
21
+ // it does not move interactive user OAuth. Local end-to-end OAuth environments
22
+ // must set OAUTH_AUTH_SERVER_URL separately when Clerk is not the intended issuer.
23
+ export const EMA_ISSUER = process.env.EMA_ISSUER || process.env.AUTH_SERVER_URL || CONTEXT7_BASE_URL;
16
24
  export const EMA_JWKS_URL = process.env.EMA_JWKS_URL || `${CONTEXT7_API_BASE_URL}/oauth/ema-jwks`;
17
25
  export const OPENAI_APPS_CHALLENGE_TOKEN = process.env.OPENAI_APPS_CHALLENGE_TOKEN;
package/dist/lib/jwt.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import * as jose from "jose";
2
- import { CLERK_DOMAIN, CONTEXT7_API_BASE_URL, EMA_ISSUER, EMA_JWKS_URL, RESOURCE_URL, } from "./constants.js";
3
- const CLERK_ISSUER = `https://${CLERK_DOMAIN}`;
4
- const clerkJwks = jose.createRemoteJWKSet(new URL(`https://${CLERK_DOMAIN}/.well-known/jwks.json`));
2
+ import { CONTEXT7_API_BASE_URL, EMA_ISSUER, EMA_JWKS_URL, OAUTH_AUTH_SERVER_URL, OAUTH_JWKS_URL, RESOURCE_URL, } from "./constants.js";
3
+ const oauthJwks = jose.createRemoteJWKSet(new URL(OAUTH_JWKS_URL));
5
4
  const emaJwks = jose.createRemoteJWKSet(new URL(EMA_JWKS_URL));
6
5
  const ENTRA_V2_ISSUER_RE = /^https:\/\/login\.microsoftonline\.com\/[0-9a-f-]{36}\/v2\.0$/;
7
6
  const jwksByTenant = new Map();
@@ -70,7 +69,7 @@ export async function validateJWT(token) {
70
69
  await jose.jwtVerify(token, emaJwks, { issuer: EMA_ISSUER, audience: RESOURCE_URL });
71
70
  return { valid: true };
72
71
  }
73
- await jose.jwtVerify(token, clerkJwks, { issuer: CLERK_ISSUER });
72
+ await jose.jwtVerify(token, oauthJwks, { issuer: OAUTH_AUTH_SERVER_URL });
74
73
  return { valid: true };
75
74
  }
76
75
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upstash/context7-mcp",
3
- "version": "4.0.1",
3
+ "version": "4.0.3",
4
4
  "mcpName": "io.github.upstash/context7",
5
5
  "description": "MCP server for Context7",
6
6
  "repository": {