diff --git a/src/everything/docs/architecture.md b/src/everything/docs/architecture.md index 3ccbc097..5aef4b52 100644 --- a/src/everything/docs/architecture.md +++ b/src/everything/docs/architecture.md @@ -1,306 +1,27 @@ -# Everything Server – Architecture and Layout +# Everything Server – Architecture +**Architecture +| [Project Structure](structure.md) +| [Startup Process](startup.md) +| [Server Features](features.md) +| [Extension Points](extension.md) +| [How It Works](how-it-works.md)** -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, prompts, and resources are registered, and how to extend the system. +This documentation summarizes the current layout and runtime architecture of the `src/everything` package. +It explains how the server starts, how transports are wired, where tools, prompts, 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, several prompts, 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, prompts, and resources are organized in their own submodules. -- 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, prompts, and resources are organized in their own submodules. Simulated logging and resource‑update notifications are opt‑in and controlled by tools. -- Two server implementations exist: +### Purpose +A minimal, modular MCP server showcasing core Model Context Protocol features. It exposes simple tools, prompts, and resources, and can be run over multiple transports (STDIO, SSE, and Streamable HTTP). - - `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. +### 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, prompts, and resources are organized in their own submodules. -- Multi‑client subscriptions: The server supports multiple concurrent clients. Each client manages its own resource subscriptions and receives notifications only for the URIs it subscribed to, independent of other clients. - -## Directory Layout - -``` -src/everything -├── index.ts -├── docs -│ ├── architecture.md -│ └── server-instructions.md -├── prompts -│ ├── index.ts -│ ├── args.ts -│ ├── completions.ts -│ ├── simple.ts -│ └── resource.ts -├── resources -│ ├── index.ts -│ ├── files.ts -│ ├── session.ts -│ ├── subscriptions.ts -│ └── templates.ts -├── server -│ ├── index.ts -│ ├── logging.ts -│ ├── roots.ts -│ └── everything.ts -├── transports -│ ├── sse.ts -│ ├── stdio.ts -│ └── streamableHttp.ts -├── tools -│ ├── index.ts -│ ├── echo.ts -│ ├── get-annotated-message.ts -│ ├── get-env.ts -│ ├── get-tiny-image.ts -│ ├── get-resource-links.ts -│ ├── get-resource-reference.ts -│ ├── get-roots-list.ts -│ ├── get-structured-content.ts -│ ├── get-sum.ts -│ ├── gzip-file-as-resource.ts -│ ├── long-running-operation.ts -│ ├── toggle-logging.ts -│ ├── toggle-subscriber-updates.ts -│ ├── trigger-elicitation-request.ts -│ └── trigger-sampling-request.ts -└── 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, prompts, and resources. - - Sets resource subscription handlers via `setSubscriptionHandlers(server)`. - - Exposes `{ server, cleanup }` to the chosen transport. Cleanup stops any running intervals in the server when the transport disconencts. - - logging.ts - - Implements simulated logging. Periodically sends randomized log messages at various levels to the connected client session. Started/stopped on demand via a dedicated tool. - - 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`, created the server via `createServer()`, and connects it. - - Calls `clientConnected()` to inform the server of the connection. - - Handles `SIGINT` to close cleanly and calls `cleanup()` to remove any live intervals. - - sse.ts - - Express server exposing: - - `GET /sse` to establish an SSE connection per session. - - `POST /message` for client messages. - - Manages multiple connected clients via a transport map. - - Starts an `SSEServerTransport`, created the server via `createServer()`, and connects it to a new transport. - - Calls `clientConnected(sessionId)` to inform the server of the connection. - - On server disconnect, calls `cleanup()` to remove any live intervals. - - 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 and reuses the transport for subsequent requests. - - Calls `clientConnected(sessionId)` to inform the server of the connection. - -- tools/ - - - index.ts - - `registerTools(server)` orchestrator; delegates to tool factory/registration methods in individual tool files. - - echo.ts - - Registers an `echo` tool that takes a message and returns `Echo: {message}`. - - get-annotated-message.ts - - Registers an `annotated-message` tool which demonstrates annotated content items by emitting a primary `text` message with `annotations` that vary by `messageType` (`"error" | "success" | "debug"`), and optionally includes an annotated `image` (tiny PNG) when `includeImage` is true. - - get-env.ts - - Registers a `get-env` tool that returns the current process environment variables as formatted JSON text; useful for debugging configuration. - - get-resource-links.ts - - Registers a `get-resource-links` tool that returns an intro `text` block followed by multiple `resource_link` items. - - get-resource-reference.ts - - Registers a `get-resource-reference` tool that returns a reference for a selected dynamic resource. - - get-roots-list.ts - - Registers a `get-roots-list` tool that returns the last list of roots sent by the client. - - gzip-file-as-resource.ts - - Registers a `gzip-file-as-resource` tool that fetches content from a URL or data URI, compresses it, and then either: - - returns a `resource_link` to a session-scoped resource (default), or - - returns an inline `resource` with the gzipped data. The resource will be still discoverable for the duration of the session via `resources/list`. - - Uses `resources/session.ts` to register the gzipped blob as a per-session resource at a URI like `demo://resource/session/` with `mimeType: application/gzip`. - - Environment controls: - - `GZIP_MAX_FETCH_SIZE` (bytes, default 10 MiB) - - `GZIP_MAX_FETCH_TIME_MILLIS` (ms, default 30000) - - `GZIP_ALLOWED_DOMAINS` (comma-separated allowlist; empty means all domains allowed) - - trigger-elicitation-request.ts - - Registers a `trigger-elicitation-request` tool that sends an `elicitation/create` request to the client/LLM and returns the elicitation result. - - trigger-sampling-request.ts - - Registers a `trigger-sampling-request` tool that sends a `sampling/createMessage` request to the client/LLM and returns the sampling result. - - get-structured-content.ts - - Registers a `get-structured-content` tool that demonstrates structuredContent block responses. - - get-sum.ts - - Registers an `get-sum` tool with a Zod input schema that sums two numbers `a` and `b` and returns the result. - - get-tiny-image.ts - - Registers a `get-tiny-image` tool, which returns a tiny PNG MCP logo as an `image` content item, along with surrounding descriptive `text` items. - - long-running-operation.ts - - Registers a `long-running-operation` tool that simulates a long-running task over a specified `duration` (seconds) and number of `steps`; emits `notifications/progress` updates when the client supplies a `progressToken`. - - toggle-logging.ts - - Registers a `toggle-logging` tool, which starts or stops simulated logging for the invoking session. - - toggle-subscriber-updates.ts - - Registers a `toggle-subscriber-updates` tool, which starts or stops simulated resource subscription update checks for the invoking session. - -- prompts/ - - - index.ts - - `registerPrompts(server)` orchestrator; delegates to prompt factory/registration methods from in individual prompt files. - - simple.ts - - Registers `simple-prompt`: a prompt with no arguments that returns a single user message. - - args.ts - - Registers `args-prompt`: a prompt with two arguments (`city` required, `state` optional) used to compose a message. - - completions.ts - - Registers `completable-prompt`: a prompt whose arguments support server-driven completions using the SDK’s `completable(...)` helper (e.g., completing `department` and context-aware `name`). - - resource.ts - - Exposes `registerEmbeddedResourcePrompt(server)` which registers `resource-prompt` — a prompt that accepts `resourceType` ("Text" or "Blob") and `resourceId` (integer), and embeds a dynamically generated resource of the requested type within the returned messages. Internally reuses helpers from `resources/templates.ts`. - -- resources/ - - - index.ts - - `registerResources(server)` orchestrator; delegates to resource factory/registration methods from individual resource files. - - templates.ts - - Registers two dynamic, template‑driven resources using `ResourceTemplate`: - - Text: `demo://resource/dynamic/text/{index}` (MIME: `text/plain`) - - Blob: `demo://resource/dynamic/blob/{index}` (MIME: `application/octet-stream`, Base64 payload) - - The `{index}` path variable must be a finite positive integer. Content is generated on demand with a timestamp. - - Exposes helpers `textResource(uri, index)`, `textResourceUri(index)`, `blobResource(uri, index)`, and `blobResourceUri(index)` so other modules can construct and embed dynamic resources directly (e.g., from prompts). - - files.ts - - Registers static file-based resources for each file in the `docs/` folder. - - URIs follow the pattern: `demo://resource/static/document/`. - - Serves markdown files as `text/markdown`, `.txt` as `text/plain`, `.json` as `application/json`, others default to `text/plain`. - -- docs/ - - - architecture.md (this document) - - server-instructions.md - - Human‑readable instructions intended to be passed to the client/LLM as for guidance on server use. Loaded by the server at startup and returned in the "initialize" exchange. - -- 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)`. - - Registers prompts via `registerPrompts(server)`. - - Sets up resource subscription handlers via `setSubscriptionHandlers(server)`. - - Returns the server, a `clientConnect(sessionId)` callback, and a `cleanup(sessionId?)` callback that stops any active intervals and removes any session‑scoped state. - -4. Each transport is responsible for network/session lifecycle: - - STDIO: simple process‑bound connection; closes on `SIGINT` and calls`clientConnect()` and `cleanup()`. - - SSE: maintains a session map keyed by `sessionId`; calls `clientConnect(sessionId)` on connection, 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`. Calls `clientConnect(sessionId)` on connection and calls `cleanup(sessionId)` on DELETE. - -## Registered Features (current minimal set) - -- Tools - - - `echo` (tools/echo.ts): Echoes the provided `message: string`. Uses Zod to validate inputs. - - `get-annotated-message` (tools/get-annotated-message.ts): Returns a `text` message annotated with `priority` and `audience` based on `messageType` (`error`, `success`, or `debug`); can optionally include an annotated `image`. - - `get-env` (tools/get-env.ts): Returns all environment variables from the running process as pretty-printed JSON text. - - `get-resource-links` (tools/get-resource-links.ts): Returns an intro `text` block followed by multiple `resource_link` items. For a requested `count` (1–10), alternates between dynamic Text and Blob resources using URIs from `resources/templates.ts`. - - `get-resource-reference` (tools/get-resource-reference.ts): Accepts `resourceType` (`text` or `blob`) and `resourceId` (positive integer). Returns a concrete `resource` content block (with its `uri`, `mimeType`, and data) with surrounding explanatory `text`. - - `get-roots-list` (tools/get-roots-list.ts): Returns the last list of roots sent by the client. - - `gzip-file-as-resource` (tools/gzip-file-as-resource.ts): Accepts a `name` and `data` (URL or data URI), fetches the data subject to size/time/domain constraints, compresses it, registers it as a session resource at `demo://resource/session/` with `mimeType: application/gzip`, and returns either a `resource_link` (default) or an inline `resource` depending on `outputType`. - - `get-structured-content` (tools/get-structured-content.ts): Demonstrates structured responses. Accepts `location` input and returns both backward‑compatible `content` (a `text` block containing JSON) and `structuredContent` validated by an `outputSchema` (temperature, conditions, humidity). - - `get-sum` (tools/get-sum.ts): For two numbers `a` and `b` calculates and returns their sum. Uses Zod to validate inputs. - - `get-tiny-image` (tools/get-tiny-image.ts): Returns a tiny PNG MCP logo as an `image` content item with brief descriptive text before and after. - - `long-running-operation` (tools/long-running-operation.ts): Simulates a multi-step operation over a given `duration` and number of `steps`; reports progress via `notifications/progress` when a `progressToken` is provided by the client. - - `toggle-logging` (tools/toggle-logging.ts): Starts or stops simulated, random‑leveled logging for the invoking session. Respects the client’s selected minimum logging level. - - `toggle-subscriber-updates` (tools/toggle-subscriber-updates.ts): Starts or stops simulated resource update notifications for URIs the invoking session has subscribed to. - - `trigger-sampling-request` (tools/trigger-sampling-request.ts): Issues a `sampling/createMessage` request to the client/LLM using provided `prompt` and optional generation controls; returns the LLM’s response payload. - -- Prompts - - - `simple-prompt` (prompts/simple.ts): No-argument prompt that returns a static user message. - - `args-prompt` (prompts/args.ts): Two-argument prompt with `city` (required) and `state` (optional) used to compose a question. - - `completable-prompt` (prompts/completions.ts): Demonstrates argument auto-completions with the SDK’s `completable` helper; `department` completions drive context-aware `name` suggestions. - - `resource-prompt` (prompts/resource.ts): Accepts `resourceType` ("Text" or "Blob") and `resourceId` (string convertible to integer) and returns messages that include an embedded dynamic resource of the selected type generated via `resources/templates.ts`. - -- Resources - - - Dynamic Text: `demo://resource/dynamic/text/{index}` (content generated on the fly) - - Dynamic Blob: `demo://resource/dynamic/blob/{index}` (base64 payload generated on the fly) - - Static Documents: `demo://resource/static/document/` (serves files from `src/everything/docs/` as static file-based resources) - - Session Scoped: `demo://resource/session/` (per-session resources registered dynamically; available only for the lifetime of the session) - -- Resource Subscriptions and Notifications - - - Clients may subscribe/unsubscribe to resource URIs using the MCP `resources/subscribe` and `resources/unsubscribe` requests. - - Simulated update notifications are opt‑in and off by default. Use the `toggle-subscriber-updates` tool to start/stop a per‑session interval that emits `notifications/resources/updated { uri }` only for URIs that session has subscribed to. - - Multiple concurrent clients are supported; each client’s subscriptions are tracked per session and notifications are delivered independently via the server instance associated with that session. - -- Logging - - Simulated logging is available but off by default. Use the `toggle-logging` tool to start/stop periodic log messages of varying levels (debug, info, notice, warning, error, critical, alert, emergency) per session. Clients can control the minimum level they receive via the standard MCP `logging/setLevel` request. - -## Extension Points - -- Adding Tools - - - Create a new file under `tools/` with your `registerXTool(server)` function that registers the tool via `server.registerTool(...)`. - - Export and call it from `tools/index.ts` inside `registerTools(server)`. - -- Adding Prompts - - - Create a new file under `prompts/` with your `registerXPrompt(server)` function that registers the prompt via `server.registerPrompt(...)`. - - Export and call it from `prompts/index.ts` inside `registerPrompts(server)`. - -- Adding Resources - - - Create a new file under `resources/` with your `registerXResources(server)` function using `server.registerResource(...)` (optionally with `ResourceTemplate`). - - Export and call it from `resources/index.ts` inside `registerResources(server)`. - -## Resource Subscriptions – How It Works - -- Module: `resources/subscriptions.ts` - - - Tracks subscribers per URI: `Map>`. - - Installs handlers via `setSubscriptionHandlers(server)` to process subscribe/unsubscribe requests and keep the map updated. - - Updates are started/stopped on demand by the `toggle-subscriber-updates` tool, which calls `beginSimulatedResourceUpdates(server, sessionId)` and `stopSimulatedResourceUpdates(sessionId)`. - - `cleanup(sessionId?)` calls `stopSimulatedResourceUpdates(sessionId)` to clear intervals and remove session‑scoped state. - -- Design note: Each client session has its own `McpServer` instance; periodic checks run per session and invoke `server.notification(...)` on that instance, so messages are delivered only to the intended client. - -## Session‑scoped Resources – How It Works - -- Module: `resources/session.ts` - - - `getSessionResourceURI(name: string)`: Builds a session resource URI: `demo://resource/session/`. - - `registerSessionResource(server, resource, type, payload)`: Registers a resource with the given `uri`, `name`, and `mimeType`, returning a `resource_link`. The content is served from memory for the life of the session only. Supports `type: "text" | "blob"` and returns data in the corresponding field. - - Intended usage: tools can create and expose per-session artifacts without persisting them. For example, `tools/gzip-file-as-resource.ts` gzips fetched content, registers it as a session resource with `mimeType: application/gzip`, and returns either a `resource_link` or an inline `resource` based on `outputType`. - -## Simulated Logging – How It Works - -- Module: `server/logging.ts` - - - Periodically sends randomized log messages at different levels. Messages can include the session ID for clarity during demos. - - Started/stopped on demand via the `toggle-logging` tool, which calls `beginSimulatedLogging(server, sessionId?)` and `stopSimulatedLogging(sessionId?)`. Note that transport disconnect triggers `cleanup()` which also stops any active intervals. - - Uses `server.sendLoggingMessage({ level, data }, sessionId?)` so that the client’s configured minimum logging level is respected by the SDK. - -- Adding Transports - - Implement a new transport module under `transports/`. - - Add a case to `index.ts` so the CLI can select it. +### Multi‑client +The server supports multiple concurrent clients. Tracking per session data is demonstrated with +resource subscriptions and simulated logging. ## Build and Distribution @@ -308,6 +29,12 @@ At `src/everything`: - 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 +## [Project Structure](structure.md) -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. +## [Startup Process](startup.md) + +## [Server Features](features.md) + +## [Extension Points](extension.md) + +## [How It Works](how-it-works.md) diff --git a/src/everything/docs/extension.md b/src/everything/docs/extension.md new file mode 100644 index 00000000..544286cd --- /dev/null +++ b/src/everything/docs/extension.md @@ -0,0 +1,22 @@ +# Everything Server - Extension Points +**[Architecture](architecture.md) +| [Project Structure](structure.md) +| [Startup Process](startup.md) +| [Server Features](features.md) +| Extension Points +| [How It Works](how-it-works.md)** + +## Adding Tools + +- Create a new file under `tools/` with your `registerXTool(server)` function that registers the tool via `server.registerTool(...)`. +- Export and call it from `tools/index.ts` inside `registerTools(server)`. + +## Adding Prompts + +- Create a new file under `prompts/` with your `registerXPrompt(server)` function that registers the prompt via `server.registerPrompt(...)`. +- Export and call it from `prompts/index.ts` inside `registerPrompts(server)`. + +## Adding Resources + +- Create a new file under `resources/` with your `registerXResources(server)` function using `server.registerResource(...)` (optionally with `ResourceTemplate`). +- Export and call it from `resources/index.ts` inside `registerResources(server)`. diff --git a/src/everything/docs/features.md b/src/everything/docs/features.md new file mode 100644 index 00000000..0ccc0813 --- /dev/null +++ b/src/everything/docs/features.md @@ -0,0 +1,51 @@ +# Everything Server - Features +**[Architecture](architecture.md) +| [Project Structure](structure.md) +| [Startup Process](startup.md) +| Server Features +| [Extension Points](extension.md) +| [How It Works](how-it-works.md)** + +## Tools + +- `echo` (tools/echo.ts): Echoes the provided `message: string`. Uses Zod to validate inputs. +- `get-annotated-message` (tools/get-annotated-message.ts): Returns a `text` message annotated with `priority` and `audience` based on `messageType` (`error`, `success`, or `debug`); can optionally include an annotated `image`. +- `get-env` (tools/get-env.ts): Returns all environment variables from the running process as pretty-printed JSON text. +- `get-resource-links` (tools/get-resource-links.ts): Returns an intro `text` block followed by multiple `resource_link` items. For a requested `count` (1–10), alternates between dynamic Text and Blob resources using URIs from `resources/templates.ts`. +- `get-resource-reference` (tools/get-resource-reference.ts): Accepts `resourceType` (`text` or `blob`) and `resourceId` (positive integer). Returns a concrete `resource` content block (with its `uri`, `mimeType`, and data) with surrounding explanatory `text`. +- `get-roots-list` (tools/get-roots-list.ts): Returns the last list of roots sent by the client. +- `gzip-file-as-resource` (tools/gzip-file-as-resource.ts): Accepts a `name` and `data` (URL or data URI), fetches the data subject to size/time/domain constraints, compresses it, registers it as a session resource at `demo://resource/session/` with `mimeType: application/gzip`, and returns either a `resource_link` (default) or an inline `resource` depending on `outputType`. +- `get-structured-content` (tools/get-structured-content.ts): Demonstrates structured responses. Accepts `location` input and returns both backward‑compatible `content` (a `text` block containing JSON) and `structuredContent` validated by an `outputSchema` (temperature, conditions, humidity). +- `get-sum` (tools/get-sum.ts): For two numbers `a` and `b` calculates and returns their sum. Uses Zod to validate inputs. +- `get-tiny-image` (tools/get-tiny-image.ts): Returns a tiny PNG MCP logo as an `image` content item with brief descriptive text before and after. +- `long-running-operation` (tools/long-running-operation.ts): Simulates a multi-step operation over a given `duration` and number of `steps`; reports progress via `notifications/progress` when a `progressToken` is provided by the client. +- `toggle-logging` (tools/toggle-logging.ts): Starts or stops simulated, random‑leveled logging for the invoking session. Respects the client’s selected minimum logging level. +- `toggle-subscriber-updates` (tools/toggle-subscriber-updates.ts): Starts or stops simulated resource update notifications for URIs the invoking session has subscribed to. +- `trigger-sampling-request` (tools/trigger-sampling-request.ts): Issues a `sampling/createMessage` request to the client/LLM using provided `prompt` and optional generation controls; returns the LLM’s response payload. + +## Prompts + +- `simple-prompt` (prompts/simple.ts): No-argument prompt that returns a static user message. +- `args-prompt` (prompts/args.ts): Two-argument prompt with `city` (required) and `state` (optional) used to compose a question. +- `completable-prompt` (prompts/completions.ts): Demonstrates argument auto-completions with the SDK’s `completable` helper; `department` completions drive context-aware `name` suggestions. +- `resource-prompt` (prompts/resource.ts): Accepts `resourceType` ("Text" or "Blob") and `resourceId` (string convertible to integer) and returns messages that include an embedded dynamic resource of the selected type generated via `resources/templates.ts`. + +## Resources + +- Dynamic Text: `demo://resource/dynamic/text/{index}` (content generated on the fly) +- Dynamic Blob: `demo://resource/dynamic/blob/{index}` (base64 payload generated on the fly) +- Static Documents: `demo://resource/static/document/` (serves files from `src/everything/docs/` as static file-based resources) +- Session Scoped: `demo://resource/session/` (per-session resources registered dynamically; available only for the lifetime of the session) + +## Resource Subscriptions and Notifications + +- Simulated update notifications are opt‑in and off by default. +- Clients may subscribe/unsubscribe to resource URIs using the MCP `resources/subscribe` and `resources/unsubscribe` requests. +- Use the `toggle-subscriber-updates` tool to start/stop a per‑session interval that emits `notifications/resources/updated { uri }` only for URIs that session has subscribed to. +- Multiple concurrent clients are supported; each client’s subscriptions are tracked per session and notifications are delivered independently via the server instance associated with that session. + +## Simulated Logging + +- Simulated logging is available but off by default. +- Use the `toggle-logging` tool to start/stop periodic log messages of varying levels (debug, info, notice, warning, error, critical, alert, emergency) per session. +- Clients can control the minimum level they receive via the standard MCP `logging/setLevel` request. diff --git a/src/everything/docs/how-it-works.md b/src/everything/docs/how-it-works.md new file mode 100644 index 00000000..5b8621ca --- /dev/null +++ b/src/everything/docs/how-it-works.md @@ -0,0 +1,34 @@ +# Everything Server - How It Works +**[Architecture](architecture.md) +| [Project Structure](structure.md) +| [Startup Process](startup.md) +| [Server Features](features.md) +| [Extension Points](extension.md) +| How It Works** + +## Resource Subscriptions + +Each client manages its own resource subscriptions and receives notifications only for the URIs it subscribed to, independent of other clients. + +### Module: `resources/subscriptions.ts` + +- Tracks subscribers per URI: `Map>`. +- Installs handlers via `setSubscriptionHandlers(server)` to process subscribe/unsubscribe requests and keep the map updated. +- Updates are started/stopped on demand by the `toggle-subscriber-updates` tool, which calls `beginSimulatedResourceUpdates(server, sessionId)` and `stopSimulatedResourceUpdates(sessionId)`. +- `cleanup(sessionId?)` calls `stopSimulatedResourceUpdates(sessionId)` to clear intervals and remove session‑scoped state. + +## Session‑scoped Resources + +### Module: `resources/session.ts` + +- `getSessionResourceURI(name: string)`: Builds a session resource URI: `demo://resource/session/`. +- `registerSessionResource(server, resource, type, payload)`: Registers a resource with the given `uri`, `name`, and `mimeType`, returning a `resource_link`. The content is served from memory for the life of the session only. Supports `type: "text" | "blob"` and returns data in the corresponding field. +- Intended usage: tools can create and expose per-session artifacts without persisting them. For example, `tools/gzip-file-as-resource.ts` compresses fetched content, registers it as a session resource with `mimeType: application/gzip`, and returns either a `resource_link` or an inline `resource` based on `outputType`. + +## Simulated Logging + +### Module: `server/logging.ts` + +- Periodically sends randomized log messages at different levels. Messages can include the session ID for clarity during demos. +- Started/stopped on demand via the `toggle-logging` tool, which calls `beginSimulatedLogging(server, sessionId?)` and `stopSimulatedLogging(sessionId?)`. Note that transport disconnect triggers `cleanup()` which also stops any active intervals. +- Uses `server.sendLoggingMessage({ level, data }, sessionId?)` so that the client’s configured minimum logging level is respected by the SDK. diff --git a/src/everything/docs/server-instructions.md b/src/everything/docs/instructions.md similarity index 79% rename from src/everything/docs/server-instructions.md rename to src/everything/docs/instructions.md index 5e668895..183da044 100644 --- a/src/everything/docs/server-instructions.md +++ b/src/everything/docs/instructions.md @@ -1,4 +1,13 @@ -Testing and demonstration server for MCP protocol features. +# Everything Server - Instructions +**[Architecture](architecture.md) +| [Project Structure](structure.md) +| [Startup Process](startup.md) +| [Server Features](features.md) +| [Extension Points](extension.md) +| [How It Works](how-it-works.md)** + +A testing and demonstration server for MCP protocol features. +TODO: Update this doc ## Resources diff --git a/src/everything/docs/startup.md b/src/everything/docs/startup.md new file mode 100644 index 00000000..93069fe1 --- /dev/null +++ b/src/everything/docs/startup.md @@ -0,0 +1,113 @@ +# Everything Server - Startup Flow +**[Architecture](architecture.md) +| [Project Structure](structure.md) +| Startup Process +| [Server Features](features.md) +| [Extension Points](extension.md) +| [How It Works](how-it-works.md)** + +## 1. Everything Server Launcher + +- Usage `node dist/index.js [stdio|sse|streamableHttp]` +- Runs the specified **transport manager** to handle client connections. +- Specify transport type on command line (default `stdio`) + - `stdio` → `transports/stdio.js` + - `sse` → `transports/sse.js` + - `streamableHttp` → `transports/streamableHttp.js` + +## 2. The Transport Manager + +- Creates a server instance using `createServer()` from `server/index.ts` + - Connects it to the chosen transport type from the MCP SDK. + - Calls the `clientConnected()` callback upon transport connection. +- Handles communication according to the MCP specs for the chosen transport. + - **STDIO**: + - One simple, process‑bound connection. + - Calls`clientConnect()` upon connection. + - Closes and calls `cleanup()` on `SIGINT`. + - **SSE**: + - Supports multiple client connections. + - Client transports are mapped to `sessionId`; + - Calls `clientConnect(sessionId)` upon connection. + - Hooks server’s `onclose` to clean and remove session. + - Exposes + - `/sse` **GET** (SSE stream) + - `/message` **POST** (JSON‑RPC messages) + - **Streamable HTTP**: + - Supports multiple client connections. + - Client transports are mapped to `sessionId`; + - Calls `clientConnect(sessionId)` upon connection. + - Exposes `/mcp` for + - **POST** (JSON‑RPC messages) + - **GET** (SSE stream) + - **DELETE** (termination) + - Uses an event store for resumability and stores transports by `sessionId`. + - Calls `cleanup(sessionId)` on **DELETE**. + +## 3. The Server Factory + +- Invoke `createServer()` from `server/index.ts` +- Creates a new `McpServer` instance with + - **Capabilities**: + - `tools: {}` + - `logging: {}` + - `prompts: {}` + - `resources: { subscribe: true }` + - **Server Instructions** + - Loaded from the docs folder (`server-instructions.md`). + - **Registrations** + - Registers **tools** via `registerTools(server)`. + - Registers **resources** via `registerResources(server)`. + - Registers **prompts** via `registerPrompts(server)`. + - **Other Request Handlers** + - Sets up resource subscription handlers via `setSubscriptionHandlers(server)`. + - Roots list change handler is added post-connection via + - **Returns** + - The `McpServer` instance + - A `clientConnect(sessionId)` callback that enables post-connection setup + - A `cleanup(sessionId?)` callback that stops any active intervals and removes any session‑scoped state + +## Enabling Multiple Clients + +Some of the transport managers defined in the `transports` folder can support multiple clients. +In order to do so, they must map certain data to a session identifier. + +### About the `clientConnected` callback returned by the Server Factory + +Some server functions require a `sessionId` but can't reach it via its scope. +For instance, the automatic log-level handling in the Typescript SDK tracks +the client's requested logging level by `sessionId`. In order + +So, the Server Factory provides a callback to allow the chosen Transport Manager +to provide the server with the `sessionId` (or `undefined`) for each new connection. + +### On `clientConnected` vs `server.oninitialized` for post-connection setup + +#### Q: + +> Why not hook `server.server.oninitialized` to trigger post-connection setup? +> You could call `syncRoots` in a handler, obviating the `clientConnected` hook. + +#### A: + +In `oninitialized`, a transport is connected, but there is no way to access it +or its `sessionId`. Therefore, calling any function that needs a `sessionId` is +right out. + +#### Q: + +> Why is it important to have access to the `sessionId` anywhere but in a request +> handler? + +### A: + +When setting up a server that tracks any data per session, you need to map +that data to a `sessionId`. See `logging.ts` and `subscriptions.ts` for examples. + +In an STDIO server, it doesn't matter because there is one client per server. +Features that track data by `sessionId` can accept `undefined` for that value +and still track session-scoped data for STDIO clients. + +But with HTTP protocols, you can have multiple clients. So you have to track +their logging intervals, resource subscriptions, and other session-scoped +data per client. diff --git a/src/everything/docs/structure.md b/src/everything/docs/structure.md new file mode 100644 index 00000000..fb5dfca7 --- /dev/null +++ b/src/everything/docs/structure.md @@ -0,0 +1,177 @@ +# Everything Server - Project Structure +**[Architecture](architecture.md) +| Project Structure +| [Startup Process](startup.md) +| [Server Features](features.md) +| [Extension Points](extension.md) +| [How It Works](how-it-works.md)** + +``` +src/everything + ├── index.ts + ├── package.json + ├── docs + │ ├── architecture.md + │ └── server-instructions.md + ├── prompts + │ ├── index.ts + │ ├── args.ts + │ ├── completions.ts + │ ├── simple.ts + │ └── resource.ts + ├── resources + │ ├── index.ts + │ ├── files.ts + │ ├── session.ts + │ ├── subscriptions.ts + │ └── templates.ts + ├── server + │ ├── index.ts + │ ├── logging.ts + │ ├── roots.ts + │ └── everything.ts + ├── tools + │ ├── index.ts + │ ├── echo.ts + │ ├── get-annotated-message.ts + │ ├── get-env.ts + │ ├── get-resource-links.ts + │ ├── get-resource-reference.ts + │ ├── get-roots-list.ts + │ ├── get-structured-content.ts + │ ├── get-sum.ts + │ ├── get-tiny-image.ts + │ ├── gzip-file-as-resource.ts + │ ├── long-running-operation.ts + │ ├── toggle-logging.ts + │ ├── toggle-subscriber-updates.ts + │ ├── trigger-elicitation-request.ts + │ └── trigger-sampling-request.ts + └── transports + ├── sse.ts + ├── stdio.ts + └── streamableHttp.ts +``` + +# Project Contents + +## `src/everything`: + +### `index.ts` + +- CLI entry point that selects and runs a specific transport module based on the first CLI argument: `stdio`, `sse`, or `streamableHttp`. + +### `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. + +### `docs/` + +- `architecture.md` + - This document. +- `server-instructions.md` + - Human‑readable instructions intended to be passed to the client/LLM as for guidance on server use. Loaded by the server at startup and returned in the "initialize" exchange. + +### `prompts/` + +- `index.ts` + - `registerPrompts(server)` orchestrator; delegates to prompt factory/registration methods from in individual prompt files. +- `simple.ts` + - Registers `simple-prompt`: a prompt with no arguments that returns a single user message. +- `args.ts` + - Registers `args-prompt`: a prompt with two arguments (`city` required, `state` optional) used to compose a message. +- `completions.ts` + - Registers `completable-prompt`: a prompt whose arguments support server-driven completions using the SDK’s `completable(...)` helper (e.g., completing `department` and context-aware `name`). +- `resource.ts` + - Exposes `registerEmbeddedResourcePrompt(server)` which registers `resource-prompt` — a prompt that accepts `resourceType` ("Text" or "Blob") and `resourceId` (integer), and embeds a dynamically generated resource of the requested type within the returned messages. Internally reuses helpers from `resources/templates.ts`. + +### `resources/` + +- `index.ts` + - `registerResources(server)` orchestrator; delegates to resource factory/registration methods from individual resource files. +- `templates.ts` + - Registers two dynamic, template‑driven resources using `ResourceTemplate`: + - Text: `demo://resource/dynamic/text/{index}` (MIME: `text/plain`) + - Blob: `demo://resource/dynamic/blob/{index}` (MIME: `application/octet-stream`, Base64 payload) + - The `{index}` path variable must be a finite positive integer. Content is generated on demand with a timestamp. + - Exposes helpers `textResource(uri, index)`, `textResourceUri(index)`, `blobResource(uri, index)`, and `blobResourceUri(index)` so other modules can construct and embed dynamic resources directly (e.g., from prompts). +- `files.ts` + - Registers static file-based resources for each file in the `docs/` folder. + - URIs follow the pattern: `demo://resource/static/document/`. + - Serves markdown files as `text/markdown`, `.txt` as `text/plain`, `.json` as `application/json`, others default to `text/plain`. + +### `server/` + +- `index.ts` + - Server factory that creates an `McpServer` with declared capabilities, loads server instructions, and registers tools, prompts, and resources. + - Sets resource subscription handlers via `setSubscriptionHandlers(server)`. + - Exposes `{ server, cleanup }` to the chosen transport. Cleanup stops any running intervals in the server when the transport disconencts. +- `logging.ts` + - Implements simulated logging. Periodically sends randomized log messages at various levels to the connected client session. Started/stopped on demand via a dedicated tool. +- `everything.ts` + - A full “reference/monolith” implementation demonstrating most MCP features. Not the default path used by the transports in this package. + +### `tools/` + +- `index.ts` + - `registerTools(server)` orchestrator; delegates to tool factory/registration methods in individual tool files. +- `echo.ts` + - Registers an `echo` tool that takes a message and returns `Echo: {message}`. +- `get-annotated-message.ts` + - Registers an `annotated-message` tool which demonstrates annotated content items by emitting a primary `text` message with `annotations` that vary by `messageType` (`"error" | "success" | "debug"`), and optionally includes an annotated `image` (tiny PNG) when `includeImage` is true. +- `get-env.ts` + - Registers a `get-env` tool that returns the current process environment variables as formatted JSON text; useful for debugging configuration. +- `get-resource-links.ts` + - Registers a `get-resource-links` tool that returns an intro `text` block followed by multiple `resource_link` items. +- `get-resource-reference.ts` + - Registers a `get-resource-reference` tool that returns a reference for a selected dynamic resource. +- `get-roots-list.ts` + - Registers a `get-roots-list` tool that returns the last list of roots sent by the client. +- `gzip-file-as-resource.ts` + - Registers a `gzip-file-as-resource` tool that fetches content from a URL or data URI, compresses it, and then either: + - returns a `resource_link` to a session-scoped resource (default), or + - returns an inline `resource` with the gzipped data. The resource will be still discoverable for the duration of the session via `resources/list`. + - Uses `resources/session.ts` to register the gzipped blob as a per-session resource at a URI like `demo://resource/session/` with `mimeType: application/gzip`. + - Environment controls: + - `GZIP_MAX_FETCH_SIZE` (bytes, default 10 MiB) + - `GZIP_MAX_FETCH_TIME_MILLIS` (ms, default 30000) + - `GZIP_ALLOWED_DOMAINS` (comma-separated allowlist; empty means all domains allowed) +- `trigger-elicitation-request.ts` + - Registers a `trigger-elicitation-request` tool that sends an `elicitation/create` request to the client/LLM and returns the elicitation result. +- `trigger-sampling-request.ts` + - Registers a `trigger-sampling-request` tool that sends a `sampling/createMessage` request to the client/LLM and returns the sampling result. +- `get-structured-content.ts` + - Registers a `get-structured-content` tool that demonstrates structuredContent block responses. +- `get-sum.ts` + - Registers an `get-sum` tool with a Zod input schema that sums two numbers `a` and `b` and returns the result. +- `get-tiny-image.ts` + - Registers a `get-tiny-image` tool, which returns a tiny PNG MCP logo as an `image` content item, along with surrounding descriptive `text` items. +- `long-running-operation.ts` + - Registers a `long-running-operation` tool that simulates a long-running task over a specified `duration` (seconds) and number of `steps`; emits `notifications/progress` updates when the client supplies a `progressToken`. +- `toggle-logging.ts` + - Registers a `toggle-logging` tool, which starts or stops simulated logging for the invoking session. +- `toggle-subscriber-updates.ts` + - Registers a `toggle-subscriber-updates` tool, which starts or stops simulated resource subscription update checks for the invoking session. + +### `transports/` + +- `stdio.ts` + - Starts a `StdioServerTransport`, created the server via `createServer()`, and connects it. + - Calls `clientConnected()` to inform the server of the connection. + - Handles `SIGINT` to close cleanly and calls `cleanup()` to remove any live intervals. +- `sse.ts` + - Express server exposing: + - `GET /sse` to establish an SSE connection per session. + - `POST /message` for client messages. + - Manages multiple connected clients via a transport map. + - Starts an `SSEServerTransport`, created the server via `createServer()`, and connects it to a new transport. + - Calls `clientConnected(sessionId)` to inform the server of the connection. + - On server disconnect, calls `cleanup()` to remove any live intervals. +- `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 and reuses the transport for subsequent requests. + - Calls `clientConnected(sessionId)` to inform the server of the connection. diff --git a/src/everything/index.ts b/src/everything/index.ts index cce1ea53..39d50fa6 100644 --- a/src/everything/index.ts +++ b/src/everything/index.ts @@ -21,8 +21,13 @@ async function run() { await import("./transports/streamableHttp.js"); break; default: - console.error(`Unknown script: ${scriptName}`); - console.log("Available scripts:"); + console.error(`-`.repeat(53)); + console.error(` Everything Server Launcher`); + console.error(` Usage: node ./index.js [stdio|sse|streamableHttp]`); + console.error(` Default transport: stdio`); + console.error(`-`.repeat(53)); + console.error(`Unknown transport: ${scriptName}`); + console.log("Available transports:"); console.log("- stdio"); console.log("- sse"); console.log("- streamableHttp"); @@ -34,4 +39,4 @@ async function run() { } } -run(); +await run(); diff --git a/src/everything/resources/index.ts b/src/everything/resources/index.ts index c2db970f..30c6f7dc 100644 --- a/src/everything/resources/index.ts +++ b/src/everything/resources/index.ts @@ -1,6 +1,9 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { registerResourceTemplates } from "./templates.js"; import { registerFileResources } from "./files.js"; +import { fileURLToPath } from "url"; +import { dirname, join } from "path"; +import { readFileSync } from "fs"; /** * Register the resources with the MCP server. @@ -10,3 +13,24 @@ export const registerResources = (server: McpServer) => { registerResourceTemplates(server); registerFileResources(server); }; + +/** + * Reads the server instructions from the corresponding markdown file. + * Attempts to load the content of the file located in the `docs` directory. + * If the file cannot be loaded, an error message is returned instead. + * + * @return {string} The content of the server instructions file, or an error message if reading fails. + */ +export function readInstructions(): string { + const __filename = fileURLToPath(import.meta.url); + const __dirname = dirname(__filename); + const filePath = join(__dirname, "..", "docs", "instructions.md"); + let instructions; + + try { + instructions = readFileSync(filePath, "utf-8"); + } catch (e) { + instructions = "Server instructions not loaded: " + e; + } + return instructions; +} diff --git a/src/everything/server/everything.ts b/src/everything/server/everything.ts deleted file mode 100644 index 33863063..00000000 --- a/src/everything/server/everything.ts +++ /dev/null @@ -1,1155 +0,0 @@ -import { Server } from "@modelcontextprotocol/sdk/server/index.js"; -import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js"; -import { - CallToolRequestSchema, - ClientCapabilities, - CompleteRequestSchema, - CreateMessageRequest, - CreateMessageResultSchema, - ElicitResultSchema, - GetPromptRequestSchema, - ListPromptsRequestSchema, - ListResourcesRequestSchema, - ListResourceTemplatesRequestSchema, - ListToolsRequestSchema, - LoggingLevel, - ReadResourceRequestSchema, - Resource, - RootsListChangedNotificationSchema, - ServerNotification, - ServerRequest, - SubscribeRequestSchema, - Tool, - UnsubscribeRequestSchema, - type Root, -} from "@modelcontextprotocol/sdk/types.js"; -import { z } from "zod"; -import { zodToJsonSchema } from "zod-to-json-schema"; -import { readFileSync } from "fs"; -import { fileURLToPath } from "url"; -import { dirname, join } from "path"; -import JSZip from "jszip"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -const instructions = readFileSync(join(__dirname, "instructions.md"), "utf-8"); - -type ToolInput = Tool["inputSchema"]; -type ToolOutput = Tool["outputSchema"]; - -type SendRequest = RequestHandlerExtra< - ServerRequest, - ServerNotification ->["sendRequest"]; - -/* Input schemas for tools implemented in this server */ -const EchoSchema = z.object({ - message: z.string().describe("Message to echo"), -}); - -const AddSchema = z.object({ - a: z.number().describe("First number"), - b: z.number().describe("Second number"), -}); - -const LongRunningOperationSchema = z.object({ - duration: z - .number() - .default(10) - .describe("Duration of the operation in seconds"), - steps: z.number().default(5).describe("Number of steps in the operation"), -}); - -const PrintEnvSchema = z.object({}); - -const SampleLLMSchema = z.object({ - prompt: z.string().describe("The prompt to send to the LLM"), - maxTokens: z - .number() - .default(100) - .describe("Maximum number of tokens to generate"), -}); - -const GetTinyImageSchema = z.object({}); - -const AnnotatedMessageSchema = z.object({ - messageType: z - .enum(["error", "success", "debug"]) - .describe("Type of message to demonstrate different annotation patterns"), - includeImage: z - .boolean() - .default(false) - .describe("Whether to include an example image"), -}); - -const GetResourceReferenceSchema = z.object({ - resourceId: z - .number() - .min(1) - .max(100) - .describe("ID of the resource to reference (1-100)"), -}); - -const ElicitationSchema = z.object({}); - -const GetResourceLinksSchema = z.object({ - count: z - .number() - .min(1) - .max(10) - .default(3) - .describe("Number of resource links to return (1-10)"), -}); - -const ListRootsSchema = z.object({}); - -const StructuredContentSchema = { - input: z.object({ - 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"), - }), -}; - -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"), -}); - -enum ToolName { - ECHO = "echo", - ADD = "add", - LONG_RUNNING_OPERATION = "longRunningOperation", - PRINT_ENV = "printEnv", - SAMPLE_LLM = "sampleLLM", - GET_TINY_IMAGE = "getTinyImage", - ANNOTATED_MESSAGE = "annotatedMessage", - GET_RESOURCE_REFERENCE = "getResourceReference", - ELICITATION = "startElicitation", - GET_RESOURCE_LINKS = "getResourceLinks", - STRUCTURED_CONTENT = "structuredContent", - ZIP_RESOURCES = "zip", - LIST_ROOTS = "listRoots", -} - -enum PromptName { - SIMPLE = "simple_prompt", - COMPLEX = "complex_prompt", - RESOURCE = "resource_prompt", -} - -// Example completion values -const EXAMPLE_COMPLETIONS = { - style: ["casual", "formal", "technical", "friendly"], - temperature: ["0", "0.5", "0.7", "1.0"], - resourceId: ["1", "2", "3", "4", "5"], -}; - -export const createServer = () => { - const server = new Server( - { - name: "example-servers/everything", - title: "Everything Example Server", - version: "1.0.0", - }, - { - capabilities: { - prompts: {}, - resources: { subscribe: true }, - tools: {}, - logging: {}, - completions: {}, - }, - instructions, - } - ); - - let subscriptions: Set = new Set(); - let subsUpdateInterval: NodeJS.Timeout | undefined; - - let logsUpdateInterval: NodeJS.Timeout | undefined; - // Store client capabilities - let clientCapabilities: ClientCapabilities | undefined; - - // Roots state management - let currentRoots: Root[] = []; - 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); - } - - 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 - ); - }, 15000); - } - }; - - // Helper method to request sampling from client - const requestSampling = async ( - context: string, - uri: string, - maxTokens: number = 100, - sendRequest: SendRequest - ) => { - const request: CreateMessageRequest = { - method: "sampling/createMessage", - params: { - messages: [ - { - role: "user", - content: { - type: "text", - text: `Resource ${uri} context: ${context}`, - }, - }, - ], - systemPrompt: "You are a helpful test server.", - maxTokens, - temperature: 0.7, - includeContext: "thisServer", - }, - }; - - return await sendRequest(request, CreateMessageResultSchema); - }; - - const ALL_RESOURCES: Resource[] = Array.from({ length: 100 }, (_, i) => { - const uri = `test://static/resource/${i + 1}`; - if (i % 2 === 0) { - return { - uri, - name: `Resource ${i + 1}`, - mimeType: "text/plain", - text: `Resource ${i + 1}: This is a plaintext resource`, - }; - } else { - const buffer = Buffer.from(`Resource ${i + 1}: This is a base64 blob`); - return { - uri, - name: `Resource ${i + 1}`, - mimeType: "application/octet-stream", - blob: buffer.toString("base64"), - }; - } - }); - - const PAGE_SIZE = 10; - - server.setRequestHandler(ListResourcesRequestSchema, async (request) => { - const cursor = request.params?.cursor; - let startIndex = 0; - - if (cursor) { - const decodedCursor = parseInt(atob(cursor), 10); - if (!isNaN(decodedCursor)) { - startIndex = decodedCursor; - } - } - - const endIndex = Math.min(startIndex + PAGE_SIZE, ALL_RESOURCES.length); - const resources = ALL_RESOURCES.slice(startIndex, endIndex); - - let nextCursor: string | undefined; - if (endIndex < ALL_RESOURCES.length) { - nextCursor = btoa(endIndex.toString()); - } - - return { - resources, - nextCursor, - }; - }); - - server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => { - return { - resourceTemplates: [ - { - uriTemplate: "test://static/resource/{id}", - name: "Static Resource", - description: "A static resource with a numeric ID", - }, - ], - }; - }); - - server.setRequestHandler(ReadResourceRequestSchema, async (request) => { - const uri = request.params.uri; - - if (uri.startsWith("test://static/resource/")) { - const index = parseInt(uri.split("/").pop() ?? "", 10) - 1; - if (index >= 0 && index < ALL_RESOURCES.length) { - const resource = ALL_RESOURCES[index]; - return { - contents: [resource], - }; - } - } - - throw new Error(`Unknown resource: ${uri}`); - }); - - server.setRequestHandler(SubscribeRequestSchema, async (request, extra) => { - const { uri } = request.params; - subscriptions.add(uri); - return {}; - }); - - server.setRequestHandler(UnsubscribeRequestSchema, async (request) => { - subscriptions.delete(request.params.uri); - return {}; - }); - - server.setRequestHandler(ListPromptsRequestSchema, async () => { - return { - prompts: [ - { - name: PromptName.SIMPLE, - description: "A prompt without arguments", - }, - { - name: PromptName.COMPLEX, - description: "A prompt with arguments", - arguments: [ - { - name: "temperature", - description: "Temperature setting", - required: true, - }, - { - name: "style", - description: "Output style", - required: false, - }, - ], - }, - { - name: PromptName.RESOURCE, - description: "A prompt that includes an embedded resource reference", - arguments: [ - { - name: "resourceId", - description: "Resource ID to include (1-100)", - required: true, - }, - ], - }, - ], - }; - }); - - server.setRequestHandler(GetPromptRequestSchema, async (request) => { - const { name, arguments: args } = request.params; - - if (name === PromptName.SIMPLE) { - return { - messages: [ - { - role: "user", - content: { - type: "text", - text: "This is a simple prompt without arguments.", - }, - }, - ], - }; - } - - if (name === PromptName.COMPLEX) { - return { - messages: [ - { - role: "user", - content: { - type: "text", - text: `This is a complex prompt with arguments: temperature=${args?.temperature}, style=${args?.style}`, - }, - }, - { - role: "assistant", - content: { - type: "text", - text: "I understand. You've provided a complex prompt with temperature and style arguments. How would you like me to proceed?", - }, - }, - { - role: "user", - content: { - type: "image", - data: MCP_TINY_IMAGE, - mimeType: "image/png", - }, - }, - ], - }; - } - - if (name === PromptName.RESOURCE) { - const resourceId = parseInt(args?.resourceId as string, 10); - if (isNaN(resourceId) || resourceId < 1 || resourceId > 100) { - throw new Error( - `Invalid resourceId: ${args?.resourceId}. Must be a number between 1 and 100.` - ); - } - - const resourceIndex = resourceId - 1; - const resource = ALL_RESOURCES[resourceIndex]; - - return { - messages: [ - { - role: "user", - content: { - type: "text", - text: `This prompt includes Resource ${resourceId}. Please analyze the following resource:`, - }, - }, - { - role: "user", - content: { - type: "resource", - resource: resource, - }, - }, - ], - }; - } - - throw new Error(`Unknown prompt: ${name}`); - }); - - server.setRequestHandler(ListToolsRequestSchema, async () => { - const tools: Tool[] = [ - { - name: ToolName.ECHO, - description: "Echoes back the input", - inputSchema: zodToJsonSchema(EchoSchema) as ToolInput, - }, - { - name: ToolName.ADD, - description: "Adds two numbers", - inputSchema: zodToJsonSchema(AddSchema) as ToolInput, - }, - { - name: ToolName.LONG_RUNNING_OPERATION, - description: - "Demonstrates a long running operation with progress updates", - inputSchema: zodToJsonSchema(LongRunningOperationSchema) as ToolInput, - }, - { - name: ToolName.PRINT_ENV, - description: - "Prints all environment variables, helpful for debugging MCP server configuration", - inputSchema: zodToJsonSchema(PrintEnvSchema) as ToolInput, - }, - { - name: ToolName.SAMPLE_LLM, - description: "Samples from an LLM using MCP's sampling feature", - inputSchema: zodToJsonSchema(SampleLLMSchema) as ToolInput, - }, - { - name: ToolName.GET_TINY_IMAGE, - description: "Returns the MCP_TINY_IMAGE", - inputSchema: zodToJsonSchema(GetTinyImageSchema) as ToolInput, - }, - { - name: ToolName.ANNOTATED_MESSAGE, - description: - "Demonstrates how annotations can be used to provide metadata about content", - inputSchema: zodToJsonSchema(AnnotatedMessageSchema) as ToolInput, - }, - { - name: ToolName.GET_RESOURCE_REFERENCE, - description: - "Returns a resource reference that can be used by MCP clients", - inputSchema: zodToJsonSchema(GetResourceReferenceSchema) as ToolInput, - }, - { - name: ToolName.GET_RESOURCE_LINKS, - description: - "Returns multiple resource links that reference different types of resources", - inputSchema: zodToJsonSchema(GetResourceLinksSchema) as ToolInput, - }, - { - 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, - }, - { - 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.", - inputSchema: zodToJsonSchema(ZipResourcesInputSchema) as ToolInput, - }, - ]; - 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.", - inputSchema: zodToJsonSchema(ListRootsSchema) as ToolInput, - }); - 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)", - inputSchema: zodToJsonSchema(ElicitationSchema) as ToolInput, - }); - - return { tools }; - }); - - server.setRequestHandler(CallToolRequestSchema, async (request, extra) => { - const { name, arguments: args } = request.params; - - if (name === ToolName.ECHO) { - const validatedArgs = EchoSchema.parse(args); - return { - content: [{ type: "text", text: `Echo: ${validatedArgs.message}` }], - }; - } - - if (name === ToolName.ADD) { - const validatedArgs = AddSchema.parse(args); - const sum = validatedArgs.a + validatedArgs.b; - return { - content: [ - { - type: "text", - text: `The sum of ${validatedArgs.a} and ${validatedArgs.b} is ${sum}.`, - }, - ], - }; - } - - if (name === ToolName.LONG_RUNNING_OPERATION) { - const validatedArgs = LongRunningOperationSchema.parse(args); - const { duration, steps } = validatedArgs; - const stepDuration = duration / steps; - const progressToken = request.params._meta?.progressToken; - - for (let i = 1; i < steps + 1; i++) { - await new Promise((resolve) => - setTimeout(resolve, stepDuration * 1000) - ); - - if (progressToken !== undefined) { - await server.notification( - { - method: "notifications/progress", - params: { - progress: i, - total: steps, - progressToken, - }, - }, - { relatedRequestId: extra.requestId } - ); - } - } - - return { - content: [ - { - type: "text", - text: `Long running operation completed. Duration: ${duration} seconds, Steps: ${steps}.`, - }, - ], - }; - } - - if (name === ToolName.PRINT_ENV) { - return { - content: [ - { - type: "text", - text: JSON.stringify(process.env, null, 2), - }, - ], - }; - } - - if (name === ToolName.SAMPLE_LLM) { - const validatedArgs = SampleLLMSchema.parse(args); - const { prompt, maxTokens } = validatedArgs; - - const result = await requestSampling( - prompt, - ToolName.SAMPLE_LLM, - maxTokens, - extra.sendRequest - ); - 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) - }`, - }, - ], - }; - } - - if (name === ToolName.GET_TINY_IMAGE) { - GetTinyImageSchema.parse(args); - return { - content: [ - { - type: "text", - text: "This is a tiny image:", - }, - { - type: "image", - data: MCP_TINY_IMAGE, - mimeType: "image/png", - }, - { - type: "text", - text: "The image above is the MCP tiny image.", - }, - ], - }; - } - - if (name === ToolName.ANNOTATED_MESSAGE) { - const { messageType, includeImage } = AnnotatedMessageSchema.parse(args); - - const content = []; - - // Main message with different priorities/audiences based on type - if (messageType === "error") { - content.push({ - type: "text", - text: "Error: Operation failed", - annotations: { - priority: 1.0, // Errors are highest priority - audience: ["user", "assistant"], // Both need to know about errors - }, - }); - } else if (messageType === "success") { - content.push({ - type: "text", - text: "Operation completed successfully", - annotations: { - priority: 0.7, // Success messages are important but not critical - audience: ["user"], // Success mainly for user consumption - }, - }); - } else if (messageType === "debug") { - content.push({ - type: "text", - text: "Debug: Cache hit ratio 0.95, latency 150ms", - annotations: { - priority: 0.3, // Debug info is low priority - audience: ["assistant"], // Technical details for assistant - }, - }); - } - - // Optional image with its own annotations - if (includeImage) { - content.push({ - type: "image", - data: MCP_TINY_IMAGE, - mimeType: "image/png", - annotations: { - priority: 0.5, - audience: ["user"], // Images primarily for user visualization - }, - }); - } - - return { content }; - } - - if (name === ToolName.GET_RESOURCE_REFERENCE) { - const validatedArgs = GetResourceReferenceSchema.parse(args); - const resourceId = validatedArgs.resourceId; - - const resourceIndex = resourceId - 1; - if (resourceIndex < 0 || resourceIndex >= ALL_RESOURCES.length) { - throw new Error(`Resource with ID ${resourceId} does not exist`); - } - - const resource = ALL_RESOURCES[resourceIndex]; - - return { - content: [ - { - type: "text", - text: `Returning resource reference for Resource ${resourceId}:`, - }, - { - type: "resource", - resource: resource, - }, - { - type: "text", - text: `You can access this resource using the URI: ${resource.uri}`, - }, - ], - }; - } - - 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", - }, - }, - required: ["name"], - }, - }, - }, - ElicitResultSchema, - { timeout: 10 * 60 * 1000 /* 10 minutes */ } - ); - - // Handle different response actions - const content = []; - - if (elicitationResult.action === "accept" && elicitationResult.content) { - content.push({ - type: "text", - text: `✅ User provided the requested information!`, - }); - - // Only access elicitationResult.content when action is accept - 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.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.petType) lines.push(`- Pet Type: ${userData.petType}`); - - content.push({ - type: "text", - text: `User inputs:\n${lines.join("\n")}`, - }); - } else if (elicitationResult.action === "decline") { - content.push({ - type: "text", - text: `❌ User declined to provide the requested information.`, - }); - } else if (elicitationResult.action === "cancel") { - content.push({ - type: "text", - text: `⚠️ User cancelled the elicitation dialog.`, - }); - } - - // Include raw result for debugging - content.push({ - type: "text", - text: `\nRaw result: ${JSON.stringify(elicitationResult, null, 2)}`, - }); - - return { content }; - } - - if (name === ToolName.GET_RESOURCE_LINKS) { - const { count } = GetResourceLinksSchema.parse(args); - const content = []; - - // Add intro text - content.push({ - type: "text", - text: `Here are ${count} resource links to resources available in this server (see full output in tool response if your client does not support resource_link yet):`, - }); - - // Return resource links to actual resources from ALL_RESOURCES - const actualCount = Math.min(count, ALL_RESOURCES.length); - for (let i = 0; i < actualCount; i++) { - const resource = ALL_RESOURCES[i]; - content.push({ - type: "resource_link", - uri: resource.uri, - name: resource.name, - description: `Resource ${i + 1}: ${ - resource.mimeType === "text/plain" - ? "plaintext resource" - : "binary blob resource" - }`, - mimeType: resource.mimeType, - }); - } - - return { content }; - } - - if (name === ToolName.STRUCTURED_CONTENT) { - // The same response is returned for every input. - const validatedArgs = StructuredContentSchema.input.parse(args); - - const weather = { - temperature: 22.5, - conditions: "Partly cloudy", - humidity: 65, - }; - - const backwardCompatiblecontent = { - type: "text", - text: JSON.stringify(weather), - }; - - return { - content: [backwardCompatiblecontent], - structuredContent: weather, - }; - } - - if (name === ToolName.ZIP_RESOURCES) { - const { files } = ZipResourcesInputSchema.parse(args); - - const zip = new JSZip(); - - for (const [fileName, fileUrl] of Object.entries(files)) { - try { - const response = await fetch(fileUrl); - if (!response.ok) { - 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) - }` - ); - } - } - - const uri = `data:application/zip;base64,${await zip.generateAsync({ - type: "base64", - })}`; - - return { - content: [ - { - type: "resource_link", - mimeType: "application/zip", - uri, - }, - ], - }; - } - - if (name === ToolName.LIST_ROOTS) { - ListRootsSchema.parse(args); - - if (!clientSupportsRoots) { - return { - 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.", - }, - ], - }; - } - - if (currentRoots.length === 0) { - return { - content: [ - { - type: "text", - 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", - }, - ], - }; - } - - 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` + - "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.", - }, - ], - }; - } - - throw new Error(`Unknown tool: ${name}`); - }); - - server.setRequestHandler(CompleteRequestSchema, async (request) => { - const { ref, argument } = request.params; - - if (ref.type === "ref/resource") { - const resourceId = ref.uri.split("/").pop(); - if (!resourceId) return { completion: { values: [] } }; - - // Filter resource IDs that start with the input value - const values = EXAMPLE_COMPLETIONS.resourceId.filter((id) => - id.startsWith(argument.value) - ); - return { completion: { values, hasMore: false, total: values.length } }; - } - - if (ref.type === "ref/prompt") { - // Handle completion for prompt arguments - const completions = - EXAMPLE_COMPLETIONS[argument.name as keyof typeof EXAMPLE_COMPLETIONS]; - if (!completions) return { completion: { values: [] } }; - - const values = completions.filter((value) => - value.startsWith(argument.value) - ); - return { completion: { values, hasMore: false, total: values.length } }; - } - - throw new Error(`Unknown reference type`); - }); - - // 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; - - // 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: `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(); - - if (clientCapabilities?.roots) { - clientSupportsRoots = true; - try { - const response = await server.listRoots(); - if (response && "roots" in response) { - currentRoots = response.roots; - - await server.sendLoggingMessage( - { - level: "info", - logger: "everything-server", - data: `Initial roots received: ${currentRoots.length} root(s) from client`, - }, - sessionId - ); - } else { - await server.sendLoggingMessage( - { - level: "warning", - logger: "everything-server", - data: "Client returned no roots set", - }, - sessionId - ); - } - } catch (error) { - await server.sendLoggingMessage( - { - level: "error", - logger: "everything-server", - data: `Failed to request initial roots from client: ${ - error instanceof Error ? error.message : String(error) - }`, - }, - sessionId - ); - } - } else { - await server.sendLoggingMessage( - { - level: "info", - logger: "everything-server", - data: "Client does not support MCP roots protocol", - }, - sessionId - ); - } - }; - - const cleanup = async () => { - if (subsUpdateInterval) clearInterval(subsUpdateInterval); - if (logsUpdateInterval) clearInterval(logsUpdateInterval); - }; - - return { server, cleanup, startNotificationIntervals }; -}; - -const MCP_TINY_IMAGE = - "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"; diff --git a/src/everything/server/index.ts b/src/everything/server/index.ts index 4f9bb1f2..f2e0a44f 100644 --- a/src/everything/server/index.ts +++ b/src/everything/server/index.ts @@ -1,19 +1,40 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { dirname, join } from "path"; -import { readFileSync } from "fs"; -import { fileURLToPath } from "url"; import { setSubscriptionHandlers, stopSimulatedResourceUpdates, } from "../resources/subscriptions.js"; import { registerTools } from "../tools/index.js"; -import { registerResources } from "../resources/index.js"; +import { registerResources, readInstructions } from "../resources/index.js"; import { registerPrompts } from "../prompts/index.js"; import { stopSimulatedLogging } from "./logging.js"; -import { setRootsListChangedHandler } from "./roots.js"; +import { syncRoots } from "./roots.js"; -// Everything Server factory -export const createServer = () => { +// Server Factory response +export type ServerFactoryResponse = { + server: McpServer; + clientConnected: (sessionId?: string) => void; + cleanup: (sessionId?: string) => void; +}; + +/** + * `ServerInstance` factory + * + * This function initializes a `McpServer` with specific capabilities and instructions, + * registers tools, resources, and prompts, and configures resource subscription handlers. + * + * It returns the server instance along with callbacks for post-connection setup and cleanup tasks. + * + * @function + * @returns {ServerFactoryResponse} An object containing the server instance, a `clientConnected` callback + * for managing new client sessions, and a `cleanup` function for handling server-side cleanup when + * a session ends. + * + * Properties of the returned object: + * - `server` {Object}: The initialized server instance. + * - `clientConnected` {Function}: A post-connect callback to enable operations that require a `sessionId`. + * - `cleanup` {Function}: Function to perform cleanup operations for a closing session. + */ +export const createServer: () => ServerFactoryResponse = () => { // Read the server instructions const instructions = readInstructions(); @@ -49,32 +70,17 @@ export const createServer = () => { // Set resource subscription handlers setSubscriptionHandlers(server); - // Return server instance, client connection handler, and cleanup function + // Return the ServerFactoryResponse return { server, clientConnected: (sessionId?: string) => { - // Set the roots list changed handler - setRootsListChangedHandler(server, sessionId); + // Set a roots list changed handler and fetch the initial roots list from the client + syncRoots(server, sessionId); }, cleanup: (sessionId?: string) => { // Stop any simulated logging or resource updates that may have been initiated. stopSimulatedLogging(sessionId); stopSimulatedResourceUpdates(sessionId); }, - }; + } satisfies ServerFactoryResponse; }; - -// Read the server instructions from a file -function readInstructions(): string { - const __filename = fileURLToPath(import.meta.url); - const __dirname = dirname(__filename); - const filePath = join(__dirname, "..", "docs", "server-instructions.md"); - let instructions; - - try { - instructions = readFileSync(filePath, "utf-8"); - } catch (e) { - instructions = "Server instructions not loaded: " + e; - } - return instructions; -} diff --git a/src/everything/server/logging.ts b/src/everything/server/logging.ts index 5c03606f..82edea16 100644 --- a/src/everything/server/logging.ts +++ b/src/everything/server/logging.ts @@ -55,7 +55,7 @@ export const beginSimulatedLogging = ( // Send once immediately sendSimulatedLoggingMessage(sessionId); - // Sen + // Send a randomly-leveled log message every 5 seconds logsUpdateIntervals.set( sessionId, setInterval(() => sendSimulatedLoggingMessage(sessionId), 5000) diff --git a/src/everything/server/roots.ts b/src/everything/server/roots.ts index 3525df17..0ab89e26 100644 --- a/src/everything/server/roots.ts +++ b/src/everything/server/roots.ts @@ -11,53 +11,67 @@ const roots: Map = new Map< >(); /** - * Sets a handler for the "RootsListChanged" notification from the client. + * Sync the root directories from the client by requesting and updating the roots list for + * the specified session. * - * This handler updates the local roots list when notified and logs relevant - * acknowledgement or error. + * Also sets up a notification handler to listen for changes in the roots list, ensuring that + * updates are automatically fetched and handled in real-time. * - * @param {McpServer} mcpServer - The instance of the McpServer managing server communication. - * @param {string | undefined} sessionId - An optional session ID used for logging purposes. + * @param {McpServer} server - An instance of the MCP server used to communicate with the client. + * @param {string} [sessionId] - An optional session id used to associate the roots list with a specific client session. + * + * @throws {Error} In case of a failure to request the roots from the client, an error log message is sent. */ -export const setRootsListChangedHandler = ( - mcpServer: McpServer, - sessionId?: string -) => { - const server = mcpServer.server; +export const syncRoots = (server: McpServer, sessionId?: string) => { + // Function to request the updated roots list from the client + const requestRoots = async () => { + try { + // Request the updated roots list from the client + const response = await server.server.listRoots(); + if (response && "roots" in response) { + // Store the roots list for this client + roots.set(sessionId, response.roots); - // Set the notification handler - server.setNotificationHandler( - RootsListChangedNotificationSchema, - async () => { - try { - // Request the updated roots list from the client - const response = await server.listRoots(); - if (response && "roots" in response) { - // Store the roots list for this client - roots.set(sessionId, response.roots); - - // Notify the client of roots received - await server.sendLoggingMessage( - { - level: "info", - logger: "everything-server", - data: `Roots updated: ${response.roots.length} root(s) received from client`, - }, - sessionId - ); - } - } catch (error) { + // Notify the client of roots received await server.sendLoggingMessage( { - level: "error", + level: "info", logger: "everything-server", - data: `Failed to request roots from client: ${ - error instanceof Error ? error.message : String(error) - }`, + data: `Roots updated: ${response.roots.length} root(s) received from client`, + }, + sessionId + ); + } else { + await server.sendLoggingMessage( + { + level: "info", + logger: "everything-server", + data: "Client returned no roots set", }, 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 + ); } + }; + + // Set the list changed notification handler + server.server.setNotificationHandler( + RootsListChangedNotificationSchema, + requestRoots ); + + // Request initial roots list after a brief delay + // Allows initial POST request to complete on streamableHttp transports + setTimeout(() => requestRoots(), 350); }; diff --git a/src/everything/transports/sse.ts b/src/everything/transports/sse.ts index a8c3fc4b..199e1fcb 100644 --- a/src/everything/transports/sse.ts +++ b/src/everything/transports/sse.ts @@ -5,6 +5,7 @@ import cors from "cors"; console.error("Starting SSE server..."); +// Express app with permissive CORS for testing with Inspector direct connect mode const app = express(); app.use( cors({ @@ -13,16 +14,20 @@ app.use( preflightContinue: false, optionsSuccessStatus: 204, }) -); // Enable CORS for all routes so Inspector can connect +); + +// Map sessionId to transport for each client const transports: Map = new Map< string, SSEServerTransport >(); +// Handle GET requests for new SSE streams app.get("/sse", async (req, res) => { let transport: SSEServerTransport; const { server, clientConnected, cleanup } = createServer(); + // Session Id should not exist for GET /sse requests if (req?.query?.sessionId) { const sessionId = req?.query?.sessionId as string; transport = transports.get(sessionId) as SSEServerTransport; @@ -31,15 +36,14 @@ app.get("/sse", async (req, res) => { transport.sessionId ); } else { - // Create and store transport for new session + // Create and store transport for the new session transport = new SSEServerTransport("/message", res); transports.set(transport.sessionId, transport); - // Connect server to transport + // Connect server to transport and invoke clientConnected callback await server.connect(transport); const sessionId = transport.sessionId; clientConnected(sessionId); - console.error("Client Connected: ", sessionId); // Handle close of connection @@ -47,13 +51,17 @@ app.get("/sse", async (req, res) => { const sessionId = transport.sessionId; console.error("Client Disconnected: ", sessionId); transports.delete(sessionId); - await cleanup(sessionId); + cleanup(sessionId); }; } }); +// Handle POST requests for client messages app.post("/message", async (req, res) => { + // Session Id should exist for POST /message requests const sessionId = req?.query?.sessionId as string; + + // Get the transport for this session and use it to handle the request const transport = transports.get(sessionId); if (transport) { console.error("Client Message from", sessionId); @@ -63,6 +71,7 @@ app.post("/message", async (req, res) => { } }); +// Start the express server const PORT = process.env.PORT || 3001; app.listen(PORT, () => { console.error(`Server is running on port ${PORT}`); diff --git a/src/everything/transports/stdio.ts b/src/everything/transports/stdio.ts index 0e3b1726..6ca65f1f 100644 --- a/src/everything/transports/stdio.ts +++ b/src/everything/transports/stdio.ts @@ -5,12 +5,22 @@ import { createServer } from "../server/index.js"; console.error("Starting default (STDIO) server..."); -async function main() { +/** + * The main method + * - Initializes the StdioServerTransport, sets up the server, + * - Connects the transport to the server, invokes the `clientConnected` callback, + * - Handles cleanup on process exit. + * + * @return {Promise} A promise that resolves when the main function has executed and the process exits. + */ +async function main(): Promise { const transport = new StdioServerTransport(); const { server, clientConnected, cleanup } = createServer(); + // Connect transport to server and invoke clientConnected callback await server.connect(transport); clientConnected(); + // Cleanup on exit process.on("SIGINT", async () => { await server.close(); diff --git a/src/everything/transports/streamableHttp.ts b/src/everything/transports/streamableHttp.ts index c0181cf5..1e903e10 100644 --- a/src/everything/transports/streamableHttp.ts +++ b/src/everything/transports/streamableHttp.ts @@ -7,6 +7,7 @@ import cors from "cors"; console.log("Starting Streamable HTTP server..."); +// Express app with permissive CORS for testing with Inspector direct connect mode const app = express(); app.use( cors({ @@ -16,13 +17,15 @@ app.use( optionsSuccessStatus: 204, exposedHeaders: ["mcp-session-id", "last-event-id", "mcp-protocol-version"], }) -); // Enable CORS for all routes so Inspector can connect +); +// Map sessionId to server transport for each client const transports: Map = new Map< string, StreamableHTTPServerTransport >(); +// Handle POST requests for client messages app.post("/mcp", async (req: Request, res: Response) => { console.log("Received MCP POST request"); try { @@ -47,7 +50,6 @@ app.post("/mcp", async (req: Request, res: Response) => { // 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); - clientConnected(sessionId); }, }); @@ -66,9 +68,8 @@ app.post("/mcp", async (req: Request, res: Response) => { // Connect the transport to the MCP server BEFORE handling the request // so responses can flow back through the same transport await server.connect(transport); - + clientConnected(transport.sessionId); await transport.handleRequest(req, res); - return; } else { // Invalid request - no session ID or not initialization request @@ -102,7 +103,7 @@ app.post("/mcp", async (req: Request, res: Response) => { } }); -// Handle GET requests for SSE streams (using built-in support from StreamableHTTP) +// Handle GET requests for SSE streams app.get("/mcp", async (req: Request, res: Response) => { console.log("Received MCP GET request"); const sessionId = req.headers["mcp-session-id"] as string | undefined; @@ -130,7 +131,7 @@ app.get("/mcp", async (req: Request, res: Response) => { await transport!.handleRequest(req, res); }); -// Handle DELETE requests for session termination (according to MCP spec) +// Handle DELETE requests for session termination app.delete("/mcp", async (req: Request, res: Response) => { const sessionId = req.headers["mcp-session-id"] as string | undefined; if (!sessionId || !transports.has(sessionId)) { @@ -172,6 +173,7 @@ const server = app.listen(PORT, () => { console.error(`MCP Streamable HTTP Server listening on port ${PORT}`); }); +// Handle server errors server.on("error", (err: unknown) => { const code = typeof err === "object" && err !== null && "code" in err