@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 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.
@@ -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 ctx = requestContext.getStore();
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 ctx = requestContext.getStore();
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 clientIp = getClientIp(req);
217
- 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
+ };
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({ clientIp, apiKey }, async () => {
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
- const apiKey = cliOptions.apiKey || process.env.CONTEXT7_API_KEY;
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 requestContext.run({ apiKey }, async () => {
274
- await server.connect(transport);
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 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.0",
3
+ "version": "2.0.2",
4
4
  "mcpName": "io.github.upstash/context7",
5
5
  "description": "MCP server for Context7",
6
6
  "repository": {