@upstash/context7-mcp 4.0.5 → 4.0.6

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,8 @@ 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
+ return `${req.baseUrl}${req.path}` === "/mcp/oauth" || Boolean(plugin);
24
26
  }
25
27
  // Parse CLI arguments using commander
26
28
  const program = new Command()
@@ -252,7 +254,9 @@ async function main() {
252
254
  // Only private/local infrastructure may supply forwarding headers. Express
253
255
  // then walks the chain right-to-left and ignores attacker-added prefixes.
254
256
  app.set("trust proxy", ["loopback", "linklocal", "uniquelocal", "100.64.0.0/10"]);
255
- app.use(express.json());
257
+ // Registered ahead of the MCP router so its error responses carry the CORS
258
+ // headers too; browser clients would otherwise see a CORS failure instead
259
+ // of the status.
256
260
  app.use((req, res, next) => {
257
261
  res.setHeader("Access-Control-Allow-Origin", "*");
258
262
  res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,DELETE");
@@ -370,13 +374,16 @@ async function main() {
370
374
  }
371
375
  }
372
376
  };
373
- app.all("/mcp", async (req, res) => {
374
- await handleMcpRequest(req, res);
375
- });
377
+ // JSON bodies and JSON-RPC error envelopes are the MCP contract only, so the
378
+ // parser and its error boundary live on the MCP router: every other route
379
+ // stays out of the parser and keeps its own response shape.
380
+ const mcpRouter = express.Router();
381
+ mcpRouter.use(express.json());
382
+ mcpRouter.use(mcpBodyErrorHandler);
383
+ mcpRouter.all("/", (req, res) => handleMcpRequest(req, res));
376
384
  // OAuth-protected endpoint - requires authentication
377
- app.all("/mcp/oauth", async (req, res) => {
378
- await handleMcpRequest(req, res);
379
- });
385
+ mcpRouter.all("/oauth", (req, res) => handleMcpRequest(req, res));
386
+ app.use("/mcp", mcpRouter);
380
387
  app.get("/ping", (_req, res) => {
381
388
  res.json({ status: "ok", message: "pong" });
382
389
  });
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.6",
4
4
  "mcpName": "io.github.upstash/context7",
5
5
  "description": "MCP server for Context7",
6
6
  "repository": {