@upstash/context7-mcp 1.0.13 → 1.0.15

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
@@ -4,39 +4,82 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4
4
  import { z } from "zod";
5
5
  import { searchLibraries, fetchLibraryDocumentation } from "./lib/api.js";
6
6
  import { formatSearchResults } from "./lib/utils.js";
7
- import dotenv from "dotenv";
8
7
  import { createServer } from "http";
9
8
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
10
9
  import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
11
- import { parse } from "url";
12
- // Load environment variables from .env file if present
13
- dotenv.config();
14
- // Get DEFAULT_MINIMUM_TOKENS from environment variable or use default
15
- let DEFAULT_MINIMUM_TOKENS = 10000;
16
- if (process.env.DEFAULT_MINIMUM_TOKENS) {
17
- const parsedValue = parseInt(process.env.DEFAULT_MINIMUM_TOKENS, 10);
18
- if (!isNaN(parsedValue) && parsedValue > 0) {
19
- DEFAULT_MINIMUM_TOKENS = parsedValue;
20
- }
21
- else {
22
- console.warn(`Warning: Invalid DEFAULT_MINIMUM_TOKENS value provided in environment variable. Using default value of 10000`);
23
- }
10
+ import { Command } from "commander";
11
+ const DEFAULT_MINIMUM_TOKENS = 10000;
12
+ // Parse CLI arguments using commander
13
+ const program = new Command()
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")
17
+ .allowUnknownOption() // let MCP Inspector / other wrappers pass through extra flags
18
+ .parse(process.argv);
19
+ const cliOptions = program.opts();
20
+ // Validate transport option
21
+ const allowedTransports = ["stdio", "http"];
22
+ if (!allowedTransports.includes(cliOptions.transport)) {
23
+ console.error(`Invalid --transport value: '${cliOptions.transport}'. Must be one of: stdio, http.`);
24
+ process.exit(1);
25
+ }
26
+ // Transport configuration
27
+ const TRANSPORT_TYPE = (cliOptions.transport || "stdio");
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);
24
38
  }
39
+ // HTTP port configuration
40
+ const CLI_PORT = (() => {
41
+ const parsed = parseInt(cliOptions.port, 10);
42
+ return isNaN(parsed) ? undefined : parsed;
43
+ })();
25
44
  // Store SSE transports by session ID
26
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
+ }
27
71
  // Function to create a new server instance with all tools registered
28
- function createServerInstance() {
72
+ function createServerInstance(clientIp, apiKey) {
29
73
  const server = new McpServer({
30
74
  name: "Context7",
31
- description: "Retrieves up-to-date documentation and code examples for any library.",
32
- version: "v1.0.13",
33
- capabilities: {
34
- resources: {},
35
- tools: {},
36
- },
75
+ version: "1.0.15",
76
+ }, {
77
+ instructions: "Use this server to retrieve up-to-date documentation and code examples for any library.",
37
78
  });
38
79
  // Register Context7 tools
39
- 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.
40
83
 
41
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.
42
85
 
@@ -54,12 +97,14 @@ Response Format:
54
97
  - If multiple good matches exist, acknowledge this but proceed with the most relevant one
55
98
  - If no good matches exist, clearly state this and suggest query refinements
56
99
 
57
- For ambiguous queries, request clarification before proceeding with a best-guess match.`, {
58
- libraryName: z
59
- .string()
60
- .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
+ },
61
106
  }, async ({ libraryName }) => {
62
- const searchResponse = await searchLibraries(libraryName);
107
+ const searchResponse = await searchLibraries(libraryName, clientIp, apiKey);
63
108
  if (!searchResponse.results || searchResponse.results.length === 0) {
64
109
  return {
65
110
  content: [
@@ -96,24 +141,28 @@ ${resultsText}`,
96
141
  ],
97
142
  };
98
143
  });
99
- 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.", {
100
- context7CompatibleLibraryID: z
101
- .string()
102
- .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'."),
103
- topic: z
104
- .string()
105
- .optional()
106
- .describe("Topic to focus documentation on (e.g., 'hooks', 'routing')."),
107
- tokens: z
108
- .preprocess((val) => (typeof val === "string" ? Number(val) : val), z.number())
109
- .transform((val) => (val < DEFAULT_MINIMUM_TOKENS ? DEFAULT_MINIMUM_TOKENS : val))
110
- .optional()
111
- .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
+ },
112
161
  }, async ({ context7CompatibleLibraryID, tokens = DEFAULT_MINIMUM_TOKENS, topic = "" }) => {
113
162
  const fetchDocsResponse = await fetchLibraryDocumentation(context7CompatibleLibraryID, {
114
163
  tokens,
115
164
  topic,
116
- });
165
+ }, clientIp, apiKey);
117
166
  if (!fetchDocsResponse) {
118
167
  return {
119
168
  content: [
@@ -136,27 +185,58 @@ ${resultsText}`,
136
185
  return server;
137
186
  }
138
187
  async function main() {
139
- const transportType = process.env.MCP_TRANSPORT || "stdio";
140
- if (transportType === "http" || transportType === "sse") {
188
+ const transportType = TRANSPORT_TYPE;
189
+ if (transportType === "http") {
141
190
  // Get initial port from environment or use default
142
- const initialPort = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
191
+ const initialPort = CLI_PORT ?? 3000;
143
192
  // Keep track of which port we end up using
144
193
  let actualPort = initialPort;
145
194
  const httpServer = createServer(async (req, res) => {
146
- const url = parse(req.url || "").pathname;
195
+ const url = new URL(req.url || "", `http://${req.headers.host}`).pathname;
147
196
  // Set CORS headers for all responses
148
197
  res.setHeader("Access-Control-Allow-Origin", "*");
149
198
  res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,DELETE");
150
- 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");
151
201
  // Handle preflight OPTIONS requests
152
202
  if (req.method === "OPTIONS") {
153
203
  res.writeHead(200);
154
204
  res.end();
155
205
  return;
156
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"]);
157
235
  try {
236
+ // Extract client IP address using socket remote address (most reliable)
237
+ const clientIp = getClientIp(req);
158
238
  // Create new server instance for each request
159
- const requestServer = createServerInstance();
239
+ const requestServer = createServerInstance(clientIp, apiKey);
160
240
  if (url === "/mcp") {
161
241
  const transport = new StreamableHTTPServerTransport({
162
242
  sessionIdGenerator: undefined,
@@ -177,8 +257,8 @@ async function main() {
177
257
  }
178
258
  else if (url === "/messages" && req.method === "POST") {
179
259
  // Get session ID from query parameters
180
- const parsedUrl = parse(req.url || "", true);
181
- const sessionId = parsedUrl.query.sessionId;
260
+ const sessionId = new URL(req.url || "", `http://${req.headers.host}`).searchParams.get("sessionId") ??
261
+ "";
182
262
  if (!sessionId) {
183
263
  res.writeHead(400);
184
264
  res.end("Missing sessionId parameter");
@@ -225,7 +305,7 @@ async function main() {
225
305
  });
226
306
  httpServer.listen(port, () => {
227
307
  actualPort = port;
228
- 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`);
229
309
  });
230
310
  };
231
311
  // Start the server with initial port
@@ -233,7 +313,7 @@ async function main() {
233
313
  }
234
314
  else {
235
315
  // Stdio transport - this is already stateless by nature
236
- const server = createServerInstance();
316
+ const server = createServerInstance(undefined, cliOptions.apiKey);
237
317
  const transport = new StdioServerTransport();
238
318
  await server.connect(transport);
239
319
  console.error("Context7 Documentation MCP Server running on stdio");
package/dist/lib/api.js CHANGED
@@ -1,22 +1,33 @@
1
+ import { generateHeaders } from "./encryption.js";
1
2
  const CONTEXT7_API_BASE_URL = "https://context7.com/api";
2
3
  const DEFAULT_TYPE = "txt";
3
4
  /**
4
5
  * Searches for libraries matching the given query
5
6
  * @param query The search query
7
+ * @param clientIp Optional client IP address to include in headers
8
+ * @param apiKey Optional API key for authentication
6
9
  * @returns Search results or null if the request fails
7
10
  */
8
- export async function searchLibraries(query) {
11
+ export async function searchLibraries(query, clientIp, apiKey) {
9
12
  try {
10
13
  const url = new URL(`${CONTEXT7_API_BASE_URL}/v1/search`);
11
14
  url.searchParams.set("query", query);
12
- const response = await fetch(url);
15
+ const headers = generateHeaders(clientIp, apiKey);
16
+ const response = await fetch(url, { headers });
13
17
  if (!response.ok) {
14
18
  const errorCode = response.status;
15
19
  if (errorCode === 429) {
16
- console.error(`Rate limited due to too many requests. Please try again later.`);
20
+ console.error("Rate limited due to too many requests. Please try again later.");
17
21
  return {
18
22
  results: [],
19
- error: `Rate limited due to too many requests. Please try again later.`,
23
+ error: "Rate limited due to too many requests. Please try again later.",
24
+ };
25
+ }
26
+ if (errorCode === 401) {
27
+ console.error("Unauthorized. Please check your API key.");
28
+ return {
29
+ results: [],
30
+ error: "Unauthorized. Please check your API key.",
20
31
  };
21
32
  }
22
33
  console.error(`Failed to search libraries. Please try again later. Error code: ${errorCode}`);
@@ -36,9 +47,11 @@ export async function searchLibraries(query) {
36
47
  * Fetches documentation context for a specific library
37
48
  * @param libraryId The library ID to fetch documentation for
38
49
  * @param options Options for the request
50
+ * @param clientIp Optional client IP address to include in headers
51
+ * @param apiKey Optional API key for authentication
39
52
  * @returns The documentation text or null if the request fails
40
53
  */
41
- export async function fetchLibraryDocumentation(libraryId, options = {}) {
54
+ export async function fetchLibraryDocumentation(libraryId, options = {}, clientIp, apiKey) {
42
55
  try {
43
56
  if (libraryId.startsWith("/")) {
44
57
  libraryId = libraryId.slice(1);
@@ -49,15 +62,22 @@ export async function fetchLibraryDocumentation(libraryId, options = {}) {
49
62
  if (options.topic)
50
63
  url.searchParams.set("topic", options.topic);
51
64
  url.searchParams.set("type", DEFAULT_TYPE);
52
- const response = await fetch(url, {
53
- headers: {
54
- "X-Context7-Source": "mcp-server",
55
- },
56
- });
65
+ const headers = generateHeaders(clientIp, apiKey, { "X-Context7-Source": "mcp-server" });
66
+ const response = await fetch(url, { headers });
57
67
  if (!response.ok) {
58
68
  const errorCode = response.status;
59
69
  if (errorCode === 429) {
60
- const errorMessage = `Rate limited due to too many requests. Please try again later.`;
70
+ const errorMessage = "Rate limited due to too many requests. Please try again later.";
71
+ console.error(errorMessage);
72
+ return errorMessage;
73
+ }
74
+ if (errorCode === 404) {
75
+ const errorMessage = "The library you are trying to access does not exist. Please try with a different library ID.";
76
+ console.error(errorMessage);
77
+ return errorMessage;
78
+ }
79
+ if (errorCode === 401) {
80
+ const errorMessage = "Unauthorized. Please check your API key.";
61
81
  console.error(errorMessage);
62
82
  return errorMessage;
63
83
  }
@@ -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.13","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":"MCP_TRANSPORT=http node dist/index.js"},"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","dotenv":"^16.5.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.15","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","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"}}