diff --git a/README.md b/README.md index a992575..b338892 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,7 @@ [![Website](https://img.shields.io/badge/Website-context7.com-blue)](https://context7.com) [![smithery badge](https://smithery.ai/badge/@upstash/context7-mcp)](https://smithery.ai/server/@upstash/context7-mcp) [Install in VS Code (npx)](https://insiders.vscode.dev/redirect?url=vscode%3Amcp%2Finstall%3F%7B%22name%22%3A%22context7%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40upstash%2Fcontext7-mcp%40latest%22%5D%7D) -[![中文文档](https://img.shields.io/badge/docs-中文版-yellow)](./docs/README.zh-CN.md) [![한국어 문서](https://img.shields.io/badge/docs-한국어-green)](./docs/README.ko.md) [![Documentación en Español](https://img.shields.io/badge/docs-Español-orange)](./docs/README.es.md) [![Documentation en Français](https://img.shields.io/badge/docs-Français-blue)](./docs/README.fr.md) [![Documentação em Português (Brasil)](https://img.shields.io/badge/docs-Português%20(Brasil)-purple)](./docs/README.pt-BR.md) [![Documentazione in italiano](https://img.shields.io/badge/docs-Italian-red)](./docs/README.it.md) [![Dokumentasi Bahasa Indonesia](https://img.shields.io/badge/docs-Bahasa%20Indonesia-pink)](./docs/README.id-ID.md) [![Dokumentation auf Deutsch](https://img.shields.io/badge/docs-Deutsch-darkgreen)](./docs/README.de.md) [![Документация на русском языке](https://img.shields.io/badge/docs-Русский-darkblue)](./docs/README.ru.md) [![Türkçe Doküman](https://img.shields.io/badge/docs-Türkçe-blue)](./docs/README.tr.md) [![Arabic Documentation](https://img.shields.io/badge/docs-Arabic-white)](./docs/README.ar.md) - +[![中文文档](https://img.shields.io/badge/docs-中文版-yellow)](./docs/README.zh-CN.md) [![한국어 문서](https://img.shields.io/badge/docs-한국어-green)](./docs/README.ko.md) [![Documentación en Español](https://img.shields.io/badge/docs-Español-orange)](./docs/README.es.md) [![Documentation en Français](https://img.shields.io/badge/docs-Français-blue)](./docs/README.fr.md) [![Documentação em Português (Brasil)](https://img.shields.io/badge/docs-Português%20(Brasil)-purple)](./docs/README.pt-BR.md) [![Documentazione in italiano](https://img.shields.io/badge/docs-Italian-red)](./docs/README.it.md) [![Dokumentasi Bahasa Indonesia](https://img.shields.io/badge/docs-Bahasa%20Indonesia-pink)](./docs/README.id-ID.md) [![Dokumentation auf Deutsch](https://img.shields.io/badge/docs-Deutsch-darkgreen)](./docs/README.de.md) [![Документация на русском языке](https://img.shields.io/badge/docs-Русский-darkblue)](./docs/README.ru.md) [![Türkçe Dokümantasyon](https://img.shields.io/badge/docs-Türkçe-blue)](./docs/README.tr.md) [![Arabic Documentation](https://img.shields.io/badge/docs-Arabic-white)](./docs/README.ar.md) ## ❌ Without Context7 @@ -44,18 +43,34 @@ No tab-switching, no hallucinated APIs that don't exist, no outdated code genera ### Installing via Smithery -To install Context7 MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@upstash/context7-mcp): +To install Context7 MCP Server for any client automatically via [Smithery](https://smithery.ai/server/@upstash/context7-mcp): ```bash -npx -y @smithery/cli install @upstash/context7-mcp --client claude +npx -y @smithery/cli@latest install @upstash/context7-mcp --client --key ``` +You can find your Smithery key in the [Smithery.ai webpage](https://smithery.ai/server/@upstash/context7-mcp). + ### Install in Cursor Go to: `Settings` -> `Cursor Settings` -> `MCP` -> `Add new global MCP server` Pasting the following configuration into your Cursor `~/.cursor/mcp.json` file is the recommended approach. You may also install in a specific project by creating `.cursor/mcp.json` in your project folder. See [Cursor MCP docs](https://docs.cursor.com/context/model-context-protocol) for more info. +#### Cursor Remote Server Connection + +```json +{ + "mcpServers": { + "context7": { + "url": "https://mcp.context7.com/mcp" + } + } +} +``` + +#### Cursor Local Server Connection + ```json { "mcpServers": { @@ -103,6 +118,20 @@ Pasting the following configuration into your Cursor `~/.cursor/mcp.json` file i Add this to your Windsurf MCP config file. See [Windsurf MCP docs](https://docs.windsurf.com/windsurf/mcp) for more info. +#### Windsurf Remote Server Connection + +```json +{ + "mcpServers": { + "context7": { + "serverUrl": "https://mcp.context7.com/sse" + } + } +} +``` + +#### Windsurf Local Server Connection + ```json { "mcpServers": { @@ -195,7 +224,7 @@ Once saved, enter in the chat `get-library-docs` followed by your Context7 docum If you prefer to run the MCP server in a Docker container: -1. **Build the Docker Image:** +1. **Build the Docker Image:** First, create a `Dockerfile` in the project root (or anywhere you prefer): @@ -249,7 +278,9 @@ If you prefer to run the MCP server in a Docker container: *Note: This is an example configuration. Please refer to the specific examples for your MCP client (like Cursor, VS Code, etc.) earlier in this README to adapt the structure (e.g., `mcpServers` vs `servers`). Also, ensure the image name in `args` matches the tag used during the `docker build` command.* ### Install in Windows + The configuration on Windows is slightly different compared to Linux or macOS (*`Cline` is used in the example*). The same principle applies to other editors; refer to the configuration of `command` and `args`. + ```json { "mcpServers": { @@ -272,7 +303,7 @@ The configuration on Windows is slightly different compared to Linux or macOS (* - `DEFAULT_MINIMUM_TOKENS`: Set the minimum token count for documentation retrieval (default: 10000). -Examples: +Example configuration with environment variables: ```json { @@ -281,7 +312,7 @@ Examples: "command": "npx", "args": ["-y", "@upstash/context7-mcp"], "env": { - "DEFAULT_MINIMUM_TOKENS": "10000" + "DEFAULT_MINIMUM_TOKENS": "6000" } } } @@ -290,10 +321,13 @@ Examples: ### Available Tools +Context7 MCP provides the following tools that LLMs can use: + - `resolve-library-id`: Resolves a general library name into a Context7-compatible library ID. - - `libraryName` (required) + - `libraryName` (required): The name of the library to search for + - `get-library-docs`: Fetches documentation for a library using a Context7-compatible library ID. - - `context7CompatibleLibraryID` (required) + - `context7CompatibleLibraryID` (required): Exact Context7-compatible library ID (e.g., `/mongodb/docs`, `/vercel/next.js`) - `topic` (optional): Focus the docs on a specific topic (e.g., "routing", "hooks") - `tokens` (optional, default 10000): Max number of tokens to return. Values less than the configured `DEFAULT_MINIMUM_TOKENS` value or the default value of 10000 are automatically increased to that value. @@ -332,9 +366,9 @@ npx -y @modelcontextprotocol/inspector npx @upstash/context7-mcp ## Troubleshooting -### ERR_MODULE_NOT_FOUND +### Module Not Found Errors -If you see this error, try using `bunx` instead of `npx`. +If you encounter `ERR_MODULE_NOT_FOUND`, try using `bunx` instead of `npx`: ```json { @@ -347,11 +381,11 @@ If you see this error, try using `bunx` instead of `npx`. } ``` -This often resolves module resolution issues, especially in environments where `npx` does not properly install or resolve packages. +This often resolves module resolution issues in environments where `npx` doesn't properly install or resolve packages. ### ESM Resolution Issues -If you encounter an error like: `Error: Cannot find module 'uriTemplate.js'` try running with the `--experimental-vm-modules` flag: +For errors like `Error: Cannot find module 'uriTemplate.js'`, try the `--experimental-vm-modules` flag: ```json { @@ -370,7 +404,7 @@ If you encounter an error like: `Error: Cannot find module 'uriTemplate.js'` try ### TLS/Certificate Issues -Use the `--experimental-fetch` flag with `npx` to bypass TLS-related issues: +Use the `--experimental-fetch` flag to bypass TLS-related problems: ```json { @@ -387,15 +421,12 @@ Use the `--experimental-fetch` flag with `npx` to bypass TLS-related issues: } ``` -### MCP Client Errors +### General MCP Client Errors -1. Try adding `@latest` to the package name. - -2. Try using `bunx` as an alternative. - -3. Try using `deno` as an alternative. - -4. Make sure you are using Node v18 or higher to have native fetch support with `npx`. +1. Try adding `@latest` to the package name +2. Use `bunx` as an alternative to `npx` +3. Consider using `deno` as another alternative +4. Ensure you're using Node.js v18 or higher for native fetch support ## Disclaimer @@ -404,18 +435,19 @@ Context7 projects are community-contributed and while we strive to maintain high ## Connect with Us Stay updated and join our community: + - 📢 Follow us on [X](https://x.com/contextai) for the latest news and updates - 🌐 Visit our [Website](https://context7.com) -- 💬 Join our [Discord Community](https://upstash.com/discord) (if applicable) +- 💬 Join our [Discord Community](https://upstash.com/discord) ## Context7 In Media - [Better Stack: "Free Tool Makes Cursor 10x Smarter"](https://youtu.be/52FC3qObp9E) - [Cole Medin: "This is Hands Down the BEST MCP Server for AI Coding Assistants"](https://www.youtube.com/watch?v=G7gK8H6u7Rs) -- [Income stream surfers: "Context7 + SequentialThinking MCPs: Is This AGI?"](https://www.youtube.com/watch?v=-ggvzyLpK6o) +- [Income Stream Surfers: "Context7 + SequentialThinking MCPs: Is This AGI?"](https://www.youtube.com/watch?v=-ggvzyLpK6o) - [Julian Goldie SEO: "Context7: New MCP AI Agent Update"](https://www.youtube.com/watch?v=CTZm6fBYisc) - [JeredBlu: "Context 7 MCP: Get Documentation Instantly + VS Code Setup"](https://www.youtube.com/watch?v=-ls0D-rtET4) -- [Income stream surfers: "Context7: The New MCP Server That Will CHANGE AI Coding"](https://www.youtube.com/watch?v=PS-2Azb-C3M) +- [Income Stream Surfers: "Context7: The New MCP Server That Will CHANGE AI Coding"](https://www.youtube.com/watch?v=PS-2Azb-C3M) - [AICodeKing: "Context7 + Cline & RooCode: This MCP Server Makes CLINE 100X MORE EFFECTIVE!"](https://www.youtube.com/watch?v=qZfENAPMnyo) - [Sean Kochel: "5 MCP Servers For Vibe Coding Glory (Just Plug-In & Go)"](https://www.youtube.com/watch?v=LqTQi8qexJM) diff --git a/package.json b/package.json index 9188559..131d001 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@upstash/context7-mcp", - "version": "1.0.6", + "version": "1.0.0", "description": "MCP server for Context7", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", @@ -31,7 +31,7 @@ }, "homepage": "https://github.com/upstash/context7#readme", "dependencies": { - "@modelcontextprotocol/sdk": "^1.8.0", + "@modelcontextprotocol/sdk": "^1.12.0", "dotenv": "^16.5.0", "zod": "^3.24.2" }, diff --git a/src/index.ts b/src/index.ts index 9d80c96..ba8541b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,10 @@ import { z } from "zod"; import { searchLibraries, fetchLibraryDocumentation } from "./lib/api.js"; import { formatSearchResults } from "./lib/utils.js"; import dotenv from "dotenv"; +import { createServer } from "http"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; +import { parse } from "url"; // Load environment variables from .env file if present dotenv.config(); @@ -23,21 +27,25 @@ if (process.env.DEFAULT_MINIMUM_TOKENS) { } } -// Create server instance -const server = new McpServer({ - name: "Context7", - description: "Retrieves up-to-date documentation and code examples for any library.", - version: "1.0.6", - capabilities: { - resources: {}, - tools: {}, - }, -}); +// Store SSE transports by session ID +const sseTransports: Record = {}; -// Register Context7 tools -server.tool( - "resolve-library-id", - `Resolves a package/product name to a Context7-compatible library ID and returns a list of matching libraries. +// Function to create a new server instance with all tools registered +function createServerInstance() { + const server = new McpServer({ + name: "Context7", + description: "Retrieves up-to-date documentation and code examples for any library.", + version: "1.0.13", + capabilities: { + resources: {}, + tools: {}, + }, + }); + + // Register Context7 tools + server.tool( + "resolve-library-id", + `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. @@ -56,43 +64,43 @@ Response Format: - If no good matches exist, clearly state this and suggest query refinements For ambiguous queries, request clarification before proceeding with a best-guess match.`, - { - libraryName: z - .string() - .describe("Library name to search for and retrieve a Context7-compatible library ID."), - }, - async ({ libraryName }) => { - const searchResponse = await searchLibraries(libraryName); + { + libraryName: z + .string() + .describe("Library name to search for and retrieve a Context7-compatible library ID."), + }, + async ({ libraryName }) => { + const searchResponse = await searchLibraries(libraryName); + + if (!searchResponse || !searchResponse.results) { + return { + content: [ + { + type: "text", + text: "Failed to retrieve library documentation data from Context7", + }, + ], + }; + } + + if (searchResponse.results.length === 0) { + return { + content: [ + { + type: "text", + text: "No documentation libraries available", + }, + ], + }; + } + + const resultsText = formatSearchResults(searchResponse); - if (!searchResponse || !searchResponse.results) { return { content: [ { type: "text", - text: "Failed to retrieve library documentation data from Context7", - }, - ], - }; - } - - if (searchResponse.results.length === 0) { - return { - content: [ - { - type: "text", - text: "No documentation libraries available", - }, - ], - }; - } - - const resultsText = formatSearchResults(searchResponse); - - return { - content: [ - { - type: "text", - text: `Available Libraries (top matches): + text: `Available Libraries (top matches): Each result includes: - Library ID: Context7-compatible identifier (format: /org/project) @@ -107,65 +115,173 @@ For best results, select libraries based on name match, trust score, snippet cov ---------- ${resultsText}`, - }, - ], - }; - } -); - -server.tool( - "get-library-docs", - "Fetches up-to-date documentation for a library. You must call 'resolve-library-id' first to obtain the exact Context7-compatible library ID required to use this tool, UNLESS the user explicitly provides a library ID in the format '/org/project' or '/org/project/version' in their query.", - { - 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 < DEFAULT_MINIMUM_TOKENS ? DEFAULT_MINIMUM_TOKENS : val)) - .optional() - .describe( - `Maximum number of tokens of documentation to retrieve (default: ${DEFAULT_MINIMUM_TOKENS}). Higher values provide more context but consume more tokens.` - ), - }, - async ({ context7CompatibleLibraryID, tokens = DEFAULT_MINIMUM_TOKENS, topic = "" }) => { - const documentationText = await fetchLibraryDocumentation(context7CompatibleLibraryID, { - tokens, - topic, - }); - - if (!documentationText) { - 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: documentationText, - }, - ], - }; - } -); + server.tool( + "get-library-docs", + "Fetches up-to-date documentation for a library. You must call 'resolve-library-id' first to obtain the exact Context7-compatible library ID required to use this tool, UNLESS the user explicitly provides a library ID in the format '/org/project' or '/org/project/version' in their query.", + { + 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 < DEFAULT_MINIMUM_TOKENS ? DEFAULT_MINIMUM_TOKENS : val)) + .optional() + .describe( + `Maximum number of tokens of documentation to retrieve (default: ${DEFAULT_MINIMUM_TOKENS}). Higher values provide more context but consume more tokens.` + ), + }, + async ({ context7CompatibleLibraryID, tokens = DEFAULT_MINIMUM_TOKENS, topic = "" }) => { + const documentationText = await fetchLibraryDocumentation(context7CompatibleLibraryID, { + tokens, + topic, + }); + + if (!documentationText) { + 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: documentationText, + }, + ], + }; + } + ); + + return server; +} async function main() { - const transport = new StdioServerTransport(); - await server.connect(transport); - console.error("Context7 Documentation MCP Server running on stdio"); + const transportType = process.env.MCP_TRANSPORT || "stdio"; + + if (transportType === "http" || transportType === "sse") { + // Get initial port from environment or use default + const initialPort = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000; + // Keep track of which port we end up using + let actualPort = initialPort; + const httpServer = createServer(async (req, res) => { + const url = parse(req.url || "").pathname; + + // Set CORS headers for all responses + 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-session-id"); + + // Handle preflight OPTIONS requests + if (req.method === "OPTIONS") { + res.writeHead(200); + res.end(); + return; + } + + try { + // Create new server instance for each request + const requestServer = createServerInstance(); + + if (url === "/mcp") { + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + }); + await requestServer.connect(transport); + await transport.handleRequest(req, res); + } else if (url === "/sse" && req.method === "GET") { + // Create new SSE transport for GET request + const sseTransport = new SSEServerTransport("/messages", res); + // Store the transport by session ID + sseTransports[sseTransport.sessionId] = sseTransport; + // Clean up transport when connection closes + res.on("close", () => { + delete sseTransports[sseTransport.sessionId]; + }); + await requestServer.connect(sseTransport); + } else if (url === "/messages" && req.method === "POST") { + // Get session ID from query parameters + const parsedUrl = parse(req.url || "", true); + const sessionId = parsedUrl.query.sessionId as string; + + if (!sessionId) { + res.writeHead(400); + res.end("Missing sessionId parameter"); + return; + } + + // Get existing transport for this session + const sseTransport = sseTransports[sessionId]; + if (!sseTransport) { + res.writeHead(400); + res.end(`No transport found for sessionId: ${sessionId}`); + return; + } + + // Handle the POST message with the existing transport + await sseTransport.handlePostMessage(req, res); + } else if (url === "/ping") { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end("pong"); + } else { + res.writeHead(404); + res.end("Not found"); + } + } catch (error) { + console.error("Error handling request:", error); + if (!res.headersSent) { + res.writeHead(500); + res.end("Internal Server Error"); + } + } + }); + + // Function to attempt server listen with port fallback + const startServer = (port: number, maxAttempts = 10) => { + httpServer.once("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); + } + }); + + httpServer.listen(port, () => { + actualPort = port; + console.error( + `Context7 Documentation MCP Server running on ${transportType.toUpperCase()} at http://localhost:${actualPort}/mcp and legacy SSE at /sse` + ); + }); + }; + + // Start the server with initial port + startServer(initialPort); + } else { + // Stdio transport - this is already stateless by nature + const server = createServerInstance(); + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error("Context7 Documentation MCP Server running on stdio"); + } } main().catch((error) => {