[WIP] Refactor everything server to be more modular and use recommended APIs.

* Adding static resources, move server instructions to
the new docs folder, and add code formatting

* Add docs folder

* Add docs/architecture.md which describes the architecture of the project thus far.

* Refactor moved instructions.md to docs/server-instructions.md

* Add resources/static.ts
  - in addStaticResources()
    - read the file entries from the docs folder
    - register each file as a resource (no template), with a readResource function that reads the file and returns it in a contents block with the appropriate mime type and contents
  - getMimeType helper function gets the mime type for a filename
  - readSafe helper function reads the file synchronously as utf-8 or returns an error string

* Add resources/index.ts
  - import addStaticResources
  - export registerResources function
  - in registerResources()
    - call addStaticResources

* In package.json
  - add prettier devDependency
  - add prettier:check script
  - add prettier:fix script
  - in build script, copy docs folder to dist

* All other changes were prettier formatting
This commit is contained in:
cliffhall
2025-12-05 13:26:08 -05:00
parent 8845118d61
commit 1c64b36c78
17 changed files with 904 additions and 527 deletions

19
package-lock.json generated
View File

@@ -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"
}

View File

@@ -0,0 +1,4 @@
packages
dist
README.md
node_modules

View File

@@ -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
- Use consistent indentation (2 spaces) and trailing commas in multi-line objects

View File

@@ -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.
## Highlevel 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<sessionId, SSEServerTransport>` for sessions. Calls `startNotificationIntervals(sessionId)` after connect (hook currently a noop in the factory).
- streamableHttp.ts
- Express server exposing a single `/mcp` endpoint for POST (JSONRPC), 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, templatedriven 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/<filename>`.
- Serves markdown files as `text/markdown`, `.txt` as `text/plain`, `.json` as `application/json`, others default to `text/plain`.
- docs/
- server-instructions.md
- Humanreadable 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 humanreadable “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 noop).
- `startNotificationIntervals(sessionId?)`: currently a noop; wired in SSE transport for future periodic notifications.
4. Each transport is responsible for network/session lifecycle:
- STDIO: simple processbound connection; closes on `SIGINT`.
- SSE: maintains a session map keyed by `sessionId`, hooks servers `onclose` to clean and remove session, exposes `/sse` (GET) and `/message` (POST) endpoints.
- Streamable HTTP: exposes `/mcp` for POST (JSONRPC 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/<filename>` (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.

View File

@@ -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();

View File

@@ -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"
}
}

View File

@@ -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<string, unknown>) => {
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<string, unknown>) => {
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"),
},
],
};
}
);
};

View File

@@ -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);
};

View File

@@ -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/<filename>"
* - 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}`;
}
}

View File

@@ -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<ServerRequest, ServerNotification>["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
);
}
};

View File

@@ -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;
}

View File

@@ -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<CallToolResult> => {
const validatedArgs = EchoSchema.parse(args);
return {
content: [{ type: "text", text: `Echo: ${validatedArgs.message}` }],
};
});
server.registerTool(name, config, async (args): Promise<CallToolResult> => {
const validatedArgs = EchoSchema.parse(args);
return {
content: [{ type: "text", text: `Echo: ${validatedArgs.message}` }],
};
});
};

View File

@@ -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

View File

@@ -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<string, SSEServerTransport> = new Map<string, SSEServerTransport>();
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<string, SSEServerTransport> = 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}`);
}
});

View File

@@ -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<string, StreamableHTTPServerTransport> = 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);
});

View File

@@ -4,7 +4,5 @@
"outDir": "./dist",
"rootDir": "."
},
"include": [
"./**/*.ts"
]
"include": ["./**/*.ts"]
}