@upstash/context7-mcp 4.0.4 → 4.0.5
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 -6
- package/dist/lib/encryption.js +7 -26
- package/dist/lib/subscriptions.js +11 -0
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -12,8 +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 { getMaxSubscriptions } from "./lib/subscriptions.js";
|
|
15
16
|
/** Default HTTP server port */
|
|
16
17
|
const DEFAULT_PORT = 3000;
|
|
18
|
+
const CLAUDE_CODE_PLUGIN = "claude-code-plugin";
|
|
19
|
+
function getPluginFromRequest(req) {
|
|
20
|
+
return req.query.client === CLAUDE_CODE_PLUGIN ? CLAUDE_CODE_PLUGIN : undefined;
|
|
21
|
+
}
|
|
22
|
+
function requiresAuthentication(req, plugin) {
|
|
23
|
+
return req.path === "/mcp/oauth" || Boolean(plugin);
|
|
24
|
+
}
|
|
17
25
|
// Parse CLI arguments using commander
|
|
18
26
|
const program = new Command()
|
|
19
27
|
.version(SERVER_VERSION, "-v, --version", "output the current version")
|
|
@@ -296,6 +304,7 @@ async function main() {
|
|
|
296
304
|
// go idle and the gateway reaps them at streamIdleTimeout (300s).
|
|
297
305
|
const mcpHandler = createMcpHandler(() => createMcpServer(), {
|
|
298
306
|
keepAliveMs: 0,
|
|
307
|
+
maxSubscriptions: getMaxSubscriptions(),
|
|
299
308
|
onerror: (error) => console.error("MCP handler error:", error),
|
|
300
309
|
});
|
|
301
310
|
// Without onerror, request-conversion / handler.fetch throws are answered
|
|
@@ -303,8 +312,9 @@ async function main() {
|
|
|
303
312
|
const nodeHandler = toNodeHandler(mcpHandler, {
|
|
304
313
|
onerror: (error) => console.error("MCP node adapter error:", error),
|
|
305
314
|
});
|
|
306
|
-
const handleMcpRequest = async (req, res
|
|
315
|
+
const handleMcpRequest = async (req, res) => {
|
|
307
316
|
try {
|
|
317
|
+
const plugin = getPluginFromRequest(req);
|
|
308
318
|
const apiKey = extractApiKey(req);
|
|
309
319
|
const baseUrl = new URL(RESOURCE_URL).origin;
|
|
310
320
|
// OAuth discovery info header, used by MCP clients to discover the authorization server
|
|
@@ -313,7 +323,7 @@ async function main() {
|
|
|
313
323
|
// oauthMetadataResponse) — replace this hand-rolled header and the
|
|
314
324
|
// /.well-known/oauth-protected-resource route with them.
|
|
315
325
|
res.set("WWW-Authenticate", `Bearer resource_metadata="${baseUrl}/.well-known/oauth-protected-resource"`);
|
|
316
|
-
if (
|
|
326
|
+
if (requiresAuthentication(req, plugin)) {
|
|
317
327
|
if (!apiKey) {
|
|
318
328
|
return res.status(401).json({
|
|
319
329
|
jsonrpc: "2.0",
|
|
@@ -340,8 +350,9 @@ async function main() {
|
|
|
340
350
|
}
|
|
341
351
|
const context = {
|
|
342
352
|
clientIp: req.ip,
|
|
343
|
-
apiKey
|
|
353
|
+
apiKey,
|
|
344
354
|
clientInfo: extractClientInfoFromUserAgent(req.headers["user-agent"]),
|
|
355
|
+
plugin,
|
|
345
356
|
transport: "http",
|
|
346
357
|
};
|
|
347
358
|
await requestContext.run(context, async () => {
|
|
@@ -359,13 +370,12 @@ async function main() {
|
|
|
359
370
|
}
|
|
360
371
|
}
|
|
361
372
|
};
|
|
362
|
-
// Anonymous access endpoint - no authentication required
|
|
363
373
|
app.all("/mcp", async (req, res) => {
|
|
364
|
-
await handleMcpRequest(req, res
|
|
374
|
+
await handleMcpRequest(req, res);
|
|
365
375
|
});
|
|
366
376
|
// OAuth-protected endpoint - requires authentication
|
|
367
377
|
app.all("/mcp/oauth", async (req, res) => {
|
|
368
|
-
await handleMcpRequest(req, res
|
|
378
|
+
await handleMcpRequest(req, res);
|
|
369
379
|
});
|
|
370
380
|
app.get("/ping", (_req, res) => {
|
|
371
381
|
res.json({ status: "ok", message: "pong" });
|
package/dist/lib/encryption.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { createCipheriv, randomBytes } from "crypto";
|
|
2
2
|
import { isIP } from "node:net";
|
|
3
3
|
import { SERVER_VERSION } from "./constants.js";
|
|
4
|
-
const LEGACY_ALGORITHM = "aes-256-cbc";
|
|
5
4
|
const ASSERTION_ALGORITHM = "aes-256-gcm";
|
|
6
5
|
const ASSERTION_VERSION = "v1";
|
|
7
6
|
let reportedInvalidAssertionKey = false;
|
|
@@ -9,31 +8,16 @@ function validateEncryptionKey(key) {
|
|
|
9
8
|
// Must be exactly 64 hex characters (32 bytes)
|
|
10
9
|
return /^[0-9a-fA-F]{64}$/.test(key);
|
|
11
10
|
}
|
|
12
|
-
function
|
|
13
|
-
const key = process.env
|
|
11
|
+
function assertionKey() {
|
|
12
|
+
const key = process.env.MCP_CLIENT_IP_ASSERTION_KEY;
|
|
14
13
|
return key && validateEncryptionKey(key) ? Buffer.from(key, "hex") : null;
|
|
15
14
|
}
|
|
16
|
-
/**
|
|
17
|
-
* Temporary compatibility header for API deployments that predate authenticated assertions.
|
|
18
|
-
* This header is ignored by patched API deployments. Removal is tracked by CTX7-2536.
|
|
19
|
-
*/
|
|
20
|
-
function encryptLegacyClientIp(clientIp, key) {
|
|
21
|
-
try {
|
|
22
|
-
const iv = randomBytes(16);
|
|
23
|
-
const cipher = createCipheriv(LEGACY_ALGORITHM, key, iv);
|
|
24
|
-
const encrypted = Buffer.concat([cipher.update(clientIp, "utf8"), cipher.final()]);
|
|
25
|
-
return `${iv.toString("hex")}:${encrypted.toString("hex")}`;
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
15
|
/**
|
|
32
16
|
* Create a short-lived, authenticated client-IP assertion.
|
|
33
17
|
* Format: v1:<unix timestamp seconds>:<12-byte nonce hex>:<ciphertext + tag hex>
|
|
34
18
|
*/
|
|
35
19
|
export function createClientIpAssertion(clientIp, nowMs = Date.now(), nonce = randomBytes(12)) {
|
|
36
|
-
const key =
|
|
20
|
+
const key = assertionKey();
|
|
37
21
|
if (!key) {
|
|
38
22
|
if (!reportedInvalidAssertionKey) {
|
|
39
23
|
reportedInvalidAssertionKey = true;
|
|
@@ -67,14 +51,8 @@ export function generateHeaders(context) {
|
|
|
67
51
|
};
|
|
68
52
|
if (context.clientIp) {
|
|
69
53
|
const assertion = createClientIpAssertion(context.clientIp);
|
|
70
|
-
if (assertion)
|
|
54
|
+
if (assertion)
|
|
71
55
|
headers["mcp-client-ip-assertion"] = assertion;
|
|
72
|
-
// Producer-first rollout compatibility. Removal is tracked by CTX7-2536.
|
|
73
|
-
const key = encryptionKey("CLIENT_IP_ENCRYPTION_KEY");
|
|
74
|
-
const legacyValue = key ? encryptLegacyClientIp(context.clientIp, key) : null;
|
|
75
|
-
if (legacyValue)
|
|
76
|
-
headers["mcp-client-ip"] = legacyValue;
|
|
77
|
-
}
|
|
78
56
|
}
|
|
79
57
|
if (context.sessionId) {
|
|
80
58
|
headers["mcp-session-id"] = context.sessionId;
|
|
@@ -88,6 +66,9 @@ export function generateHeaders(context) {
|
|
|
88
66
|
if (context.clientInfo?.version) {
|
|
89
67
|
headers["X-Context7-Client-Version"] = context.clientInfo.version;
|
|
90
68
|
}
|
|
69
|
+
if (context.plugin) {
|
|
70
|
+
headers["X-Context7-Plugin"] = context.plugin;
|
|
71
|
+
}
|
|
91
72
|
if (context.transport) {
|
|
92
73
|
headers["X-Context7-Transport"] = context.transport;
|
|
93
74
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// 16k stayed near baseline latency in Docker; 32,768 raised tools/list p95 to ~39 ms.
|
|
2
|
+
export const DEFAULT_MAX_SUBSCRIPTIONS = 16_000;
|
|
3
|
+
export function getMaxSubscriptions(value = process.env.MCP_MAX_SUBSCRIPTIONS) {
|
|
4
|
+
if (value === undefined)
|
|
5
|
+
return DEFAULT_MAX_SUBSCRIPTIONS;
|
|
6
|
+
const parsed = Number(value);
|
|
7
|
+
if (Number.isSafeInteger(parsed) && parsed > 0)
|
|
8
|
+
return parsed;
|
|
9
|
+
console.warn(`Invalid MCP_MAX_SUBSCRIPTIONS; using the default of ${DEFAULT_MAX_SUBSCRIPTIONS}.`);
|
|
10
|
+
return DEFAULT_MAX_SUBSCRIPTIONS;
|
|
11
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@upstash/context7-mcp",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.5",
|
|
4
4
|
"mcpName": "io.github.upstash/context7",
|
|
5
5
|
"description": "MCP server for Context7",
|
|
6
6
|
"repository": {
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@modelcontextprotocol/client": "2.0.0",
|
|
47
47
|
"@types/node": "^25.0.3",
|
|
48
|
+
"esbuild": "^0.28.2",
|
|
48
49
|
"typescript": "^5.8.2",
|
|
49
50
|
"vitest": "^4.1.9"
|
|
50
51
|
},
|
|
@@ -61,6 +62,7 @@
|
|
|
61
62
|
"format:check": "prettier --check .",
|
|
62
63
|
"dev": "tsc --watch",
|
|
63
64
|
"start": "node dist/index.js --transport http",
|
|
64
|
-
"
|
|
65
|
+
"build:mcpb": "esbuild src/index.ts --bundle --platform=node --target=node20 --format=esm --outfile=mcpb/stage/server/dist/index.mjs --banner:js=\"import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);\"",
|
|
66
|
+
"pack-mcpb": "rm -rf mcpb/stage && pnpm run build:mcpb && cp package.json mcpb/stage/package.json && cp mcpb/manifest.json mcpb/stage/manifest.json && cp ../../public/icon.png mcpb/stage/icon.png && mcpb validate mcpb/stage/manifest.json && mcpb pack mcpb/stage mcpb/context7.mcpb && rm -rf mcpb/stage"
|
|
65
67
|
}
|
|
66
68
|
}
|