@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 +16 -3
- package/dist/lib/api.js +9 -2
- package/dist/lib/constants.js +11 -3
- package/dist/lib/jwt.js +3 -4
- package/package.json +1 -1
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,
|
|
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
|
-
|
|
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 =
|
|
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);
|
package/dist/lib/constants.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
3
|
-
const
|
|
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,
|
|
72
|
+
await jose.jwtVerify(token, oauthJwks, { issuer: OAUTH_AUTH_SERVER_URL });
|
|
74
73
|
return { valid: true };
|
|
75
74
|
}
|
|
76
75
|
catch (error) {
|