@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/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
+ });
@@ -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,7 @@
1
+ /**
2
+ * Documentation modes for fetching library documentation
3
+ */
4
+ export const DOCUMENTATION_MODES = {
5
+ CODE: "code",
6
+ INFO: "info",
7
+ };
@@ -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.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-mcp.git"
8
+ "url": "git+https://github.com/upstash/context7.git",
9
+ "directory": "packages/mcp"
14
10
  },
15
- "keywords": [],
16
- "author": "",
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": "build/index.js"
24
+ "context7-mcp": "dist/index.js"
21
25
  },
22
26
  "files": [
23
- "build"
27
+ "dist",
28
+ "LICENSE",
29
+ "README.md"
24
30
  ],
25
31
  "bugs": {
26
- "url": "https://github.com/upstash/context7-mcp/issues"
32
+ "url": "https://github.com/upstash/context7/issues"
27
33
  },
28
- "homepage": "https://github.com/upstash/context7-mcp#readme",
34
+ "homepage": "https://github.com/upstash/context7#readme",
29
35
  "dependencies": {
30
- "@modelcontextprotocol/sdk": "^1.8.0",
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
- "@typescript-eslint/eslint-plugin": "^8.28.0",
36
- "@typescript-eslint/parser": "^8.28.0",
37
- "eslint": "^9.23.0",
38
- "eslint-config-prettier": "^10.1.1",
39
- "eslint-plugin-prettier": "^5.2.5",
40
- "prettier": "^3.5.3",
41
- "typescript": "^5.8.2",
42
- "typescript-eslint": "^8.28.0"
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
+ }