@upstash/context7-mcp 1.0.14 → 1.0.16

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
@@ -11,36 +11,75 @@ import { Command } from "commander";
11
11
  const DEFAULT_MINIMUM_TOKENS = 10000;
12
12
  // Parse CLI arguments using commander
13
13
  const program = new Command()
14
- .option("--transport <stdio|http|sse>", "transport type", "stdio")
15
- .option("--port <number>", "port for HTTP/SSE transport", "3000")
14
+ .option("--transport <stdio|http>", "transport type", "stdio")
15
+ .option("--port <number>", "port for HTTP transport", "3000")
16
+ .option("--api-key <key>", "API key for authentication")
16
17
  .allowUnknownOption() // let MCP Inspector / other wrappers pass through extra flags
17
18
  .parse(process.argv);
18
19
  const cliOptions = program.opts();
19
20
  // Validate transport option
20
- const allowedTransports = ["stdio", "http", "sse"];
21
+ const allowedTransports = ["stdio", "http"];
21
22
  if (!allowedTransports.includes(cliOptions.transport)) {
22
- console.error(`Invalid --transport value: '${cliOptions.transport}'. Must be one of: stdio, http, sse.`);
23
+ console.error(`Invalid --transport value: '${cliOptions.transport}'. Must be one of: stdio, http.`);
23
24
  process.exit(1);
24
25
  }
25
26
  // Transport configuration
26
27
  const TRANSPORT_TYPE = (cliOptions.transport || "stdio");
27
- // HTTP/SSE port configuration
28
+ // Disallow incompatible flags based on transport
29
+ const passedPortFlag = process.argv.includes("--port");
30
+ const passedApiKeyFlag = process.argv.includes("--api-key");
31
+ if (TRANSPORT_TYPE === "http" && passedApiKeyFlag) {
32
+ console.error("The --api-key flag is not allowed when using --transport http. Use header-based auth at the HTTP layer instead.");
33
+ process.exit(1);
34
+ }
35
+ if (TRANSPORT_TYPE === "stdio" && passedPortFlag) {
36
+ console.error("The --port flag is not allowed when using --transport stdio.");
37
+ process.exit(1);
38
+ }
39
+ // HTTP port configuration
28
40
  const CLI_PORT = (() => {
29
41
  const parsed = parseInt(cliOptions.port, 10);
30
42
  return isNaN(parsed) ? undefined : parsed;
31
43
  })();
32
44
  // Store SSE transports by session ID
33
45
  const sseTransports = {};
46
+ function getClientIp(req) {
47
+ // Check both possible header casings
48
+ const forwardedFor = req.headers["x-forwarded-for"] || req.headers["X-Forwarded-For"];
49
+ if (forwardedFor) {
50
+ // X-Forwarded-For can contain multiple IPs
51
+ const ips = Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor;
52
+ const ipList = ips.split(",").map((ip) => ip.trim());
53
+ // Find the first public IP address
54
+ for (const ip of ipList) {
55
+ const plainIp = ip.replace(/^::ffff:/, "");
56
+ if (!plainIp.startsWith("10.") &&
57
+ !plainIp.startsWith("192.168.") &&
58
+ !/^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(plainIp)) {
59
+ return plainIp;
60
+ }
61
+ }
62
+ // If all are private, use the first one
63
+ return ipList[0].replace(/^::ffff:/, "");
64
+ }
65
+ // Fallback: use remote address, strip IPv6-mapped IPv4
66
+ if (req.socket?.remoteAddress) {
67
+ return req.socket.remoteAddress.replace(/^::ffff:/, "");
68
+ }
69
+ return undefined;
70
+ }
34
71
  // Function to create a new server instance with all tools registered
35
- function createServerInstance() {
72
+ function createServerInstance(clientIp, apiKey) {
36
73
  const server = new McpServer({
37
74
  name: "Context7",
38
- version: "v1.0.14",
75
+ version: "1.0.16",
39
76
  }, {
40
77
  instructions: "Use this server to retrieve up-to-date documentation and code examples for any library.",
41
78
  });
42
79
  // Register Context7 tools
43
- server.tool("resolve-library-id", `Resolves a package/product name to a Context7-compatible library ID and returns a list of matching libraries.
80
+ server.registerTool("resolve-library-id", {
81
+ title: "Resolve Context7 Library ID",
82
+ description: `Resolves a package/product name to a Context7-compatible library ID and returns a list of matching libraries.
44
83
 
45
84
  You MUST call this function before 'get-library-docs' to obtain a valid Context7-compatible library ID UNLESS the user explicitly provides a library ID in the format '/org/project' or '/org/project/version' in their query.
46
85
 
@@ -58,12 +97,14 @@ Response Format:
58
97
  - If multiple good matches exist, acknowledge this but proceed with the most relevant one
59
98
  - If no good matches exist, clearly state this and suggest query refinements
60
99
 
61
- For ambiguous queries, request clarification before proceeding with a best-guess match.`, {
62
- libraryName: z
63
- .string()
64
- .describe("Library name to search for and retrieve a Context7-compatible library ID."),
100
+ For ambiguous queries, request clarification before proceeding with a best-guess match.`,
101
+ inputSchema: {
102
+ libraryName: z
103
+ .string()
104
+ .describe("Library name to search for and retrieve a Context7-compatible library ID."),
105
+ },
65
106
  }, async ({ libraryName }) => {
66
- const searchResponse = await searchLibraries(libraryName);
107
+ const searchResponse = await searchLibraries(libraryName, clientIp, apiKey);
67
108
  if (!searchResponse.results || searchResponse.results.length === 0) {
68
109
  return {
69
110
  content: [
@@ -100,24 +141,28 @@ ${resultsText}`,
100
141
  ],
101
142
  };
102
143
  });
103
- server.tool("get-library-docs", "Fetches up-to-date documentation for a library. You must call 'resolve-library-id' first to obtain the exact Context7-compatible library ID required to use this tool, UNLESS the user explicitly provides a library ID in the format '/org/project' or '/org/project/version' in their query.", {
104
- context7CompatibleLibraryID: z
105
- .string()
106
- .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'."),
107
- topic: z
108
- .string()
109
- .optional()
110
- .describe("Topic to focus documentation on (e.g., 'hooks', 'routing')."),
111
- tokens: z
112
- .preprocess((val) => (typeof val === "string" ? Number(val) : val), z.number())
113
- .transform((val) => (val < DEFAULT_MINIMUM_TOKENS ? DEFAULT_MINIMUM_TOKENS : val))
114
- .optional()
115
- .describe(`Maximum number of tokens of documentation to retrieve (default: ${DEFAULT_MINIMUM_TOKENS}). Higher values provide more context but consume more tokens.`),
144
+ server.registerTool("get-library-docs", {
145
+ title: "Get Library Docs",
146
+ description: "Fetches up-to-date documentation for a library. You must call 'resolve-library-id' first to obtain the exact Context7-compatible library ID required to use this tool, UNLESS the user explicitly provides a library ID in the format '/org/project' or '/org/project/version' in their query.",
147
+ inputSchema: {
148
+ context7CompatibleLibraryID: z
149
+ .string()
150
+ .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'."),
151
+ topic: z
152
+ .string()
153
+ .optional()
154
+ .describe("Topic to focus documentation on (e.g., 'hooks', 'routing')."),
155
+ tokens: z
156
+ .preprocess((val) => (typeof val === "string" ? Number(val) : val), z.number())
157
+ .transform((val) => (val < DEFAULT_MINIMUM_TOKENS ? DEFAULT_MINIMUM_TOKENS : val))
158
+ .optional()
159
+ .describe(`Maximum number of tokens of documentation to retrieve (default: ${DEFAULT_MINIMUM_TOKENS}). Higher values provide more context but consume more tokens.`),
160
+ },
116
161
  }, async ({ context7CompatibleLibraryID, tokens = DEFAULT_MINIMUM_TOKENS, topic = "" }) => {
117
162
  const fetchDocsResponse = await fetchLibraryDocumentation(context7CompatibleLibraryID, {
118
163
  tokens,
119
164
  topic,
120
- });
165
+ }, clientIp, apiKey);
121
166
  if (!fetchDocsResponse) {
122
167
  return {
123
168
  content: [
@@ -141,7 +186,7 @@ ${resultsText}`,
141
186
  }
142
187
  async function main() {
143
188
  const transportType = TRANSPORT_TYPE;
144
- if (transportType === "http" || transportType === "sse") {
189
+ if (transportType === "http") {
145
190
  // Get initial port from environment or use default
146
191
  const initialPort = CLI_PORT ?? 3000;
147
192
  // Keep track of which port we end up using
@@ -151,16 +196,47 @@ async function main() {
151
196
  // Set CORS headers for all responses
152
197
  res.setHeader("Access-Control-Allow-Origin", "*");
153
198
  res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,DELETE");
154
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, MCP-Session-Id, mcp-session-id");
199
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, MCP-Session-Id, MCP-Protocol-Version, X-Context7-API-Key, Context7-API-Key, X-API-Key, Authorization");
200
+ res.setHeader("Access-Control-Expose-Headers", "MCP-Session-Id");
155
201
  // Handle preflight OPTIONS requests
156
202
  if (req.method === "OPTIONS") {
157
203
  res.writeHead(200);
158
204
  res.end();
159
205
  return;
160
206
  }
207
+ // Function to extract header value safely, handling both string and string[] cases
208
+ const extractHeaderValue = (value) => {
209
+ if (!value)
210
+ return undefined;
211
+ return typeof value === "string" ? value : value[0];
212
+ };
213
+ // Extract Authorization header and remove Bearer prefix if present
214
+ const extractBearerToken = (authHeader) => {
215
+ const header = extractHeaderValue(authHeader);
216
+ if (!header)
217
+ return undefined;
218
+ // If it starts with 'Bearer ', remove that prefix
219
+ if (header.startsWith("Bearer ")) {
220
+ return header.substring(7).trim();
221
+ }
222
+ // Otherwise return the raw value
223
+ return header;
224
+ };
225
+ // Check headers in order of preference
226
+ const apiKey = extractBearerToken(req.headers.authorization) ||
227
+ extractHeaderValue(req.headers["Context7-API-Key"]) ||
228
+ extractHeaderValue(req.headers["X-API-Key"]) ||
229
+ extractHeaderValue(req.headers["context7-api-key"]) ||
230
+ extractHeaderValue(req.headers["x-api-key"]) ||
231
+ extractHeaderValue(req.headers["Context7_API_Key"]) ||
232
+ extractHeaderValue(req.headers["X_API_Key"]) ||
233
+ extractHeaderValue(req.headers["context7_api_key"]) ||
234
+ extractHeaderValue(req.headers["x_api_key"]);
161
235
  try {
236
+ // Extract client IP address using socket remote address (most reliable)
237
+ const clientIp = getClientIp(req);
162
238
  // Create new server instance for each request
163
- const requestServer = createServerInstance();
239
+ const requestServer = createServerInstance(clientIp, apiKey);
164
240
  if (url === "/mcp") {
165
241
  const transport = new StreamableHTTPServerTransport({
166
242
  sessionIdGenerator: undefined,
@@ -229,7 +305,7 @@ async function main() {
229
305
  });
230
306
  httpServer.listen(port, () => {
231
307
  actualPort = port;
232
- console.error(`Context7 Documentation MCP Server running on ${transportType.toUpperCase()} at http://localhost:${actualPort}/mcp and legacy SSE at /sse`);
308
+ console.error(`Context7 Documentation MCP Server running on ${transportType.toUpperCase()} at http://localhost:${actualPort}/mcp with SSE endpoint at /sse`);
233
309
  });
234
310
  };
235
311
  // Start the server with initial port
@@ -237,7 +313,7 @@ async function main() {
237
313
  }
238
314
  else {
239
315
  // Stdio transport - this is already stateless by nature
240
- const server = createServerInstance();
316
+ const server = createServerInstance(undefined, cliOptions.apiKey);
241
317
  const transport = new StdioServerTransport();
242
318
  await server.connect(transport);
243
319
  console.error("Context7 Documentation MCP Server running on stdio");
package/dist/lib/api.js CHANGED
@@ -1,22 +1,53 @@
1
+ import { generateHeaders } from "./encryption.js";
2
+ import { ProxyAgent, setGlobalDispatcher } from "undici";
1
3
  const CONTEXT7_API_BASE_URL = "https://context7.com/api";
2
4
  const DEFAULT_TYPE = "txt";
5
+ // Pick up proxy configuration in a variety of common env var names.
6
+ const PROXY_URL = process.env.HTTPS_PROXY ??
7
+ process.env.https_proxy ??
8
+ process.env.HTTP_PROXY ??
9
+ process.env.http_proxy ??
10
+ null;
11
+ if (PROXY_URL && !PROXY_URL.startsWith("$") && /^(http|https):\/\//i.test(PROXY_URL)) {
12
+ try {
13
+ // Configure a global proxy agent once at startup. Subsequent fetch calls will
14
+ // automatically use this dispatcher.
15
+ // Using `any` cast because ProxyAgent implements the Dispatcher interface but
16
+ // TS may not infer it correctly in some versions.
17
+ setGlobalDispatcher(new ProxyAgent(PROXY_URL));
18
+ }
19
+ catch (error) {
20
+ // Don't crash the app if proxy initialisation fails – just log a warning.
21
+ console.error(`[Context7] Failed to configure proxy agent for provided proxy URL: ${PROXY_URL}:`, error);
22
+ }
23
+ }
3
24
  /**
4
25
  * Searches for libraries matching the given query
5
26
  * @param query The search query
27
+ * @param clientIp Optional client IP address to include in headers
28
+ * @param apiKey Optional API key for authentication
6
29
  * @returns Search results or null if the request fails
7
30
  */
8
- export async function searchLibraries(query) {
31
+ export async function searchLibraries(query, clientIp, apiKey) {
9
32
  try {
10
33
  const url = new URL(`${CONTEXT7_API_BASE_URL}/v1/search`);
11
34
  url.searchParams.set("query", query);
12
- const response = await fetch(url);
35
+ const headers = generateHeaders(clientIp, apiKey);
36
+ const response = await fetch(url, { headers });
13
37
  if (!response.ok) {
14
38
  const errorCode = response.status;
15
39
  if (errorCode === 429) {
16
- console.error(`Rate limited due to too many requests. Please try again later.`);
40
+ console.error("Rate limited due to too many requests. Please try again later.");
17
41
  return {
18
42
  results: [],
19
- error: `Rate limited due to too many requests. Please try again later.`,
43
+ error: "Rate limited due to too many requests. Please try again later.",
44
+ };
45
+ }
46
+ if (errorCode === 401) {
47
+ console.error("Unauthorized. Please check your API key.");
48
+ return {
49
+ results: [],
50
+ error: "Unauthorized. Please check your API key.",
20
51
  };
21
52
  }
22
53
  console.error(`Failed to search libraries. Please try again later. Error code: ${errorCode}`);
@@ -36,9 +67,11 @@ export async function searchLibraries(query) {
36
67
  * Fetches documentation context for a specific library
37
68
  * @param libraryId The library ID to fetch documentation for
38
69
  * @param options Options for the request
70
+ * @param clientIp Optional client IP address to include in headers
71
+ * @param apiKey Optional API key for authentication
39
72
  * @returns The documentation text or null if the request fails
40
73
  */
41
- export async function fetchLibraryDocumentation(libraryId, options = {}) {
74
+ export async function fetchLibraryDocumentation(libraryId, options = {}, clientIp, apiKey) {
42
75
  try {
43
76
  if (libraryId.startsWith("/")) {
44
77
  libraryId = libraryId.slice(1);
@@ -49,15 +82,22 @@ export async function fetchLibraryDocumentation(libraryId, options = {}) {
49
82
  if (options.topic)
50
83
  url.searchParams.set("topic", options.topic);
51
84
  url.searchParams.set("type", DEFAULT_TYPE);
52
- const response = await fetch(url, {
53
- headers: {
54
- "X-Context7-Source": "mcp-server",
55
- },
56
- });
85
+ const headers = generateHeaders(clientIp, apiKey, { "X-Context7-Source": "mcp-server" });
86
+ const response = await fetch(url, { headers });
57
87
  if (!response.ok) {
58
88
  const errorCode = response.status;
59
89
  if (errorCode === 429) {
60
- const errorMessage = `Rate limited due to too many requests. Please try again later.`;
90
+ const errorMessage = "Rate limited due to too many requests. Please try again later.";
91
+ console.error(errorMessage);
92
+ return errorMessage;
93
+ }
94
+ if (errorCode === 404) {
95
+ const errorMessage = "The library you are trying to access does not exist. Please try with a different library ID.";
96
+ console.error(errorMessage);
97
+ return errorMessage;
98
+ }
99
+ if (errorCode === 401) {
100
+ const errorMessage = "Unauthorized. Please check your API key.";
61
101
  console.error(errorMessage);
62
102
  return errorMessage;
63
103
  }
@@ -0,0 +1,35 @@
1
+ import { createCipheriv, randomBytes } from "crypto";
2
+ const ENCRYPTION_KEY = process.env.CLIENT_IP_ENCRYPTION_KEY ||
3
+ "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
4
+ const ALGORITHM = "aes-256-cbc";
5
+ function validateEncryptionKey(key) {
6
+ // Must be exactly 64 hex characters (32 bytes)
7
+ return /^[0-9a-fA-F]{64}$/.test(key);
8
+ }
9
+ function encryptClientIp(clientIp) {
10
+ if (!validateEncryptionKey(ENCRYPTION_KEY)) {
11
+ console.error("Invalid encryption key format. Must be 64 hex characters.");
12
+ return clientIp; // Fallback to unencrypted
13
+ }
14
+ try {
15
+ const iv = randomBytes(16);
16
+ const cipher = createCipheriv(ALGORITHM, Buffer.from(ENCRYPTION_KEY, "hex"), iv);
17
+ let encrypted = cipher.update(clientIp, "utf8", "hex");
18
+ encrypted += cipher.final("hex");
19
+ return iv.toString("hex") + ":" + encrypted;
20
+ }
21
+ catch (error) {
22
+ console.error("Error encrypting client IP:", error);
23
+ return clientIp; // Fallback to unencrypted
24
+ }
25
+ }
26
+ export function generateHeaders(clientIp, apiKey, extraHeaders = {}) {
27
+ const headers = { ...extraHeaders };
28
+ if (clientIp) {
29
+ headers["mcp-client-ip"] = encryptClientIp(clientIp);
30
+ }
31
+ if (apiKey) {
32
+ headers["Authorization"] = `Bearer ${apiKey}`;
33
+ }
34
+ return headers;
35
+ }
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name":"@upstash/context7-mcp","version":"v1.0.14","description":"MCP server for Context7","scripts":{"test":"echo \"Error: no test specified\" && exit 1","build":"tsc && chmod 755 dist/index.js","format":"prettier --write .","lint":"eslint \"**/*.{js,ts,tsx}\" --fix","lint:check":"eslint \"**/*.{js,ts,tsx}\"","start":"node dist/index.js --transport http"},"repository":{"type":"git","url":"git+https://github.com/upstash/context7.git"},"keywords":["modelcontextprotocol","mcp","context7"],"author":"abdush","license":"MIT","type":"module","bin":{"context7-mcp":"dist/index.js"},"files":["dist"],"bugs":{"url":"https://github.com/upstash/context7/issues"},"homepage":"https://github.com/upstash/context7#readme","dependencies":{"@modelcontextprotocol/sdk":"^1.12.0","commander":"^14.0.0","zod":"^3.24.2"},"devDependencies":{"@types/node":"^22.13.14","@typescript-eslint/eslint-plugin":"^8.28.0","@typescript-eslint/parser":"^8.28.0","eslint":"^9.23.0","eslint-config-prettier":"^10.1.1","eslint-plugin-prettier":"^5.2.5","prettier":"^3.5.3","typescript":"^5.8.2","typescript-eslint":"^8.28.0"}}
1
+ {"name":"@upstash/context7-mcp","version":"1.0.16","description":"MCP server for Context7","scripts":{"test":"echo \"Error: no test specified\" && exit 1","build":"tsc && chmod 755 dist/index.js","format":"prettier --write .","lint":"eslint \"**/*.{js,ts,tsx}\" --fix","lint:check":"eslint \"**/*.{js,ts,tsx}\"","start":"node dist/index.js --transport http","pack-dxt":"bun install && bun run build && rm -rf node_modules && bun install --production && mv dxt/.dxtignore .dxtignore && mv dxt/manifest.json manifest.json && dxt validate manifest.json && dxt pack . dxt/context7.dxt && mv manifest.json dxt/manifest.json && mv .dxtignore dxt/.dxtignore && bun install"},"repository":{"type":"git","url":"git+https://github.com/upstash/context7.git"},"keywords":["modelcontextprotocol","mcp","context7"],"author":"abdush","license":"MIT","type":"module","bin":{"context7-mcp":"dist/index.js"},"files":["dist"],"bugs":{"url":"https://github.com/upstash/context7/issues"},"homepage":"https://github.com/upstash/context7#readme","dependencies":{"@modelcontextprotocol/sdk":"^1.13.2","commander":"^14.0.0","undici":"^6.6.3","zod":"^3.24.2"},"devDependencies":{"@types/node":"^22.13.14","@typescript-eslint/eslint-plugin":"^8.28.0","@typescript-eslint/parser":"^8.28.0","eslint":"^9.23.0","eslint-config-prettier":"^10.1.1","eslint-plugin-prettier":"^5.2.5","prettier":"^3.5.3","typescript":"^5.8.2","typescript-eslint":"^8.28.0"}}