@upstash/context7-mcp 2.0.0 → 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 +55 -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.
|
|
@@ -103,10 +135,11 @@ IMPORTANT: Do not call this tool more than 3 times per question. If you cannot f
|
|
|
103
135
|
.string()
|
|
104
136
|
.describe("Library name to search for and retrieve a Context7-compatible library ID."),
|
|
105
137
|
},
|
|
138
|
+
annotations: {
|
|
139
|
+
readOnlyHint: true,
|
|
140
|
+
},
|
|
106
141
|
}, async ({ query, libraryName }) => {
|
|
107
|
-
const
|
|
108
|
-
const apiKey = ctx?.apiKey || globalApiKey;
|
|
109
|
-
const searchResponse = await searchLibraries(query, libraryName, ctx?.clientIp, apiKey);
|
|
142
|
+
const searchResponse = await searchLibraries(query, libraryName, getClientContext());
|
|
110
143
|
if (!searchResponse.results || searchResponse.results.length === 0) {
|
|
111
144
|
return {
|
|
112
145
|
content: [
|
|
@@ -160,10 +193,11 @@ IMPORTANT: Do not call this tool more than 3 times per question. If you cannot f
|
|
|
160
193
|
.string()
|
|
161
194
|
.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'. IMPORTANT: Do not include any sensitive or confidential information such as API keys, passwords, credentials, or personal data in your query."),
|
|
162
195
|
},
|
|
196
|
+
annotations: {
|
|
197
|
+
readOnlyHint: true,
|
|
198
|
+
},
|
|
163
199
|
}, async ({ query, libraryId }) => {
|
|
164
|
-
const
|
|
165
|
-
const apiKey = ctx?.apiKey || globalApiKey;
|
|
166
|
-
const response = await fetchLibraryContext({ query, libraryId }, ctx?.clientIp, apiKey);
|
|
200
|
+
const response = await fetchLibraryContext({ query, libraryId }, getClientContext());
|
|
167
201
|
return {
|
|
168
202
|
content: [
|
|
169
203
|
{
|
|
@@ -213,8 +247,12 @@ async function main() {
|
|
|
213
247
|
};
|
|
214
248
|
app.all("/mcp", async (req, res) => {
|
|
215
249
|
try {
|
|
216
|
-
const
|
|
217
|
-
|
|
250
|
+
const context = {
|
|
251
|
+
clientIp: getClientIp(req),
|
|
252
|
+
apiKey: extractApiKey(req),
|
|
253
|
+
clientInfo: extractClientInfoFromUserAgent(req.headers["user-agent"]),
|
|
254
|
+
transport: "http",
|
|
255
|
+
};
|
|
218
256
|
const transport = new StreamableHTTPServerTransport({
|
|
219
257
|
sessionIdGenerator: undefined,
|
|
220
258
|
enableJsonResponse: true,
|
|
@@ -222,7 +260,7 @@ async function main() {
|
|
|
222
260
|
res.on("close", () => {
|
|
223
261
|
transport.close();
|
|
224
262
|
});
|
|
225
|
-
await requestContext.run(
|
|
263
|
+
await requestContext.run(context, async () => {
|
|
226
264
|
await server.connect(transport);
|
|
227
265
|
await transport.handleRequest(req, res, req.body);
|
|
228
266
|
});
|
|
@@ -261,19 +299,16 @@ async function main() {
|
|
|
261
299
|
}
|
|
262
300
|
});
|
|
263
301
|
httpServer.once("listening", () => {
|
|
264
|
-
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`);
|
|
265
303
|
});
|
|
266
304
|
};
|
|
267
305
|
startServer(initialPort);
|
|
268
306
|
}
|
|
269
307
|
else {
|
|
270
|
-
|
|
271
|
-
globalApiKey = apiKey; // Store globally for tool handlers in stdio mode
|
|
308
|
+
stdioApiKey = cliOptions.apiKey || process.env.CONTEXT7_API_KEY;
|
|
272
309
|
const transport = new StdioServerTransport();
|
|
273
|
-
await
|
|
274
|
-
|
|
275
|
-
});
|
|
276
|
-
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`);
|
|
277
312
|
}
|
|
278
313
|
}
|
|
279
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
|
+
}
|