@upstash/context7-mcp 1.0.33 → 1.0.34-canary.0
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/benchmark/benchmark.js +347 -0
- package/dist/benchmark/compare-benchmark.js +289 -0
- package/dist/benchmark/run-benchmark.js +459 -0
- package/dist/benchmark/simulate.js +319 -0
- package/dist/index.js +33 -131
- package/dist/lib/api.js +19 -80
- package/dist/lib/types.js +1 -7
- package/package.json +10 -2
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
import { experimental_createMCPClient as createMCPClient } from "@ai-sdk/mcp";
|
|
3
|
+
import { generateText, stepCountIs } from "ai";
|
|
4
|
+
import { anthropic } from "@ai-sdk/anthropic";
|
|
5
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
6
|
+
import { writeFileSync, mkdirSync } from "fs";
|
|
7
|
+
import { join, dirname } from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
// Check for required environment variables
|
|
10
|
+
if (!process.env.CONTEXT7_API_KEY) {
|
|
11
|
+
console.error("Error: CONTEXT7_API_KEY environment variable is required");
|
|
12
|
+
console.error("Set it in your .env file or export it in your shell");
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = dirname(__filename);
|
|
17
|
+
// Package root is two levels up from dist/benchmark/
|
|
18
|
+
const packageRoot = join(__dirname, "..", "..");
|
|
19
|
+
/**
|
|
20
|
+
* Creates a logging function bound to a specific simulation state
|
|
21
|
+
*/
|
|
22
|
+
function createLogger(state) {
|
|
23
|
+
return (message) => {
|
|
24
|
+
console.log(message);
|
|
25
|
+
state.consoleLog += message + "\n";
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Simulates an LLM-powered conversation using Context7 MCP server
|
|
30
|
+
*
|
|
31
|
+
* Usage:
|
|
32
|
+
* - CLI: pnpm run simulate "your question here"
|
|
33
|
+
* - Programmatic: import { simulate } from './simulate.js'; await simulate("your question");
|
|
34
|
+
*/
|
|
35
|
+
export async function simulate(question, uniqueId) {
|
|
36
|
+
// Create local state for this simulation (prevents race conditions)
|
|
37
|
+
const state = {
|
|
38
|
+
consoleLog: "",
|
|
39
|
+
toolCallsMap: new Map(),
|
|
40
|
+
totalTokens: 0,
|
|
41
|
+
};
|
|
42
|
+
// Create logger bound to this simulation's state
|
|
43
|
+
const log = createLogger(state);
|
|
44
|
+
// If no question provided, try to get from command line args
|
|
45
|
+
if (!question) {
|
|
46
|
+
question = process.argv[2];
|
|
47
|
+
}
|
|
48
|
+
if (!question) {
|
|
49
|
+
console.error("Error: Please provide a question as an argument");
|
|
50
|
+
console.error('Usage: pnpm run simulate "your question here"');
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
// Store original question for display
|
|
54
|
+
const originalQuestion = question;
|
|
55
|
+
// Append Context7 instruction to the question
|
|
56
|
+
question = `${question}. Use Context7 to fetch docs.`;
|
|
57
|
+
const startTime = new Date();
|
|
58
|
+
log("=".repeat(80));
|
|
59
|
+
log("Context7 MCP Simulation");
|
|
60
|
+
log("=".repeat(80));
|
|
61
|
+
log(`Question: ${originalQuestion}`);
|
|
62
|
+
log(`Timestamp: ${startTime.toISOString()}`);
|
|
63
|
+
log("");
|
|
64
|
+
let mcpClient = null;
|
|
65
|
+
try {
|
|
66
|
+
// Get API key from environment
|
|
67
|
+
const apiKey = process.env.CONTEXT7_API_KEY;
|
|
68
|
+
if (!apiKey) {
|
|
69
|
+
throw new Error("CONTEXT7_API_KEY environment variable is required");
|
|
70
|
+
}
|
|
71
|
+
// Initialize MCP client using AI SDK's experimental_createMCPClient
|
|
72
|
+
log("─".repeat(80));
|
|
73
|
+
log("Initializing MCP Connection");
|
|
74
|
+
log("─".repeat(80));
|
|
75
|
+
const mcpServerPath = join(packageRoot, "dist", "index.js");
|
|
76
|
+
mcpClient = await createMCPClient({
|
|
77
|
+
transport: new StdioClientTransport({
|
|
78
|
+
command: "node",
|
|
79
|
+
args: [mcpServerPath, "--api-key", apiKey],
|
|
80
|
+
env: {
|
|
81
|
+
...process.env,
|
|
82
|
+
CONTEXT7_API_KEY: apiKey,
|
|
83
|
+
},
|
|
84
|
+
}),
|
|
85
|
+
});
|
|
86
|
+
log("✅ Connected to Context7 MCP server");
|
|
87
|
+
log("");
|
|
88
|
+
// Get AI SDK-compatible tools
|
|
89
|
+
const tools = await mcpClient.tools();
|
|
90
|
+
const toolNames = Object.keys(tools);
|
|
91
|
+
log(`📦 Available tools: ${toolNames.join(", ")}`);
|
|
92
|
+
log("");
|
|
93
|
+
// Use AI model to interact with tools
|
|
94
|
+
log("─".repeat(80));
|
|
95
|
+
log("🤖 AI Processing");
|
|
96
|
+
log("─".repeat(80));
|
|
97
|
+
log("Model: Claude Haiku 4.5");
|
|
98
|
+
log("");
|
|
99
|
+
const model = anthropic("claude-haiku-4-5");
|
|
100
|
+
const result = await generateText({
|
|
101
|
+
model,
|
|
102
|
+
tools,
|
|
103
|
+
stopWhen: stepCountIs(15),
|
|
104
|
+
messages: [
|
|
105
|
+
{
|
|
106
|
+
role: "user",
|
|
107
|
+
content: question,
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
onStepFinish: (step) => {
|
|
111
|
+
if (step.toolCalls) {
|
|
112
|
+
step.toolCalls.forEach((tc) => {
|
|
113
|
+
const input = tc.input || tc.args || {};
|
|
114
|
+
log("─".repeat(80));
|
|
115
|
+
log(`🔧 Tool Call: ${tc.toolName}`);
|
|
116
|
+
log("─".repeat(80));
|
|
117
|
+
log(`Arguments:`);
|
|
118
|
+
log("```json");
|
|
119
|
+
log(JSON.stringify(input, null, 2));
|
|
120
|
+
log("```");
|
|
121
|
+
log("");
|
|
122
|
+
// Store for structured report using toolCallId as key
|
|
123
|
+
state.toolCallsMap.set(tc.toolCallId, {
|
|
124
|
+
tool: tc.toolName,
|
|
125
|
+
args: input,
|
|
126
|
+
response: null,
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
if (step.toolResults) {
|
|
131
|
+
step.toolResults.forEach((tr) => {
|
|
132
|
+
const resultText = JSON.stringify(tr, null, 2);
|
|
133
|
+
log(`Result:`);
|
|
134
|
+
log("```json");
|
|
135
|
+
if (resultText.length > 2000) {
|
|
136
|
+
log(resultText.substring(0, 2000));
|
|
137
|
+
log(`... (truncated ${resultText.length - 2000} characters)`);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
log(resultText);
|
|
141
|
+
}
|
|
142
|
+
log("```");
|
|
143
|
+
log("");
|
|
144
|
+
// Store response for structured report by matching toolCallId
|
|
145
|
+
const toolCall = state.toolCallsMap.get(tr.toolCallId);
|
|
146
|
+
if (toolCall) {
|
|
147
|
+
const output = tr.output || tr;
|
|
148
|
+
toolCall.response = output;
|
|
149
|
+
// Extract usage data from MCP response structure
|
|
150
|
+
try {
|
|
151
|
+
// Check if output has MCP format: {content: [{type: "text", text: "..."}]}
|
|
152
|
+
if (output && output.content && Array.isArray(output.content)) {
|
|
153
|
+
output.content.forEach((item) => {
|
|
154
|
+
if (item.type === "text" && item.text) {
|
|
155
|
+
try {
|
|
156
|
+
const parsed = JSON.parse(item.text);
|
|
157
|
+
// Check for new API format: {data: "...", usage: {...}}
|
|
158
|
+
if (parsed.usage && parsed.usage.totalTokens) {
|
|
159
|
+
toolCall.usage = parsed.usage;
|
|
160
|
+
state.totalTokens += parsed.usage.totalTokens;
|
|
161
|
+
log(`Token usage: ${parsed.usage.totalTokens} total (input: ${parsed.usage.inputTokens}, output: ${parsed.usage.outputTokens})`);
|
|
162
|
+
log("");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// Not JSON or doesn't have usage data
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// If parsing fails, it's not JSON or doesn't have usage data, which is fine
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
log("─".repeat(80));
|
|
181
|
+
log("✅ AI Final Response");
|
|
182
|
+
log("─".repeat(80));
|
|
183
|
+
log(result.text);
|
|
184
|
+
log("");
|
|
185
|
+
const endTime = new Date();
|
|
186
|
+
const duration = endTime.getTime() - startTime.getTime();
|
|
187
|
+
// Summary
|
|
188
|
+
log("=".repeat(80));
|
|
189
|
+
log("Summary");
|
|
190
|
+
log("=".repeat(80));
|
|
191
|
+
log(`✅ Simulation completed in ${(duration / 1000).toFixed(2)}s`);
|
|
192
|
+
log(`⏱️ Start: ${startTime.toISOString()}`);
|
|
193
|
+
log(`⏱️ End: ${endTime.toISOString()}`);
|
|
194
|
+
if (state.totalTokens > 0) {
|
|
195
|
+
log(`🪙 Total Tokens: ${state.totalTokens}`);
|
|
196
|
+
}
|
|
197
|
+
log("=".repeat(80));
|
|
198
|
+
// Create reports directory under benchmark folder
|
|
199
|
+
const reportsDir = join(packageRoot, "src", "benchmark", "reports");
|
|
200
|
+
mkdirSync(reportsDir, { recursive: true });
|
|
201
|
+
// Generate filenames with timestamp and optional unique identifier
|
|
202
|
+
const baseFilename = uniqueId
|
|
203
|
+
? `${startTime.toISOString().replace(/[:.]/g, "-").split("Z")[0]}_${uniqueId}`
|
|
204
|
+
: startTime.toISOString().replace(/[:.]/g, "-").split("Z")[0];
|
|
205
|
+
const reportPath = join(reportsDir, `${baseFilename}.md`);
|
|
206
|
+
const rawReportPath = join(reportsDir, `${baseFilename}_raw.md`);
|
|
207
|
+
// Convert Map to array for report generation (preserves insertion order)
|
|
208
|
+
const structuredToolCalls = Array.from(state.toolCallsMap.values());
|
|
209
|
+
// Generate structured markdown report
|
|
210
|
+
let structuredReport = `# Context7 MCP Simulation Report\n\n`;
|
|
211
|
+
structuredReport += `**Date**: ${startTime.toISOString()}\n`;
|
|
212
|
+
structuredReport += `**Duration**: ${(duration / 1000).toFixed(2)}s\n`;
|
|
213
|
+
if (state.totalTokens > 0) {
|
|
214
|
+
structuredReport += `**Total Tokens**: ${state.totalTokens}\n`;
|
|
215
|
+
}
|
|
216
|
+
structuredReport += `\n## Question\n\n${originalQuestion}\n\n`;
|
|
217
|
+
// Add tool calls and responses
|
|
218
|
+
if (structuredToolCalls.length > 0) {
|
|
219
|
+
structuredReport += `## Tool Calls\n\n`;
|
|
220
|
+
structuredToolCalls.forEach((call, idx) => {
|
|
221
|
+
structuredReport += `### Tool Call ${idx + 1}: ${call.tool}\n\n`;
|
|
222
|
+
structuredReport += `**Parameters:**\n\`\`\`json\n${JSON.stringify(call.args, null, 2)}\n\`\`\`\n\n`;
|
|
223
|
+
if (call.usage) {
|
|
224
|
+
structuredReport += `**Token Usage:**\n`;
|
|
225
|
+
structuredReport += `- Input: ${call.usage.inputTokens}\n`;
|
|
226
|
+
structuredReport += `- Output: ${call.usage.outputTokens}\n`;
|
|
227
|
+
structuredReport += `- Total: ${call.usage.totalTokens}\n\n`;
|
|
228
|
+
}
|
|
229
|
+
structuredReport += `**Response:**\n\`\`\`json\n`;
|
|
230
|
+
const responseText = JSON.stringify(call.response, null, 2);
|
|
231
|
+
if (responseText.length > 3000) {
|
|
232
|
+
structuredReport +=
|
|
233
|
+
responseText.substring(0, 3000) +
|
|
234
|
+
`...\n(truncated ${responseText.length - 3000} characters)\n`;
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
structuredReport += responseText + "\n";
|
|
238
|
+
}
|
|
239
|
+
structuredReport += `\`\`\`\n\n`;
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
// Add final AI response
|
|
243
|
+
structuredReport += `## AI Final Response\n\n${result.text}\n\n`;
|
|
244
|
+
// Add console log section
|
|
245
|
+
structuredReport += `---\n\n## Console Log\n\n\`\`\`\n${state.consoleLog}\n\`\`\`\n`;
|
|
246
|
+
// Save structured markdown report
|
|
247
|
+
writeFileSync(reportPath, structuredReport);
|
|
248
|
+
// Generate raw markdown report (just question and raw text responses)
|
|
249
|
+
let rawReport = `QUESTION: ${originalQuestion}\n\n`;
|
|
250
|
+
rawReport += `${"─".repeat(80)}\n\n`;
|
|
251
|
+
rawReport += `CONTEXT:\n\n`;
|
|
252
|
+
// Add raw tool responses (extract text content only, skip resolve-library-id)
|
|
253
|
+
structuredToolCalls.forEach((call) => {
|
|
254
|
+
// Skip resolve-library-id tool responses
|
|
255
|
+
if (call.tool === "resolve-library-id") {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (call.response && call.response.content) {
|
|
259
|
+
const content = call.response.content;
|
|
260
|
+
// Extract text from MCP response format: {content: [{type: "text", text: "..."}]}
|
|
261
|
+
if (Array.isArray(content)) {
|
|
262
|
+
content.forEach((item) => {
|
|
263
|
+
if (item.type === "text" && item.text) {
|
|
264
|
+
// Try to parse the text as JSON (new API format with {data, usage})
|
|
265
|
+
try {
|
|
266
|
+
const parsed = JSON.parse(item.text);
|
|
267
|
+
// New API format: {data: "text", usage: {...}}
|
|
268
|
+
if (parsed.data && typeof parsed.data === "string") {
|
|
269
|
+
rawReport += parsed.data + "\n\n";
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
// Not JSON, treat as plain text (legacy format)
|
|
275
|
+
rawReport += item.text + "\n\n";
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
else if (typeof content === "string") {
|
|
281
|
+
// Try to parse string content as JSON
|
|
282
|
+
try {
|
|
283
|
+
const parsed = JSON.parse(content);
|
|
284
|
+
if (parsed.data && typeof parsed.data === "string") {
|
|
285
|
+
rawReport += parsed.data + "\n\n";
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
// Not JSON, treat as plain text
|
|
291
|
+
rawReport += content + "\n\n";
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
// Save raw markdown report
|
|
297
|
+
writeFileSync(rawReportPath, rawReport);
|
|
298
|
+
log("");
|
|
299
|
+
log(`📝 Structured report saved to: ${reportPath}`);
|
|
300
|
+
log(`📝 Raw report saved to: ${rawReportPath}`);
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
const errorMsg = `❌ Simulation failed: ${error}`;
|
|
304
|
+
log(errorMsg);
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
finally {
|
|
308
|
+
if (mcpClient) {
|
|
309
|
+
await mcpClient.close();
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// Run simulation if this file is executed directly
|
|
314
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
315
|
+
simulate().catch((error) => {
|
|
316
|
+
console.error("Fatal error:", error);
|
|
317
|
+
process.exit(1);
|
|
318
|
+
});
|
|
319
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -2,15 +2,11 @@
|
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import {
|
|
6
|
-
import { formatSearchResults } from "./lib/utils.js";
|
|
7
|
-
import { DOCUMENTATION_MODES } from "./lib/types.js";
|
|
5
|
+
import { fetchLibraryContext } from "./lib/api.js";
|
|
8
6
|
import express from "express";
|
|
9
7
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
10
8
|
import { Command } from "commander";
|
|
11
9
|
import { AsyncLocalStorage } from "async_hooks";
|
|
12
|
-
/** Default number of results to return per page */
|
|
13
|
-
const DEFAULT_RESULTS_LIMIT = 10;
|
|
14
10
|
/** Default HTTP server port */
|
|
15
11
|
const DEFAULT_PORT = 3000;
|
|
16
12
|
// Parse CLI arguments using commander
|
|
@@ -48,148 +44,62 @@ const CLI_PORT = (() => {
|
|
|
48
44
|
const requestContext = new AsyncLocalStorage();
|
|
49
45
|
// Store API key globally for stdio mode (where requestContext may not be available in tool handlers)
|
|
50
46
|
let globalApiKey;
|
|
47
|
+
const stripIpv6Prefix = (ip) => ip.replace(/^::ffff:/, "");
|
|
48
|
+
const isPrivateIp = (ip) => ip.startsWith("10.") || ip.startsWith("192.168.") || /^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(ip);
|
|
51
49
|
function getClientIp(req) {
|
|
52
|
-
const forwardedFor = req.headers["x-forwarded-for"]
|
|
50
|
+
const forwardedFor = req.headers["x-forwarded-for"];
|
|
53
51
|
if (forwardedFor) {
|
|
54
52
|
const ips = Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor;
|
|
55
|
-
const ipList = ips.split(",").map((ip) => ip.trim());
|
|
56
|
-
|
|
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:/, "");
|
|
53
|
+
const ipList = ips.split(",").map((ip) => stripIpv6Prefix(ip.trim()));
|
|
54
|
+
return ipList.find((ip) => !isPrivateIp(ip)) ?? ipList[0];
|
|
68
55
|
}
|
|
69
|
-
return undefined;
|
|
56
|
+
return req.socket?.remoteAddress ? stripIpv6Prefix(req.socket.remoteAddress) : undefined;
|
|
70
57
|
}
|
|
71
58
|
const server = new McpServer({
|
|
72
59
|
name: "Context7",
|
|
73
|
-
version: "
|
|
60
|
+
version: "2.0.0",
|
|
74
61
|
}, {
|
|
62
|
+
capabilities: {
|
|
63
|
+
tools: { listChanged: true },
|
|
64
|
+
},
|
|
75
65
|
instructions: "Use this server to retrieve up-to-date documentation and code examples for any library.",
|
|
76
66
|
});
|
|
77
|
-
server.registerTool("
|
|
78
|
-
title: "
|
|
79
|
-
description: `
|
|
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.
|
|
67
|
+
server.registerTool("get-docs", {
|
|
68
|
+
title: "Get Library Documentation",
|
|
69
|
+
description: `Retrieves up-to-date documentation and code examples from Context7 for any programming library or framework.
|
|
82
70
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
-
|
|
87
|
-
-
|
|
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.`,
|
|
71
|
+
USE THIS TOOL TO:
|
|
72
|
+
- Get current, accurate documentation for libraries (e.g., React, Next.js, Express, LangChain)
|
|
73
|
+
- Find working code examples and implementation patterns
|
|
74
|
+
- Answer "how do I..." questions about specific libraries
|
|
75
|
+
- Look up API references, configuration options, and best practices`,
|
|
99
76
|
inputSchema: {
|
|
100
|
-
|
|
77
|
+
query: z
|
|
101
78
|
.string()
|
|
102
|
-
.describe("
|
|
103
|
-
|
|
104
|
-
}, async ({ libraryName }) => {
|
|
105
|
-
const ctx = requestContext.getStore();
|
|
106
|
-
const apiKey = ctx?.apiKey || globalApiKey;
|
|
107
|
-
const searchResponse = await searchLibraries(libraryName, ctx?.clientIp, apiKey);
|
|
108
|
-
if (!searchResponse.results || searchResponse.results.length === 0) {
|
|
109
|
-
return {
|
|
110
|
-
content: [
|
|
111
|
-
{
|
|
112
|
-
type: "text",
|
|
113
|
-
text: searchResponse.error
|
|
114
|
-
? searchResponse.error
|
|
115
|
-
: "Failed to retrieve library documentation data from Context7",
|
|
116
|
-
},
|
|
117
|
-
],
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
const resultsText = formatSearchResults(searchResponse);
|
|
121
|
-
const responseText = `Available Libraries:
|
|
122
|
-
|
|
123
|
-
Each result includes:
|
|
124
|
-
- Library ID: Context7-compatible identifier (format: /org/project)
|
|
125
|
-
- Name: Library or package name
|
|
126
|
-
- Description: Short summary
|
|
127
|
-
- Code Snippets: Number of available code examples
|
|
128
|
-
- Source Reputation: Authority indicator (High, Medium, Low, or Unknown)
|
|
129
|
-
- Benchmark Score: Quality indicator (100 is the highest score)
|
|
130
|
-
- 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.
|
|
131
|
-
|
|
132
|
-
For best results, select libraries based on name match, source reputation, snippet coverage, benchmark score, and relevance to your use case.
|
|
133
|
-
|
|
134
|
-
----------
|
|
135
|
-
|
|
136
|
-
${resultsText}`;
|
|
137
|
-
return {
|
|
138
|
-
content: [
|
|
139
|
-
{
|
|
140
|
-
type: "text",
|
|
141
|
-
text: responseText,
|
|
142
|
-
},
|
|
143
|
-
],
|
|
144
|
-
};
|
|
145
|
-
});
|
|
146
|
-
server.registerTool("get-library-docs", {
|
|
147
|
-
title: "Get Library Docs",
|
|
148
|
-
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.",
|
|
149
|
-
inputSchema: {
|
|
150
|
-
context7CompatibleLibraryID: z
|
|
79
|
+
.describe("The question or task you need help with. Be specific and include relevant details. Good: 'How to set up authentication with JWT in Express.js' or 'React useEffect cleanup function examples'. Bad: 'auth' or 'hooks'. IMPORTANT: Do not include any sensitive or confidential information such as API keys, passwords, credentials, or personal data in your query."),
|
|
80
|
+
library: z
|
|
151
81
|
.string()
|
|
152
|
-
.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'."),
|
|
153
|
-
mode: z
|
|
154
|
-
.enum(["code", "info"])
|
|
155
82
|
.optional()
|
|
156
|
-
.
|
|
157
|
-
.describe("Documentation mode: 'code' for API references and code examples (default), 'info' for conceptual guides, narrative information, and architectural questions."),
|
|
83
|
+
.describe("Library or framework name (e.g., 'react', 'express') OR exact library ID if provided by the user with or without version (e.g., '/vercel/next.js', '/vercel/next.js@v14.3.0-canary.87'). If omitted, auto-selects based on query."),
|
|
158
84
|
topic: z
|
|
159
85
|
.string()
|
|
160
86
|
.optional()
|
|
161
|
-
.describe("
|
|
162
|
-
|
|
163
|
-
.
|
|
164
|
-
.int()
|
|
165
|
-
.min(1)
|
|
166
|
-
.max(10)
|
|
87
|
+
.describe("Narrow down results to a specific topic within the library. Examples: 'hooks', 'routing', 'middleware', 'authentication', 'state management'."),
|
|
88
|
+
mode: z
|
|
89
|
+
.enum(["code", "info"])
|
|
167
90
|
.optional()
|
|
168
|
-
.
|
|
91
|
+
.default("code")
|
|
92
|
+
.describe("Type of content to prioritize. Use 'code' (default) when you need working code examples, API usage patterns, and implementation snippets. Use 'info' when you need conceptual narrative explanations, architectural overviews, or understanding how something works."),
|
|
169
93
|
},
|
|
170
|
-
}, async ({
|
|
94
|
+
}, async ({ query, library, topic, mode = "code" }) => {
|
|
171
95
|
const ctx = requestContext.getStore();
|
|
172
96
|
const apiKey = ctx?.apiKey || globalApiKey;
|
|
173
|
-
const
|
|
174
|
-
page,
|
|
175
|
-
limit: DEFAULT_RESULTS_LIMIT,
|
|
176
|
-
topic,
|
|
177
|
-
}, ctx?.clientIp, apiKey);
|
|
178
|
-
if (!fetchDocsResponse) {
|
|
179
|
-
return {
|
|
180
|
-
content: [
|
|
181
|
-
{
|
|
182
|
-
type: "text",
|
|
183
|
-
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.",
|
|
184
|
-
},
|
|
185
|
-
],
|
|
186
|
-
};
|
|
187
|
-
}
|
|
97
|
+
const response = await fetchLibraryContext({ query, library, topic, mode }, ctx?.clientIp, apiKey);
|
|
188
98
|
return {
|
|
189
99
|
content: [
|
|
190
100
|
{
|
|
191
101
|
type: "text",
|
|
192
|
-
text:
|
|
102
|
+
text: response.data,
|
|
193
103
|
},
|
|
194
104
|
],
|
|
195
105
|
};
|
|
@@ -198,7 +108,6 @@ async function main() {
|
|
|
198
108
|
const transportType = TRANSPORT_TYPE;
|
|
199
109
|
if (transportType === "http") {
|
|
200
110
|
const initialPort = CLI_PORT ?? DEFAULT_PORT;
|
|
201
|
-
let actualPort = initialPort;
|
|
202
111
|
const app = express();
|
|
203
112
|
app.use(express.json());
|
|
204
113
|
app.use((req, res, next) => {
|
|
@@ -228,14 +137,8 @@ async function main() {
|
|
|
228
137
|
};
|
|
229
138
|
const extractApiKey = (req) => {
|
|
230
139
|
return (extractBearerToken(req.headers.authorization) ||
|
|
231
|
-
extractHeaderValue(req.headers["Context7-API-Key"]) ||
|
|
232
|
-
extractHeaderValue(req.headers["X-API-Key"]) ||
|
|
233
140
|
extractHeaderValue(req.headers["context7-api-key"]) ||
|
|
234
|
-
extractHeaderValue(req.headers["x-api-key"])
|
|
235
|
-
extractHeaderValue(req.headers["Context7_API_Key"]) ||
|
|
236
|
-
extractHeaderValue(req.headers["X_API_Key"]) ||
|
|
237
|
-
extractHeaderValue(req.headers["context7_api_key"]) ||
|
|
238
|
-
extractHeaderValue(req.headers["x_api_key"]));
|
|
141
|
+
extractHeaderValue(req.headers["x-api-key"]));
|
|
239
142
|
};
|
|
240
143
|
app.all("/mcp", async (req, res) => {
|
|
241
144
|
try {
|
|
@@ -287,8 +190,7 @@ async function main() {
|
|
|
287
190
|
}
|
|
288
191
|
});
|
|
289
192
|
httpServer.once("listening", () => {
|
|
290
|
-
|
|
291
|
-
console.error(`Context7 Documentation MCP Server running on HTTP at http://localhost:${actualPort}/mcp`);
|
|
193
|
+
console.error(`Context7 Documentation MCP Server running on HTTP at http://localhost:${port}/mcp`);
|
|
292
194
|
});
|
|
293
195
|
};
|
|
294
196
|
startServer(initialPort);
|