@upstash/context7-mcp 1.0.0 → 1.0.1-canary-20251124144836
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/README.md +1480 -37
- package/dist/index.js +308 -0
- package/dist/lib/api.js +145 -0
- package/dist/lib/encryption.js +38 -0
- package/dist/lib/types.js +7 -0
- package/dist/lib/utils.js +77 -0
- package/package.json +34 -24
- package/build/index.js +0 -103
- package/build/lib/api.js +0 -70
- package/build/lib/types.js +0 -1
- package/build/lib/utils.js +0 -127
package/dist/index.js
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { searchLibraries, fetchLibraryDocumentation } from "./lib/api.js";
|
|
6
|
+
import { formatSearchResults } from "./lib/utils.js";
|
|
7
|
+
import { DOCUMENTATION_MODES } from "./lib/types.js";
|
|
8
|
+
import express from "express";
|
|
9
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
10
|
+
import { Command } from "commander";
|
|
11
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
12
|
+
/** Default number of results to return per page */
|
|
13
|
+
const DEFAULT_RESULTS_LIMIT = 10;
|
|
14
|
+
/** Default HTTP server port */
|
|
15
|
+
const DEFAULT_PORT = 3000;
|
|
16
|
+
// Parse CLI arguments using commander
|
|
17
|
+
const program = new Command()
|
|
18
|
+
.option("--transport <stdio|http>", "transport type", "stdio")
|
|
19
|
+
.option("--port <number>", "port for HTTP transport", DEFAULT_PORT.toString())
|
|
20
|
+
.option("--api-key <key>", "API key for authentication (or set CONTEXT7_API_KEY env var)")
|
|
21
|
+
.allowUnknownOption() // let MCP Inspector / other wrappers pass through extra flags
|
|
22
|
+
.parse(process.argv);
|
|
23
|
+
const cliOptions = program.opts();
|
|
24
|
+
// Validate transport option
|
|
25
|
+
const allowedTransports = ["stdio", "http"];
|
|
26
|
+
if (!allowedTransports.includes(cliOptions.transport)) {
|
|
27
|
+
console.error(`Invalid --transport value: '${cliOptions.transport}'. Must be one of: stdio, http.`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
// Transport configuration
|
|
31
|
+
const TRANSPORT_TYPE = (cliOptions.transport || "stdio");
|
|
32
|
+
// Disallow incompatible flags based on transport
|
|
33
|
+
const passedPortFlag = process.argv.includes("--port");
|
|
34
|
+
const passedApiKeyFlag = process.argv.includes("--api-key");
|
|
35
|
+
if (TRANSPORT_TYPE === "http" && passedApiKeyFlag) {
|
|
36
|
+
console.error("The --api-key flag is not allowed when using --transport http. Use header-based auth at the HTTP layer instead.");
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
if (TRANSPORT_TYPE === "stdio" && passedPortFlag) {
|
|
40
|
+
console.error("The --port flag is not allowed when using --transport stdio.");
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
// HTTP port configuration
|
|
44
|
+
const CLI_PORT = (() => {
|
|
45
|
+
const parsed = parseInt(cliOptions.port, 10);
|
|
46
|
+
return isNaN(parsed) ? undefined : parsed;
|
|
47
|
+
})();
|
|
48
|
+
const requestContext = new AsyncLocalStorage();
|
|
49
|
+
// Store API key globally for stdio mode (where requestContext may not be available in tool handlers)
|
|
50
|
+
let globalApiKey;
|
|
51
|
+
function getClientIp(req) {
|
|
52
|
+
const forwardedFor = req.headers["x-forwarded-for"] || req.headers["X-Forwarded-For"];
|
|
53
|
+
if (forwardedFor) {
|
|
54
|
+
const ips = Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor;
|
|
55
|
+
const ipList = ips.split(",").map((ip) => ip.trim());
|
|
56
|
+
for (const ip of ipList) {
|
|
57
|
+
const plainIp = ip.replace(/^::ffff:/, "");
|
|
58
|
+
if (!plainIp.startsWith("10.") &&
|
|
59
|
+
!plainIp.startsWith("192.168.") &&
|
|
60
|
+
!/^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(plainIp)) {
|
|
61
|
+
return plainIp;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return ipList[0].replace(/^::ffff:/, "");
|
|
65
|
+
}
|
|
66
|
+
if (req.socket?.remoteAddress) {
|
|
67
|
+
return req.socket.remoteAddress.replace(/^::ffff:/, "");
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
const server = new McpServer({
|
|
72
|
+
name: "Context7",
|
|
73
|
+
version: "1.0.13",
|
|
74
|
+
}, {
|
|
75
|
+
instructions: "Use this server to retrieve up-to-date documentation and code examples for any library.",
|
|
76
|
+
});
|
|
77
|
+
server.registerTool("resolve-library-id", {
|
|
78
|
+
title: "Resolve Context7 Library ID",
|
|
79
|
+
description: `Resolves a package/product name to a Context7-compatible library ID and returns a list of matching libraries.
|
|
80
|
+
|
|
81
|
+
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.
|
|
82
|
+
|
|
83
|
+
Selection Process:
|
|
84
|
+
1. Analyze the query to understand what library/package the user is looking for
|
|
85
|
+
2. Return the most relevant match based on:
|
|
86
|
+
- Name similarity to the query (exact matches prioritized)
|
|
87
|
+
- Description relevance to the query's intent
|
|
88
|
+
- Documentation coverage (prioritize libraries with higher Code Snippet counts)
|
|
89
|
+
- Source reputation (consider libraries with High or Medium reputation more authoritative)
|
|
90
|
+
- Benchmark Score: Quality indicator (100 is the highest score)
|
|
91
|
+
|
|
92
|
+
Response Format:
|
|
93
|
+
- Return the selected library ID in a clearly marked section
|
|
94
|
+
- Provide a brief explanation for why this library was chosen
|
|
95
|
+
- If multiple good matches exist, acknowledge this but proceed with the most relevant one
|
|
96
|
+
- If no good matches exist, clearly state this and suggest query refinements
|
|
97
|
+
|
|
98
|
+
For ambiguous queries, request clarification before proceeding with a best-guess match.`,
|
|
99
|
+
inputSchema: {
|
|
100
|
+
libraryName: z
|
|
101
|
+
.string()
|
|
102
|
+
.describe("Library name to search for and retrieve a Context7-compatible library ID."),
|
|
103
|
+
},
|
|
104
|
+
}, async ({ libraryName }) => {
|
|
105
|
+
const ctx = requestContext.getStore();
|
|
106
|
+
const searchResponse = await searchLibraries(libraryName, ctx?.clientIp, ctx?.apiKey);
|
|
107
|
+
if (!searchResponse.results || searchResponse.results.length === 0) {
|
|
108
|
+
return {
|
|
109
|
+
content: [
|
|
110
|
+
{
|
|
111
|
+
type: "text",
|
|
112
|
+
text: searchResponse.error
|
|
113
|
+
? searchResponse.error
|
|
114
|
+
: "Failed to retrieve library documentation data from Context7",
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const resultsText = formatSearchResults(searchResponse);
|
|
120
|
+
const responseText = `Available Libraries:
|
|
121
|
+
|
|
122
|
+
Each result includes:
|
|
123
|
+
- Library ID: Context7-compatible identifier (format: /org/project)
|
|
124
|
+
- Name: Library or package name
|
|
125
|
+
- Description: Short summary
|
|
126
|
+
- Code Snippets: Number of available code examples
|
|
127
|
+
- Source Reputation: Authority indicator (High, Medium, Low, or Unknown)
|
|
128
|
+
- Benchmark Score: Quality indicator (100 is the highest score)
|
|
129
|
+
- Versions: List of versions if available. Use one of those versions if the user provides a version in their query. The format of the version is /org/project/version.
|
|
130
|
+
|
|
131
|
+
For best results, select libraries based on name match, source reputation, snippet coverage, benchmark score, and relevance to your use case.
|
|
132
|
+
|
|
133
|
+
----------
|
|
134
|
+
|
|
135
|
+
${resultsText}`;
|
|
136
|
+
return {
|
|
137
|
+
content: [
|
|
138
|
+
{
|
|
139
|
+
type: "text",
|
|
140
|
+
text: responseText,
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
};
|
|
144
|
+
});
|
|
145
|
+
server.registerTool("get-library-docs", {
|
|
146
|
+
title: "Get Library Docs",
|
|
147
|
+
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. Use mode='code' (default) for API references and code examples, or mode='info' for conceptual guides, narrative information, and architectural questions.",
|
|
148
|
+
inputSchema: {
|
|
149
|
+
context7CompatibleLibraryID: z
|
|
150
|
+
.string()
|
|
151
|
+
.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'."),
|
|
152
|
+
mode: z
|
|
153
|
+
.enum(["code", "info"])
|
|
154
|
+
.optional()
|
|
155
|
+
.default("code")
|
|
156
|
+
.describe("Documentation mode: 'code' for API references and code examples (default), 'info' for conceptual guides, narrative information, and architectural questions."),
|
|
157
|
+
topic: z
|
|
158
|
+
.string()
|
|
159
|
+
.optional()
|
|
160
|
+
.describe("Topic to focus documentation on (e.g., 'hooks', 'routing')."),
|
|
161
|
+
page: z
|
|
162
|
+
.number()
|
|
163
|
+
.int()
|
|
164
|
+
.min(1)
|
|
165
|
+
.max(10)
|
|
166
|
+
.optional()
|
|
167
|
+
.describe("Page number for pagination (start: 1, default: 1). If the context is not sufficient, try page=2, page=3, page=4, etc. with the same topic."),
|
|
168
|
+
},
|
|
169
|
+
}, async ({ context7CompatibleLibraryID, mode = DOCUMENTATION_MODES.CODE, page = 1, topic }) => {
|
|
170
|
+
const ctx = requestContext.getStore();
|
|
171
|
+
const apiKey = ctx?.apiKey || globalApiKey;
|
|
172
|
+
const fetchDocsResponse = await fetchLibraryDocumentation(context7CompatibleLibraryID, mode, {
|
|
173
|
+
page,
|
|
174
|
+
limit: DEFAULT_RESULTS_LIMIT,
|
|
175
|
+
topic,
|
|
176
|
+
}, ctx?.clientIp, apiKey);
|
|
177
|
+
if (!fetchDocsResponse) {
|
|
178
|
+
return {
|
|
179
|
+
content: [
|
|
180
|
+
{
|
|
181
|
+
type: "text",
|
|
182
|
+
text: "Documentation not found or not finalized for this library. This might have happened because you used an invalid Context7-compatible library ID. To get a valid Context7-compatible library ID, use the 'resolve-library-id' with the package name you wish to retrieve documentation for.",
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
content: [
|
|
189
|
+
{
|
|
190
|
+
type: "text",
|
|
191
|
+
text: fetchDocsResponse,
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
};
|
|
195
|
+
});
|
|
196
|
+
async function main() {
|
|
197
|
+
const transportType = TRANSPORT_TYPE;
|
|
198
|
+
if (transportType === "http") {
|
|
199
|
+
const initialPort = CLI_PORT ?? DEFAULT_PORT;
|
|
200
|
+
let actualPort = initialPort;
|
|
201
|
+
const app = express();
|
|
202
|
+
app.use(express.json());
|
|
203
|
+
app.use((req, res, next) => {
|
|
204
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
205
|
+
res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,DELETE");
|
|
206
|
+
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");
|
|
207
|
+
res.setHeader("Access-Control-Expose-Headers", "MCP-Session-Id");
|
|
208
|
+
if (req.method === "OPTIONS") {
|
|
209
|
+
res.sendStatus(200);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
next();
|
|
213
|
+
});
|
|
214
|
+
const extractHeaderValue = (value) => {
|
|
215
|
+
if (!value)
|
|
216
|
+
return undefined;
|
|
217
|
+
return typeof value === "string" ? value : value[0];
|
|
218
|
+
};
|
|
219
|
+
const extractBearerToken = (authHeader) => {
|
|
220
|
+
const header = extractHeaderValue(authHeader);
|
|
221
|
+
if (!header)
|
|
222
|
+
return undefined;
|
|
223
|
+
if (header.startsWith("Bearer ")) {
|
|
224
|
+
return header.substring(7).trim();
|
|
225
|
+
}
|
|
226
|
+
return header;
|
|
227
|
+
};
|
|
228
|
+
const extractApiKey = (req) => {
|
|
229
|
+
return (extractBearerToken(req.headers.authorization) ||
|
|
230
|
+
extractHeaderValue(req.headers["Context7-API-Key"]) ||
|
|
231
|
+
extractHeaderValue(req.headers["X-API-Key"]) ||
|
|
232
|
+
extractHeaderValue(req.headers["context7-api-key"]) ||
|
|
233
|
+
extractHeaderValue(req.headers["x-api-key"]) ||
|
|
234
|
+
extractHeaderValue(req.headers["Context7_API_Key"]) ||
|
|
235
|
+
extractHeaderValue(req.headers["X_API_Key"]) ||
|
|
236
|
+
extractHeaderValue(req.headers["context7_api_key"]) ||
|
|
237
|
+
extractHeaderValue(req.headers["x_api_key"]));
|
|
238
|
+
};
|
|
239
|
+
app.all("/mcp", async (req, res) => {
|
|
240
|
+
try {
|
|
241
|
+
const clientIp = getClientIp(req);
|
|
242
|
+
const apiKey = extractApiKey(req);
|
|
243
|
+
const transport = new StreamableHTTPServerTransport({
|
|
244
|
+
sessionIdGenerator: undefined,
|
|
245
|
+
enableJsonResponse: true,
|
|
246
|
+
});
|
|
247
|
+
res.on("close", () => {
|
|
248
|
+
transport.close();
|
|
249
|
+
});
|
|
250
|
+
await requestContext.run({ clientIp, apiKey }, async () => {
|
|
251
|
+
await server.connect(transport);
|
|
252
|
+
await transport.handleRequest(req, res, req.body);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
console.error("Error handling MCP request:", error);
|
|
257
|
+
if (!res.headersSent) {
|
|
258
|
+
res.status(500).json({
|
|
259
|
+
jsonrpc: "2.0",
|
|
260
|
+
error: { code: -32603, message: "Internal server error" },
|
|
261
|
+
id: null,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
app.get("/ping", (_req, res) => {
|
|
267
|
+
res.json({ status: "ok", message: "pong" });
|
|
268
|
+
});
|
|
269
|
+
// Catch-all 404 handler - must be after all other routes
|
|
270
|
+
app.use((_req, res) => {
|
|
271
|
+
res.status(404).json({
|
|
272
|
+
error: "not_found",
|
|
273
|
+
message: "Endpoint not found. Use /mcp for MCP protocol communication.",
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
const startServer = (port, maxAttempts = 10) => {
|
|
277
|
+
const httpServer = app.listen(port);
|
|
278
|
+
httpServer.once("error", (err) => {
|
|
279
|
+
if (err.code === "EADDRINUSE" && port < initialPort + maxAttempts) {
|
|
280
|
+
console.warn(`Port ${port} is in use, trying port ${port + 1}...`);
|
|
281
|
+
startServer(port + 1, maxAttempts);
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
console.error(`Failed to start server: ${err.message}`);
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
httpServer.once("listening", () => {
|
|
289
|
+
actualPort = port;
|
|
290
|
+
console.error(`Context7 Documentation MCP Server running on HTTP at http://localhost:${actualPort}/mcp`);
|
|
291
|
+
});
|
|
292
|
+
};
|
|
293
|
+
startServer(initialPort);
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
const apiKey = cliOptions.apiKey || process.env.CONTEXT7_API_KEY;
|
|
297
|
+
globalApiKey = apiKey; // Store globally for tool handlers in stdio mode
|
|
298
|
+
const transport = new StdioServerTransport();
|
|
299
|
+
await requestContext.run({ apiKey }, async () => {
|
|
300
|
+
await server.connect(transport);
|
|
301
|
+
});
|
|
302
|
+
console.error("Context7 Documentation MCP Server running on stdio");
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
main().catch((error) => {
|
|
306
|
+
console.error("Fatal error in main():", error);
|
|
307
|
+
process.exit(1);
|
|
308
|
+
});
|
package/dist/lib/api.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { generateHeaders } from "./encryption.js";
|
|
2
|
+
import { ProxyAgent, setGlobalDispatcher } from "undici";
|
|
3
|
+
import { DOCUMENTATION_MODES } from "./types.js";
|
|
4
|
+
import { maskApiKey } from "./utils.js";
|
|
5
|
+
const CONTEXT7_API_BASE_URL = "https://context7.com/api";
|
|
6
|
+
const DEFAULT_TYPE = "txt";
|
|
7
|
+
/**
|
|
8
|
+
* Parses a Context7-compatible library ID into its components
|
|
9
|
+
* @param libraryId The library ID (e.g., "/vercel/next.js" or "/vercel/next.js/v14.3.0")
|
|
10
|
+
* @returns Object with username, library, and optional tag
|
|
11
|
+
*/
|
|
12
|
+
function parseLibraryId(libraryId) {
|
|
13
|
+
// Remove leading slash if present
|
|
14
|
+
const cleaned = libraryId.startsWith("/") ? libraryId.slice(1) : libraryId;
|
|
15
|
+
const parts = cleaned.split("/");
|
|
16
|
+
if (parts.length < 2) {
|
|
17
|
+
throw new Error(`Invalid library ID format: ${libraryId}. Expected format: /username/library or /username/library/tag`);
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
username: parts[0],
|
|
21
|
+
library: parts[1],
|
|
22
|
+
tag: parts[2], // undefined if not present
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Generates appropriate error messages based on HTTP status codes
|
|
27
|
+
* @param errorCode The HTTP error status code
|
|
28
|
+
* @param apiKey Optional API key (used for rate limit message)
|
|
29
|
+
* @returns Error message string
|
|
30
|
+
*/
|
|
31
|
+
function createErrorMessage(errorCode, apiKey) {
|
|
32
|
+
switch (errorCode) {
|
|
33
|
+
case 429:
|
|
34
|
+
return apiKey
|
|
35
|
+
? "Rate limited due to too many requests. Please try again later."
|
|
36
|
+
: "Rate limited due to too many requests. You can create a free API key at https://context7.com/dashboard for higher rate limits.";
|
|
37
|
+
case 404:
|
|
38
|
+
return "The library you are trying to access does not exist. Please try with a different library ID.";
|
|
39
|
+
case 401:
|
|
40
|
+
if (!apiKey) {
|
|
41
|
+
return "Unauthorized. Please provide an API key.";
|
|
42
|
+
}
|
|
43
|
+
return `Unauthorized. Please check your API key. The API key you provided (possibly incorrect) is: ${maskApiKey(apiKey)}. API keys should start with 'ctx7sk'`;
|
|
44
|
+
default:
|
|
45
|
+
return `Failed to fetch documentation. Please try again later. Error code: ${errorCode}`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Pick up proxy configuration in a variety of common env var names.
|
|
49
|
+
const PROXY_URL = process.env.HTTPS_PROXY ??
|
|
50
|
+
process.env.https_proxy ??
|
|
51
|
+
process.env.HTTP_PROXY ??
|
|
52
|
+
process.env.http_proxy ??
|
|
53
|
+
null;
|
|
54
|
+
if (PROXY_URL && !PROXY_URL.startsWith("$") && /^(http|https):\/\//i.test(PROXY_URL)) {
|
|
55
|
+
try {
|
|
56
|
+
// Configure a global proxy agent once at startup. Subsequent fetch calls will
|
|
57
|
+
// automatically use this dispatcher.
|
|
58
|
+
// Using `any` cast because ProxyAgent implements the Dispatcher interface but
|
|
59
|
+
// TS may not infer it correctly in some versions.
|
|
60
|
+
setGlobalDispatcher(new ProxyAgent(PROXY_URL));
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
// Don't crash the app if proxy initialisation fails – just log a warning.
|
|
64
|
+
console.error(`[Context7] Failed to configure proxy agent for provided proxy URL: ${PROXY_URL}:`, error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Searches for libraries matching the given query
|
|
69
|
+
* @param query The search query
|
|
70
|
+
* @param clientIp Optional client IP address to include in headers
|
|
71
|
+
* @param apiKey Optional API key for authentication
|
|
72
|
+
* @returns Search results or null if the request fails
|
|
73
|
+
*/
|
|
74
|
+
export async function searchLibraries(query, clientIp, apiKey) {
|
|
75
|
+
try {
|
|
76
|
+
const url = new URL(`${CONTEXT7_API_BASE_URL}/v2/search`);
|
|
77
|
+
url.searchParams.set("query", query);
|
|
78
|
+
const headers = generateHeaders(clientIp, apiKey);
|
|
79
|
+
const response = await fetch(url, { headers });
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
const errorCode = response.status;
|
|
82
|
+
const errorMessage = createErrorMessage(errorCode, apiKey);
|
|
83
|
+
console.error(errorMessage);
|
|
84
|
+
return {
|
|
85
|
+
results: [],
|
|
86
|
+
error: errorMessage,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const searchData = await response.json();
|
|
90
|
+
return searchData;
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
const errorMessage = `Error searching libraries: ${error}`;
|
|
94
|
+
console.error(errorMessage);
|
|
95
|
+
return { results: [], error: errorMessage };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Fetches documentation context for a specific library
|
|
100
|
+
* @param libraryId The library ID to fetch documentation for
|
|
101
|
+
* @param docMode Documentation mode (CODE for API references and code examples, INFO for conceptual guides)
|
|
102
|
+
* @param options Optional request parameters (page, limit, topic)
|
|
103
|
+
* @param clientIp Optional client IP address to include in headers
|
|
104
|
+
* @param apiKey Optional API key for authentication
|
|
105
|
+
* @returns The documentation text or null if the request fails
|
|
106
|
+
*/
|
|
107
|
+
export async function fetchLibraryDocumentation(libraryId, docMode, options = {}, clientIp, apiKey) {
|
|
108
|
+
try {
|
|
109
|
+
const { username, library, tag } = parseLibraryId(libraryId);
|
|
110
|
+
// Build URL path
|
|
111
|
+
let urlPath = `${CONTEXT7_API_BASE_URL}/v2/docs/${docMode}/${username}/${library}`;
|
|
112
|
+
if (tag) {
|
|
113
|
+
urlPath += `/${tag}`;
|
|
114
|
+
}
|
|
115
|
+
const url = new URL(urlPath);
|
|
116
|
+
url.searchParams.set("type", DEFAULT_TYPE);
|
|
117
|
+
if (options.topic)
|
|
118
|
+
url.searchParams.set("topic", options.topic);
|
|
119
|
+
if (options.page)
|
|
120
|
+
url.searchParams.set("page", options.page.toString());
|
|
121
|
+
if (options.limit)
|
|
122
|
+
url.searchParams.set("limit", options.limit.toString());
|
|
123
|
+
const headers = generateHeaders(clientIp, apiKey, { "X-Context7-Source": "mcp-server" });
|
|
124
|
+
const response = await fetch(url, { headers });
|
|
125
|
+
if (!response.ok) {
|
|
126
|
+
const errorCode = response.status;
|
|
127
|
+
const errorMessage = createErrorMessage(errorCode, apiKey);
|
|
128
|
+
console.error(errorMessage);
|
|
129
|
+
return errorMessage;
|
|
130
|
+
}
|
|
131
|
+
const text = await response.text();
|
|
132
|
+
if (!text || text === "No content available" || text === "No context data available") {
|
|
133
|
+
const suggestion = docMode === DOCUMENTATION_MODES.CODE
|
|
134
|
+
? " Try mode='info' for guides and tutorials."
|
|
135
|
+
: " Try mode='code' for API references and code examples.";
|
|
136
|
+
return `No ${docMode} documentation available for this library.${suggestion}`;
|
|
137
|
+
}
|
|
138
|
+
return text;
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
const errorMessage = `Error fetching library documentation. Please try again later. ${error}`;
|
|
142
|
+
console.error(errorMessage);
|
|
143
|
+
return errorMessage;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createCipheriv, randomBytes } from "crypto";
|
|
2
|
+
const DEFAULT_ENCRYPTION_KEY = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
|
|
3
|
+
const ENCRYPTION_KEY = process.env.CLIENT_IP_ENCRYPTION_KEY || DEFAULT_ENCRYPTION_KEY;
|
|
4
|
+
const ALGORITHM = "aes-256-cbc";
|
|
5
|
+
if (ENCRYPTION_KEY === DEFAULT_ENCRYPTION_KEY) {
|
|
6
|
+
console.warn("WARNING: Using default CLIENT_IP_ENCRYPTION_KEY.");
|
|
7
|
+
}
|
|
8
|
+
function validateEncryptionKey(key) {
|
|
9
|
+
// Must be exactly 64 hex characters (32 bytes)
|
|
10
|
+
return /^[0-9a-fA-F]{64}$/.test(key);
|
|
11
|
+
}
|
|
12
|
+
function encryptClientIp(clientIp) {
|
|
13
|
+
if (!validateEncryptionKey(ENCRYPTION_KEY)) {
|
|
14
|
+
console.error("Invalid encryption key format. Must be 64 hex characters.");
|
|
15
|
+
return clientIp; // Fallback to unencrypted
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const iv = randomBytes(16);
|
|
19
|
+
const cipher = createCipheriv(ALGORITHM, Buffer.from(ENCRYPTION_KEY, "hex"), iv);
|
|
20
|
+
let encrypted = cipher.update(clientIp, "utf8", "hex");
|
|
21
|
+
encrypted += cipher.final("hex");
|
|
22
|
+
return iv.toString("hex") + ":" + encrypted;
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
console.error("Error encrypting client IP:", error);
|
|
26
|
+
return clientIp; // Fallback to unencrypted
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function generateHeaders(clientIp, apiKey, extraHeaders = {}) {
|
|
30
|
+
const headers = { ...extraHeaders };
|
|
31
|
+
if (clientIp) {
|
|
32
|
+
headers["mcp-client-ip"] = encryptClientIp(clientIp);
|
|
33
|
+
}
|
|
34
|
+
if (apiKey) {
|
|
35
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
36
|
+
}
|
|
37
|
+
return headers;
|
|
38
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps numeric source reputation score to an interpretable label for LLM consumption.
|
|
3
|
+
*
|
|
4
|
+
* @returns One of: "High", "Medium", "Low", or "Unknown"
|
|
5
|
+
*/
|
|
6
|
+
function getSourceReputationLabel(sourceReputation) {
|
|
7
|
+
if (sourceReputation === undefined || sourceReputation < 0)
|
|
8
|
+
return "Unknown";
|
|
9
|
+
if (sourceReputation >= 7)
|
|
10
|
+
return "High";
|
|
11
|
+
if (sourceReputation >= 4)
|
|
12
|
+
return "Medium";
|
|
13
|
+
return "Low";
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Formats a search result into a human-readable string representation.
|
|
17
|
+
* Only shows code snippet count and GitHub stars when available (not equal to -1).
|
|
18
|
+
*
|
|
19
|
+
* @param result The SearchResult object to format
|
|
20
|
+
* @returns A formatted string with library information
|
|
21
|
+
*/
|
|
22
|
+
export function formatSearchResult(result) {
|
|
23
|
+
// Always include these basic details
|
|
24
|
+
const formattedResult = [
|
|
25
|
+
`- Title: ${result.title}`,
|
|
26
|
+
`- Context7-compatible library ID: ${result.id}`,
|
|
27
|
+
`- Description: ${result.description}`,
|
|
28
|
+
];
|
|
29
|
+
// Only add code snippets count if it's a valid value
|
|
30
|
+
if (result.totalSnippets !== -1 && result.totalSnippets !== undefined) {
|
|
31
|
+
formattedResult.push(`- Code Snippets: ${result.totalSnippets}`);
|
|
32
|
+
}
|
|
33
|
+
// Always add categorized source reputation
|
|
34
|
+
const reputationLabel = getSourceReputationLabel(result.trustScore);
|
|
35
|
+
formattedResult.push(`- Source Reputation: ${reputationLabel}`);
|
|
36
|
+
// Only add benchmark score if it's a valid value
|
|
37
|
+
if (result.benchmarkScore !== undefined && result.benchmarkScore > 0) {
|
|
38
|
+
formattedResult.push(`- Benchmark Score: ${result.benchmarkScore}`);
|
|
39
|
+
}
|
|
40
|
+
// Only add versions if it's a valid value
|
|
41
|
+
if (result.versions !== undefined && result.versions.length > 0) {
|
|
42
|
+
formattedResult.push(`- Versions: ${result.versions.join(", ")}`);
|
|
43
|
+
}
|
|
44
|
+
// Join all parts with newlines
|
|
45
|
+
return formattedResult.join("\n");
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Formats a search response into a human-readable string representation.
|
|
49
|
+
* Each result is formatted using formatSearchResult.
|
|
50
|
+
*
|
|
51
|
+
* @param searchResponse The SearchResponse object to format
|
|
52
|
+
* @returns A formatted string with search results
|
|
53
|
+
*/
|
|
54
|
+
export function formatSearchResults(searchResponse) {
|
|
55
|
+
if (!searchResponse.results || searchResponse.results.length === 0) {
|
|
56
|
+
return "No documentation libraries found matching your query.";
|
|
57
|
+
}
|
|
58
|
+
const formattedResults = searchResponse.results.map(formatSearchResult);
|
|
59
|
+
return formattedResults.join("\n----------\n");
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Masks an API key by showing only the first 10 characters and last 4 characters.
|
|
63
|
+
* This prevents full API keys from being exposed in logs while maintaining some
|
|
64
|
+
* identifiability for debugging.
|
|
65
|
+
*
|
|
66
|
+
* @param apiKey The API key to mask
|
|
67
|
+
* @returns Masked API key string (e.g., "ctx7sk-abc...xyz1") or "[NO-API-KEY]" if no key provided
|
|
68
|
+
*/
|
|
69
|
+
export function maskApiKey(apiKey) {
|
|
70
|
+
if (apiKey.length <= 14) {
|
|
71
|
+
// If the key is too short to mask meaningfully, just show first part
|
|
72
|
+
return apiKey.substring(0, 7) + "...";
|
|
73
|
+
}
|
|
74
|
+
const firstPart = apiKey.substring(0, 10);
|
|
75
|
+
const lastPart = apiKey.substring(apiKey.length - 4);
|
|
76
|
+
return `${firstPart}...${lastPart}`;
|
|
77
|
+
}
|
package/package.json
CHANGED
|
@@ -1,44 +1,54 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@upstash/context7-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1-canary-20251124144836",
|
|
4
|
+
"mcpName": "io.github.upstash/context7",
|
|
4
5
|
"description": "MCP server for Context7",
|
|
5
|
-
"scripts": {
|
|
6
|
-
"test": "echo \"Error: no test specified\" && exit 1",
|
|
7
|
-
"build": "tsc && chmod 755 build/index.js",
|
|
8
|
-
"format": "prettier --write .",
|
|
9
|
-
"lint": "eslint \"**/*.{js,ts,tsx}\" --fix"
|
|
10
|
-
},
|
|
11
6
|
"repository": {
|
|
12
7
|
"type": "git",
|
|
13
|
-
"url": "git+https://github.com/upstash/context7
|
|
8
|
+
"url": "git+https://github.com/upstash/context7.git",
|
|
9
|
+
"directory": "packages/mcp"
|
|
14
10
|
},
|
|
15
|
-
"keywords": [
|
|
16
|
-
|
|
11
|
+
"keywords": [
|
|
12
|
+
"modelcontextprotocol",
|
|
13
|
+
"mcp",
|
|
14
|
+
"context7",
|
|
15
|
+
"vibe-coding",
|
|
16
|
+
"developer tools",
|
|
17
|
+
"documentation",
|
|
18
|
+
"context"
|
|
19
|
+
],
|
|
20
|
+
"author": "abdush",
|
|
17
21
|
"license": "MIT",
|
|
18
22
|
"type": "module",
|
|
19
23
|
"bin": {
|
|
20
|
-
"context7-mcp": "
|
|
24
|
+
"context7-mcp": "dist/index.js"
|
|
21
25
|
},
|
|
22
26
|
"files": [
|
|
23
|
-
"
|
|
27
|
+
"dist",
|
|
28
|
+
"LICENSE",
|
|
29
|
+
"README.md"
|
|
24
30
|
],
|
|
25
31
|
"bugs": {
|
|
26
|
-
"url": "https://github.com/upstash/context7
|
|
32
|
+
"url": "https://github.com/upstash/context7/issues"
|
|
27
33
|
},
|
|
28
|
-
"homepage": "https://github.com/upstash/context7
|
|
34
|
+
"homepage": "https://github.com/upstash/context7#readme",
|
|
29
35
|
"dependencies": {
|
|
30
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
36
|
+
"@modelcontextprotocol/sdk": "^1.17.5",
|
|
37
|
+
"@types/express": "^5.0.4",
|
|
38
|
+
"commander": "^14.0.0",
|
|
39
|
+
"express": "^5.1.0",
|
|
40
|
+
"undici": "^6.6.3",
|
|
31
41
|
"zod": "^3.24.2"
|
|
32
42
|
},
|
|
33
43
|
"devDependencies": {
|
|
34
44
|
"@types/node": "^22.13.14",
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
45
|
+
"typescript": "^5.8.2"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsc && chmod 755 dist/index.js",
|
|
49
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
50
|
+
"dev": "tsc --watch",
|
|
51
|
+
"start": "node dist/index.js --transport http",
|
|
52
|
+
"pack-mcpb": "pnpm install && pnpm run build && rm -rf node_modules && pnpm install --prod && mv mcpb/.mcpbignore .mcpbignore && mv mcpb/manifest.json manifest.json && mv public/icon.png icon.png && mcpb validate manifest.json && mcpb pack . mcpb/context7.mcpb && mv manifest.json mcpb/manifest.json && mv .mcpbignore mcpb/.mcpbignore && mv icon.png public/icon.png && bun install"
|
|
43
53
|
}
|
|
44
|
-
}
|
|
54
|
+
}
|