@upstash/context7-mcp 3.2.1 → 3.2.3
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 +3 -26
- package/dist/lib/client-ip.js +60 -0
- package/dist/lib/constants.js +4 -0
- package/dist/lib/jwt.js +6 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import { randomUUID } from "node:crypto";
|
|
|
15
15
|
import { createSessionStore } from "./lib/sessionStore.js";
|
|
16
16
|
import { SERVER_VERSION, RESOURCE_URL, AUTH_SERVER_URL, OPENAI_APPS_CHALLENGE_TOKEN, } from "./lib/constants.js";
|
|
17
17
|
import { maybeElicitAuthSignIn } from "./lib/auth/auth-prompt.js";
|
|
18
|
+
import { getClientIp } from "./lib/client-ip.js";
|
|
18
19
|
/** Default HTTP server port */
|
|
19
20
|
const DEFAULT_PORT = 3000;
|
|
20
21
|
// Parse CLI arguments using commander
|
|
@@ -73,30 +74,6 @@ function getClientContext() {
|
|
|
73
74
|
sessionId: stdioSessionId,
|
|
74
75
|
};
|
|
75
76
|
}
|
|
76
|
-
/**
|
|
77
|
-
* Extract client IP address from request headers.
|
|
78
|
-
* Handles X-Forwarded-For header for proxied requests.
|
|
79
|
-
*/
|
|
80
|
-
function getClientIp(req) {
|
|
81
|
-
const forwardedFor = req.headers["x-forwarded-for"] || req.headers["X-Forwarded-For"];
|
|
82
|
-
if (forwardedFor) {
|
|
83
|
-
const ips = Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor;
|
|
84
|
-
const ipList = ips.split(",").map((ip) => ip.trim());
|
|
85
|
-
for (const ip of ipList) {
|
|
86
|
-
const plainIp = ip.replace(/^::ffff:/, "");
|
|
87
|
-
if (!plainIp.startsWith("10.") &&
|
|
88
|
-
!plainIp.startsWith("192.168.") &&
|
|
89
|
-
!/^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(plainIp)) {
|
|
90
|
-
return plainIp;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return ipList[0].replace(/^::ffff:/, "");
|
|
94
|
-
}
|
|
95
|
-
if (req.socket?.remoteAddress) {
|
|
96
|
-
return req.socket.remoteAddress.replace(/^::ffff:/, "");
|
|
97
|
-
}
|
|
98
|
-
return undefined;
|
|
99
|
-
}
|
|
100
77
|
function createMcpServer() {
|
|
101
78
|
const server = new McpServer({
|
|
102
79
|
name: "Context7",
|
|
@@ -110,7 +87,7 @@ function createMcpServer() {
|
|
|
110
87
|
},
|
|
111
88
|
],
|
|
112
89
|
}, {
|
|
113
|
-
instructions: `Use this server to fetch current documentation whenever the user asks about a library, framework, SDK, API, CLI tool, or cloud service
|
|
90
|
+
instructions: `Use this server to fetch current documentation whenever the user asks about a library, framework, SDK, API, CLI tool, or cloud service — even well-known ones like React, Next.js, Prisma, Express, Tailwind, Django, or Spring Boot. This includes API syntax, configuration, version migration, library-specific debugging, setup instructions, and CLI tool usage. Use even when you think you know the answer — your training data may not reflect recent changes. Prefer this over web search for library docs.
|
|
114
91
|
|
|
115
92
|
Do not use for: refactoring, writing scripts from scratch, debugging business logic, code review, or general programming concepts.`,
|
|
116
93
|
});
|
|
@@ -203,7 +180,7 @@ Do not call this tool more than 3 times per question.`,
|
|
|
203
180
|
.describe("Exact Context7-compatible library ID (e.g., '/mongodb/docs', '/vercel/next.js', '/supabase/supabase', '/vercel/next.js/v14.3.0-canary.87') retrieved from 'resolve-library-id' or directly from user query in the format '/org/project' or '/org/project/version'."),
|
|
204
181
|
query: z
|
|
205
182
|
.string()
|
|
206
|
-
.describe("The question or task you need help with. Be specific and include relevant details. Good: 'How to set up authentication with JWT in Express.js' or 'React useEffect cleanup function examples'. Bad: 'auth' or 'hooks'. The query is sent to the Context7 API for processing. Do not include any sensitive or confidential information such as API keys, passwords, credentials, personal data, or proprietary code in your query."),
|
|
183
|
+
.describe("The question or task you need help with, scoped to a single concept. Be specific and include relevant details, but keep each query to one topic — if the user's question spans multiple distinct concepts, make a separate call per concept instead of combining them, unless the question is about how the concepts interact. Good: 'How to set up authentication with JWT in Express.js' or 'React useEffect cleanup function examples'. Bad (too vague): 'auth' or 'hooks'. Bad (too broad): 'routing and auth and caching in Next.js'. The query is sent to the Context7 API for processing. Do not include any sensitive or confidential information such as API keys, passwords, credentials, personal data, or proprietary code in your query."),
|
|
207
184
|
},
|
|
208
185
|
annotations: {
|
|
209
186
|
readOnlyHint: true,
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
function stripIpv4MappedPrefix(ip) {
|
|
2
|
+
return ip.replace(/^::ffff:/i, "");
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Returns true for RFC1918, CGNAT, loopback, link-local, and IPv6 private ranges.
|
|
6
|
+
*/
|
|
7
|
+
export function isPrivateOrLocalIp(ip) {
|
|
8
|
+
const plainIp = stripIpv4MappedPrefix(ip).toLowerCase();
|
|
9
|
+
if (plainIp.includes(".")) {
|
|
10
|
+
return (plainIp.startsWith("10.") ||
|
|
11
|
+
plainIp.startsWith("192.168.") ||
|
|
12
|
+
/^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(plainIp) ||
|
|
13
|
+
/^100\.(6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\./.test(plainIp) ||
|
|
14
|
+
plainIp.startsWith("127.") ||
|
|
15
|
+
plainIp.startsWith("169.254."));
|
|
16
|
+
}
|
|
17
|
+
// ::1 loopback in any textual form (e.g. "0::1", "0:0:0:0:0:0:0:1")
|
|
18
|
+
if (/^[0:]+1$/.test(plainIp)) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
// First hextets in fe80::/10 and fc00::/7 start with a non-zero digit, so a
|
|
22
|
+
// valid textual form always spells out all 4 digits.
|
|
23
|
+
// fe80::/10 link-local
|
|
24
|
+
if (/^fe[89ab][0-9a-f]:/.test(plainIp)) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
// fc00::/7 unique local
|
|
28
|
+
if (/^f[cd][0-9a-f]{2}:/.test(plainIp)) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
function pickClientIpFromForwardedList(ipList) {
|
|
34
|
+
for (const ip of ipList) {
|
|
35
|
+
const plainIp = stripIpv4MappedPrefix(ip);
|
|
36
|
+
if (!isPrivateOrLocalIp(plainIp)) {
|
|
37
|
+
return plainIp;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (ipList.length === 0) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
return stripIpv4MappedPrefix(ipList[0]);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Extract client IP address from request headers.
|
|
47
|
+
* Handles X-Forwarded-For header for proxied requests.
|
|
48
|
+
*/
|
|
49
|
+
export function getClientIp(req) {
|
|
50
|
+
const forwardedFor = req.headers["x-forwarded-for"] || req.headers["X-Forwarded-For"];
|
|
51
|
+
if (forwardedFor) {
|
|
52
|
+
const ips = Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor;
|
|
53
|
+
const ipList = ips.split(",").map((ip) => ip.trim());
|
|
54
|
+
return pickClientIpFromForwardedList(ipList);
|
|
55
|
+
}
|
|
56
|
+
if (req.socket?.remoteAddress) {
|
|
57
|
+
return stripIpv4MappedPrefix(req.socket.remoteAddress);
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
package/dist/lib/constants.js
CHANGED
|
@@ -10,4 +10,8 @@ export const CLERK_DOMAIN = "clerk.context7.com";
|
|
|
10
10
|
export const CONTEXT7_API_BASE_URL = process.env.CONTEXT7_API_URL || `${CONTEXT7_BASE_URL}/api`;
|
|
11
11
|
export const RESOURCE_URL = process.env.RESOURCE_URL || MCP_RESOURCE_URL;
|
|
12
12
|
export const AUTH_SERVER_URL = process.env.AUTH_SERVER_URL || CONTEXT7_BASE_URL;
|
|
13
|
+
// Enterprise-Managed Auth (id-jag): access tokens minted by the Context7
|
|
14
|
+
// authorization server, validated against its public JWKS.
|
|
15
|
+
export const EMA_ISSUER = AUTH_SERVER_URL;
|
|
16
|
+
export const EMA_JWKS_URL = process.env.EMA_JWKS_URL || `${CONTEXT7_API_BASE_URL}/oauth/ema-jwks`;
|
|
13
17
|
export const OPENAI_APPS_CHALLENGE_TOKEN = process.env.OPENAI_APPS_CHALLENGE_TOKEN;
|
package/dist/lib/jwt.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as jose from "jose";
|
|
2
|
-
import { CLERK_DOMAIN, CONTEXT7_API_BASE_URL } from "./constants.js";
|
|
2
|
+
import { CLERK_DOMAIN, CONTEXT7_API_BASE_URL, EMA_ISSUER, EMA_JWKS_URL, RESOURCE_URL, } from "./constants.js";
|
|
3
3
|
const CLERK_ISSUER = `https://${CLERK_DOMAIN}`;
|
|
4
4
|
const clerkJwks = jose.createRemoteJWKSet(new URL(`https://${CLERK_DOMAIN}/.well-known/jwks.json`));
|
|
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$/;
|
|
6
7
|
const jwksByTenant = new Map();
|
|
7
8
|
function entraJwks(tenantId) {
|
|
@@ -65,6 +66,10 @@ export async function validateJWT(token) {
|
|
|
65
66
|
}
|
|
66
67
|
return { valid: true };
|
|
67
68
|
}
|
|
69
|
+
if (iss === EMA_ISSUER) {
|
|
70
|
+
await jose.jwtVerify(token, emaJwks, { issuer: EMA_ISSUER, audience: RESOURCE_URL });
|
|
71
|
+
return { valid: true };
|
|
72
|
+
}
|
|
68
73
|
await jose.jwtVerify(token, clerkJwks, { issuer: CLERK_ISSUER });
|
|
69
74
|
return { valid: true };
|
|
70
75
|
}
|