@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 +13 -0
- package/dist/index.js +74 -3
- package/dist/lib/api.js +1 -1
- package/dist/lib/constants.js +11 -1
- package/dist/lib/jwt.js +29 -0
- package/package.json +16 -15
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
package/dist/lib/constants.js
CHANGED
|
@@ -1,2 +1,12 @@
|
|
|
1
|
-
import
|
|
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;
|
package/dist/lib/jwt.js
ADDED
|
@@ -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
|
|
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": "^
|
|
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
|
+
}
|