diff --git a/package-lock.json b/package-lock.json index 11bffb72..652b3fd9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2674,6 +2674,22 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -3731,7 +3747,7 @@ }, "src/everything": { "name": "@modelcontextprotocol/server-everything", - "version": "0.6.2", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.24.0", @@ -3747,6 +3763,7 @@ "devDependencies": { "@types/cors": "^2.8.19", "@types/express": "^5.0.6", + "prettier": "^2.8.8", "shx": "^0.3.4", "typescript": "^5.6.2" } diff --git a/src/everything/.prettierignore b/src/everything/.prettierignore new file mode 100644 index 00000000..b6ce5590 --- /dev/null +++ b/src/everything/.prettierignore @@ -0,0 +1,4 @@ +packages +dist +README.md +node_modules diff --git a/src/everything/CLAUDE.md b/src/everything/CLAUDE.md index 9135020c..758af475 100644 --- a/src/everything/CLAUDE.md +++ b/src/everything/CLAUDE.md @@ -1,13 +1,16 @@ # MCP "Everything" Server - Development Guidelines ## Build, Test & Run Commands + - Build: `npm run build` - Compiles TypeScript to JavaScript - Watch mode: `npm run watch` - Watches for changes and rebuilds automatically -- Run server: `npm run start` - Starts the MCP server using stdio transport +- Run STDIO server: `npm run start:stdio` - Starts the MCP server using stdio transport - Run SSE server: `npm run start:sse` - Starts the MCP server with SSE transport +- Run StreamableHttp server: `npm run start:stremableHttp` - Starts the MCP server with StreamableHttp transport - Prepare release: `npm run prepare` - Builds the project for publishing ## Code Style Guidelines + - Use ES modules with `.js` extension in import paths - Strictly type all functions and variables with TypeScript - Follow zod schema patterns for tool input validation @@ -17,4 +20,4 @@ - Implement proper cleanup for timers and resources in server shutdown - Follow camelCase for variables/functions, PascalCase for types/classes, UPPER_CASE for constants - Handle errors with try/catch blocks and provide clear error messages -- Use consistent indentation (2 spaces) and trailing commas in multi-line objects \ No newline at end of file +- Use consistent indentation (2 spaces) and trailing commas in multi-line objects diff --git a/src/everything/docs/architecture.md b/src/everything/docs/architecture.md new file mode 100644 index 00000000..01bfbad3 --- /dev/null +++ b/src/everything/docs/architecture.md @@ -0,0 +1,163 @@ +# Everything Server – Architecture and Layout + +This document summarizes the current layout and runtime architecture of the `src/everything` package. It explains how the server starts, how transports are wired, where tools and resources are registered, and how to extend the system. + +## High‑level Overview + +- Purpose: A minimal, modular MCP server showcasing core Model Context Protocol features. It exposes a simple tool and both static and dynamic resources, and can be run over multiple transports (STDIO, SSE, and Streamable HTTP). +- Design: A small “server factory” constructs the MCP server and registers features. Transports are separate entry points that create/connect the server and handle network concerns. Tools and resources are organized in their own submodules. +- Two server implementations exist: + - `server/index.ts`: The lightweight, modular server used by transports in this package. + - `server/everything.ts`: A comprehensive reference server (much larger, many tools/prompts/resources) kept for reference/testing but not wired up by default in the entry points. + +## Directory Layout + +``` +src/everything +├── index.ts +├── server +│ ├── index.ts +│ └── everything.ts +├── transports +│ ├── stdio.ts +│ ├── sse.ts +│ └── streamableHttp.ts +├── tools +│ ├── index.ts +│ └── echo.ts +├── resources +│ ├── index.ts +│ ├── dynamic.ts +│ └── static.ts +├── docs +│ ├── server-instructions.md +│ └── architecture.md +└── package.json +``` + +At `src/everything`: + +- index.ts + + - CLI entry that selects and runs a specific transport module based on the first CLI argument: `stdio`, `sse`, or `streamableHttp`. + +- server/ + + - index.ts + - Server factory that creates an `McpServer` with declared capabilities, loads server instructions, and registers tools and resources. + - Exposes `{ server, cleanup, startNotificationIntervals }` to the chosen transport. + - everything.ts + - A full “reference/monolith” implementation demonstrating most MCP features. Not the default path used by the transports in this package. + +- transports/ + + - stdio.ts + - Starts a `StdioServerTransport`, creates the server via `createServer()`, and connects it. Handles `SIGINT` to close cleanly. + - sse.ts + - Express server exposing: + - `GET /sse` to establish an SSE connection per session. + - `POST /message` for client messages. + - Manages a `Map` for sessions. Calls `startNotificationIntervals(sessionId)` after connect (hook currently a no‑op in the factory). + - streamableHttp.ts + - Express server exposing a single `/mcp` endpoint for POST (JSON‑RPC), GET (SSE stream), and DELETE (session termination) using `StreamableHTTPServerTransport`. + - Uses an `InMemoryEventStore` for resumable sessions and tracks transports by `sessionId`. Connects a fresh server instance on initialization POST, then reuses transport for subsequent requests. + +- tools/ + + - index.ts + - `registerTools(server)` orchestrator, currently delegates to `addToolEcho`. + - echo.ts + - Defines a minimal `echo` tool with a Zod input schema and returns `Echo: {message}`. + +- resources/ + + - index.ts + - `registerResources(server)` orchestrator; delegates to static and dynamic resources. + - dynamic.ts + - Registers two dynamic, template‑driven resources using `ResourceTemplate`: + - Text: `test://dynamic/resource/text/{index}` (MIME: `text/plain`) + - Blob: `test://dynamic/resource/blob/{index}` (MIME: `application/octet-stream`, Base64 payload) + - The `{index}` path variable must be a finite integer. Content is generated on demand with a GMT timestamp. + - static.ts + - Registers static resources for each file in the `docs/` folder. + - URIs follow the pattern: `test://static/docs/`. + - Serves markdown files as `text/markdown`, `.txt` as `text/plain`, `.json` as `application/json`, others default to `text/plain`. + +- docs/ + + - server-instructions.md + - Human‑readable instructions intended to be passed to the client/LLM as MCP server instructions. Loaded by the server at startup. + - architecture.md (this document) + +- package.json + - Package metadata and scripts: + - `build`: TypeScript compile to `dist/`, copies `docs/` into `dist/` and marks the compiled entry scripts as executable. + - `start:stdio`, `start:sse`, `start:streamableHttp`: Run built transports from `dist/`. + - Declares dependencies on `@modelcontextprotocol/sdk`, `express`, `cors`, `zod`, etc. + +## Startup and Runtime Flow + +1. A transport is chosen via the CLI entry `index.ts`: + + - `node dist/index.js stdio` → loads `transports/stdio.js` + - `node dist/index.js sse` → loads `transports/sse.js` + - `node dist/index.js streamableHttp` → loads `transports/streamableHttp.js` + +2. The transport creates the server via `createServer()` from `server/index.ts` and connects it to the chosen transport type from the MCP SDK. + +3. The server factory (`server/index.ts`) does the following: + + - Creates `new McpServer({ name, title, version }, { capabilities, instructions })`. + - Capabilities: + - `tools: {}` + - `logging: {}` + - `prompts: {}` + - `resources: { subscribe: true }` + - Loads human‑readable “server instructions” from the docs folder (`server-instructions.md`). + - Registers tools via `registerTools(server)`. + - Registers resources via `registerResources(server)`. + - Returns the server and two lifecycle hooks: + - `cleanup`: transport may call on shutdown (currently a no‑op). + - `startNotificationIntervals(sessionId?)`: currently a no‑op; wired in SSE transport for future periodic notifications. + +4. Each transport is responsible for network/session lifecycle: + - STDIO: simple process‑bound connection; closes on `SIGINT`. + - SSE: maintains a session map keyed by `sessionId`, hooks server’s `onclose` to clean and remove session, exposes `/sse` (GET) and `/message` (POST) endpoints. + - Streamable HTTP: exposes `/mcp` for POST (JSON‑RPC messages), GET (SSE stream), and DELETE (termination). Uses an event store for resumability and stores transports by `sessionId`. + +## Registered Features (current minimal set) + +- Tools + + - `echo` (tools/echo.ts): Echoes the provided `message: string`. Uses Zod to validate inputs. + +- Resources + - Dynamic Text: `test://dynamic/resource/text/{index}` (content generated on the fly) + - Dynamic Blob: `test://dynamic/resource/blob/{index}` (base64 payload generated on the fly) + - Static Docs: `test://static/docs/` (serves files from `src/everything/docs/` as static resources) + +## Extension Points + +- Adding Tools + + - Create a new file under `tools/` with your `addToolX(server)` function that registers the tool via `server.registerTool(...)`. + - Export and call it from `tools/index.ts` inside `registerTools(server)`. + +- Adding Resources + + - Create a new file under `resources/` with your `addXResources(server)` function using `server.registerResource(...)` (optionally with `ResourceTemplate`). + - Export and call it from `resources/index.ts` inside `registerResources(server)`. + +- Adding Transports + - Implement a new transport module under `transports/`. + - Add a case to `index.ts` so the CLI can select it. + +## Build and Distribution + +- TypeScript sources are compiled into `dist/` via `npm run build`. +- The `build` script copies `docs/` into `dist/` so instruction files ship alongside the compiled server. +- The CLI bin is configured in `package.json` as `mcp-server-everything` → `dist/index.js`. + +## Relationship to the Full Reference Server + +The large `server/everything.ts` shows a comprehensive MCP server showcasing many features (tools with schemas, prompts, resource operations, notifications, etc.). The current transports in this package use the lean factory from `server/index.ts` instead, keeping the runtime small and focused while preserving the reference implementation for learning and experimentation. diff --git a/src/everything/instructions.md b/src/everything/docs/server-instructions.md similarity index 100% rename from src/everything/instructions.md rename to src/everything/docs/server-instructions.md diff --git a/src/everything/index.ts b/src/everything/index.ts index d755c408..cce1ea53 100644 --- a/src/everything/index.ts +++ b/src/everything/index.ts @@ -2,36 +2,36 @@ // Parse command line arguments first const args = process.argv.slice(2); -const scriptName = args[0] || 'stdio'; +const scriptName = args[0] || "stdio"; async function run() { - try { - // Dynamically import only the requested module to prevent all modules from initializing - switch (scriptName) { - case 'stdio': - // Import and run the default server - await import('./transports/stdio.js'); - break; - case 'sse': - // Import and run the SSE server - await import('./transports/sse.js'); - break; - case 'streamableHttp': - // Import and run the streamable HTTP server - await import('./transports/streamableHttp.js'); - break; - default: - console.error(`Unknown script: ${scriptName}`); - console.log('Available scripts:'); - console.log('- stdio'); - console.log('- sse'); - console.log('- streamableHttp'); - process.exit(1); - } - } catch (error) { - console.error('Error running script:', error); + try { + // Dynamically import only the requested module to prevent all modules from initializing + switch (scriptName) { + case "stdio": + // Import and run the default server + await import("./transports/stdio.js"); + break; + case "sse": + // Import and run the SSE server + await import("./transports/sse.js"); + break; + case "streamableHttp": + // Import and run the streamable HTTP server + await import("./transports/streamableHttp.js"); + break; + default: + console.error(`Unknown script: ${scriptName}`); + console.log("Available scripts:"); + console.log("- stdio"); + console.log("- sse"); + console.log("- streamableHttp"); process.exit(1); } + } catch (error) { + console.error("Error running script:", error); + process.exit(1); + } } run(); diff --git a/src/everything/package.json b/src/everything/package.json index 951edc5a..945b782c 100644 --- a/src/everything/package.json +++ b/src/everything/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-everything", - "version": "0.6.2", + "version": "2.0.0", "description": "MCP server that exercises all the features of the MCP protocol", "license": "MIT", "mcpName": "io.github.modelcontextprotocol/server-everything", @@ -19,12 +19,14 @@ "dist" ], "scripts": { - "build": "tsc && shx cp instructions.md dist/ && shx chmod +x dist/*.js", + "build": "tsc && shx cp -r docs dist/ && shx chmod +x dist/*.js", "prepare": "npm run build", "watch": "tsc --watch", "start:stdio": "node dist/index.js stdio", "start:sse": "node dist/index.js sse", - "start:streamableHttp": "node dist/index.js streamableHttp" + "start:streamableHttp": "node dist/index.js streamableHttp", + "prettier-fix": "prettier --write .", + "prettier-check": "prettier --check ." }, "dependencies": { "@modelcontextprotocol/sdk": "^1.24.0", @@ -38,6 +40,7 @@ "@types/cors": "^2.8.19", "@types/express": "^5.0.6", "shx": "^0.3.4", - "typescript": "^5.6.2" + "typescript": "^5.6.2", + "prettier": "^2.8.8" } } diff --git a/src/everything/resources/dynamic.ts b/src/everything/resources/dynamic.ts index 2acc9356..05a052cf 100644 --- a/src/everything/resources/dynamic.ts +++ b/src/everything/resources/dynamic.ts @@ -1,4 +1,7 @@ -import {McpServer, ResourceTemplate} from "@modelcontextprotocol/sdk/server/mcp.js"; +import { + McpServer, + ResourceTemplate, +} from "@modelcontextprotocol/sdk/server/mcp.js"; /** * Register dynamic resources with the MCP server. @@ -16,84 +19,104 @@ import {McpServer, ResourceTemplate} from "@modelcontextprotocol/sdk/server/mcp. * @param server */ export const addDynamicResources = (server: McpServer) => { - const uriBase: string = "test://dynamic/resource"; - const textUri: string = `${uriBase}/text/{index}` - const blobUri: string = `${uriBase}/blob/{index}` + const uriBase: string = "test://dynamic/resource"; + const textUriBase: string = `${uriBase}/text`; + const blobUriBase: string = `${uriBase}/blob`; + const textUriTemplate: string = `${textUriBase}/{index}`; + const blobUriTemplate: string = `${blobUriBase}/{index}`; - // Format a GMT timestamp like "7:30AM GMT on November 3" - const formatGmtTimestamp = () => { - const d = new Date(); - const h24 = d.getUTCHours(); - const minutes = d.getUTCMinutes(); - const ampm = h24 >= 12 ? "PM" : "AM"; - let h12 = h24 % 12; - if (h12 === 0) h12 = 12; - const mm = String(minutes).padStart(2, "0"); - const months = [ - "January", "February", "March", "April", "May", "June", - "July", "August", "September", "October", "November", "December" - ]; - const monthName = months[d.getUTCMonth()]; - const day = d.getUTCDate(); - return `${h12}:${mm}${ampm} GMT on ${monthName} ${day}`; - }; + // Format a GMT timestamp like "7:30AM GMT on November 3" + const formatGmtTimestamp = () => { + const d = new Date(); + const h24 = d.getUTCHours(); + const minutes = d.getUTCMinutes(); + const ampm = h24 >= 12 ? "PM" : "AM"; + let h12 = h24 % 12; + if (h12 === 0) h12 = 12; + const mm = String(minutes).padStart(2, "0"); + const months = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ]; + const monthName = months[d.getUTCMonth()]; + const day = d.getUTCDate(); + return `${h12}:${mm}${ampm} GMT on ${monthName} ${day}`; + }; - const parseIndex = (uri: URL, variables: Record) => { - const uriError = `Unknown resource: ${uri}`; - if (uri.toString() !== textUri && uri.toString() !== blobUri) { - throw new Error(uriError); - } - const idxStr = String((variables as any).index ?? ""); - const idx = Number(idxStr); - if (Number.isFinite(idx) && Number.isInteger(idx)) { - return idx; - } else { - throw new Error(uriError); - } - }; + const parseIndex = (uri: URL, variables: Record) => { + const uriError = `Unknown resource: ${uri.toString()}`; + if ( + uri.toString().startsWith(textUriBase) && + uri.toString().startsWith(blobUriBase) + ) { + throw new Error(uriError); + } else { + const idxStr = String((variables as any).index ?? ""); + const idx = Number(idxStr); + if (Number.isFinite(idx) && Number.isInteger(idx)) { + return idx; + } else { + throw new Error(uriError); + } + } + }; - // Text resource registration - server.registerResource( - "Dynamic Text Resource", - new ResourceTemplate(textUri, { list: undefined }), - { + // Text resource registration + server.registerResource( + "Dynamic Text Resource", + new ResourceTemplate(textUriTemplate, { list: undefined }), + { + mimeType: "text/plain", + description: + "Plaintext dynamic resource fabricated from the {index} variable, which must be an integer.", + }, + async (uri, variables) => { + const index = parseIndex(uri, variables); + return { + contents: [ + { + uri: uri.toString(), mimeType: "text/plain", - description: "Plaintext dynamic resource fabricated from the {index} variable, which must be an integer.", - }, - async (uri, variables) => { - const index = parseIndex(uri, variables); - return { - contents: [ - { - uri: uri.toString(), - mimeType: "text/plain", - text: `Resource ${index}: This is a plaintext resource created at ${formatGmtTimestamp()}`, - }, - ], - }; - } - ); + text: `Resource ${index}: This is a plaintext resource created at ${formatGmtTimestamp()}`, + }, + ], + }; + } + ); - // Blob resource registration - server.registerResource( - "Dynamic Blob Resource", - new ResourceTemplate(blobUri, { list: undefined }), - { + // Blob resource registration + server.registerResource( + "Dynamic Blob Resource", + new ResourceTemplate(blobUriTemplate, { list: undefined }), + { + mimeType: "application/octet-stream", + description: + "Binary (base64) dynamic resource fabricated from the {index} variable, which must be an integer.", + }, + async (uri, variables) => { + const index = parseIndex(uri, variables); + const buffer = Buffer.from( + `Resource ${index}: This is a base64 blob created at ${formatGmtTimestamp()}` + ); + return { + contents: [ + { + uri: uri.toString(), mimeType: "application/octet-stream", - description: "Binary (base64) dynamic resource fabricated from the {index} variable, which must be an integer.", - }, - async (uri, variables) => { - const index = parseIndex(uri, variables); - const buffer = Buffer.from(`Resource ${index}: This is a base64 blob created at ${formatGmtTimestamp()}`); - return { - contents: [ - { - uri: uri.toString(), - mimeType: "application/octet-stream", - blob: buffer.toString("base64"), - }, - ], - }; - } - ); + blob: buffer.toString("base64"), + }, + ], + }; + } + ); }; diff --git a/src/everything/resources/index.ts b/src/everything/resources/index.ts index 6f2c1b18..fbf9f328 100644 --- a/src/everything/resources/index.ts +++ b/src/everything/resources/index.ts @@ -1,11 +1,12 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { addDynamicResources } from "./dynamic.js"; - +import { addStaticResources } from "./static.js"; /** * Register the resources with the MCP server. * @param server */ export const registerResources = (server: McpServer) => { - addDynamicResources(server); + addDynamicResources(server); + addStaticResources(server); }; diff --git a/src/everything/resources/static.ts b/src/everything/resources/static.ts new file mode 100644 index 00000000..d2a8015d --- /dev/null +++ b/src/everything/resources/static.ts @@ -0,0 +1,78 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { dirname, join } from "path"; +import { fileURLToPath } from "url"; +import { readdirSync, readFileSync, statSync } from "fs"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +/** + * Register static resources for each file in the docs folder. + * + * - Each file in src/everything/docs is exposed as an individual static resource + * - URIs follow the pattern: "test://static/docs/" + * - Markdown files are served as text/markdown; others as text/plain + * + * @param server + */ +export const addStaticResources = (server: McpServer) => { + const docsDir = join(__dirname, "..", "docs"); + + let entries: string[] = []; + try { + entries = readdirSync(docsDir); + } catch (e) { + // If docs folder is missing or unreadable, just skip registration + return; + } + + for (const name of entries) { + const fullPath = join(docsDir, name); + try { + const st = statSync(fullPath); + if (!st.isFile()) continue; + } catch { + continue; + } + + const uri = `test://static/docs/${encodeURIComponent(name)}`; + const mimeType = getMimeType(name); + const displayName = `Docs: ${name}`; + const description = `Static documentation file exposed from /docs: ${name}`; + + server.registerResource( + displayName, + uri, + { mimeType, description }, + async (uri) => { + const text = readFileSafe(fullPath); + return { + contents: [ + { + uri: uri.toString(), + mimeType, + text, + }, + ], + }; + } + ); + } +}; + +function getMimeType(fileName: string): string { + const lower = fileName.toLowerCase(); + if (lower.endsWith(".md") || lower.endsWith(".markdown")) + return "text/markdown"; + if (lower.endsWith(".txt")) return "text/plain"; + if (lower.endsWith(".json")) return "application/json"; + return "text/plain"; +} + +function readFileSafe(path: string): string { + try { + return readFileSync(path, "utf-8"); + } catch (e) { + return `Error reading file: ${path}. ${e}`; + } +} diff --git a/src/everything/server/everything.ts b/src/everything/server/everything.ts index fece43df..7e686ea4 100644 --- a/src/everything/server/everything.ts +++ b/src/everything/server/everything.ts @@ -21,7 +21,7 @@ import { SubscribeRequestSchema, Tool, UnsubscribeRequestSchema, - type Root + type Root, } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; import { zodToJsonSchema } from "zod-to-json-schema"; @@ -37,7 +37,10 @@ const instructions = readFileSync(join(__dirname, "instructions.md"), "utf-8"); type ToolInput = Tool["inputSchema"]; type ToolOutput = Tool["outputSchema"]; -type SendRequest = RequestHandlerExtra["sendRequest"]; +type SendRequest = RequestHandlerExtra< + ServerRequest, + ServerNotification +>["sendRequest"]; /* Input schemas for tools implemented in this server */ const EchoSchema = z.object({ @@ -54,10 +57,7 @@ const LongRunningOperationSchema = z.object({ .number() .default(10) .describe("Duration of the operation in seconds"), - steps: z - .number() - .default(5) - .describe("Number of steps in the operation"), + steps: z.number().default(5).describe("Number of steps in the operation"), }); const PrintEnvSchema = z.object({}); @@ -105,28 +105,20 @@ const ListRootsSchema = z.object({}); const StructuredContentSchema = { input: z.object({ - location: z - .string() - .trim() - .min(1) - .describe("City name or zip code"), + location: z.string().trim().min(1).describe("City name or zip code"), }), output: z.object({ - temperature: z - .number() - .describe("Temperature in celsius"), - conditions: z - .string() - .describe("Weather conditions description"), - humidity: z - .number() - .describe("Humidity percentage"), - }) + temperature: z.number().describe("Temperature in celsius"), + conditions: z.string().describe("Weather conditions description"), + humidity: z.number().describe("Humidity percentage"), + }), }; const ZipResourcesInputSchema = z.object({ - files: z.record(z.string().url().describe("URL of the file to include in the zip")).describe("Mapping of file names to URLs to include in the zip"), + files: z + .record(z.string().url().describe("URL of the file to include in the zip")) + .describe("Mapping of file names to URLs to include in the zip"), }); enum ToolName { @@ -142,7 +134,7 @@ enum ToolName { GET_RESOURCE_LINKS = "getResourceLinks", STRUCTURED_CONTENT = "structuredContent", ZIP_RESOURCES = "zip", - LIST_ROOTS = "listRoots" + LIST_ROOTS = "listRoots", } enum PromptName { @@ -171,9 +163,9 @@ export const createServer = () => { resources: { subscribe: true }, tools: {}, logging: {}, - completions: {} + completions: {}, }, - instructions + instructions, } ); @@ -190,36 +182,48 @@ export const createServer = () => { let clientSupportsRoots = false; let sessionId: string | undefined; - // Function to start notification intervals when a client connects - const startNotificationIntervals = (sid?: string|undefined) => { - sessionId = sid; - if (!subsUpdateInterval) { - subsUpdateInterval = setInterval(() => { - for (const uri of subscriptions) { - server.notification({ - method: "notifications/resources/updated", - params: { uri }, - }); - } - }, 10000); - } + // Function to start notification intervals when a client connects + const startNotificationIntervals = (sid?: string | undefined) => { + sessionId = sid; + if (!subsUpdateInterval) { + subsUpdateInterval = setInterval(() => { + for (const uri of subscriptions) { + server.notification({ + method: "notifications/resources/updated", + params: { uri }, + }); + } + }, 10000); + } - const maybeAppendSessionId = sessionId ? ` - SessionId ${sessionId}`: ""; - const messages: { level: LoggingLevel; data: string }[] = [ - { level: "debug", data: `Debug-level message${maybeAppendSessionId}` }, - { level: "info", data: `Info-level message${maybeAppendSessionId}` }, - { level: "notice", data: `Notice-level message${maybeAppendSessionId}` }, - { level: "warning", data: `Warning-level message${maybeAppendSessionId}` }, - { level: "error", data: `Error-level message${maybeAppendSessionId}` }, - { level: "critical", data: `Critical-level message${maybeAppendSessionId}` }, - { level: "alert", data: `Alert level-message${maybeAppendSessionId}` }, - { level: "emergency", data: `Emergency-level message${maybeAppendSessionId}` }, - ]; + const maybeAppendSessionId = sessionId ? ` - SessionId ${sessionId}` : ""; + const messages: { level: LoggingLevel; data: string }[] = [ + { level: "debug", data: `Debug-level message${maybeAppendSessionId}` }, + { level: "info", data: `Info-level message${maybeAppendSessionId}` }, + { level: "notice", data: `Notice-level message${maybeAppendSessionId}` }, + { + level: "warning", + data: `Warning-level message${maybeAppendSessionId}`, + }, + { level: "error", data: `Error-level message${maybeAppendSessionId}` }, + { + level: "critical", + data: `Critical-level message${maybeAppendSessionId}`, + }, + { level: "alert", data: `Alert level-message${maybeAppendSessionId}` }, + { + level: "emergency", + data: `Emergency-level message${maybeAppendSessionId}`, + }, + ]; - if (!logsUpdateInterval) { - console.error("Starting logs update interval"); - logsUpdateInterval = setInterval(async () => { - await server.sendLoggingMessage( messages[Math.floor(Math.random() * messages.length)], sessionId); + if (!logsUpdateInterval) { + console.error("Starting logs update interval"); + logsUpdateInterval = setInterval(async () => { + await server.sendLoggingMessage( + messages[Math.floor(Math.random() * messages.length)], + sessionId + ); }, 15000); } }; @@ -251,7 +255,6 @@ export const createServer = () => { }; return await sendRequest(request, CreateMessageResultSchema); - }; const ALL_RESOURCES: Resource[] = Array.from({ length: 100 }, (_, i) => { @@ -514,31 +517,39 @@ export const createServer = () => { name: ToolName.STRUCTURED_CONTENT, description: "Returns structured content along with an output schema for client data validation", - inputSchema: zodToJsonSchema(StructuredContentSchema.input) as ToolInput, - outputSchema: zodToJsonSchema(StructuredContentSchema.output) as ToolOutput, + inputSchema: zodToJsonSchema( + StructuredContentSchema.input + ) as ToolInput, + outputSchema: zodToJsonSchema( + StructuredContentSchema.output + ) as ToolOutput, }, { name: ToolName.ZIP_RESOURCES, - description: "Compresses the provided resource files (mapping of name to URI, which can be a data URI) to a zip file, which it returns as a data URI resource link.", + description: + "Compresses the provided resource files (mapping of name to URI, which can be a data URI) to a zip file, which it returns as a data URI resource link.", inputSchema: zodToJsonSchema(ZipResourcesInputSchema) as ToolInput, - } + }, ]; - if (clientCapabilities!.roots) tools.push ({ + if (clientCapabilities!.roots) + tools.push({ name: ToolName.LIST_ROOTS, description: - "Lists the current MCP roots provided by the client. Demonstrates the roots protocol capability even though this server doesn't access files.", + "Lists the current MCP roots provided by the client. Demonstrates the roots protocol capability even though this server doesn't access files.", inputSchema: zodToJsonSchema(ListRootsSchema) as ToolInput, - }); - if (clientCapabilities!.elicitation) tools.push ({ + }); + if (clientCapabilities!.elicitation) + tools.push({ name: ToolName.ELICITATION, - description: "Elicitation test tool that demonstrates how to request user input with various field types (string, boolean, email, uri, date, integer, number, enum)", + description: + "Elicitation test tool that demonstrates how to request user input with various field types (string, boolean, email, uri, date, integer, number, enum)", inputSchema: zodToJsonSchema(ElicitationSchema) as ToolInput, - }); + }); return { tools }; }); - server.setRequestHandler(CallToolRequestSchema, async (request,extra) => { + server.setRequestHandler(CallToolRequestSchema, async (request, extra) => { const { name, arguments: args } = request.params; if (name === ToolName.ECHO) { @@ -573,14 +584,17 @@ export const createServer = () => { ); if (progressToken !== undefined) { - await server.notification({ - method: "notifications/progress", - params: { - progress: i, - total: steps, - progressToken, + await server.notification( + { + method: "notifications/progress", + params: { + progress: i, + total: steps, + progressToken, + }, }, - },{relatedRequestId: extra.requestId}); + { relatedRequestId: extra.requestId } + ); } } @@ -617,7 +631,20 @@ export const createServer = () => { ); return { content: [ - { type: "text", text: `LLM sampling result: ${Array.isArray(result.content) ? result.content.map(c => c.type === "text" ? c.text : JSON.stringify(c)).join("") : (result.content.type === "text" ? result.content.text : JSON.stringify(result.content))}` }, + { + type: "text", + text: `LLM sampling result: ${ + Array.isArray(result.content) + ? result.content + .map((c) => + c.type === "text" ? c.text : JSON.stringify(c) + ) + .join("") + : result.content.type === "text" + ? result.content.text + : JSON.stringify(result.content) + }`, + }, ], }; } @@ -726,80 +753,87 @@ export const createServer = () => { if (name === ToolName.ELICITATION) { ElicitationSchema.parse(args); - const elicitationResult = await extra.sendRequest({ - method: 'elicitation/create', - params: { - message: 'Please provide inputs for the following fields:', - requestedSchema: { - type: 'object', - properties: { - name: { - title: 'Full Name', - type: 'string', - description: 'Your full, legal name', - }, - check: { - title: 'Agree to terms', - type: 'boolean', - description: 'A boolean check', - }, - color: { - title: 'Favorite Color', - type: 'string', - description: 'Favorite color (open text)', - default: 'blue', - }, - email: { - title: 'Email Address', - type: 'string', - format: 'email', - description: 'Your email address (will be verified, and never shared with anyone else)', - }, - homepage: { - type: 'string', - format: 'uri', - description: 'Homepage / personal site', - }, - birthdate: { - title: 'Birthdate', - type: 'string', - format: 'date', - description: 'Your date of birth (will never be shared with anyone else)', - }, - integer: { - title: 'Favorite Integer', - type: 'integer', - description: 'Your favorite integer (do not give us your phone number, pin, or other sensitive info)', - minimum: 1, - maximum: 100, - default: 42, - }, - number: { - title: 'Favorite Number', - type: 'number', - description: 'Favorite number (there are no wrong answers)', - minimum: 0, - maximum: 1000, - default: 3.14, - }, - petType: { - title: 'Pet type', - type: 'string', - enum: ['cats', 'dogs', 'birds', 'fish', 'reptiles'], - enumNames: ['Cats', 'Dogs', 'Birds', 'Fish', 'Reptiles'], - default: 'dogs', - description: 'Your favorite pet type', + const elicitationResult = await extra.sendRequest( + { + method: "elicitation/create", + params: { + message: "Please provide inputs for the following fields:", + requestedSchema: { + type: "object", + properties: { + name: { + title: "Full Name", + type: "string", + description: "Your full, legal name", + }, + check: { + title: "Agree to terms", + type: "boolean", + description: "A boolean check", + }, + color: { + title: "Favorite Color", + type: "string", + description: "Favorite color (open text)", + default: "blue", + }, + email: { + title: "Email Address", + type: "string", + format: "email", + description: + "Your email address (will be verified, and never shared with anyone else)", + }, + homepage: { + type: "string", + format: "uri", + description: "Homepage / personal site", + }, + birthdate: { + title: "Birthdate", + type: "string", + format: "date", + description: + "Your date of birth (will never be shared with anyone else)", + }, + integer: { + title: "Favorite Integer", + type: "integer", + description: + "Your favorite integer (do not give us your phone number, pin, or other sensitive info)", + minimum: 1, + maximum: 100, + default: 42, + }, + number: { + title: "Favorite Number", + type: "number", + description: "Favorite number (there are no wrong answers)", + minimum: 0, + maximum: 1000, + default: 3.14, + }, + petType: { + title: "Pet type", + type: "string", + enum: ["cats", "dogs", "birds", "fish", "reptiles"], + enumNames: ["Cats", "Dogs", "Birds", "Fish", "Reptiles"], + default: "dogs", + description: "Your favorite pet type", + }, }, + required: ["name"], }, - required: ['name'], }, }, - }, ElicitResultSchema, { timeout: 10 * 60 * 1000 /* 10 minutes */ }); + ElicitResultSchema, + { timeout: 10 * 60 * 1000 /* 10 minutes */ } + ); // Handle different response actions const content = []; - if (elicitationResult.action === 'accept' && elicitationResult.content) { + if (elicitationResult.action === "accept" && elicitationResult.content) { content.push({ type: "text", text: `✅ User provided the requested information!`, @@ -809,25 +843,29 @@ export const createServer = () => { const userData = elicitationResult.content; const lines = []; if (userData.name) lines.push(`- Name: ${userData.name}`); - if (userData.check !== undefined) lines.push(`- Agreed to terms: ${userData.check}`); + if (userData.check !== undefined) + lines.push(`- Agreed to terms: ${userData.check}`); if (userData.color) lines.push(`- Favorite Color: ${userData.color}`); if (userData.email) lines.push(`- Email: ${userData.email}`); if (userData.homepage) lines.push(`- Homepage: ${userData.homepage}`); - if (userData.birthdate) lines.push(`- Birthdate: ${userData.birthdate}`); - if (userData.integer !== undefined) lines.push(`- Favorite Integer: ${userData.integer}`); - if (userData.number !== undefined) lines.push(`- Favorite Number: ${userData.number}`); + if (userData.birthdate) + lines.push(`- Birthdate: ${userData.birthdate}`); + if (userData.integer !== undefined) + lines.push(`- Favorite Integer: ${userData.integer}`); + if (userData.number !== undefined) + lines.push(`- Favorite Number: ${userData.number}`); if (userData.petType) lines.push(`- Pet Type: ${userData.petType}`); content.push({ type: "text", - text: `User inputs:\n${lines.join('\n')}`, + text: `User inputs:\n${lines.join("\n")}`, }); - } else if (elicitationResult.action === 'decline') { + } else if (elicitationResult.action === "decline") { content.push({ type: "text", text: `❌ User declined to provide the requested information.`, }); - } else if (elicitationResult.action === 'cancel') { + } else if (elicitationResult.action === "cancel") { content.push({ type: "text", text: `⚠️ User cancelled the elicitation dialog.`, @@ -861,10 +899,11 @@ export const createServer = () => { type: "resource_link", uri: resource.uri, name: resource.name, - description: `Resource ${i + 1}: ${resource.mimeType === "text/plain" - ? "plaintext resource" - : "binary blob resource" - }`, + description: `Resource ${i + 1}: ${ + resource.mimeType === "text/plain" + ? "plaintext resource" + : "binary blob resource" + }`, mimeType: resource.mimeType, }); } @@ -879,17 +918,17 @@ export const createServer = () => { const weather = { temperature: 22.5, conditions: "Partly cloudy", - humidity: 65 - } + humidity: 65, + }; const backwardCompatiblecontent = { type: "text", - text: JSON.stringify(weather) - } + text: JSON.stringify(weather), + }; return { content: [backwardCompatiblecontent], - structuredContent: weather + structuredContent: weather, }; } @@ -902,16 +941,24 @@ export const createServer = () => { try { const response = await fetch(fileUrl); if (!response.ok) { - throw new Error(`Failed to fetch ${fileUrl}: ${response.statusText}`); + throw new Error( + `Failed to fetch ${fileUrl}: ${response.statusText}` + ); } const arrayBuffer = await response.arrayBuffer(); zip.file(fileName, arrayBuffer); } catch (error) { - throw new Error(`Error fetching file ${fileUrl}: ${error instanceof Error ? error.message : String(error)}`); + throw new Error( + `Error fetching file ${fileUrl}: ${ + error instanceof Error ? error.message : String(error) + }` + ); } } - const uri = `data:application/zip;base64,${await zip.generateAsync({ type: "base64" })}`; + const uri = `data:application/zip;base64,${await zip.generateAsync({ + type: "base64", + })}`; return { content: [ @@ -932,10 +979,11 @@ export const createServer = () => { content: [ { type: "text", - text: "The MCP client does not support the roots protocol.\n\n" + - "This means the server cannot access information about the client's workspace directories or file system roots." - } - ] + text: + "The MCP client does not support the roots protocol.\n\n" + + "This means the server cannot access information about the client's workspace directories or file system roots.", + }, + ], }; } @@ -944,29 +992,35 @@ export const createServer = () => { content: [ { type: "text", - text: "The client supports roots but no roots are currently configured.\n\n" + + text: + "The client supports roots but no roots are currently configured.\n\n" + "This could mean:\n" + "1. The client hasn't provided any roots yet\n" + "2. The client provided an empty roots list\n" + - "3. The roots configuration is still being loaded" - } - ] + "3. The roots configuration is still being loaded", + }, + ], }; } - const rootsList = currentRoots.map((root, index) => { - return `${index + 1}. ${root.name || 'Unnamed Root'}\n URI: ${root.uri}`; - }).join('\n\n'); + const rootsList = currentRoots + .map((root, index) => { + return `${index + 1}. ${root.name || "Unnamed Root"}\n URI: ${ + root.uri + }`; + }) + .join("\n\n"); return { content: [ { type: "text", - text: `Current MCP Roots (${currentRoots.length} total):\n\n${rootsList}\n\n` + + text: + `Current MCP Roots (${currentRoots.length} total):\n\n${rootsList}\n\n` + "Note: This server demonstrates the roots protocol capability but doesn't actually access files. " + - "The roots are provided by the MCP client and can be used by servers that need file system access." - } - ] + "The roots are provided by the MCP client and can be used by servers that need file system access.", + }, + ], }; } @@ -1003,65 +1057,90 @@ export const createServer = () => { }); // Roots protocol handlers - server.setNotificationHandler(RootsListChangedNotificationSchema, async () => { - try { - // Request the updated roots list from the client - const response = await server.listRoots(); - if (response && 'roots' in response) { - currentRoots = response.roots; + server.setNotificationHandler( + RootsListChangedNotificationSchema, + async () => { + try { + // Request the updated roots list from the client + const response = await server.listRoots(); + if (response && "roots" in response) { + currentRoots = response.roots; - // Log the roots update for demonstration - await server.sendLoggingMessage({ - level: "info", + // Log the roots update for demonstration + await server.sendLoggingMessage( + { + level: "info", + logger: "everything-server", + data: `Roots updated: ${currentRoots.length} root(s) received from client`, + }, + sessionId + ); + } + } catch (error) { + await server.sendLoggingMessage( + { + level: "error", logger: "everything-server", - data: `Roots updated: ${currentRoots.length} root(s) received from client`, - }, sessionId); + data: `Failed to request roots from client: ${ + error instanceof Error ? error.message : String(error) + }`, + }, + sessionId + ); } - } catch (error) { - await server.sendLoggingMessage({ - level: "error", - logger: "everything-server", - data: `Failed to request roots from client: ${error instanceof Error ? error.message : String(error)}`, - }, sessionId); } - }); + ); // Handle post-initialization setup for roots server.oninitialized = async () => { - clientCapabilities = server.getClientCapabilities(); + clientCapabilities = server.getClientCapabilities(); if (clientCapabilities?.roots) { clientSupportsRoots = true; try { const response = await server.listRoots(); - if (response && 'roots' in response) { + if (response && "roots" in response) { currentRoots = response.roots; - await server.sendLoggingMessage({ + await server.sendLoggingMessage( + { level: "info", logger: "everything-server", data: `Initial roots received: ${currentRoots.length} root(s) from client`, - }, sessionId); + }, + sessionId + ); } else { - await server.sendLoggingMessage({ + await server.sendLoggingMessage( + { level: "warning", logger: "everything-server", data: "Client returned no roots set", - }, sessionId); + }, + sessionId + ); } } catch (error) { - await server.sendLoggingMessage({ + await server.sendLoggingMessage( + { level: "error", logger: "everything-server", - data: `Failed to request initial roots from client: ${error instanceof Error ? error.message : String(error)}`, - }, sessionId); + data: `Failed to request initial roots from client: ${ + error instanceof Error ? error.message : String(error) + }`, + }, + sessionId + ); } } else { - await server.sendLoggingMessage({ + await server.sendLoggingMessage( + { level: "info", logger: "everything-server", data: "Client does not support MCP roots protocol", - }, sessionId); + }, + sessionId + ); } }; diff --git a/src/everything/server/index.ts b/src/everything/server/index.ts index f03a0c96..a4349144 100644 --- a/src/everything/server/index.ts +++ b/src/everything/server/index.ts @@ -10,45 +10,48 @@ const instructions = readInstructions(); // Create the MCP resource server export const createServer = () => { - const server = new McpServer( - { - name: "mcp-servers/everything", - title: "Everything Reference Server", - version: "2.0.0", + const server = new McpServer( + { + name: "mcp-servers/everything", + title: "Everything Reference Server", + version: "2.0.0", + }, + { + capabilities: { + tools: {}, + logging: {}, + prompts: {}, + resources: { + subscribe: true, }, - { - capabilities: { - tools: {}, - logging: {}, - prompts: {}, - resources: { - subscribe: true, - } - }, - instructions, - }, - ); + }, + instructions, + } + ); - // Register the tools - registerTools(server); + // Register the tools + registerTools(server); - // Register the resources - registerResources(server); + // Register the resources + registerResources(server); - return { - server, - cleanup: () => {}, - startNotificationIntervals: (sessionId?: string) => {} - }; + return { + server, + cleanup: () => {}, + startNotificationIntervals: (sessionId?: string) => {}, + }; }; function readInstructions(): string { - let instructions; + let instructions; - try { - instructions = readFileSync(join(__dirname, "../instructions.md"), "utf-8"); - } catch (e) { - instructions = "Server instructions not loaded: " + e; - } - return instructions; + try { + instructions = readFileSync( + join(__dirname, "..", "docs", "server-instructions.md"), + "utf-8" + ); + } catch (e) { + instructions = "Server instructions not loaded: " + e; + } + return instructions; } diff --git a/src/everything/tools/echo.ts b/src/everything/tools/echo.ts index 166d43ff..87f351ef 100644 --- a/src/everything/tools/echo.ts +++ b/src/everything/tools/echo.ts @@ -3,21 +3,21 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; export const EchoSchema = z.object({ - message: z.string().describe("Message to echo"), + message: z.string().describe("Message to echo"), }); const name = "echo"; const config = { - title: "Echo Tool", - description: "Echoes back the input string", - inputSchema: EchoSchema, + title: "Echo Tool", + description: "Echoes back the input string", + inputSchema: EchoSchema, }; export const addToolEcho = (server: McpServer) => { - server.registerTool(name, config, async (args): Promise => { - const validatedArgs = EchoSchema.parse(args); - return { - content: [{ type: "text", text: `Echo: ${validatedArgs.message}` }], - }; - }); + server.registerTool(name, config, async (args): Promise => { + const validatedArgs = EchoSchema.parse(args); + return { + content: [{ type: "text", text: `Echo: ${validatedArgs.message}` }], + }; + }); }; diff --git a/src/everything/tools/index.ts b/src/everything/tools/index.ts index 3e58c34f..380b7900 100644 --- a/src/everything/tools/index.ts +++ b/src/everything/tools/index.ts @@ -1,7 +1,6 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { addToolEcho } from "./echo.js"; - /** * Register the tools with the MCP server. * @param server diff --git a/src/everything/transports/sse.ts b/src/everything/transports/sse.ts index 26cb8efb..68690c0b 100644 --- a/src/everything/transports/sse.ts +++ b/src/everything/transports/sse.ts @@ -1,27 +1,35 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import express from "express"; import { createServer } from "../server/index.js"; -import cors from 'cors'; +import cors from "cors"; -console.error('Starting SSE server...'); +console.error("Starting SSE server..."); const app = express(); -app.use(cors({ - "origin": "*", // use "*" with caution in production - "methods": "GET,POST", - "preflightContinue": false, - "optionsSuccessStatus": 204, -})); // Enable CORS for all routes so Inspector can connect -const transports: Map = new Map(); +app.use( + cors({ + origin: "*", // use "*" with caution in production + methods: "GET,POST", + preflightContinue: false, + optionsSuccessStatus: 204, + }) +); // Enable CORS for all routes so Inspector can connect +const transports: Map = new Map< + string, + SSEServerTransport +>(); app.get("/sse", async (req, res) => { let transport: SSEServerTransport; const { server, cleanup, startNotificationIntervals } = createServer(); if (req?.query?.sessionId) { - const sessionId = (req?.query?.sessionId as string); + const sessionId = req?.query?.sessionId as string; transport = transports.get(sessionId) as SSEServerTransport; - console.error("Client Reconnecting? This shouldn't happen; when client has a sessionId, GET /sse should not be called again.", transport.sessionId); + console.error( + "Client Reconnecting? This shouldn't happen; when client has a sessionId, GET /sse should not be called again.", + transport.sessionId + ); } else { // Create and store transport for new session transport = new SSEServerTransport("/message", res); @@ -40,19 +48,17 @@ app.get("/sse", async (req, res) => { transports.delete(transport.sessionId); await cleanup(); }; - } - }); app.post("/message", async (req, res) => { - const sessionId = (req?.query?.sessionId as string); + const sessionId = req?.query?.sessionId as string; const transport = transports.get(sessionId); if (transport) { console.error("Client Message from", sessionId); await transport.handlePostMessage(req, res); } else { - console.error(`No transport found for sessionId ${sessionId}`) + console.error(`No transport found for sessionId ${sessionId}`); } }); diff --git a/src/everything/transports/streamableHttp.ts b/src/everything/transports/streamableHttp.ts index 526db127..96a7be4c 100644 --- a/src/everything/transports/streamableHttp.ts +++ b/src/everything/transports/streamableHttp.ts @@ -9,198 +9,198 @@ console.log("Starting Streamable HTTP server..."); const app = express(); app.use( - cors({ - origin: "*", // use "*" with caution in production - methods: "GET,POST,DELETE", - preflightContinue: false, - optionsSuccessStatus: 204, - exposedHeaders: ["mcp-session-id", "last-event-id", "mcp-protocol-version"], - }), + cors({ + origin: "*", // use "*" with caution in production + methods: "GET,POST,DELETE", + preflightContinue: false, + optionsSuccessStatus: 204, + exposedHeaders: ["mcp-session-id", "last-event-id", "mcp-protocol-version"], + }) ); // Enable CORS for all routes so Inspector can connect const transports: Map = new Map< - string, - StreamableHTTPServerTransport + string, + StreamableHTTPServerTransport >(); app.post("/mcp", async (req: Request, res: Response) => { - console.log("Received MCP POST request"); - try { - // Check for existing session ID - const sessionId = req.headers["mcp-session-id"] as string | undefined; + console.log("Received MCP POST request"); + try { + // Check for existing session ID + const sessionId = req.headers["mcp-session-id"] as string | undefined; - let transport: StreamableHTTPServerTransport; + let transport: StreamableHTTPServerTransport; - if (sessionId && transports.has(sessionId)) { - // Reuse existing transport - transport = transports.get(sessionId)!; - } else if (!sessionId) { - const { server } = createServer(); + if (sessionId && transports.has(sessionId)) { + // Reuse existing transport + transport = transports.get(sessionId)!; + } else if (!sessionId) { + const { server } = createServer(); - // New initialization request - const eventStore = new InMemoryEventStore(); - transport = new StreamableHTTPServerTransport({ - sessionIdGenerator: () => randomUUID(), - eventStore, // Enable resumability - onsessioninitialized: (sessionId: string) => { - // Store the transport by session ID when session is initialized - // This avoids race conditions where requests might come in before the session is stored - console.log(`Session initialized with ID: ${sessionId}`); - transports.set(sessionId, transport); - }, - }); + // New initialization request + const eventStore = new InMemoryEventStore(); + transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + eventStore, // Enable resumability + onsessioninitialized: (sessionId: string) => { + // Store the transport by session ID when session is initialized + // This avoids race conditions where requests might come in before the session is stored + console.log(`Session initialized with ID: ${sessionId}`); + transports.set(sessionId, transport); + }, + }); - // Set up onclose handler to clean up transport when closed - server.server.onclose = async () => { - const sid = transport.sessionId; - if (sid && transports.has(sid)) { - console.log( - `Transport closed for session ${sid}, removing from transports map`, - ); - transports.delete(sid); - } - }; - - // Connect the transport to the MCP server BEFORE handling the request - // so responses can flow back through the same transport - await server.connect(transport); - - await transport.handleRequest(req, res); - - return; - } else { - // Invalid request - no session ID or not initialization request - res.status(400).json({ - jsonrpc: "2.0", - error: { - code: -32000, - message: "Bad Request: No valid session ID provided", - }, - id: req?.body?.id, - }); - return; + // Set up onclose handler to clean up transport when closed + server.server.onclose = async () => { + const sid = transport.sessionId; + if (sid && transports.has(sid)) { + console.log( + `Transport closed for session ${sid}, removing from transports map` + ); + transports.delete(sid); } + }; - // Handle the request with existing transport - no need to reconnect - // The existing transport is already connected to the server - await transport.handleRequest(req, res); - } catch (error) { - console.log("Error handling MCP request:", error); - if (!res.headersSent) { - res.status(500).json({ - jsonrpc: "2.0", - error: { - code: -32603, - message: "Internal server error", - }, - id: req?.body?.id, - }); - return; - } + // Connect the transport to the MCP server BEFORE handling the request + // so responses can flow back through the same transport + await server.connect(transport); + + await transport.handleRequest(req, res); + + return; + } else { + // Invalid request - no session ID or not initialization request + res.status(400).json({ + jsonrpc: "2.0", + error: { + code: -32000, + message: "Bad Request: No valid session ID provided", + }, + id: req?.body?.id, + }); + return; } + + // Handle the request with existing transport - no need to reconnect + // The existing transport is already connected to the server + await transport.handleRequest(req, res); + } catch (error) { + console.log("Error handling MCP request:", error); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: "2.0", + error: { + code: -32603, + message: "Internal server error", + }, + id: req?.body?.id, + }); + return; + } + } }); // Handle GET requests for SSE streams (using built-in support from StreamableHTTP) app.get("/mcp", async (req: Request, res: Response) => { - console.log("Received MCP GET request"); - const sessionId = req.headers["mcp-session-id"] as string | undefined; - if (!sessionId || !transports.has(sessionId)) { - res.status(400).json({ - jsonrpc: "2.0", - error: { - code: -32000, - message: "Bad Request: No valid session ID provided", - }, - id: req?.body?.id, - }); - return; - } + console.log("Received MCP GET request"); + const sessionId = req.headers["mcp-session-id"] as string | undefined; + if (!sessionId || !transports.has(sessionId)) { + res.status(400).json({ + jsonrpc: "2.0", + error: { + code: -32000, + message: "Bad Request: No valid session ID provided", + }, + id: req?.body?.id, + }); + return; + } - // Check for Last-Event-ID header for resumability - const lastEventId = req.headers["last-event-id"] as string | undefined; - if (lastEventId) { - console.log(`Client reconnecting with Last-Event-ID: ${lastEventId}`); - } else { - console.log(`Establishing new SSE stream for session ${sessionId}`); - } + // Check for Last-Event-ID header for resumability + const lastEventId = req.headers["last-event-id"] as string | undefined; + if (lastEventId) { + console.log(`Client reconnecting with Last-Event-ID: ${lastEventId}`); + } else { + console.log(`Establishing new SSE stream for session ${sessionId}`); + } - const transport = transports.get(sessionId); - await transport!.handleRequest(req, res); + const transport = transports.get(sessionId); + await transport!.handleRequest(req, res); }); // Handle DELETE requests for session termination (according to MCP spec) app.delete("/mcp", async (req: Request, res: Response) => { - const sessionId = req.headers["mcp-session-id"] as string | undefined; - if (!sessionId || !transports.has(sessionId)) { - res.status(400).json({ - jsonrpc: "2.0", - error: { - code: -32000, - message: "Bad Request: No valid session ID provided", - }, - id: req?.body?.id, - }); - return; - } + const sessionId = req.headers["mcp-session-id"] as string | undefined; + if (!sessionId || !transports.has(sessionId)) { + res.status(400).json({ + jsonrpc: "2.0", + error: { + code: -32000, + message: "Bad Request: No valid session ID provided", + }, + id: req?.body?.id, + }); + return; + } - console.log(`Received session termination request for session ${sessionId}`); + console.log(`Received session termination request for session ${sessionId}`); - try { - const transport = transports.get(sessionId); - await transport!.handleRequest(req, res); - } catch (error) { - console.log("Error handling session termination:", error); - if (!res.headersSent) { - res.status(500).json({ - jsonrpc: "2.0", - error: { - code: -32603, - message: "Error handling session termination", - }, - id: req?.body?.id, - }); - return; - } + try { + const transport = transports.get(sessionId); + await transport!.handleRequest(req, res); + } catch (error) { + console.log("Error handling session termination:", error); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: "2.0", + error: { + code: -32603, + message: "Error handling session termination", + }, + id: req?.body?.id, + }); + return; } + } }); // Start the server const PORT = process.env.PORT || 3001; const server = app.listen(PORT, () => { - console.error(`MCP Streamable HTTP Server listening on port ${PORT}`); + console.error(`MCP Streamable HTTP Server listening on port ${PORT}`); }); server.on("error", (err: unknown) => { - const code = - typeof err === "object" && err !== null && "code" in err - ? (err as { code?: unknown }).code - : undefined; - if (code === "EADDRINUSE") { - console.error( - `Failed to start: Port ${PORT} is already in use. Set PORT to a free port or stop the conflicting process.`, - ); - } else { - console.error("HTTP server encountered an error while starting:", err); - } - // Ensure a non-zero exit so npm reports the failure instead of silently exiting - process.exit(1); + const code = + typeof err === "object" && err !== null && "code" in err + ? (err as { code?: unknown }).code + : undefined; + if (code === "EADDRINUSE") { + console.error( + `Failed to start: Port ${PORT} is already in use. Set PORT to a free port or stop the conflicting process.` + ); + } else { + console.error("HTTP server encountered an error while starting:", err); + } + // Ensure a non-zero exit so npm reports the failure instead of silently exiting + process.exit(1); }); // Handle server shutdown process.on("SIGINT", async () => { - console.log("Shutting down server..."); + console.log("Shutting down server..."); - // Close all active transports to properly clean up resources - for (const sessionId in transports) { - try { - console.log(`Closing transport for session ${sessionId}`); - await transports.get(sessionId)!.close(); - transports.delete(sessionId); - } catch (error) { - console.log(`Error closing transport for session ${sessionId}:`, error); - } + // Close all active transports to properly clean up resources + for (const sessionId in transports) { + try { + console.log(`Closing transport for session ${sessionId}`); + await transports.get(sessionId)!.close(); + transports.delete(sessionId); + } catch (error) { + console.log(`Error closing transport for session ${sessionId}:`, error); } + } - console.log("Server shutdown complete"); - process.exit(0); + console.log("Server shutdown complete"); + process.exit(0); }); diff --git a/src/everything/tsconfig.json b/src/everything/tsconfig.json index ec5da158..829d52d6 100644 --- a/src/everything/tsconfig.json +++ b/src/everything/tsconfig.json @@ -4,7 +4,5 @@ "outDir": "./dist", "rootDir": "." }, - "include": [ - "./**/*.ts" - ] + "include": ["./**/*.ts"] }