@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 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, requireAuth) => {
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 (requireAuth) {
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: 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, false);
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, true);
378
+ await handleMcpRequest(req, res);
369
379
  });
370
380
  app.get("/ping", (_req, res) => {
371
381
  res.json({ status: "ok", message: "pong" });
@@ -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 encryptionKey(name) {
13
- const key = process.env[name];
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 = encryptionKey("MCP_CLIENT_IP_ASSERTION_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.4",
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
- "pack-mcpb": "pnpm install && pnpm run build && rm -rf node_modules && pnpm install --prod && cp mcpb/manifest.json manifest.json && cp mcpb/.mcpbignore .mcpbignore && cp ../../public/icon.png icon.png && mcpb validate manifest.json && mcpb pack . mcpb/context7.mcpb && rm manifest.json .mcpbignore icon.png && pnpm install"
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
  }