index.ts•12.9 kB
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { searchLibraries, fetchLibraryDocumentation } from "./lib/api.js";
import { formatSearchResults } from "./lib/utils.js";
import { SearchResponse } from "./lib/types.js";
import express from "express";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { Command } from "commander";
import { AsyncLocalStorage } from "async_hooks";
/** Minimum allowed tokens for documentation retrieval */
const MINIMUM_TOKENS = 1000;
/** Default tokens when none specified */
const DEFAULT_TOKENS = 5000;
/** Default HTTP server port */
const DEFAULT_PORT = 3000;
// Parse CLI arguments using commander
const program = new Command()
.option("--transport <stdio|http>", "transport type", "stdio")
.option("--port <number>", "port for HTTP transport", DEFAULT_PORT.toString())
.option("--api-key <key>", "API key for authentication (or set CONTEXT7_API_KEY env var)")
.allowUnknownOption() // let MCP Inspector / other wrappers pass through extra flags
.parse(process.argv);
const cliOptions = program.opts<{
transport: string;
port: string;
apiKey?: string;
}>();
// Validate transport option
const allowedTransports = ["stdio", "http"];
if (!allowedTransports.includes(cliOptions.transport)) {
console.error(
`Invalid --transport value: '${cliOptions.transport}'. Must be one of: stdio, http.`
);
process.exit(1);
}
// Transport configuration
const TRANSPORT_TYPE = (cliOptions.transport || "stdio") as "stdio" | "http";
// Disallow incompatible flags based on transport
const passedPortFlag = process.argv.includes("--port");
const passedApiKeyFlag = process.argv.includes("--api-key");
if (TRANSPORT_TYPE === "http" && passedApiKeyFlag) {
console.error(
"The --api-key flag is not allowed when using --transport http. Use header-based auth at the HTTP layer instead."
);
process.exit(1);
}
if (TRANSPORT_TYPE === "stdio" && passedPortFlag) {
console.error("The --port flag is not allowed when using --transport stdio.");
process.exit(1);
}
// HTTP port configuration
const CLI_PORT = (() => {
const parsed = parseInt(cliOptions.port, 10);
return isNaN(parsed) ? undefined : parsed;
})();
const requestContext = new AsyncLocalStorage<{
clientIp?: string;
apiKey?: string;
}>();
function getClientIp(req: express.Request): string | undefined {
const forwardedFor = req.headers["x-forwarded-for"] || req.headers["X-Forwarded-For"];
if (forwardedFor) {
const ips = Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor;
const ipList = ips.split(",").map((ip) => ip.trim());
for (const ip of ipList) {
const plainIp = ip.replace(/^::ffff:/, "");
if (
!plainIp.startsWith("10.") &&
!plainIp.startsWith("192.168.") &&
!/^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(plainIp)
) {
return plainIp;
}
}
return ipList[0].replace(/^::ffff:/, "");
}
if (req.socket?.remoteAddress) {
return req.socket.remoteAddress.replace(/^::ffff:/, "");
}
return undefined;
}
const server = new McpServer(
{
name: "Context7",
version: "1.0.13",
},
{
instructions:
"Use this server to retrieve up-to-date documentation and code examples for any library.",
}
);
server.registerTool(
"resolve-library-id",
{
title: "Resolve Context7 Library ID",
description: `Resolves a package/product name to a Context7-compatible library ID and returns a list of matching libraries.
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.
Selection Process:
1. Analyze the query to understand what library/package the user is looking for
2. Return the most relevant match based on:
- Name similarity to the query (exact matches prioritized)
- Description relevance to the query's intent
- Documentation coverage (prioritize libraries with higher Code Snippet counts)
- Trust score (consider libraries with scores of 7-10 more authoritative)
Response Format:
- Return the selected library ID in a clearly marked section
- Provide a brief explanation for why this library was chosen
- If multiple good matches exist, acknowledge this but proceed with the most relevant one
- If no good matches exist, clearly state this and suggest query refinements
For ambiguous queries, request clarification before proceeding with a best-guess match.`,
inputSchema: {
libraryName: z
.string()
.describe("Library name to search for and retrieve a Context7-compatible library ID."),
},
},
async ({ libraryName }) => {
const ctx = requestContext.getStore();
const searchResponse: SearchResponse = await searchLibraries(
libraryName,
ctx?.clientIp,
ctx?.apiKey
);
if (!searchResponse.results || searchResponse.results.length === 0) {
return {
content: [
{
type: "text",
text: searchResponse.error
? searchResponse.error
: "Failed to retrieve library documentation data from Context7",
},
],
};
}
const resultsText = formatSearchResults(searchResponse);
const responseText = `Available Libraries (top matches):
Each result includes:
- Library ID: Context7-compatible identifier (format: /org/project)
- Name: Library or package name
- Description: Short summary
- Code Snippets: Number of available code examples
- Trust Score: Authority indicator
- 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.
For best results, select libraries based on name match, trust score, snippet coverage, and relevance to your use case.
----------
${resultsText}`;
return {
content: [
{
type: "text",
text: responseText,
},
],
};
}
);
server.registerTool(
"get-library-docs",
{
title: "Get Library Docs",
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.",
inputSchema: {
context7CompatibleLibraryID: z
.string()
.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'."
),
topic: z
.string()
.optional()
.describe("Topic to focus documentation on (e.g., 'hooks', 'routing')."),
tokens: z
.preprocess((val) => (typeof val === "string" ? Number(val) : val), z.number())
.transform((val) => (val < MINIMUM_TOKENS ? MINIMUM_TOKENS : val))
.optional()
.describe(
`Maximum number of tokens of documentation to retrieve (default: ${DEFAULT_TOKENS}). Higher values provide more context but consume more tokens.`
),
},
},
async ({ context7CompatibleLibraryID, tokens = DEFAULT_TOKENS, topic = "" }) => {
const ctx = requestContext.getStore();
const fetchDocsResponse = await fetchLibraryDocumentation(
context7CompatibleLibraryID,
{
tokens,
topic,
},
ctx?.clientIp,
ctx?.apiKey
);
if (!fetchDocsResponse) {
return {
content: [
{
type: "text",
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.",
},
],
};
}
return {
content: [
{
type: "text",
text: fetchDocsResponse,
},
],
};
}
);
async function main() {
const transportType = TRANSPORT_TYPE;
if (transportType === "http") {
const initialPort = CLI_PORT ?? DEFAULT_PORT;
let actualPort = initialPort;
const app = express();
app.use(express.json());
app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,DELETE");
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"
);
res.setHeader("Access-Control-Expose-Headers", "MCP-Session-Id");
if (req.method === "OPTIONS") {
res.sendStatus(200);
return;
}
next();
});
const extractHeaderValue = (value: string | string[] | undefined): string | undefined => {
if (!value) return undefined;
return typeof value === "string" ? value : value[0];
};
const extractBearerToken = (authHeader: string | string[] | undefined): string | undefined => {
const header = extractHeaderValue(authHeader);
if (!header) return undefined;
if (header.startsWith("Bearer ")) {
return header.substring(7).trim();
}
return header;
};
const extractApiKey = (req: express.Request): string | undefined => {
return (
extractBearerToken(req.headers.authorization) ||
extractHeaderValue(req.headers["Context7-API-Key"]) ||
extractHeaderValue(req.headers["X-API-Key"]) ||
extractHeaderValue(req.headers["context7-api-key"]) ||
extractHeaderValue(req.headers["x-api-key"]) ||
extractHeaderValue(req.headers["Context7_API_Key"]) ||
extractHeaderValue(req.headers["X_API_Key"]) ||
extractHeaderValue(req.headers["context7_api_key"]) ||
extractHeaderValue(req.headers["x_api_key"])
);
};
app.all("/mcp", async (req: express.Request, res: express.Response) => {
try {
const clientIp = getClientIp(req);
const apiKey = extractApiKey(req);
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
enableJsonResponse: true,
});
res.on("close", () => {
transport.close();
});
await requestContext.run({ clientIp, apiKey }, async () => {
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
} catch (error) {
console.error("Error handling MCP request:", error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: "2.0",
error: { code: -32603, message: "Internal server error" },
id: null,
});
}
}
});
app.get("/ping", (_req: express.Request, res: express.Response) => {
res.json({ status: "ok", message: "pong" });
});
// Catch-all 404 handler - must be after all other routes
app.use((_req: express.Request, res: express.Response) => {
res.status(404).json({
error: "not_found",
message: "Endpoint not found. Use /mcp for MCP protocol communication.",
});
});
const startServer = (port: number, maxAttempts = 10) => {
const httpServer = app.listen(port, () => {
actualPort = port;
console.error(
`Context7 Documentation MCP Server running on HTTP at http://localhost:${actualPort}/mcp`
);
});
httpServer.on("error", (err: NodeJS.ErrnoException) => {
if (err.code === "EADDRINUSE" && port < initialPort + maxAttempts) {
console.warn(`Port ${port} is in use, trying port ${port + 1}...`);
startServer(port + 1, maxAttempts);
} else {
console.error(`Failed to start server: ${err.message}`);
process.exit(1);
}
});
};
startServer(initialPort);
} else {
const apiKey = cliOptions.apiKey || process.env.CONTEXT7_API_KEY;
const transport = new StdioServerTransport();
await requestContext.run({ apiKey }, async () => {
await server.connect(transport);
});
console.error("Context7 Documentation MCP Server running on stdio");
}
}
main().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});