@upstash/context7-mcp 2.0.2 → 2.1.0-canary-20260108172005

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
@@ -1428,6 +1428,19 @@ CONTEXT7_API_KEY=your_api_key_here
1428
1428
 
1429
1429
  </details>
1430
1430
 
1431
+ ### OAuth Authentication
1432
+
1433
+ Context7 MCP server supports OAuth 2.0 authentication for MCP clients that implement the [MCP OAuth specification](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization).
1434
+
1435
+ To use OAuth, change the endpoint from `/mcp` to `/mcp/oauth` in your client configuration:
1436
+
1437
+ ```diff
1438
+ - "url": "https://mcp.context7.com/mcp"
1439
+ + "url": "https://mcp.context7.com/mcp/oauth"
1440
+ ```
1441
+
1442
+ > **Note:** OAuth is not supported with stdio transport. For local MCP connections, use API key authentication instead.
1443
+
1431
1444
  <details>
1432
1445
  <summary><b>Testing with MCP Inspector</b></summary>
1433
1446
 
package/dist/index.js CHANGED
@@ -4,11 +4,12 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4
4
  import { z } from "zod";
5
5
  import { searchLibraries, fetchLibraryContext } from "./lib/api.js";
6
6
  import { formatSearchResults, extractClientInfoFromUserAgent } from "./lib/utils.js";
7
+ import { isJWT, validateJWT } from "./lib/jwt.js";
7
8
  import express from "express";
8
9
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
9
10
  import { Command } from "commander";
10
11
  import { AsyncLocalStorage } from "async_hooks";
11
- import { SERVER_VERSION } from "./lib/constants.js";
12
+ import { SERVER_VERSION, RESOURCE_URL, AUTH_SERVER_URL } from "./lib/constants.js";
12
13
  /** Default HTTP server port */
13
14
  const DEFAULT_PORT = 3000;
14
15
  // Parse CLI arguments using commander
@@ -245,11 +246,41 @@ async function main() {
245
246
  extractHeaderValue(req.headers["context7_api_key"]) ||
246
247
  extractHeaderValue(req.headers["x_api_key"]));
247
248
  };
248
- app.all("/mcp", async (req, res) => {
249
+ const handleMcpRequest = async (req, res, requireAuth) => {
249
250
  try {
251
+ const apiKey = extractApiKey(req);
252
+ const resourceUrl = RESOURCE_URL;
253
+ const baseUrl = new URL(resourceUrl).origin;
254
+ // OAuth discovery info header, used by MCP clients to discover the authorization server
255
+ res.set("WWW-Authenticate", `Bearer resource_metadata="${baseUrl}/.well-known/oauth-protected-resource"`);
256
+ if (requireAuth) {
257
+ if (!apiKey) {
258
+ return res.status(401).json({
259
+ jsonrpc: "2.0",
260
+ error: {
261
+ code: -32001,
262
+ message: "Authentication required. Please authenticate to use this MCP server.",
263
+ },
264
+ id: null,
265
+ });
266
+ }
267
+ if (isJWT(apiKey)) {
268
+ const validationResult = await validateJWT(apiKey);
269
+ if (!validationResult.valid) {
270
+ return res.status(401).json({
271
+ jsonrpc: "2.0",
272
+ error: {
273
+ code: -32001,
274
+ message: validationResult.error || "Invalid token. Please re-authenticate.",
275
+ },
276
+ id: null,
277
+ });
278
+ }
279
+ }
280
+ }
250
281
  const context = {
251
282
  clientIp: getClientIp(req),
252
- apiKey: extractApiKey(req),
283
+ apiKey: apiKey,
253
284
  clientInfo: extractClientInfoFromUserAgent(req.headers["user-agent"]),
254
285
  transport: "http",
255
286
  };
@@ -275,10 +306,50 @@ async function main() {
275
306
  });
276
307
  }
277
308
  }
309
+ };
310
+ // Anonymous access endpoint - no authentication required
311
+ app.all("/mcp", async (req, res) => {
312
+ await handleMcpRequest(req, res, false);
313
+ });
314
+ // OAuth-protected endpoint - requires authentication
315
+ app.all("/mcp/oauth", async (req, res) => {
316
+ await handleMcpRequest(req, res, true);
278
317
  });
279
318
  app.get("/ping", (_req, res) => {
280
319
  res.json({ status: "ok", message: "pong" });
281
320
  });
321
+ // OAuth 2.0 Protected Resource Metadata (RFC 9728)
322
+ // Used by MCP clients to discover the authorization server
323
+ app.get("/.well-known/oauth-protected-resource", (_req, res) => {
324
+ res.json({
325
+ resource: RESOURCE_URL,
326
+ authorization_servers: [AUTH_SERVER_URL],
327
+ scopes_supported: ["profile", "email"],
328
+ bearer_methods_supported: ["header"],
329
+ });
330
+ });
331
+ app.get("/.well-known/oauth-authorization-server", async (_req, res) => {
332
+ const authServerUrl = AUTH_SERVER_URL;
333
+ try {
334
+ const response = await fetch(`${authServerUrl}/.well-known/oauth-authorization-server`);
335
+ if (!response.ok) {
336
+ console.error("[OAuth] Upstream error:", response.status);
337
+ return res.status(response.status).json({
338
+ error: "upstream_error",
339
+ message: "Failed to fetch authorization server metadata",
340
+ });
341
+ }
342
+ const metadata = await response.json();
343
+ res.json(metadata);
344
+ }
345
+ catch (error) {
346
+ console.error("[OAuth] Error fetching OAuth metadata:", error);
347
+ res.status(502).json({
348
+ error: "proxy_error",
349
+ message: "Failed to proxy authorization server metadata",
350
+ });
351
+ }
352
+ });
282
353
  // Catch-all 404 handler - must be after all other routes
283
354
  app.use((_req, res) => {
284
355
  res.status(404).json({
package/dist/lib/api.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { generateHeaders } from "./encryption.js";
2
2
  import { ProxyAgent, setGlobalDispatcher } from "undici";
3
- const CONTEXT7_API_BASE_URL = "https://context7.com/api";
3
+ import { CONTEXT7_API_BASE_URL } from "./constants.js";
4
4
  /**
5
5
  * Parses error response from the Context7 API
6
6
  * Extracts the server's error message, falling back to status-based messages if parsing fails
@@ -1,2 +1,12 @@
1
- import pkg from "../../package.json" with { type: "json" };
1
+ import { readFileSync } from "fs";
2
+ import { fileURLToPath } from "url";
3
+ import { dirname, join } from "path";
4
+ const __dirname = dirname(fileURLToPath(import.meta.url));
5
+ const pkg = JSON.parse(readFileSync(join(__dirname, "../../package.json"), "utf-8"));
2
6
  export const SERVER_VERSION = pkg.version;
7
+ const CONTEXT7_BASE_URL = "https://context7.com";
8
+ const MCP_RESOURCE_URL = "https://mcp.context7.com";
9
+ export const CLERK_DOMAIN = "clerk.context7.com";
10
+ export const CONTEXT7_API_BASE_URL = process.env.CONTEXT7_API_URL || `${CONTEXT7_BASE_URL}/api`;
11
+ export const RESOURCE_URL = process.env.RESOURCE_URL || MCP_RESOURCE_URL;
12
+ export const AUTH_SERVER_URL = process.env.AUTH_SERVER_URL || CONTEXT7_BASE_URL;
@@ -0,0 +1,29 @@
1
+ import * as jose from "jose";
2
+ import { CLERK_DOMAIN } from "./constants.js";
3
+ const JWKS_URL = `https://${CLERK_DOMAIN}/.well-known/jwks.json`;
4
+ const ISSUER = `https://${CLERK_DOMAIN}`;
5
+ const jwks = jose.createRemoteJWKSet(new URL(JWKS_URL));
6
+ export function isJWT(token) {
7
+ const parts = token.split(".");
8
+ return parts.length === 3;
9
+ }
10
+ export async function validateJWT(token) {
11
+ try {
12
+ await jose.jwtVerify(token, jwks, {
13
+ issuer: ISSUER,
14
+ });
15
+ return { valid: true };
16
+ }
17
+ catch (error) {
18
+ if (error instanceof jose.errors.JWTExpired) {
19
+ return { valid: false, error: "Token expired" };
20
+ }
21
+ if (error instanceof jose.errors.JWTClaimValidationFailed) {
22
+ return { valid: false, error: "Invalid token claims" };
23
+ }
24
+ if (error instanceof jose.errors.JWSSignatureVerificationFailed) {
25
+ return { valid: false, error: "Invalid signature" };
26
+ }
27
+ return { valid: false, error: "Invalid token" };
28
+ }
29
+ }
package/package.json CHANGED
@@ -1,8 +1,20 @@
1
1
  {
2
2
  "name": "@upstash/context7-mcp",
3
- "version": "2.0.2",
3
+ "version": "2.1.0-canary-20260108172005",
4
4
  "mcpName": "io.github.upstash/context7",
5
5
  "description": "MCP server for Context7",
6
+ "scripts": {
7
+ "build": "tsc && chmod 755 dist/index.js",
8
+ "test": "echo \"No tests yet\"",
9
+ "typecheck": "tsc --noEmit",
10
+ "lint": "eslint .",
11
+ "lint:check": "eslint .",
12
+ "format": "prettier --write .",
13
+ "format:check": "prettier --check .",
14
+ "dev": "tsc --watch",
15
+ "start": "node dist/index.js --transport http",
16
+ "pack-mcpb": "pnpm install && pnpm run build && rm -rf node_modules && pnpm install --prod && cp mcpb/manifest.json manifest.json && cp ../../public/icon.png icon.png && mcpb validate manifest.json && mcpb pack . mcpb/context7.mcpb && rm manifest.json icon.png && pnpm install"
17
+ },
6
18
  "repository": {
7
19
  "type": "git",
8
20
  "url": "git+https://github.com/upstash/context7.git",
@@ -37,23 +49,12 @@
37
49
  "@types/express": "^5.0.4",
38
50
  "commander": "^14.0.0",
39
51
  "express": "^5.1.0",
52
+ "jose": "^6.1.3",
40
53
  "undici": "^6.6.3",
41
54
  "zod": "^3.24.2"
42
55
  },
43
56
  "devDependencies": {
44
- "@types/node": "^22.13.14",
57
+ "@types/node": "^25.0.3",
45
58
  "typescript": "^5.8.2"
46
- },
47
- "scripts": {
48
- "build": "tsc && chmod 755 dist/index.js",
49
- "test": "echo \"No tests yet\"",
50
- "typecheck": "tsc --noEmit",
51
- "lint": "eslint .",
52
- "lint:check": "eslint .",
53
- "format": "prettier --write .",
54
- "format:check": "prettier --check .",
55
- "dev": "tsc --watch",
56
- "start": "node dist/index.js --transport http",
57
- "pack-mcpb": "pnpm install && pnpm run build && rm -rf node_modules && pnpm install --prod && cp mcpb/manifest.json manifest.json && cp ../../public/icon.png icon.png && mcpb validate manifest.json && mcpb pack . mcpb/context7.mcpb && rm manifest.json icon.png && pnpm install"
58
59
  }
59
- }
60
+ }