mirror of
https://github.com/modelcontextprotocol/servers.git
synced 2026-04-17 21:54:05 +02:00
[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:
19
package-lock.json
generated
19
package-lock.json
generated
@@ -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"
|
||||
}
|
||||
|
||||
4
src/everything/.prettierignore
Normal file
4
src/everything/.prettierignore
Normal file
@@ -0,0 +1,4 @@
|
||||
packages
|
||||
dist
|
||||
README.md
|
||||
node_modules
|
||||
@@ -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
|
||||
|
||||
163
src/everything/docs/architecture.md
Normal file
163
src/everything/docs/architecture.md
Normal 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.
|
||||
|
||||
## 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<sessionId, SSEServerTransport>` 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/<filename>`.
|
||||
- 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/<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.
|
||||
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
78
src/everything/resources/static.ts
Normal file
78
src/everything/resources/static.ts
Normal 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}`;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}` }],
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -4,7 +4,5 @@
|
||||
"outDir": "./dist",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
]
|
||||
"include": ["./**/*.ts"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user