@upstash/context7-mcp 4.0.5 → 4.0.7

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 CHANGED
@@ -1621,6 +1621,34 @@ Stay updated and join our community:
1621
1621
  - [AICodeKing: "Context7 + Cline & RooCode: This MCP Server Makes CLINE 100X MORE EFFECTIVE!"](https://www.youtube.com/watch?v=qZfENAPMnyo)
1622
1622
  - [Sean Kochel: "5 MCP Servers For Vibe Coding Glory (Just Plug-In & Go)"](https://www.youtube.com/watch?v=LqTQi8qexJM)
1623
1623
 
1624
+ ## Vercel Marketplace OIDC
1625
+
1626
+ Vercel Marketplace resources can call the remote MCP server without a
1627
+ long-lived Context7 API key. Obtain a fresh per-resource access token from
1628
+ Vercel's Marketplace integration runtime and send it as the bearer credential:
1629
+
1630
+ ```ts
1631
+ import { getIntegrationToken } from "@vercel/integrations";
1632
+
1633
+ const authorization = `Bearer ${await getIntegrationToken("context7")}`;
1634
+ ```
1635
+
1636
+ The `getIntegrationToken` signature is based on Vercel's current provider
1637
+ specification and may change before Marketplace OIDC is generally available.
1638
+
1639
+ Use `authorization` as the `Authorization` header when creating the MCP HTTP
1640
+ transport. Create the transport inside the request that uses it so a short-lived
1641
+ token is not retained across function invocations. Its `resource` claim must
1642
+ match the Context7 resource created during Marketplace provisioning.
1643
+
1644
+ The hosted MCP service must be configured with the exact issuer and audience
1645
+ assigned by Vercel when the Context7 Marketplace product is created:
1646
+
1647
+ ```sh
1648
+ VERCEL_MARKETPLACE_OIDC_ISSUER=https://integrations.vercel.com/oac_...
1649
+ VERCEL_MARKETPLACE_OIDC_AUDIENCE=https://integrations.vercel.com/context7/icfg_...
1650
+ ```
1651
+
1624
1652
  ## 📄 License
1625
1653
 
1626
1654
  MIT
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ 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
15
  import { getMaxSubscriptions } from "./lib/subscriptions.js";
16
+ import { mcpBodyErrorHandler } from "./lib/mcp-body-error-handler.js";
16
17
  /** Default HTTP server port */
17
18
  const DEFAULT_PORT = 3000;
18
19
  const CLAUDE_CODE_PLUGIN = "claude-code-plugin";
@@ -20,7 +21,11 @@ function getPluginFromRequest(req) {
20
21
  return req.query.client === CLAUDE_CODE_PLUGIN ? CLAUDE_CODE_PLUGIN : undefined;
21
22
  }
22
23
  function requiresAuthentication(req, plugin) {
23
- return req.path === "/mcp/oauth" || Boolean(plugin);
24
+ // The MCP routes live on a router mounted at /mcp, so req.path is relative to it.
25
+ const isOAuthEndpoint = `${req.baseUrl}${req.path}` === "/mcp/oauth";
26
+ // The current official Claude plugin expands an unset API key to an empty header.
27
+ const hasEmptyPluginAuthorization = plugin === CLAUDE_CODE_PLUGIN && req.headers.authorization === "";
28
+ return isOAuthEndpoint || (Boolean(plugin) && !hasEmptyPluginAuthorization);
24
29
  }
25
30
  // Parse CLI arguments using commander
26
31
  const program = new Command()
@@ -252,7 +257,9 @@ async function main() {
252
257
  // Only private/local infrastructure may supply forwarding headers. Express
253
258
  // then walks the chain right-to-left and ignores attacker-added prefixes.
254
259
  app.set("trust proxy", ["loopback", "linklocal", "uniquelocal", "100.64.0.0/10"]);
255
- app.use(express.json());
260
+ // Registered ahead of the MCP router so its error responses carry the CORS
261
+ // headers too; browser clients would otherwise see a CORS failure instead
262
+ // of the status.
256
263
  app.use((req, res, next) => {
257
264
  res.setHeader("Access-Control-Allow-Origin", "*");
258
265
  res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,DELETE");
@@ -370,13 +377,16 @@ async function main() {
370
377
  }
371
378
  }
372
379
  };
373
- app.all("/mcp", async (req, res) => {
374
- await handleMcpRequest(req, res);
375
- });
380
+ // JSON bodies and JSON-RPC error envelopes are the MCP contract only, so the
381
+ // parser and its error boundary live on the MCP router: every other route
382
+ // stays out of the parser and keeps its own response shape.
383
+ const mcpRouter = express.Router();
384
+ mcpRouter.use(express.json());
385
+ mcpRouter.use(mcpBodyErrorHandler);
386
+ mcpRouter.all("/", (req, res) => handleMcpRequest(req, res));
376
387
  // OAuth-protected endpoint - requires authentication
377
- app.all("/mcp/oauth", async (req, res) => {
378
- await handleMcpRequest(req, res);
379
- });
388
+ mcpRouter.all("/oauth", (req, res) => handleMcpRequest(req, res));
389
+ app.use("/mcp", mcpRouter);
380
390
  app.get("/ping", (_req, res) => {
381
391
  res.json({ status: "ok", message: "pong" });
382
392
  });
package/dist/lib/jwt.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as jose from "jose";
2
2
  import { CONTEXT7_API_BASE_URL, EMA_ISSUER, EMA_JWKS_URL, OAUTH_AUTH_SERVER_URL, OAUTH_JWKS_URL, RESOURCE_URL, } from "./constants.js";
3
+ import { isVercelMarketplaceIssuer, validateVercelMarketplaceJwt } from "./vercelMarketplaceJwt.js";
3
4
  const oauthJwks = jose.createRemoteJWKSet(new URL(OAUTH_JWKS_URL));
4
5
  const emaJwks = jose.createRemoteJWKSet(new URL(EMA_JWKS_URL));
5
6
  const ENTRA_V2_ISSUER_RE = /^https:\/\/login\.microsoftonline\.com\/[0-9a-f-]{36}\/v2\.0$/;
@@ -69,6 +70,9 @@ export async function validateJWT(token) {
69
70
  await jose.jwtVerify(token, emaJwks, { issuer: EMA_ISSUER, audience: RESOURCE_URL });
70
71
  return { valid: true };
71
72
  }
73
+ if (isVercelMarketplaceIssuer(iss)) {
74
+ return validateVercelMarketplaceJwt(token, iss);
75
+ }
72
76
  await jose.jwtVerify(token, oauthJwks, { issuer: OAUTH_AUTH_SERVER_URL });
73
77
  return { valid: true };
74
78
  }
@@ -0,0 +1,23 @@
1
+ // Error boundary for the MCP router's JSON body parser. express.json() reports
2
+ // a body it will not accept via next(err); left unhandled, Express answers with
3
+ // an HTML stack trace naming dependency versions and absolute paths.
4
+ export const mcpBodyErrorHandler = (err, req, res, next) => {
5
+ const { status, type } = err;
6
+ // body-parser tags every rejection with a 4xx; anything else is not a body
7
+ // problem. Once a response has started, only Express can wind it down.
8
+ if (typeof status !== "number" || status < 400 || status >= 500 || res.headersSent) {
9
+ return next(err);
10
+ }
11
+ // Method, path and size only: the body is the rejected input, and the
12
+ // headers carry the API key.
13
+ console.error(`Rejected request body (${status}, ${String(type ?? "unknown")}): ${req.method} ${req.baseUrl}${req.path} bytes=${req.headers["content-length"] ?? "unknown"}`);
14
+ // Fixed messages: body-parser's own quote offsets into the body.
15
+ const parseFailure = err instanceof SyntaxError && type === "entity.parse.failed";
16
+ res.status(status).json({
17
+ jsonrpc: "2.0",
18
+ error: parseFailure
19
+ ? { code: -32700, message: "Parse error" }
20
+ : { code: -32600, message: "Invalid Request" },
21
+ id: null,
22
+ });
23
+ };
@@ -0,0 +1,54 @@
1
+ import * as jose from "jose";
2
+ const VERCEL_INTEGRATIONS_ORIGIN = "https://integrations.vercel.com";
3
+ const VERCEL_ISSUER_PATH = /^\/oac_[A-Za-z0-9]+$/;
4
+ const VERCEL_AUDIENCE_PATH = /^\/[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\/icfg_[A-Za-z0-9]+$/;
5
+ let jwks;
6
+ function isExpectedVercelUrl(value, pathPattern) {
7
+ try {
8
+ const url = new URL(value);
9
+ return (url.origin === VERCEL_INTEGRATIONS_ORIGIN &&
10
+ !url.username &&
11
+ !url.password &&
12
+ !url.search &&
13
+ !url.hash &&
14
+ pathPattern.test(url.pathname));
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
20
+ function getConfig() {
21
+ const issuer = process.env.VERCEL_MARKETPLACE_OIDC_ISSUER ?? "";
22
+ const audience = process.env.VERCEL_MARKETPLACE_OIDC_AUDIENCE ?? "";
23
+ if (!isVercelMarketplaceIssuer(issuer) || !isExpectedVercelUrl(audience, VERCEL_AUDIENCE_PATH)) {
24
+ return null;
25
+ }
26
+ return { issuer, audience };
27
+ }
28
+ function getJwks(issuer) {
29
+ if (!jwks) {
30
+ jwks = jose.createRemoteJWKSet(new URL(`${issuer}/.well-known/jwks`));
31
+ }
32
+ return jwks;
33
+ }
34
+ export function isVercelMarketplaceIssuer(issuer) {
35
+ return isExpectedVercelUrl(issuer, VERCEL_ISSUER_PATH);
36
+ }
37
+ export async function validateVercelMarketplaceJwt(token, tokenIssuer) {
38
+ const config = getConfig();
39
+ if (!config)
40
+ return { valid: false, error: "Vercel Marketplace OIDC not configured" };
41
+ if (tokenIssuer !== config.issuer) {
42
+ return { valid: false, error: "Untrusted Vercel Marketplace issuer" };
43
+ }
44
+ const { payload } = await jose.jwtVerify(token, getJwks(config.issuer), {
45
+ algorithms: ["RS256"],
46
+ audience: config.audience,
47
+ issuer: config.issuer,
48
+ clockTolerance: 60,
49
+ });
50
+ if (typeof payload.resource !== "string" || !payload.resource) {
51
+ return { valid: false, error: "Missing Vercel Marketplace resource" };
52
+ }
53
+ return { valid: true };
54
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upstash/context7-mcp",
3
- "version": "4.0.5",
3
+ "version": "4.0.7",
4
4
  "mcpName": "io.github.upstash/context7",
5
5
  "description": "MCP server for Context7",
6
6
  "repository": {