@upstash/context7-mcp 2.0.1 → 2.0.2
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 +49 -20
- package/dist/lib/api.js +11 -23
- package/dist/lib/constants.js +2 -0
- package/dist/lib/encryption.js +23 -6
- package/dist/lib/utils.js +13 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,11 +3,12 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { searchLibraries, fetchLibraryContext } from "./lib/api.js";
|
|
6
|
-
import { formatSearchResults } from "./lib/utils.js";
|
|
6
|
+
import { formatSearchResults, extractClientInfoFromUserAgent } from "./lib/utils.js";
|
|
7
7
|
import express from "express";
|
|
8
8
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
9
9
|
import { Command } from "commander";
|
|
10
10
|
import { AsyncLocalStorage } from "async_hooks";
|
|
11
|
+
import { SERVER_VERSION } from "./lib/constants.js";
|
|
11
12
|
/** Default HTTP server port */
|
|
12
13
|
const DEFAULT_PORT = 3000;
|
|
13
14
|
// Parse CLI arguments using commander
|
|
@@ -43,8 +44,29 @@ const CLI_PORT = (() => {
|
|
|
43
44
|
return isNaN(parsed) ? undefined : parsed;
|
|
44
45
|
})();
|
|
45
46
|
const requestContext = new AsyncLocalStorage();
|
|
46
|
-
//
|
|
47
|
-
let
|
|
47
|
+
// Global state for stdio mode only
|
|
48
|
+
let stdioApiKey;
|
|
49
|
+
let stdioClientInfo;
|
|
50
|
+
/**
|
|
51
|
+
* Get the effective client context
|
|
52
|
+
*/
|
|
53
|
+
function getClientContext() {
|
|
54
|
+
const ctx = requestContext.getStore();
|
|
55
|
+
// HTTP mode: context is fully populated from request
|
|
56
|
+
if (ctx) {
|
|
57
|
+
return ctx;
|
|
58
|
+
}
|
|
59
|
+
// stdio mode: use globals
|
|
60
|
+
return {
|
|
61
|
+
apiKey: stdioApiKey,
|
|
62
|
+
clientInfo: stdioClientInfo,
|
|
63
|
+
transport: "stdio",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Extract client IP address from request headers.
|
|
68
|
+
* Handles X-Forwarded-For header for proxied requests.
|
|
69
|
+
*/
|
|
48
70
|
function getClientIp(req) {
|
|
49
71
|
const forwardedFor = req.headers["x-forwarded-for"] || req.headers["X-Forwarded-For"];
|
|
50
72
|
if (forwardedFor) {
|
|
@@ -67,10 +89,20 @@ function getClientIp(req) {
|
|
|
67
89
|
}
|
|
68
90
|
const server = new McpServer({
|
|
69
91
|
name: "Context7",
|
|
70
|
-
version:
|
|
92
|
+
version: SERVER_VERSION,
|
|
71
93
|
}, {
|
|
72
94
|
instructions: "Use this server to retrieve up-to-date documentation and code examples for any library.",
|
|
73
95
|
});
|
|
96
|
+
// Capture client info from MCP initialize handshake
|
|
97
|
+
server.server.oninitialized = () => {
|
|
98
|
+
const clientVersion = server.server.getClientVersion();
|
|
99
|
+
if (clientVersion) {
|
|
100
|
+
stdioClientInfo = {
|
|
101
|
+
ide: clientVersion.name,
|
|
102
|
+
version: clientVersion.version,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
};
|
|
74
106
|
server.registerTool("resolve-library-id", {
|
|
75
107
|
title: "Resolve Context7 Library ID",
|
|
76
108
|
description: `Resolves a package/product name to a Context7-compatible library ID and returns matching libraries.
|
|
@@ -107,9 +139,7 @@ IMPORTANT: Do not call this tool more than 3 times per question. If you cannot f
|
|
|
107
139
|
readOnlyHint: true,
|
|
108
140
|
},
|
|
109
141
|
}, async ({ query, libraryName }) => {
|
|
110
|
-
const
|
|
111
|
-
const apiKey = ctx?.apiKey || globalApiKey;
|
|
112
|
-
const searchResponse = await searchLibraries(query, libraryName, ctx?.clientIp, apiKey);
|
|
142
|
+
const searchResponse = await searchLibraries(query, libraryName, getClientContext());
|
|
113
143
|
if (!searchResponse.results || searchResponse.results.length === 0) {
|
|
114
144
|
return {
|
|
115
145
|
content: [
|
|
@@ -167,9 +197,7 @@ IMPORTANT: Do not call this tool more than 3 times per question. If you cannot f
|
|
|
167
197
|
readOnlyHint: true,
|
|
168
198
|
},
|
|
169
199
|
}, async ({ query, libraryId }) => {
|
|
170
|
-
const
|
|
171
|
-
const apiKey = ctx?.apiKey || globalApiKey;
|
|
172
|
-
const response = await fetchLibraryContext({ query, libraryId }, ctx?.clientIp, apiKey);
|
|
200
|
+
const response = await fetchLibraryContext({ query, libraryId }, getClientContext());
|
|
173
201
|
return {
|
|
174
202
|
content: [
|
|
175
203
|
{
|
|
@@ -219,8 +247,12 @@ async function main() {
|
|
|
219
247
|
};
|
|
220
248
|
app.all("/mcp", async (req, res) => {
|
|
221
249
|
try {
|
|
222
|
-
const
|
|
223
|
-
|
|
250
|
+
const context = {
|
|
251
|
+
clientIp: getClientIp(req),
|
|
252
|
+
apiKey: extractApiKey(req),
|
|
253
|
+
clientInfo: extractClientInfoFromUserAgent(req.headers["user-agent"]),
|
|
254
|
+
transport: "http",
|
|
255
|
+
};
|
|
224
256
|
const transport = new StreamableHTTPServerTransport({
|
|
225
257
|
sessionIdGenerator: undefined,
|
|
226
258
|
enableJsonResponse: true,
|
|
@@ -228,7 +260,7 @@ async function main() {
|
|
|
228
260
|
res.on("close", () => {
|
|
229
261
|
transport.close();
|
|
230
262
|
});
|
|
231
|
-
await requestContext.run(
|
|
263
|
+
await requestContext.run(context, async () => {
|
|
232
264
|
await server.connect(transport);
|
|
233
265
|
await transport.handleRequest(req, res, req.body);
|
|
234
266
|
});
|
|
@@ -267,19 +299,16 @@ async function main() {
|
|
|
267
299
|
}
|
|
268
300
|
});
|
|
269
301
|
httpServer.once("listening", () => {
|
|
270
|
-
console.error(`Context7 Documentation MCP Server running on HTTP at http://localhost:${port}/mcp`);
|
|
302
|
+
console.error(`Context7 Documentation MCP Server v${SERVER_VERSION} running on HTTP at http://localhost:${port}/mcp`);
|
|
271
303
|
});
|
|
272
304
|
};
|
|
273
305
|
startServer(initialPort);
|
|
274
306
|
}
|
|
275
307
|
else {
|
|
276
|
-
|
|
277
|
-
globalApiKey = apiKey; // Store globally for tool handlers in stdio mode
|
|
308
|
+
stdioApiKey = cliOptions.apiKey || process.env.CONTEXT7_API_KEY;
|
|
278
309
|
const transport = new StdioServerTransport();
|
|
279
|
-
await
|
|
280
|
-
|
|
281
|
-
});
|
|
282
|
-
console.error("Context7 Documentation MCP Server running on stdio");
|
|
310
|
+
await server.connect(transport);
|
|
311
|
+
console.error(`Context7 Documentation MCP Server v${SERVER_VERSION} running on stdio`);
|
|
283
312
|
}
|
|
284
313
|
}
|
|
285
314
|
main().catch((error) => {
|
package/dist/lib/api.js
CHANGED
|
@@ -18,7 +18,6 @@ async function parseErrorResponse(response, apiKey) {
|
|
|
18
18
|
catch {
|
|
19
19
|
// JSON parsing failed, fall through to default
|
|
20
20
|
}
|
|
21
|
-
// Fallback for non-JSON responses
|
|
22
21
|
const status = response.status;
|
|
23
22
|
if (status === 429) {
|
|
24
23
|
return apiKey
|
|
@@ -33,7 +32,6 @@ async function parseErrorResponse(response, apiKey) {
|
|
|
33
32
|
}
|
|
34
33
|
return `Request failed with status ${status}. Please try again later.`;
|
|
35
34
|
}
|
|
36
|
-
// Pick up proxy configuration in a variety of common env var names.
|
|
37
35
|
const PROXY_URL = process.env.HTTPS_PROXY ??
|
|
38
36
|
process.env.https_proxy ??
|
|
39
37
|
process.env.HTTP_PROXY ??
|
|
@@ -41,14 +39,9 @@ const PROXY_URL = process.env.HTTPS_PROXY ??
|
|
|
41
39
|
null;
|
|
42
40
|
if (PROXY_URL && !PROXY_URL.startsWith("$") && /^(http|https):\/\//i.test(PROXY_URL)) {
|
|
43
41
|
try {
|
|
44
|
-
// Configure a global proxy agent once at startup. Subsequent fetch calls will
|
|
45
|
-
// automatically use this dispatcher.
|
|
46
|
-
// Using `any` cast because ProxyAgent implements the Dispatcher interface but
|
|
47
|
-
// TS may not infer it correctly in some versions.
|
|
48
42
|
setGlobalDispatcher(new ProxyAgent(PROXY_URL));
|
|
49
43
|
}
|
|
50
44
|
catch (error) {
|
|
51
|
-
// Don't crash the app if proxy initialisation fails – just log a warning.
|
|
52
45
|
console.error(`[Context7] Failed to configure proxy agent for provided proxy URL: ${PROXY_URL}:`, error);
|
|
53
46
|
}
|
|
54
47
|
}
|
|
@@ -56,24 +49,20 @@ if (PROXY_URL && !PROXY_URL.startsWith("$") && /^(http|https):\/\//i.test(PROXY_
|
|
|
56
49
|
* Searches for libraries matching the given query
|
|
57
50
|
* @param query The user's question or task (used for LLM relevance ranking)
|
|
58
51
|
* @param libraryName The library name to search for in the database
|
|
59
|
-
* @param
|
|
60
|
-
* @
|
|
61
|
-
* @returns Search results or null if the request fails
|
|
52
|
+
* @param context Client context including IP, API key, and client info
|
|
53
|
+
* @returns Search results or error
|
|
62
54
|
*/
|
|
63
|
-
export async function searchLibraries(query, libraryName,
|
|
55
|
+
export async function searchLibraries(query, libraryName, context = {}) {
|
|
64
56
|
try {
|
|
65
57
|
const url = new URL(`${CONTEXT7_API_BASE_URL}/v2/libs/search`);
|
|
66
58
|
url.searchParams.set("query", query);
|
|
67
59
|
url.searchParams.set("libraryName", libraryName);
|
|
68
|
-
const headers = generateHeaders(
|
|
60
|
+
const headers = generateHeaders(context);
|
|
69
61
|
const response = await fetch(url, { headers });
|
|
70
62
|
if (!response.ok) {
|
|
71
|
-
const errorMessage = await parseErrorResponse(response, apiKey);
|
|
63
|
+
const errorMessage = await parseErrorResponse(response, context.apiKey);
|
|
72
64
|
console.error(errorMessage);
|
|
73
|
-
return {
|
|
74
|
-
results: [],
|
|
75
|
-
error: errorMessage,
|
|
76
|
-
};
|
|
65
|
+
return { results: [], error: errorMessage };
|
|
77
66
|
}
|
|
78
67
|
const searchData = await response.json();
|
|
79
68
|
return searchData;
|
|
@@ -86,20 +75,19 @@ export async function searchLibraries(query, libraryName, clientIp, apiKey) {
|
|
|
86
75
|
}
|
|
87
76
|
/**
|
|
88
77
|
* Fetches intelligent, reranked context for a natural language query
|
|
89
|
-
* @param request The context request parameters (query,
|
|
90
|
-
* @param
|
|
91
|
-
* @param apiKey Optional API key for authentication
|
|
78
|
+
* @param request The context request parameters (query, libraryId)
|
|
79
|
+
* @param context Client context including IP, API key, and client info
|
|
92
80
|
* @returns Context response with data
|
|
93
81
|
*/
|
|
94
|
-
export async function fetchLibraryContext(request,
|
|
82
|
+
export async function fetchLibraryContext(request, context = {}) {
|
|
95
83
|
try {
|
|
96
84
|
const url = new URL(`${CONTEXT7_API_BASE_URL}/v2/context`);
|
|
97
85
|
url.searchParams.set("query", request.query);
|
|
98
86
|
url.searchParams.set("libraryId", request.libraryId);
|
|
99
|
-
const headers = generateHeaders(
|
|
87
|
+
const headers = generateHeaders(context);
|
|
100
88
|
const response = await fetch(url, { headers });
|
|
101
89
|
if (!response.ok) {
|
|
102
|
-
const errorMessage = await parseErrorResponse(response, apiKey);
|
|
90
|
+
const errorMessage = await parseErrorResponse(response, context.apiKey);
|
|
103
91
|
console.error(errorMessage);
|
|
104
92
|
return { data: errorMessage };
|
|
105
93
|
}
|
package/dist/lib/encryption.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createCipheriv, randomBytes } from "crypto";
|
|
2
|
+
import { SERVER_VERSION } from "./constants.js";
|
|
2
3
|
const DEFAULT_ENCRYPTION_KEY = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
|
|
3
4
|
const ENCRYPTION_KEY = process.env.CLIENT_IP_ENCRYPTION_KEY || DEFAULT_ENCRYPTION_KEY;
|
|
4
5
|
const ALGORITHM = "aes-256-cbc";
|
|
@@ -26,13 +27,29 @@ function encryptClientIp(clientIp) {
|
|
|
26
27
|
return clientIp; // Fallback to unencrypted
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Generate headers for Context7 API requests.
|
|
32
|
+
* Handles client IP encryption, authentication, and telemetry headers.
|
|
33
|
+
*/
|
|
34
|
+
export function generateHeaders(context) {
|
|
35
|
+
const headers = {
|
|
36
|
+
"X-Context7-Source": "mcp-server",
|
|
37
|
+
"X-Context7-Server-Version": SERVER_VERSION,
|
|
38
|
+
};
|
|
39
|
+
if (context.clientIp) {
|
|
40
|
+
headers["mcp-client-ip"] = encryptClientIp(context.clientIp);
|
|
33
41
|
}
|
|
34
|
-
if (apiKey) {
|
|
35
|
-
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
42
|
+
if (context.apiKey) {
|
|
43
|
+
headers["Authorization"] = `Bearer ${context.apiKey}`;
|
|
44
|
+
}
|
|
45
|
+
if (context.clientInfo?.ide) {
|
|
46
|
+
headers["X-Context7-Client-IDE"] = context.clientInfo.ide;
|
|
47
|
+
}
|
|
48
|
+
if (context.clientInfo?.version) {
|
|
49
|
+
headers["X-Context7-Client-Version"] = context.clientInfo.version;
|
|
50
|
+
}
|
|
51
|
+
if (context.transport) {
|
|
52
|
+
headers["X-Context7-Transport"] = context.transport;
|
|
36
53
|
}
|
|
37
54
|
return headers;
|
|
38
55
|
}
|
package/dist/lib/utils.js
CHANGED
|
@@ -58,3 +58,16 @@ export function formatSearchResults(searchResponse) {
|
|
|
58
58
|
const formattedResults = searchResponse.results.map(formatSearchResult);
|
|
59
59
|
return formattedResults.join("\n----------\n");
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Extract client info from User-Agent header.
|
|
63
|
+
* Parses formats like "Cursor/2.2.44 (darwin arm64)" or "claude-code/2.0.71"
|
|
64
|
+
*/
|
|
65
|
+
export function extractClientInfoFromUserAgent(userAgent) {
|
|
66
|
+
if (!userAgent)
|
|
67
|
+
return undefined;
|
|
68
|
+
const match = userAgent.match(/^([^\/\s]+)\/([^\s(]+)/);
|
|
69
|
+
if (match) {
|
|
70
|
+
return { ide: match[1], version: match[2] };
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|