2025-04-03 12:32:03 +03:00
# ! / u s r / b i n / e n v n o d e
2025-03-31 15:19:29 +03:00
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" ;
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" ;
import { z } from "zod" ;
2025-04-17 01:03:45 +03:00
import { searchLibraries , fetchLibraryDocumentation } from "./lib/api.js" ;
import { formatSearchResults } from "./lib/utils.js" ;
2025-06-07 00:20:52 +03:00
import { SearchResponse } from "./lib/types.js" ;
2025-05-28 00:34:49 +03:00
import { createServer } from "http" ;
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js" ;
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js" ;
2025-06-11 14:49:50 +03:00
import { Command } from "commander" ;
2025-04-17 01:03:45 +03:00
2025-06-11 14:49:50 +03:00
const DEFAULT_MINIMUM_TOKENS = 10000 ;
// Parse CLI arguments using commander
const program = new Command ( )
. option ( "--transport <stdio|http|sse>" , "transport type" , "stdio" )
. option ( "--port <number>" , "port for HTTP/SSE transport" , "3000" )
. allowUnknownOption ( ) // let MCP Inspector / other wrappers pass through extra flags
. parse ( process . argv ) ;
const cliOptions = program . opts < {
transport : string ;
port : string ;
} > ( ) ;
2025-06-11 14:58:26 +03:00
// Validate transport option
const allowedTransports = [ "stdio" , "http" , "sse" ] ;
if ( ! allowedTransports . includes ( cliOptions . transport ) ) {
console . error (
` Invalid --transport value: ' ${ cliOptions . transport } '. Must be one of: stdio, http, sse. `
) ;
process . exit ( 1 ) ;
}
2025-06-11 14:49:50 +03:00
// Transport configuration
const TRANSPORT_TYPE = ( cliOptions . transport || "stdio" ) as "stdio" | "http" | "sse" ;
// HTTP/SSE port configuration
const CLI_PORT = ( ( ) = > {
const parsed = parseInt ( cliOptions . port , 10 ) ;
return isNaN ( parsed ) ? undefined : parsed ;
} ) ( ) ;
2025-03-31 15:19:29 +03:00
2025-05-28 00:34:49 +03:00
// Store SSE transports by session ID
const sseTransports : Record < string , SSEServerTransport > = { } ;
// Function to create a new server instance with all tools registered
function createServerInstance() {
fix: Fix `InitializeResult` schema mismatch in Context7 server response
The response from the server was not matching the expected `InitializeResult` schema [^1].
1. The `serverInfo` field contained a `description` that was not part of the schema.
2. The `capabilities` field was incorrectly nested in `serverInfo`.
3. The `capabilities` field in the response included `resources`, even though this server does not support resources.
This change also adds an `instructions` field to the response, which provides guidance on how to use the server.
The SDK takes care of registering capabilities [^2], so we don't need to add this when instantiating the `server` object.
Response does not match `InitializeResult` schema:
```json
{
"capabilities": {
"tools": {
"listChanged": true
}
},
"serverInfo": {
"name": "Context7",
"version": "1.0.13",
"description": "Retrieves up-to-date documentation and code examples for any library.",
"capabilities": {
"resources": {},
"tools": {}
}
}
}
```
Response matches `InitializeResult` schema:
```json
{
"capabilities": {
"tools": {
"listChanged": true
}
},
"serverInfo": {
"name": "Context7",
"version": "1.0.13"
},
"instructions": "Use this server to retrieve up-to-date documentation and code examples for any library."
}
```
[^1]: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-03-26/schema.ts#L179-L193
[^2]: https://github.com/modelcontextprotocol/typescript-sdk/blob/2cf4f0ca86ff841aca53ac8ef5f3227ba3789386/src/server/mcp.ts#L101
Fixes https://github.com/upstash/context7/issues/282
2025-06-12 11:55:15 +02:00
const server = new McpServer (
{
name : "Context7" ,
version : "1.0.13" ,
2025-05-28 00:34:49 +03:00
} ,
fix: Fix `InitializeResult` schema mismatch in Context7 server response
The response from the server was not matching the expected `InitializeResult` schema [^1].
1. The `serverInfo` field contained a `description` that was not part of the schema.
2. The `capabilities` field was incorrectly nested in `serverInfo`.
3. The `capabilities` field in the response included `resources`, even though this server does not support resources.
This change also adds an `instructions` field to the response, which provides guidance on how to use the server.
The SDK takes care of registering capabilities [^2], so we don't need to add this when instantiating the `server` object.
Response does not match `InitializeResult` schema:
```json
{
"capabilities": {
"tools": {
"listChanged": true
}
},
"serverInfo": {
"name": "Context7",
"version": "1.0.13",
"description": "Retrieves up-to-date documentation and code examples for any library.",
"capabilities": {
"resources": {},
"tools": {}
}
}
}
```
Response matches `InitializeResult` schema:
```json
{
"capabilities": {
"tools": {
"listChanged": true
}
},
"serverInfo": {
"name": "Context7",
"version": "1.0.13"
},
"instructions": "Use this server to retrieve up-to-date documentation and code examples for any library."
}
```
[^1]: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-03-26/schema.ts#L179-L193
[^2]: https://github.com/modelcontextprotocol/typescript-sdk/blob/2cf4f0ca86ff841aca53ac8ef5f3227ba3789386/src/server/mcp.ts#L101
Fixes https://github.com/upstash/context7/issues/282
2025-06-12 11:55:15 +02:00
{
instructions :
"Use this server to retrieve up-to-date documentation and code examples for any library." ,
}
) ;
2025-03-31 15:19:29 +03:00
2025-05-28 00:34:49 +03:00
// 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.
2025-05-02 00:09:19 +03:00
2025-05-24 17:57:46 +03:00
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 .
2025-05-02 00:09:19 +03:00
2025-05-17 02:49:46 +03:00
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 . ` ,
2025-05-28 00:34:49 +03:00
{
libraryName : z
. string ( )
. describe ( "Library name to search for and retrieve a Context7-compatible library ID." ) ,
} ,
async ( { libraryName } ) = > {
2025-06-07 00:20:52 +03:00
const searchResponse : SearchResponse = await searchLibraries ( libraryName ) ;
2025-05-28 00:34:49 +03:00
2025-06-07 00:20:52 +03:00
if ( ! searchResponse . results || searchResponse . results . length === 0 ) {
2025-05-28 00:34:49 +03:00
return {
content : [
{
type : "text" ,
2025-06-07 00:20:52 +03:00
text : searchResponse.error
? searchResponse . error
: "Failed to retrieve library documentation data from Context7" ,
2025-05-28 00:34:49 +03:00
} ,
] ,
} ;
}
const resultsText = formatSearchResults ( searchResponse ) ;
2025-03-31 15:19:29 +03:00
return {
content : [
{
type : "text" ,
2025-05-28 00:34:49 +03:00
text : ` Available Libraries (top matches):
2025-05-02 00:09:19 +03:00
Each result includes :
2025-05-24 17:57:46 +03:00
- Library ID : Context7 - compatible identifier ( format : / o r g / p r o j e c t )
2025-05-02 00:09:19 +03:00
- Name : Library or package name
- Description : Short summary
- Code Snippets : Number of available code examples
2025-05-17 02:49:46 +03:00
- Trust Score : Authority indicator
2025-05-22 11:41:03 -07:00
- Versions : List of versions if available . Use one of those versions if and only if the user explicitly provides a version in their query .
2025-05-02 00:09:19 +03:00
2025-05-17 02:49:46 +03:00
For best results , select libraries based on name match , trust score , snippet coverage , and relevance to your use case .
2025-05-02 00:09:19 +03:00
2025-05-17 02:49:46 +03:00
-- -- -- -- --
2025-05-02 00:09:19 +03:00
$ { resultsText } ` ,
2025-05-28 00:34:49 +03:00
} ,
] ,
} ;
}
) ;
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 = "" } ) = > {
2025-06-07 00:20:52 +03:00
const fetchDocsResponse = await fetchLibraryDocumentation ( context7CompatibleLibraryID , {
2025-05-28 00:34:49 +03:00
tokens ,
topic ,
} ) ;
2025-06-07 00:20:52 +03:00
if ( ! fetchDocsResponse ) {
2025-05-28 00:34:49 +03:00
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." ,
} ,
] ,
} ;
}
2025-03-31 15:19:29 +03:00
return {
content : [
{
type : "text" ,
2025-06-07 00:20:52 +03:00
text : fetchDocsResponse ,
2025-03-31 15:19:29 +03:00
} ,
] ,
} ;
}
2025-05-28 00:34:49 +03:00
) ;
2025-04-03 11:51:17 +03:00
2025-05-28 00:34:49 +03:00
return server ;
}
2025-03-31 15:19:29 +03:00
async function main() {
2025-06-11 14:49:50 +03:00
const transportType = TRANSPORT_TYPE ;
2025-05-28 00:34:49 +03:00
if ( transportType === "http" || transportType === "sse" ) {
2025-05-28 14:37:30 +03:00
// Get initial port from environment or use default
2025-06-11 14:49:50 +03:00
const initialPort = CLI_PORT ? ? 3000 ;
2025-05-28 14:37:30 +03:00
// Keep track of which port we end up using
let actualPort = initialPort ;
2025-05-28 00:34:49 +03:00
const httpServer = createServer ( async ( req , res ) = > {
2025-06-11 14:49:50 +03:00
const url = new URL ( req . url || "" , ` http:// ${ req . headers . host } ` ) . pathname ;
2025-05-28 00:34:49 +03:00
// Set CORS headers for all responses
2025-05-28 12:31:03 +03:00
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" ) ;
2025-05-28 00:34:49 +03:00
// Handle preflight OPTIONS requests
2025-05-28 12:31:03 +03:00
if ( req . method === "OPTIONS" ) {
2025-05-28 00:34:49 +03:00
res . writeHead ( 200 ) ;
res . end ( ) ;
return ;
}
try {
// Create new server instance for each request
const requestServer = createServerInstance ( ) ;
if ( url === "/mcp" ) {
const transport = new StreamableHTTPServerTransport ( {
2025-05-28 12:31:03 +03:00
sessionIdGenerator : undefined ,
2025-05-28 00:34:49 +03:00
} ) ;
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
2025-06-11 14:58:26 +03:00
const sessionId =
new URL ( req . url || "" , ` http:// ${ req . headers . host } ` ) . searchParams . get ( "sessionId" ) ? ?
"" ;
2025-05-28 12:31:03 +03:00
2025-05-28 00:34:49 +03:00
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" ) {
2025-05-28 12:31:03 +03:00
res . writeHead ( 200 , { "Content-Type" : "text/plain" } ) ;
res . end ( "pong" ) ;
2025-05-28 00:34:49 +03:00
} 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" ) ;
}
}
} ) ;
2025-05-28 14:37:30 +03:00
// 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 ) ;
2025-05-28 00:34:49 +03:00
} 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" ) ;
}
2025-03-31 15:19:29 +03:00
}
main ( ) . catch ( ( error ) = > {
console . error ( "Fatal error in main():" , error ) ;
process . exit ( 1 ) ;
} ) ;