@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 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
- // Store API key globally for stdio mode (where requestContext may not be available in tool handlers)
47
- let globalApiKey;
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: "2.0.0",
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 ctx = requestContext.getStore();
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 ctx = requestContext.getStore();
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 clientIp = getClientIp(req);
223
- const apiKey = extractApiKey(req);
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({ clientIp, apiKey }, async () => {
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
- const apiKey = cliOptions.apiKey || process.env.CONTEXT7_API_KEY;
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 requestContext.run({ apiKey }, async () => {
280
- await server.connect(transport);
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 clientIp Optional client IP address to include in headers
60
- * @param apiKey Optional API key for authentication
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, clientIp, apiKey) {
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(clientIp, apiKey);
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, topic, library, mode)
90
- * @param clientIp Optional client IP address to include in headers
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, clientIp, apiKey) {
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(clientIp, apiKey, { "X-Context7-Source": "mcp-server" });
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
  }
@@ -0,0 +1,2 @@
1
+ import pkg from "../../package.json" with { type: "json" };
2
+ export const SERVER_VERSION = pkg.version;
@@ -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
- export function generateHeaders(clientIp, apiKey, extraHeaders = {}) {
30
- const headers = { ...extraHeaders };
31
- if (clientIp) {
32
- headers["mcp-client-ip"] = encryptClientIp(clientIp);
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upstash/context7-mcp",
3
- "version": "2.0.1",
3
+ "version": "2.0.2",
4
4
  "mcpName": "io.github.upstash/context7",
5
5
  "description": "MCP server for Context7",
6
6
  "repository": {