├── .gitignore ├── README.md ├── examples ├── client │ ├── package.json │ ├── src │ │ ├── modelcontextprotocol.ts │ │ ├── multiple.ts │ │ ├── server │ │ │ └── other-server.ts │ │ ├── shared.ts │ │ └── stdio.ts │ ├── stdio.log │ ├── tsconfig.json │ ├── tsup.config.ts │ └── vite.config.ts └── server │ ├── package.json │ ├── src │ ├── modelcontextprotocol.ts │ ├── shared.ts │ ├── sse.ts │ └── stdio.ts │ ├── tsconfig.json │ ├── tsup.config.ts │ └── vite.config.ts ├── package.json ├── packages ├── client │ ├── package.json │ ├── src │ │ ├── client.ts │ │ ├── index.ts │ │ ├── messenger.ts │ │ ├── notifications.ts │ │ └── transport │ │ │ ├── deferred.ts │ │ │ ├── index.ts │ │ │ ├── stdio.ts │ │ │ └── transport.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── server │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── messenger.ts │ │ ├── prompts │ │ │ ├── prompt-kit.ts │ │ │ └── prompt.ts │ │ ├── server.ts │ │ └── transport │ │ │ ├── sse.ts │ │ │ └── stdio.ts │ ├── tsconfig.json │ ├── tsup.config.bundled_g47gx86k3cq.mjs │ └── tsup.config.ts └── shared │ ├── package.json │ ├── src │ ├── error.ts │ ├── index.ts │ ├── mcp.ts │ └── schema.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tsconfig.base.json ├── tsconfig.json └── turbo.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | tsconfig.tsbuildinfo 4 | .turbo -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Effect MCP 2 | 3 | An implementation of the [Model Context Protocol (MCP)](https://github.com/modelcontextprotocol) using the [Effect](https://effect.website) library. 4 | 5 | ## Overview 6 | 7 | Effect MCP provides a TypeScript implementation of the Model Context Protocol, which enables AI models to interact with external tools and resources. This implementation leverages the Effect library for robust, type-safe, and functional programming patterns. 8 | 9 | ## Features 10 | 11 | - JSON-RPC based communication 12 | - Type-safe API using Effect and Schema 13 | - Stdio transport implementation 14 | - Tool registration and execution 15 | - Integration with Effect's AI toolkit 16 | 17 | ## Installation 18 | 19 | ```bash 20 | pnpm install 21 | ``` 22 | 23 | ## Usage 24 | 25 | See the `examples/basic` directory for a simple example of how to use the MCP server. 26 | 27 | ## Project Structure 28 | 29 | - `packages/client`: Core MCP client implementation 30 | - `packages/server`: Core MCP server implementation 31 | - `examples/client`: Basic examples of using the MCP server compared with vanilla TS MCP client 32 | - `examples/server`: Basic examples of using the MCP server compared with vanilla TS MCP server 33 | 34 | ## ⚠️ Active Development 35 | 36 | This project is in active development and is not yet ready for production use. Many features are still being implemented. 37 | 38 | Also MCP itself is still early and under development. There is currently a somewhat implicit 1:1 relationship between client and server. In my opinion this is likely to change and will require much refactoring. Hopefully this wont impact the surface API too much. 39 | 40 | ## Implementation Checklist 41 | 42 | ### MCP Server 43 | 44 | - [x] Basic JSON-RPC message handling 45 | - [x] Stdio transport implementation 46 | - [x] SSE transport implementation 47 | - [x] Server initialization 48 | - [x] Ping implementation 49 | - [ ] Add comprehensive error handling 50 | - [ ] Add tests 51 | - [ ] Ensure error codes match Spec 52 | 53 | #### MCP Client -> Server Requests 54 | 55 | - [x] Ping 56 | - [x] Initialize 57 | - [ ] Complete 58 | - [ ] Set Log Level 59 | - [x] Get Prompt 60 | - [x] List Prompts 61 | - [ ] List Resources 62 | - [ ] List Resource Templates 63 | - [ ] Read Resource 64 | - [ ] Notification Subscribe 65 | - [ ] Notification Unsubscribe 66 | - [x] List Tools 67 | - [x] Call Tool 68 | 69 | #### MCP Server -> Client Requests 70 | 71 | - [ ] Ping 72 | - [ ] Create Message 73 | - [ ] List Roots 74 | 75 | ### MCP Client 76 | 77 | - [x] Client Stdio Transport 78 | - [ ] Client SSE Transport 79 | 80 | ## License 81 | 82 | MIT 83 | -------------------------------------------------------------------------------- /examples/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client-examples", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/index.js", 6 | "type": "module", 7 | "scripts": { 8 | "build": "tsup" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@effect-mcp/client": "workspace:*", 15 | "@effect-mcp/server": "workspace:*", 16 | "@effect/ai": "^0.10.2", 17 | "@effect/platform": "^0.77.2", 18 | "@effect/platform-node": "^0.73.2", 19 | "@modelcontextprotocol/sdk": "^1.6.0", 20 | "effect": "^3.13.2", 21 | "zod": "^3.24.2" 22 | }, 23 | "devDependencies": { 24 | "tsup": "^8.3.6", 25 | "typescript": "^5.7.3", 26 | "vite": "^6.1.1", 27 | "vite-node": "^3.0.7" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/client/src/modelcontextprotocol.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { Client as McpClient } from "@modelcontextprotocol/sdk/client/index.js"; 4 | import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; 5 | import { serverCwd } from "./shared.js"; 6 | 7 | const client = new McpClient({ 8 | name: "Echo", 9 | version: "1.0.0", 10 | }); 11 | 12 | const transport = new StdioClientTransport({ 13 | command: "node", 14 | args: ["./dist/stdio.js"], 15 | cwd: serverCwd, 16 | }); 17 | await client.connect(transport); 18 | 19 | const prompts = await client.listPrompts(); 20 | 21 | console.dir(prompts, { depth: null }); 22 | 23 | const result = await client.getPrompt({ 24 | name: "Echo", 25 | arguments: { 26 | message: "Hello, world!", 27 | }, 28 | }); 29 | 30 | console.dir(result, { depth: null }); 31 | 32 | await transport.close(); 33 | -------------------------------------------------------------------------------- /examples/client/src/multiple.ts: -------------------------------------------------------------------------------- 1 | import { McpClient, StdioClientTransport } from "@effect-mcp/client"; 2 | import { Command } from "@effect/platform"; 3 | import { NodeContext, NodeRuntime } from "@effect/platform-node"; 4 | import { Effect, Logger, LogLevel } from "effect"; 5 | import { clientCwd, serverCwd } from "./shared.js"; 6 | 7 | const server1Cmd = Command.make("node", "./dist/modelcontextprotocol.js").pipe( 8 | Command.workingDirectory(serverCwd) 9 | ); 10 | 11 | const server2Cmd = Command.make("node", "./dist/server/other-server.js").pipe( 12 | Command.workingDirectory(clientCwd) 13 | ); 14 | 15 | const servers = { 16 | echo: server1Cmd, 17 | calculator: server2Cmd, 18 | }; 19 | 20 | const program = Effect.gen(function* () { 21 | const results = yield* Effect.forEach( 22 | Object.entries(servers), 23 | ([name, cmd]) => 24 | Effect.gen(function* () { 25 | const client = yield* McpClient.make({ 26 | name: name, 27 | version: "1.0.0", 28 | }).pipe(Effect.provide(StdioClientTransport.layer(cmd))); 29 | 30 | yield* client.initialize; 31 | 32 | const tools = yield* client.listToolsAwait({}); 33 | const prompts = yield* client.listPromptsAwait({}); 34 | 35 | return [name, { tools, prompts }] as const; 36 | }) 37 | ); 38 | 39 | return Object.fromEntries(results); 40 | }).pipe(Effect.flatMap((results) => Effect.logDebug(JSON.stringify(results)))); 41 | 42 | program.pipe( 43 | Effect.provide(NodeContext.layer), 44 | Logger.withMinimumLogLevel(LogLevel.Debug), 45 | Effect.scoped, 46 | NodeRuntime.runMain 47 | ); 48 | -------------------------------------------------------------------------------- /examples/client/src/server/other-server.ts: -------------------------------------------------------------------------------- 1 | import { 2 | McpServer, 3 | Prompt, 4 | PromptKit, 5 | StdioServerTransport, 6 | } from "@effect-mcp/server"; 7 | import { AiToolkit } from "@effect/ai"; 8 | import { NodeContext, NodeRuntime } from "@effect/platform-node"; 9 | import { Effect, Layer, Schema } from "effect"; 10 | 11 | /** 12 | * Tools 13 | */ 14 | 15 | class Calculator extends Schema.TaggedRequest()( 16 | "Calculator", 17 | { 18 | success: Schema.String, 19 | failure: Schema.String, 20 | payload: { 21 | expression: Schema.String, 22 | }, 23 | }, 24 | { description: "Evaluate a mathematical expression" } 25 | ) {} 26 | 27 | const toolkit = AiToolkit.empty.add(Calculator); 28 | 29 | const ToolkitLive = toolkit.implement((handlers) => 30 | handlers.handle("Calculator", (params) => { 31 | try { 32 | // Simple eval for demonstration purposes 33 | const result = eval(params.expression); 34 | return Effect.succeed(`Result: ${result}`); 35 | } catch (error) { 36 | return Effect.succeed(`Error: Invalid expression`); 37 | } 38 | }) 39 | ); 40 | 41 | /** 42 | * Prompts 43 | */ 44 | 45 | const PromptkitLive = PromptKit.empty 46 | .add( 47 | Prompt.effect( 48 | { 49 | name: "Calculate", 50 | description: "Evaluate a mathematical expression", 51 | arguments: { 52 | expression: Schema.String, 53 | }, 54 | }, 55 | (params) => 56 | Effect.succeed([ 57 | { 58 | role: "user", 59 | content: { 60 | type: "text", 61 | text: `Calculate: ${params.expression}`, 62 | }, 63 | }, 64 | ]) 65 | ) 66 | ) 67 | .add( 68 | Prompt.effect( 69 | { 70 | name: "Weather", 71 | description: "Get a weather forecast for a location", 72 | arguments: { 73 | location: Schema.String, 74 | includeHumidity: Schema.String.pipe(Schema.optional), 75 | }, 76 | }, 77 | (params) => 78 | Effect.gen(function* () { 79 | const humidity = 80 | params.includeHumidity === "true" ? `The humidity is 65%` : ""; 81 | return [ 82 | { 83 | role: "assistant", 84 | content: { 85 | type: "text", 86 | text: `The weather in ${params.location} is currently sunny and 72°F. ${humidity} Have a wonderful day!`, 87 | }, 88 | }, 89 | ]; 90 | }) 91 | ) 92 | ) 93 | .finalize(); 94 | /** 95 | * Server 96 | */ 97 | 98 | export const ServerLive = McpServer.layer({ 99 | name: "WeatherCalculator", 100 | version: "0.0.1", 101 | }).pipe( 102 | Layer.provide(ToolkitLive), 103 | Layer.provide(PromptkitLive), 104 | Layer.provide(Layer.scope) 105 | ); 106 | 107 | const AppLive = Layer.provideMerge(ServerLive, NodeContext.layer).pipe( 108 | Layer.provideMerge(Layer.scope) 109 | ); 110 | StdioServerTransport.make.pipe(Effect.provide(AppLive), NodeRuntime.runMain); 111 | -------------------------------------------------------------------------------- /examples/client/src/shared.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import path from "path"; 4 | import { fileURLToPath } from "url"; 5 | 6 | // ES Module equivalent for __dirname 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = path.dirname(__filename); 9 | export const serverCwd = path.resolve(__dirname, "..", "..", "server"); 10 | export const clientCwd = path.resolve(__dirname, ".."); 11 | 12 | -------------------------------------------------------------------------------- /examples/client/src/stdio.ts: -------------------------------------------------------------------------------- 1 | import { McpClient, StdioClientTransport } from "@effect-mcp/client"; 2 | import { Command } from "@effect/platform"; 3 | import { NodeContext, NodeRuntime } from "@effect/platform-node"; 4 | import { Effect, Logger, LogLevel } from "effect"; 5 | import { serverCwd } from "./shared.js"; 6 | 7 | const command = Command.make("node", "./dist/stdio.js").pipe( 8 | Command.workingDirectory(serverCwd) 9 | ); 10 | 11 | const client = McpClient.layer({ 12 | name: "Echo", 13 | version: "1.0.0", 14 | }); 15 | 16 | const transport = StdioClientTransport.layer(command); 17 | 18 | const program = Effect.gen(function* () { 19 | const client = yield* McpClient.McpClient; 20 | 21 | yield* client.initialize; 22 | 23 | const tools = yield* client.listToolsAwait({}); 24 | const prompts = yield* client.listPromptsAwait({}); 25 | 26 | return { 27 | prompts, 28 | tools, 29 | }; 30 | }).pipe(Effect.flatMap((result) => Effect.logDebug(JSON.stringify(result)))); 31 | 32 | program.pipe( 33 | Effect.provide(client), 34 | Effect.provide(transport), 35 | Effect.provide(NodeContext.layer), 36 | Logger.withMinimumLogLevel(LogLevel.Debug), 37 | Effect.scoped, 38 | NodeRuntime.runMain 39 | ); 40 | -------------------------------------------------------------------------------- /examples/client/stdio.log: -------------------------------------------------------------------------------- 1 | timestamp=2025-02-27T21:46:34.421Z level=INFO fiber=#0 message="Received message: {\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{},\"clientInfo\":{\"name\":\"calculator\",\"version\":\"1.0.0\"}},\"jsonrpc\":\"2.0\",\"id\":\"ea256c9a-296d-4bd4-8be5-df8750e16d77\"}" 2 | timestamp=2025-02-27T21:46:34.423Z level=INFO fiber=#12 message="Handling request:" message="{\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{},\"clientInfo\":{\"name\":\"calculator\",\"version\":\"1.0.0\"}}}" 3 | timestamp=2025-02-27T21:46:34.423Z level=INFO fiber=#12 message="Initializing server with client info:" message="{\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{},\"clientInfo\":{\"name\":\"calculator\",\"version\":\"1.0.0\"}}}" 4 | timestamp=2025-02-27T21:46:34.423Z level=INFO fiber=#12 message="Sending result:" message="{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{\"tools\":{},\"prompts\":{},\"resources\":{},\"resourceTemplates\":{}},\"serverInfo\":{\"name\":\"WeatherCalculator\",\"version\":\"0.0.1\"}}" 5 | timestamp=2025-02-27T21:46:34.424Z level=INFO fiber=#11 message="Sending message:" message="{\"jsonrpc\":\"2.0\",\"id\":\"ea256c9a-296d-4bd4-8be5-df8750e16d77\",\"result\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{\"tools\":{},\"prompts\":{},\"resources\":{},\"resourceTemplates\":{}},\"serverInfo\":{\"name\":\"WeatherCalculator\",\"version\":\"0.0.1\"}}}" 6 | timestamp=2025-02-27T21:46:34.429Z level=INFO fiber=#0 message="Received message: {\"method\":\"notifications/initialized\",\"params\":{},\"jsonrpc\":\"2.0\"}" 7 | timestamp=2025-02-27T21:46:34.430Z level=DEBUG fiber=#13 message="Not implemented" message="[{\"method\":\"notifications/initialized\",\"params\":{}}]" 8 | timestamp=2025-02-27T21:46:34.430Z level=INFO fiber=#0 message="Received message: {\"method\":\"tools/list\",\"params\":{},\"jsonrpc\":\"2.0\",\"id\":\"dbfc6e79-77e1-41f5-931e-9c3dd27e2ded\"}" 9 | timestamp=2025-02-27T21:46:34.430Z level=INFO fiber=#14 message="Handling request:" message="{\"method\":\"tools/list\",\"params\":{}}" 10 | timestamp=2025-02-27T21:46:34.431Z level=INFO fiber=#14 message="Sending result:" message="{\"tools\":[{\"name\":\"Calculator\",\"inputSchema\":{\"type\":\"object\",\"required\":[\"_tag\",\"expression\"],\"properties\":{\"_tag\":{\"type\":\"string\",\"enum\":[\"Calculator\"]},\"expression\":{\"type\":\"string\"}},\"additionalProperties\":false,\"description\":\"Evaluate a mathematical expression\"},\"description\":\"Evaluate a mathematical expression\"}]}" 11 | timestamp=2025-02-27T21:46:34.431Z level=INFO fiber=#11 message="Sending message:" message="{\"jsonrpc\":\"2.0\",\"id\":\"dbfc6e79-77e1-41f5-931e-9c3dd27e2ded\",\"result\":{\"tools\":[{\"name\":\"Calculator\",\"inputSchema\":{\"type\":\"object\",\"required\":[\"_tag\",\"expression\"],\"properties\":{\"_tag\":{\"type\":\"string\",\"enum\":[\"Calculator\"]},\"expression\":{\"type\":\"string\"}},\"additionalProperties\":false,\"description\":\"Evaluate a mathematical expression\"},\"description\":\"Evaluate a mathematical expression\"}]}}" 12 | timestamp=2025-02-27T21:46:34.433Z level=INFO fiber=#0 message="Received message: {\"method\":\"prompts/list\",\"params\":{},\"jsonrpc\":\"2.0\",\"id\":\"9f143108-eea9-489d-bd37-61600f51c625\"}" 13 | timestamp=2025-02-27T21:46:34.433Z level=INFO fiber=#15 message="Handling request:" message="{\"method\":\"prompts/list\",\"params\":{}}" 14 | timestamp=2025-02-27T21:46:34.434Z level=INFO fiber=#15 message="Sending result:" message="{\"prompts\":[{\"name\":\"Calculate\",\"description\":\"Evaluate a mathematical expression\",\"arguments\":[{\"name\":\"expression\",\"description\":\"\",\"required\":true}]},{\"name\":\"Weather\",\"description\":\"Get a weather forecast for a location\",\"arguments\":[{\"name\":\"location\",\"description\":\"\",\"required\":true},{\"name\":\"includeHumidity\",\"description\":\"\",\"required\":false}]}]}" 15 | timestamp=2025-02-27T21:46:34.434Z level=INFO fiber=#11 message="Sending message:" message="{\"jsonrpc\":\"2.0\",\"id\":\"9f143108-eea9-489d-bd37-61600f51c625\",\"result\":{\"prompts\":[{\"name\":\"Calculate\",\"description\":\"Evaluate a mathematical expression\",\"arguments\":[{\"name\":\"expression\",\"description\":\"\",\"required\":true}]},{\"name\":\"Weather\",\"description\":\"Get a weather forecast for a location\",\"arguments\":[{\"name\":\"location\",\"description\":\"\",\"required\":true},{\"name\":\"includeHumidity\",\"description\":\"\",\"required\":false}]}]}}" 16 | timestamp=2025-02-27T21:46:41.689Z level=INFO fiber=#0 message="Received message: {\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{},\"clientInfo\":{\"name\":\"calculator\",\"version\":\"1.0.0\"}},\"jsonrpc\":\"2.0\",\"id\":\"f4b701ea-a485-426b-91c1-60b6a0707469\"}" 17 | timestamp=2025-02-27T21:46:41.691Z level=INFO fiber=#12 message="Handling request:" message="{\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{},\"clientInfo\":{\"name\":\"calculator\",\"version\":\"1.0.0\"}}}" 18 | timestamp=2025-02-27T21:46:41.691Z level=INFO fiber=#12 message="Initializing server with client info:" message="{\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{},\"clientInfo\":{\"name\":\"calculator\",\"version\":\"1.0.0\"}}}" 19 | timestamp=2025-02-27T21:46:41.691Z level=INFO fiber=#12 message="Sending result:" message="{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{\"tools\":{},\"prompts\":{},\"resources\":{},\"resourceTemplates\":{}},\"serverInfo\":{\"name\":\"WeatherCalculator\",\"version\":\"0.0.1\"}}" 20 | timestamp=2025-02-27T21:46:41.691Z level=INFO fiber=#11 message="Sending message:" message="{\"jsonrpc\":\"2.0\",\"id\":\"f4b701ea-a485-426b-91c1-60b6a0707469\",\"result\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{\"tools\":{},\"prompts\":{},\"resources\":{},\"resourceTemplates\":{}},\"serverInfo\":{\"name\":\"WeatherCalculator\",\"version\":\"0.0.1\"}}}" 21 | timestamp=2025-02-27T21:46:41.695Z level=INFO fiber=#0 message="Received message: {\"method\":\"notifications/initialized\",\"params\":{},\"jsonrpc\":\"2.0\"}" 22 | timestamp=2025-02-27T21:46:41.695Z level=DEBUG fiber=#13 message="Not implemented" message="[{\"method\":\"notifications/initialized\",\"params\":{}}]" 23 | timestamp=2025-02-27T21:46:41.695Z level=INFO fiber=#0 message="Received message: {\"method\":\"tools/list\",\"params\":{},\"jsonrpc\":\"2.0\",\"id\":\"7e8e9f92-c5e2-4597-8c14-790b13ff186c\"}" 24 | timestamp=2025-02-27T21:46:41.696Z level=INFO fiber=#14 message="Handling request:" message="{\"method\":\"tools/list\",\"params\":{}}" 25 | timestamp=2025-02-27T21:46:41.696Z level=INFO fiber=#14 message="Sending result:" message="{\"tools\":[{\"name\":\"Calculator\",\"inputSchema\":{\"type\":\"object\",\"required\":[\"_tag\",\"expression\"],\"properties\":{\"_tag\":{\"type\":\"string\",\"enum\":[\"Calculator\"]},\"expression\":{\"type\":\"string\"}},\"additionalProperties\":false,\"description\":\"Evaluate a mathematical expression\"},\"description\":\"Evaluate a mathematical expression\"}]}" 26 | timestamp=2025-02-27T21:46:41.696Z level=INFO fiber=#11 message="Sending message:" message="{\"jsonrpc\":\"2.0\",\"id\":\"7e8e9f92-c5e2-4597-8c14-790b13ff186c\",\"result\":{\"tools\":[{\"name\":\"Calculator\",\"inputSchema\":{\"type\":\"object\",\"required\":[\"_tag\",\"expression\"],\"properties\":{\"_tag\":{\"type\":\"string\",\"enum\":[\"Calculator\"]},\"expression\":{\"type\":\"string\"}},\"additionalProperties\":false,\"description\":\"Evaluate a mathematical expression\"},\"description\":\"Evaluate a mathematical expression\"}]}}" 27 | timestamp=2025-02-27T21:46:41.698Z level=INFO fiber=#0 message="Received message: {\"method\":\"prompts/list\",\"params\":{},\"jsonrpc\":\"2.0\",\"id\":\"404aa1d5-a7a3-4f4a-83c3-4b63f1b360ff\"}" 28 | timestamp=2025-02-27T21:46:41.698Z level=INFO fiber=#15 message="Handling request:" message="{\"method\":\"prompts/list\",\"params\":{}}" 29 | timestamp=2025-02-27T21:46:41.698Z level=INFO fiber=#15 message="Sending result:" message="{\"prompts\":[{\"name\":\"Calculate\",\"description\":\"Evaluate a mathematical expression\",\"arguments\":[{\"name\":\"expression\",\"description\":\"\",\"required\":true}]},{\"name\":\"Weather\",\"description\":\"Get a weather forecast for a location\",\"arguments\":[{\"name\":\"location\",\"description\":\"\",\"required\":true},{\"name\":\"includeHumidity\",\"description\":\"\",\"required\":false}]}]}" 30 | timestamp=2025-02-27T21:46:41.698Z level=INFO fiber=#11 message="Sending message:" message="{\"jsonrpc\":\"2.0\",\"id\":\"404aa1d5-a7a3-4f4a-83c3-4b63f1b360ff\",\"result\":{\"prompts\":[{\"name\":\"Calculate\",\"description\":\"Evaluate a mathematical expression\",\"arguments\":[{\"name\":\"expression\",\"description\":\"\",\"required\":true}]},{\"name\":\"Weather\",\"description\":\"Get a weather forecast for a location\",\"arguments\":[{\"name\":\"location\",\"description\":\"\",\"required\":true},{\"name\":\"includeHumidity\",\"description\":\"\",\"required\":false}]}]}}" 31 | -------------------------------------------------------------------------------- /examples/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { "extends": "../../tsconfig.base.json" } 2 | -------------------------------------------------------------------------------- /examples/client/tsup.config.ts: -------------------------------------------------------------------------------- 1 | // tsup.config.ts 2 | import { defineConfig } from "tsup"; 3 | 4 | export default defineConfig({ 5 | format: ["esm"], 6 | entry: ["src/**/*.ts"], 7 | clean: true, 8 | target: "esnext", 9 | minify: true, 10 | sourcemap: false, 11 | tsconfig: "tsconfig.json", 12 | }); 13 | -------------------------------------------------------------------------------- /examples/client/vite.config.ts: -------------------------------------------------------------------------------- 1 | // https://vite.dev/config/ 2 | export default { 3 | resolve: { 4 | conditions: ["@effect-mcp/dev"], 5 | }, 6 | ssr: { 7 | resolve: { 8 | conditions: ["@effect-mcp/dev"], 9 | }, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /examples/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/index.js", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "vite-node src/index.ts", 9 | "build": "tsup" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "@effect-mcp/server": "workspace:*", 16 | "@effect/ai": "^0.10.2", 17 | "@effect/platform": "^0.77.2", 18 | "@effect/platform-node": "^0.73.2", 19 | "@modelcontextprotocol/sdk": "^1.6.0", 20 | "effect": "^3.13.2", 21 | "zod": "^3.24.2" 22 | }, 23 | "devDependencies": { 24 | "tsup": "^8.3.6", 25 | "typescript": "^5.7.3", 26 | "vite": "^6.1.1", 27 | "vite-node": "^3.0.7" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/server/src/modelcontextprotocol.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { 4 | McpServer, 5 | ResourceTemplate, 6 | } from "@modelcontextprotocol/sdk/server/mcp.js"; 7 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 8 | import { z } from "zod"; 9 | 10 | const server = new McpServer({ 11 | name: "Echo", 12 | version: "1.0.0", 13 | }); 14 | 15 | server.resource( 16 | "Echo", 17 | new ResourceTemplate("echo://{message}", { list: undefined }), 18 | async (uri, { message }) => ({ 19 | contents: [ 20 | { 21 | uri: uri.href, 22 | text: `Resource echo: ${message}`, 23 | }, 24 | ], 25 | }) 26 | ); 27 | 28 | server.tool("Echo", { message: z.string() }, async ({ message }) => ({ 29 | content: [{ type: "text", text: `Tool echo: ${message}` }], 30 | })); 31 | 32 | server.prompt("Echo", { message: z.string() }, ({ message }) => ({ 33 | messages: [ 34 | { 35 | role: "user", 36 | content: { 37 | type: "text", 38 | text: `Please process this message: ${message}`, 39 | }, 40 | }, 41 | ], 42 | })); 43 | 44 | const transport = new StdioServerTransport(); 45 | await server.connect(transport); 46 | -------------------------------------------------------------------------------- /examples/server/src/shared.ts: -------------------------------------------------------------------------------- 1 | import { McpServer, Prompt, PromptKit } from "@effect-mcp/server"; 2 | import { AiToolkit } from "@effect/ai"; 3 | import { Effect, Layer, Schema } from "effect"; 4 | 5 | /** 6 | * Tools 7 | */ 8 | 9 | class Echo extends Schema.TaggedRequest()( 10 | "Echo", 11 | { 12 | success: Schema.String, 13 | failure: Schema.String, 14 | payload: { 15 | message: Schema.String, 16 | }, 17 | }, 18 | { description: "Echo a message" } 19 | ) {} 20 | 21 | const toolkit = AiToolkit.empty.add(Echo); 22 | 23 | const ToolkitLive = toolkit.implement((handlers) => 24 | handlers.handle("Echo", (params) => Effect.succeed(`Echo: ${params.message}`)) 25 | ); 26 | 27 | /** 28 | * Prompts 29 | */ 30 | 31 | const PromptkitLive = PromptKit.empty 32 | .add( 33 | Prompt.effect( 34 | { 35 | name: "Echo", 36 | description: "Echo a message", 37 | arguments: { 38 | message: Schema.String, 39 | }, 40 | }, 41 | (params) => 42 | Effect.succeed([ 43 | { 44 | role: "user", 45 | content: { 46 | type: "text", 47 | text: `Echo: ${params.message}`, 48 | }, 49 | }, 50 | ]) 51 | ) 52 | ) 53 | .add( 54 | Prompt.effect( 55 | { 56 | name: "Greet", 57 | description: "Greet someone with a friendly message", 58 | arguments: { 59 | name: Schema.String, 60 | includeTime: Schema.String.pipe(Schema.optional), 61 | }, 62 | }, 63 | (params) => 64 | Effect.gen(function* () { 65 | const time = params.includeTime === "true" 66 | ? `The time is ${new Date().toLocaleTimeString()}` 67 | : ""; 68 | return [ 69 | { 70 | role: "assistant", 71 | content: { 72 | type: "text", 73 | text: `Hello ${params.name}! ${time} Hope you're having a great day!`, 74 | }, 75 | }, 76 | ]; 77 | }) 78 | ) 79 | ) 80 | .finalize(); 81 | /** 82 | * Server 83 | */ 84 | 85 | export const ServerLive = McpServer.layer({ 86 | name: "Echo", 87 | version: "0.0.1", 88 | }).pipe( 89 | Layer.provide(ToolkitLive), 90 | Layer.provide(PromptkitLive), 91 | Layer.provide(Layer.scope) 92 | ); 93 | -------------------------------------------------------------------------------- /examples/server/src/sse.ts: -------------------------------------------------------------------------------- 1 | import { SSEServerTransport } from "@effect-mcp/server"; 2 | import { HttpRouter, HttpServer, type HttpPlatform } from "@effect/platform"; 3 | import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"; 4 | import { Layer } from "effect"; 5 | import { createServer } from "node:http"; 6 | import { ServerLive } from "./shared.js"; 7 | 8 | const router = HttpRouter.empty.pipe( 9 | HttpRouter.append(SSEServerTransport.SSERoute("/messages")), 10 | HttpRouter.append(SSEServerTransport.MessageRoute()) 11 | ); 12 | 13 | export const listen = ( 14 | app: Layer.Layer< 15 | never, 16 | never, 17 | HttpPlatform.HttpPlatform | HttpServer.HttpServer 18 | >, 19 | port: number 20 | ) => 21 | NodeRuntime.runMain( 22 | Layer.launch( 23 | Layer.provide( 24 | app, 25 | NodeHttpServer.layer(() => createServer(), { port }) 26 | ) 27 | ) 28 | ); 29 | 30 | const app = router.pipe(HttpServer.serve(), Layer.provide(ServerLive)); 31 | 32 | 33 | listen(app, 3001); 34 | -------------------------------------------------------------------------------- /examples/server/src/stdio.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { StdioServerTransport } from "@effect-mcp/server"; 4 | import { NodeContext, NodeRuntime } from "@effect/platform-node"; 5 | import { Effect, Layer } from "effect"; 6 | import { ServerLive } from "./shared.js"; 7 | 8 | const AppLive = Layer.provideMerge(ServerLive, NodeContext.layer).pipe( 9 | Layer.provideMerge(Layer.scope) 10 | ); 11 | 12 | StdioServerTransport.make.pipe(Effect.provide(AppLive), NodeRuntime.runMain); 13 | -------------------------------------------------------------------------------- /examples/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { "extends": "../../tsconfig.base.json" } 2 | -------------------------------------------------------------------------------- /examples/server/tsup.config.ts: -------------------------------------------------------------------------------- 1 | // tsup.config.ts 2 | import { defineConfig } from "tsup"; 3 | 4 | export default defineConfig({ 5 | format: ["esm"], 6 | entry: ["src/*.ts"], 7 | clean: true, 8 | target: "esnext", 9 | minify: true, 10 | sourcemap: false, 11 | tsconfig: "tsconfig.json", 12 | }); 13 | -------------------------------------------------------------------------------- /examples/server/vite.config.ts: -------------------------------------------------------------------------------- 1 | // https://vite.dev/config/ 2 | export default { 3 | resolve: { 4 | conditions: ["@effect-mcp/dev"], 5 | }, 6 | ssr: { 7 | resolve: { 8 | conditions: ["@effect-mcp/dev"], 9 | }, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "effect-mcp", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "description": "Effect MCP", 6 | "keywords": [ 7 | "effect", 8 | "mcp", 9 | "modelcontextprotocol" 10 | ], 11 | "author": "Garrett Hardin", 12 | "license": "MIT", 13 | "scripts": { 14 | "build": "turbo run build", 15 | "build:watch": "turbo watch build" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^22.13.5", 19 | "turbo": "^2.4.2", 20 | "typescript": "^5.7.3" 21 | }, 22 | "workspaces": [ 23 | "examples/*", 24 | "packages/*" 25 | ], 26 | "packageManager": "pnpm@9.14.2" 27 | } 28 | -------------------------------------------------------------------------------- /packages/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@effect-mcp/client", 3 | "version": "0.0.1", 4 | "description": "Effect MCP Client", 5 | "type": "module", 6 | "scripts": { 7 | "build": "pnpm run build:code && pnpm run build:types", 8 | "build:code": "tsup", 9 | "build:types": "rm -f tsconfig.tsbuildinfo && tsc --emitDeclarationOnly" 10 | }, 11 | "keywords": [ 12 | "effect", 13 | "mcp", 14 | "modelcontextprotocol" 15 | ], 16 | "author": "Garrett Hardin", 17 | "license": "MIT", 18 | "files": [ 19 | "dist", 20 | "src" 21 | ], 22 | "exports": { 23 | ".": { 24 | "import": { 25 | "@effect-mcp/dev": "./src/index.ts", 26 | "types": "./dist/index.d.ts", 27 | "default": "./dist/index.js" 28 | }, 29 | "require": { 30 | "@effect-mcp/dev": "./src/index.ts", 31 | "types": "./dist/index.d.ts", 32 | "default": "./dist/index.cjs" 33 | } 34 | } 35 | }, 36 | "dependencies": { 37 | "@effect-mcp/shared": "workspace:*", 38 | "@effect/platform": "^0.77.2" 39 | }, 40 | "peerDependencies": { 41 | "effect": "^3.13.2", 42 | "typescript": "^5.7.3" 43 | }, 44 | "devDependencies": { 45 | "tsup": "^8.3.6" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/client/src/client.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallToolRequest, 3 | CallToolResult, 4 | CancelledNotification, 5 | CompleteRequest, 6 | CompleteResult, 7 | EmptyResult, 8 | GetPromptRequest, 9 | GetPromptResult, 10 | Implementation, 11 | InitializedNotification, 12 | InitializeRequest, 13 | InitializeResult, 14 | JSONRPCError, 15 | JsonRpcError, 16 | JsonRpcErrorCode, 17 | JSONRPCMessage, 18 | JSONRPCNotification, 19 | JSONRPCRequest, 20 | JSONRPCResponse, 21 | LATEST_PROTOCOL_VERSION, 22 | ListPromptsRequest, 23 | ListPromptsResult, 24 | ListResourcesRequest, 25 | ListResourcesResult, 26 | ListResourceTemplatesRequest, 27 | ListResourceTemplatesResult, 28 | ListToolsRequest, 29 | ListToolsResult, 30 | PingRequest, 31 | ReadResourceRequest, 32 | ReadResourceResult, 33 | RequestId, 34 | ServerNotification, 35 | ServerRequest, 36 | ServerResult, 37 | SubscribeRequest, 38 | UnsubscribeRequest, 39 | } from "@effect-mcp/shared"; 40 | import * as Context from "effect/Context"; 41 | import * as Deferred from "effect/Deferred"; 42 | import * as Duration from "effect/Duration"; 43 | import * as Effect from "effect/Effect"; 44 | import * as HashMap from "effect/HashMap"; 45 | import * as Layer from "effect/Layer"; 46 | import * as Mailbox from "effect/Mailbox"; 47 | import * as Match from "effect/Match"; 48 | import * as Option from "effect/Option"; 49 | import * as Ref from "effect/Ref"; 50 | import * as Schema from "effect/Schema"; 51 | import * as Stream from "effect/Stream"; 52 | import { DeferredMap } from "./transport/deferred.js"; 53 | import { Transport } from "./transport/transport.js"; 54 | const generateId = () => RequestId.make(crypto.randomUUID()); 55 | 56 | export type McpClientOpts = { 57 | timeout?: Duration.Duration; 58 | }; 59 | 60 | const _notImplemented = (...args: any[]) => 61 | Effect.gen(function* () { 62 | yield* Effect.logDebug(`Not implemented`, args); 63 | }); 64 | 65 | export namespace McpClient { 66 | export interface Service { 67 | initialize: Effect.Effect; 68 | complete: (params: CompleteRequest["params"]) => Effect.Effect; 69 | completeAwait: ( 70 | params: CompleteRequest["params"] 71 | ) => Effect.Effect; 72 | getPrompt: (params: GetPromptRequest["params"]) => Effect.Effect; 73 | getPromptAwait: ( 74 | params: GetPromptRequest["params"] 75 | ) => Effect.Effect; 76 | listPrompts: ( 77 | params: ListPromptsRequest["params"] 78 | ) => Effect.Effect; 79 | listPromptsAwait: ( 80 | params: ListPromptsRequest["params"] 81 | ) => Effect.Effect; 82 | readResource: ( 83 | params: ReadResourceRequest["params"] 84 | ) => Effect.Effect; 85 | readResourceAwait: ( 86 | params: ReadResourceRequest["params"] 87 | ) => Effect.Effect; 88 | listResources: ( 89 | params: ListResourcesRequest["params"] 90 | ) => Effect.Effect; 91 | listResourcesAwait: ( 92 | params: ListResourcesRequest["params"] 93 | ) => Effect.Effect; 94 | listResourceTemplates: ( 95 | params: ListResourceTemplatesRequest["params"] 96 | ) => Effect.Effect; 97 | listResourceTemplatesAwait: ( 98 | params: ListResourceTemplatesRequest["params"] 99 | ) => Effect.Effect; 100 | callTool: (params: CallToolRequest["params"]) => Effect.Effect; 101 | callToolAwait: ( 102 | params: CallToolRequest["params"] 103 | ) => Effect.Effect; 104 | listTools: (params: ListToolsRequest["params"]) => Effect.Effect; 105 | listToolsAwait: ( 106 | params: ListToolsRequest["params"] 107 | ) => Effect.Effect; 108 | ping: () => Effect.Effect; 109 | pingAwait: () => Effect.Effect; 110 | } 111 | } 112 | 113 | export class McpClient extends Context.Tag("McpClient")< 114 | McpClient, 115 | McpClient.Service 116 | >() {} 117 | 118 | export const make = (config: Implementation, opts?: McpClientOpts) => 119 | Effect.gen(function* () { 120 | const transport = yield* Transport; 121 | const server = yield* Ref.make(null); 122 | const deferredRequests = yield* DeferredMap; 123 | 124 | /** 125 | * 126 | * Internal methods 127 | * 128 | */ 129 | const handleResponse = Effect.fn("HandleResponse")(function* ( 130 | message: JSONRPCResponse 131 | ) { 132 | return yield* Effect.gen(function* () { 133 | const response = yield* Schema.decode(ServerResult)( 134 | message.result 135 | ).pipe( 136 | Effect.mapError((e) => 137 | JsonRpcError.fromCode("ParseError", e.message, e.cause) 138 | ) 139 | ); 140 | 141 | const deferred = yield* deferredRequests.pipe( 142 | Ref.get, 143 | Effect.map((map) => HashMap.get(map, message.id)) 144 | ); 145 | 146 | if (Option.isSome(deferred)) { 147 | yield* Deferred.succeed(deferred.value, response); 148 | } 149 | }).pipe( 150 | Effect.catchTag("JsonRpcError", (err) => 151 | transport.sendError(message.id, err) 152 | ) 153 | ); 154 | }); 155 | 156 | const handleError = Effect.fn("HandleError")(function* ( 157 | message: JSONRPCError 158 | ) { 159 | const deferred = yield* deferredRequests.pipe( 160 | Ref.get, 161 | Effect.map((map) => HashMap.get(map, message.id)) 162 | ); 163 | 164 | if (Option.isSome(deferred)) { 165 | yield* Deferred.fail(deferred.value, new JsonRpcError(message.error)); 166 | } 167 | }); 168 | 169 | const handleNotification = Effect.fn("HandleNotification")(function* ( 170 | message: JSONRPCNotification 171 | ) { 172 | return yield* Effect.gen(function* () { 173 | const notification = yield* Schema.decodeUnknown(ServerNotification)( 174 | message 175 | ); 176 | // yield* notifications.notify(notification); 177 | }).pipe( 178 | Effect.catchTag("ParseError", (err) => 179 | Effect.logError( 180 | `Error handling notification: ${err.message}`, 181 | message 182 | ) 183 | ) 184 | ); 185 | }); 186 | 187 | const _handlePing = Effect.fn("HandlePing")(function* ( 188 | id: RequestId, 189 | message: PingRequest 190 | ) { 191 | const response = EmptyResult.make({ 192 | id, 193 | }); 194 | 195 | yield* transport.sendResult(id, response); 196 | }); 197 | 198 | const handleRequest = Effect.fn("HandleRequest")(function* ( 199 | rawMessage: JSONRPCRequest 200 | ) { 201 | return yield* Effect.gen(function* () { 202 | const request = yield* Schema.decodeUnknown(ServerRequest)({ 203 | method: rawMessage.method, 204 | params: rawMessage.params, 205 | }).pipe( 206 | Effect.mapError((error) => 207 | JsonRpcError.fromCode("ParseError", error.message, error.cause) 208 | ) 209 | ); 210 | 211 | yield* Match.value(request).pipe( 212 | Match.when({ method: "ping" }, (msg) => 213 | _handlePing(rawMessage.id, msg) 214 | ), 215 | Match.when({ method: "roots/list" }, _notImplemented), 216 | Match.when({ method: "sampling/createMessage" }, _notImplemented), 217 | Match.exhaustive 218 | ); 219 | }).pipe( 220 | Effect.tapError((err) => 221 | Effect.logError(`Error handling request: ${err.message}`) 222 | ), 223 | Effect.catchTag("JsonRpcError", (err) => 224 | transport.sendError(rawMessage.id, err) 225 | ) 226 | ); 227 | }); 228 | 229 | const handleMessage = Effect.fn("HandleMessage")(function* ( 230 | message: JSONRPCMessage 231 | ) { 232 | yield* Match.value(message).pipe( 233 | Match.when( 234 | (message): message is JSONRPCError => 235 | "error" in message && typeof message.error === "object", 236 | (msg) => handleError(msg) 237 | ), 238 | Match.when( 239 | (message): message is JSONRPCResponse => 240 | "result" in message && typeof message.result === "object", 241 | (msg) => handleResponse(msg) 242 | ), 243 | Match.when( 244 | (message): message is JSONRPCRequest => "id" in message, 245 | (msg) => handleRequest(msg) 246 | ), 247 | Match.orElse((msg) => handleNotification(msg)) 248 | ); 249 | }); 250 | 251 | // Listen for incoming messages 252 | yield* Mailbox.toStream(transport.inbound).pipe( 253 | Stream.mapEffect(Schema.decodeUnknown(JSONRPCMessage)), 254 | Stream.catchTag("ParseError", (err) => 255 | Effect.logError(`Error handling message: ${err.message}`) 256 | ), 257 | Stream.filter((msg) => msg !== void 0), 258 | Stream.tap((msg) => Effect.logDebug(`Handling message:`, msg)), 259 | Stream.runForEach(handleMessage), 260 | Effect.fork 261 | ); 262 | 263 | /** 264 | * Await a response from the server 265 | * 266 | * @param id - The request id 267 | * @param schema - The schema to decode the response 268 | * @returns The decoded response 269 | */ 270 | const _awaitResponse = ( 271 | id: RequestId, 272 | schema: Schema.Schema 273 | ) => 274 | Effect.gen(function* () { 275 | const deferred = yield* Deferred.make(); 276 | yield* Ref.update(deferredRequests, (map) => 277 | HashMap.set( 278 | map, 279 | id, 280 | deferred as unknown as Deferred.Deferred 281 | ) 282 | ); 283 | const response = yield* Deferred.await(deferred); 284 | return yield* Schema.decode(schema)(response).pipe( 285 | Effect.mapError((e) => 286 | JsonRpcError.fromCode("ParseError", e.message, e.cause) 287 | ) 288 | ); 289 | }).pipe( 290 | Effect.timeoutFail({ 291 | duration: opts?.timeout ?? "15 seconds", 292 | onTimeout: () => 293 | JsonRpcError.fromCode("RequestTimeout", `Request ${id} timed out`), 294 | }), 295 | Effect.tapError((error) => 296 | Effect.gen(function* () { 297 | yield* Ref.update(deferredRequests, (map) => 298 | HashMap.remove(map, id) 299 | ); 300 | if (error.code === JsonRpcErrorCode.RequestTimeout) { 301 | yield* transport.sendNotification( 302 | CancelledNotification.make({ 303 | method: "notifications/cancelled", 304 | params: { 305 | requestId: id, 306 | reason: `Request ${id} timed out`, 307 | }, 308 | }) 309 | ); 310 | } 311 | }) 312 | ) 313 | ); 314 | 315 | /** 316 | * 317 | * Client -> Server Request methods 318 | * 319 | */ 320 | 321 | /** 322 | * Initialize the client 323 | */ 324 | const initialize = Effect.gen(function* () { 325 | const id = generateId(); 326 | 327 | const request = InitializeRequest.make({ 328 | method: "initialize", 329 | params: { 330 | protocolVersion: LATEST_PROTOCOL_VERSION, 331 | // TODO: Add capabilities 332 | capabilities: { 333 | // sampling: {}, 334 | // roots: {}, 335 | }, 336 | clientInfo: config, 337 | }, 338 | }); 339 | 340 | yield* transport.sendRequest(id, request); 341 | const result = yield* _awaitResponse(id, InitializeResult); 342 | // TODO: Check server matches supported protocol version 343 | yield* Ref.set(server, result); 344 | 345 | const initialized = InitializedNotification.make({ 346 | method: "notifications/initialized", 347 | params: {}, 348 | }); 349 | 350 | yield* transport.sendNotification(initialized); 351 | 352 | return result; 353 | }); 354 | 355 | /** 356 | * Get a completion from the server 357 | */ 358 | const complete = Effect.fn("Complete")(function* ( 359 | params: CompleteRequest["params"] 360 | ) { 361 | const id = generateId(); 362 | 363 | const request = CompleteRequest.make({ 364 | method: "completion/complete", 365 | params, 366 | }); 367 | 368 | yield* transport.sendRequest(id, request); 369 | return id; 370 | }); 371 | 372 | /** 373 | * Await a completion from the server 374 | */ 375 | const completeAwait = Effect.fn("CompleteAwait")(function* ( 376 | params: CompleteRequest["params"] 377 | ) { 378 | const id = yield* complete(params); 379 | return yield* _awaitResponse(id, CompleteResult); 380 | }); 381 | 382 | /** 383 | * Get a prompt from the server 384 | */ 385 | const getPrompt = Effect.fn("GetPrompt")(function* ( 386 | params: GetPromptRequest["params"] 387 | ) { 388 | const id = generateId(); 389 | 390 | const request = GetPromptRequest.make({ 391 | method: "prompts/get", 392 | params, 393 | }); 394 | 395 | yield* transport.sendRequest(id, request); 396 | 397 | return id; 398 | }); 399 | 400 | /** 401 | * Await a prompt from the server 402 | */ 403 | const getPromptAwait = Effect.fn("GetPromptAwait")(function* ( 404 | params: GetPromptRequest["params"] 405 | ) { 406 | const id = yield* getPrompt(params); 407 | return yield* _awaitResponse(id, GetPromptResult); 408 | }); 409 | 410 | /** 411 | * List prompts from the server 412 | */ 413 | const listPrompts = Effect.fn("ListPrompts")(function* ( 414 | params: ListPromptsRequest["params"] 415 | ) { 416 | // Check if server supports prompts 417 | const id = generateId(); 418 | 419 | // Should we parse this here? 420 | const request = ListPromptsRequest.make({ 421 | method: "prompts/list", 422 | params, 423 | }); 424 | 425 | yield* transport.sendRequest(id, request); 426 | return id; 427 | }); 428 | 429 | const listPromptsAwait = Effect.fn("ListPromptsAwait")(function* ( 430 | params: ListPromptsRequest["params"] 431 | ) { 432 | const id = yield* listPrompts(params); 433 | return yield* _awaitResponse(id, ListPromptsResult); 434 | }); 435 | 436 | /** 437 | * Read a resource from the server 438 | */ 439 | const readResource = Effect.fn("ReadResource")(function* ( 440 | params: ReadResourceRequest["params"] 441 | ) { 442 | // Check if server supports resource reading 443 | const id = generateId(); 444 | 445 | const request = ReadResourceRequest.make({ 446 | method: "resources/read", 447 | params, 448 | }); 449 | 450 | yield* transport.sendRequest(id, request); 451 | return id; 452 | }); 453 | 454 | /** 455 | * Await a resource from the server 456 | */ 457 | const readResourceAwait = Effect.fn("ReadResourceAwait")(function* ( 458 | params: ReadResourceRequest["params"] 459 | ) { 460 | const id = yield* readResource(params); 461 | return yield* _awaitResponse(id, ReadResourceResult); 462 | }); 463 | 464 | /** 465 | * List resources from the server 466 | */ 467 | const listResources = Effect.fn("ListResources")(function* ( 468 | params: ListResourcesRequest["params"] 469 | ) { 470 | // Check if server supports resources 471 | const id = generateId(); 472 | 473 | const request = ListResourcesRequest.make({ 474 | method: "resources/list", 475 | params, 476 | }); 477 | 478 | yield* transport.sendRequest(id, request); 479 | return id; 480 | }); 481 | 482 | /** 483 | * Await a list of resources from the server 484 | */ 485 | const listResourcesAwait = Effect.fn("ListResourcesAwait")(function* ( 486 | params: ListResourcesRequest["params"] 487 | ) { 488 | const id = yield* listResources(params); 489 | return yield* _awaitResponse(id, ListResourcesResult); 490 | }); 491 | 492 | /** 493 | * List resource templates from the server 494 | */ 495 | const listResourceTemplates = Effect.fn("ListResourceTemplates")(function* ( 496 | params: ListResourceTemplatesRequest["params"] 497 | ) { 498 | // Check if server supports resource templates 499 | const id = generateId(); 500 | 501 | const request = ListResourceTemplatesRequest.make({ 502 | method: "resources/templates/list", 503 | params, 504 | }); 505 | 506 | yield* transport.sendRequest(id, request); 507 | return id; 508 | }); 509 | 510 | /** 511 | * Await a list of resource templates from the server 512 | */ 513 | const listResourceTemplatesAwait = Effect.fn("ListResourceTemplatesAwait")( 514 | function* (params: ListResourceTemplatesRequest["params"]) { 515 | const id = yield* listResourceTemplates(params); 516 | return yield* _awaitResponse(id, ListResourceTemplatesResult); 517 | } 518 | ); 519 | 520 | /** 521 | * Call a tool from the server 522 | */ 523 | const callTool = Effect.fn("CallTool")(function* ( 524 | params: CallToolRequest["params"] 525 | ) { 526 | // Check if server supports tool calls 527 | const id = generateId(); 528 | 529 | const request = CallToolRequest.make({ 530 | method: "tools/call", 531 | params, 532 | }); 533 | 534 | yield* transport.sendRequest(id, request); 535 | return id; 536 | }); 537 | 538 | /** 539 | * Await a tool call from the server 540 | */ 541 | const callToolAwait = Effect.fn("CallToolAwait")(function* ( 542 | params: CallToolRequest["params"] 543 | ) { 544 | const id = yield* callTool(params); 545 | return yield* _awaitResponse(id, CallToolResult); 546 | }); 547 | 548 | /** 549 | * List tools from the server 550 | */ 551 | const listTools = Effect.fn("ListTools")(function* ( 552 | params: ListToolsRequest["params"] 553 | ) { 554 | // Check if server supports tool calls 555 | const id = generateId(); 556 | 557 | const request = ListToolsRequest.make({ 558 | method: "tools/list", 559 | params, 560 | }); 561 | 562 | yield* transport.sendRequest(id, request); 563 | return id; 564 | }); 565 | 566 | /** 567 | * Await a list of tools from the server 568 | */ 569 | const listToolsAwait = Effect.fn("ListToolsAwait")(function* ( 570 | params: ListToolsRequest["params"] 571 | ) { 572 | const id = yield* listTools(params); 573 | return yield* _awaitResponse(id, ListToolsResult); 574 | }); 575 | 576 | /** 577 | * Subscribe to a resource from the server 578 | */ 579 | const subscribe = Effect.fn("Subscribe")(function* ( 580 | params: SubscribeRequest["params"] 581 | ) { 582 | // TODO: Implement 583 | // Optionally yield a subscription service 584 | // If not service, return an error 585 | // Check if server supports subscriptions, if not return an error 586 | // If subscriptions service and server supports subscriptions, send event 587 | }); 588 | 589 | /** 590 | * Unsubscribe from a resource from the server 591 | */ 592 | const unsubscribe = Effect.fn("Unsubscribe")(function* ( 593 | params: UnsubscribeRequest["params"] 594 | ) { 595 | // TODO: Implement 596 | // Optionally yield a subscription service 597 | // If not service, return an error 598 | // Check if server supports subscriptions, if not return an error 599 | // If subscriptions service and server supports subscriptions, send event 600 | }); 601 | 602 | /** 603 | * Ping the server 604 | */ 605 | const ping = Effect.fn("Ping")(function* () { 606 | const id = generateId(); 607 | 608 | const request = PingRequest.make({ 609 | method: "ping", 610 | }); 611 | 612 | yield* transport.sendRequest(id, request); 613 | return id; 614 | }); 615 | 616 | /** 617 | * Await a ping from the server 618 | */ 619 | const pingAwait = Effect.fn("PingAwait")(function* () { 620 | const id = yield* ping(); 621 | return yield* _awaitResponse(id, EmptyResult); 622 | }); 623 | 624 | yield* Effect.addFinalizer(() => transport.close); 625 | 626 | return { 627 | initialize, 628 | complete, 629 | completeAwait, 630 | getPrompt, 631 | getPromptAwait, 632 | listPrompts, 633 | listPromptsAwait, 634 | readResource, 635 | readResourceAwait, 636 | listResources, 637 | listResourcesAwait, 638 | listResourceTemplates, 639 | listResourceTemplatesAwait, 640 | // subscribe, 641 | // unsubscribe, 642 | callTool, 643 | callToolAwait, 644 | listTools, 645 | listToolsAwait, 646 | ping, 647 | pingAwait, 648 | } satisfies McpClient.Service; 649 | }).pipe(Effect.provide(DeferredMap.Empty)); 650 | 651 | export const layer = (config: Implementation, opts?: McpClientOpts) => 652 | Layer.effect(McpClient, make(config, opts)); 653 | -------------------------------------------------------------------------------- /packages/client/src/index.ts: -------------------------------------------------------------------------------- 1 | export * as McpClient from "./client.js"; 2 | export * as StdioClientTransport from "./transport/stdio.js"; 3 | export * as Messenger from "./messenger.js"; -------------------------------------------------------------------------------- /packages/client/src/messenger.ts: -------------------------------------------------------------------------------- 1 | import { 2 | JsonRpcError, 3 | JSONRPCMessage, 4 | Notification, 5 | Request, 6 | RequestId, 7 | Result, 8 | } from "@effect-mcp/shared"; 9 | import * as Mailbox from "effect/Mailbox"; 10 | import * as Effect from "effect/Effect"; 11 | 12 | export class Messenger extends Effect.Service()( 13 | "@effect-mcp/server/Messenger", 14 | { 15 | effect: Effect.gen(function* () { 16 | const outbound = yield* Mailbox.make(); 17 | 18 | const sendResult = Effect.fn("SendResult")(function* ( 19 | id: RequestId, 20 | result: Result 21 | ) { 22 | yield* Effect.log(`Sending result:`, result); 23 | yield* outbound.offer({ 24 | jsonrpc: "2.0", 25 | id: id, 26 | result: result, 27 | }); 28 | }); 29 | 30 | const sendError = Effect.fn("SendError")(function* ( 31 | id: RequestId, 32 | error: JsonRpcError 33 | ) { 34 | yield* Effect.log(`Sending error:`, error); 35 | yield* outbound.offer({ 36 | jsonrpc: "2.0", 37 | id: id, 38 | error: error, 39 | }); 40 | }); 41 | 42 | const sendNotification = Effect.fn("SendNotification")(function* ( 43 | notification: Notification 44 | ) { 45 | yield* Effect.log(`Sending notification:`, notification); 46 | yield* outbound.offer({ 47 | jsonrpc: "2.0", 48 | method: notification.method, 49 | params: notification.params, 50 | }); 51 | }); 52 | 53 | const sendRequest = Effect.fn("SendRequest")(function* ( 54 | id: RequestId, 55 | request: Request 56 | ) { 57 | yield* Effect.log(`Sending request:`, request); 58 | yield* outbound.offer({ 59 | jsonrpc: "2.0", 60 | id: id, 61 | method: request.method, 62 | params: request.params, 63 | }); 64 | }); 65 | 66 | return { 67 | outbound, 68 | sendResult, 69 | sendError, 70 | sendNotification, 71 | sendRequest, 72 | }; 73 | }), 74 | } 75 | ) {} 76 | -------------------------------------------------------------------------------- /packages/client/src/notifications.ts: -------------------------------------------------------------------------------- 1 | import type { ServerNotification } from "@effect-mcp/shared"; 2 | import * as Effect from "effect/Effect"; 3 | import * as PubSub from "effect/PubSub"; 4 | 5 | export class McpNotificationService extends Effect.Service()( 6 | "@effect-mcp/client/McpNotificationService", 7 | { 8 | effect: Effect.gen(function* () { 9 | const notifications = yield* PubSub.bounded(100); 10 | 11 | const notify = Effect.fn("Notify")(function* ( 12 | notification: ServerNotification 13 | ) { 14 | yield* notifications.publish(notification); 15 | }); 16 | 17 | return { 18 | notifications, 19 | notify, 20 | }; 21 | }), 22 | } 23 | ) {} 24 | -------------------------------------------------------------------------------- /packages/client/src/transport/deferred.ts: -------------------------------------------------------------------------------- 1 | import { JsonRpcError, RequestId, ServerResult } from "@effect-mcp/shared"; 2 | import * as Context from "effect/Context"; 3 | import * as Deferred from "effect/Deferred"; 4 | import * as HashMap from "effect/HashMap"; 5 | import * as Layer from "effect/Layer"; 6 | import * as Ref from "effect/Ref"; 7 | 8 | export class DeferredMap extends Context.Tag("DeferredMap")< 9 | DeferredMap, 10 | Ref.Ref< 11 | HashMap.HashMap> 12 | > 13 | >() { 14 | static Empty = Layer.effect( 15 | DeferredMap, 16 | Ref.make( 17 | HashMap.empty>() 18 | ) 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /packages/client/src/transport/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./stdio.js"; 2 | -------------------------------------------------------------------------------- /packages/client/src/transport/stdio.ts: -------------------------------------------------------------------------------- 1 | import { JsonRpcError, JSONRPCMessage } from "@effect-mcp/shared"; 2 | import * as Command from "@effect/platform/Command"; 3 | import * as Chunk from "effect/Chunk"; 4 | import * as Effect from "effect/Effect"; 5 | import * as Layer from "effect/Layer"; 6 | import * as Mailbox from "effect/Mailbox"; 7 | import * as Schema from "effect/Schema"; 8 | import * as Stream from "effect/Stream"; 9 | import { Messenger } from "../messenger.js"; 10 | import { Transport } from "./transport.js"; 11 | 12 | const decode = (msg: any) => 13 | Schema.decodeUnknown(JSONRPCMessage)(msg).pipe( 14 | Effect.mapError((e) => 15 | JsonRpcError.fromCode("ParseError", "Failed to parse message", e) 16 | ) 17 | ); 18 | 19 | const encode = (msg: JSONRPCMessage) => 20 | Schema.encode(JSONRPCMessage)(msg).pipe( 21 | Effect.mapError((e) => 22 | JsonRpcError.fromCode("ParseError", "Failed to encode message", e) 23 | ) 24 | ); 25 | 26 | export const make = (command: Command.Command) => 27 | Effect.gen(function* () { 28 | const messenger = yield* Messenger; 29 | const inbound = yield* Mailbox.make(); 30 | 31 | const stdin = Mailbox.toStream(messenger.outbound).pipe( 32 | Stream.mapEffect(encode), 33 | Stream.tap((response) => Effect.log(`Sending message:`, response)), 34 | Stream.map((response) => JSON.stringify(response) + "\n"), 35 | Stream.encodeText, 36 | Stream.catchAll((e) => Effect.log(`Error encoding message:`, e)), 37 | Stream.filter((msg) => msg !== void 0) 38 | ); 39 | 40 | const process = yield* command.pipe( 41 | Command.stdin(stdin), 42 | Command.stderr("inherit"), 43 | Command.start 44 | ); 45 | 46 | const decoder = new TextDecoder(); 47 | 48 | // // Start listening for messages via stdout 49 | yield* process.stdout 50 | .pipe( 51 | Stream.mapChunks(Chunk.map((bytes) => decoder.decode(bytes))), 52 | Stream.splitLines, 53 | Stream.tap((msg) => Effect.log(`Received message:`, msg)), 54 | Stream.mapEffect((msg) => 55 | Effect.try({ 56 | try: () => JSON.parse(msg), 57 | catch: (e) => 58 | JsonRpcError.fromCode("ParseError", "Failed to parse message", e), 59 | }) 60 | ), 61 | Stream.mapEffect(decode), 62 | Stream.runForEach((msg) => inbound.offer(msg)) 63 | ) 64 | .pipe(Effect.fork); 65 | 66 | const close = process 67 | .kill("SIGINT") 68 | .pipe( 69 | Effect.catchAll((e) => Effect.logError(`Error closing process:`, e)) 70 | ); 71 | 72 | return { 73 | inbound, 74 | close, 75 | sendRequest: messenger.sendRequest, 76 | sendNotification: messenger.sendNotification, 77 | sendResult: messenger.sendResult, 78 | sendError: messenger.sendError, 79 | } satisfies Transport.Service; 80 | }).pipe(Effect.provide(Messenger.Default)); 81 | 82 | export const layer = (command: Command.Command) => 83 | Layer.effect(Transport, make(command)); 84 | -------------------------------------------------------------------------------- /packages/client/src/transport/transport.ts: -------------------------------------------------------------------------------- 1 | import { 2 | JsonRpcError, 3 | JSONRPCMessage, 4 | Notification, 5 | Request, 6 | RequestId, 7 | ServerResult, 8 | } from "@effect-mcp/shared"; 9 | import * as Context from "effect/Context"; 10 | import * as Deferred from "effect/Deferred"; 11 | import * as Effect from "effect/Effect"; 12 | import * as HashMap from "effect/HashMap"; 13 | import * as Mailbox from "effect/Mailbox"; 14 | import * as Ref from "effect/Ref"; 15 | 16 | export declare namespace Transport { 17 | export type Service = { 18 | inbound: Mailbox.Mailbox; 19 | close: Effect.Effect; 20 | sendRequest: ( 21 | id: RequestId, 22 | request: Request 23 | ) => Effect.Effect; 24 | sendNotification: (notification: Notification) => Effect.Effect; 25 | sendError: (id: RequestId, error: JsonRpcError) => Effect.Effect; 26 | sendResult: (id: RequestId, result: ServerResult) => Effect.Effect; 27 | }; 28 | } 29 | 30 | export class Transport extends Context.Tag("Transport")< 31 | Transport, 32 | Transport.Service 33 | >() {} 34 | -------------------------------------------------------------------------------- /packages/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "references": [ 4 | { 5 | "path": "../shared" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /packages/client/tsup.config.ts: -------------------------------------------------------------------------------- 1 | // tsup.config.ts 2 | import { defineConfig } from "tsup"; 3 | 4 | export default defineConfig({ 5 | format: ["esm", "cjs"], 6 | entry: ["src/index.ts"], 7 | clean: true, 8 | target: "esnext", 9 | treeshake: false, 10 | keepNames: true, 11 | tsconfig: "tsconfig.json", 12 | external: ["effect"], 13 | }); 14 | -------------------------------------------------------------------------------- /packages/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@effect-mcp/server", 3 | "version": "0.0.1", 4 | "description": "Effect MCP Server", 5 | "type": "module", 6 | "scripts": { 7 | "build": "pnpm run build:code && pnpm run build:types", 8 | "build:code": "tsup", 9 | "build:types": "rm -f tsconfig.tsbuildinfo && tsc --emitDeclarationOnly" 10 | }, 11 | "keywords": [ 12 | "effect", 13 | "mcp", 14 | "modelcontextprotocol" 15 | ], 16 | "author": "Garrett Hardin", 17 | "license": "MIT", 18 | "files": [ 19 | "dist", 20 | "src" 21 | ], 22 | "exports": { 23 | ".": { 24 | "import": { 25 | "@effect-mcp/dev": "./src/index.ts", 26 | "types": "./dist/index.d.ts", 27 | "default": "./dist/index.js" 28 | }, 29 | "require": { 30 | "@effect-mcp/dev": "./src/index.ts", 31 | "types": "./dist/index.d.ts", 32 | "default": "./dist/index.cjs" 33 | } 34 | } 35 | }, 36 | "dependencies": { 37 | "@effect-mcp/shared": "workspace:*", 38 | "@effect/ai": "^0.10.2", 39 | "@effect/platform": "^0.77.2" 40 | }, 41 | "peerDependencies": { 42 | "effect": "^3.13.2", 43 | "typescript": "^5.7.3" 44 | }, 45 | "devDependencies": { 46 | "tsup": "^8.3.6" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/server/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./messenger.js"; 2 | export * as PromptKit from "./prompts/prompt-kit.js"; 3 | export * as Prompt from "./prompts/prompt.js"; 4 | export * as McpServer from "./server.js"; 5 | export * as SSEServerTransport from "./transport/sse.js"; 6 | export * as StdioServerTransport from "./transport/stdio.js"; 7 | -------------------------------------------------------------------------------- /packages/server/src/messenger.ts: -------------------------------------------------------------------------------- 1 | import { 2 | JsonRpcError, 3 | JSONRPCMessage, 4 | Notification, 5 | Request, 6 | RequestId, 7 | Result, 8 | } from "@effect-mcp/shared"; 9 | import * as Effect from "effect/Effect"; 10 | import * as PubSub from "effect/PubSub"; 11 | 12 | export class Messenger extends Effect.Service()( 13 | "@effect-mcp/server/Messenger", 14 | { 15 | effect: Effect.gen(function* () { 16 | const outbound = yield* PubSub.bounded(100); 17 | 18 | const sendResult = Effect.fn("SendResult")(function* ( 19 | id: RequestId, 20 | result: Result 21 | ) { 22 | yield* Effect.log(`Sending result:`, result); 23 | yield* PubSub.publish(outbound, { 24 | jsonrpc: "2.0", 25 | id: id, 26 | result: result, 27 | }); 28 | }); 29 | 30 | const sendError = Effect.fn("SendError")(function* ( 31 | id: RequestId, 32 | error: JsonRpcError 33 | ) { 34 | yield* Effect.log(`Sending error:`, error); 35 | yield* PubSub.publish(outbound, { 36 | jsonrpc: "2.0", 37 | id: id, 38 | error: error, 39 | }); 40 | }); 41 | 42 | const sendNotification = Effect.fn("SendNotification")(function* ( 43 | notification: Notification 44 | ) { 45 | yield* Effect.log(`Sending notification:`, notification); 46 | yield* PubSub.publish(outbound, { 47 | jsonrpc: "2.0", 48 | method: notification.method, 49 | params: notification.params, 50 | }); 51 | }); 52 | 53 | const sendRequest = Effect.fn("SendRequest")(function* ( 54 | request: Request 55 | ) { 56 | yield* Effect.log(`Sending request:`, request); 57 | yield* PubSub.publish(outbound, { 58 | jsonrpc: "2.0", 59 | method: request.method, 60 | params: request.params, 61 | }); 62 | }); 63 | 64 | return { 65 | outbound, 66 | sendResult, 67 | sendError, 68 | sendNotification, 69 | sendRequest, 70 | }; 71 | }), 72 | } 73 | ) {} 74 | -------------------------------------------------------------------------------- /packages/server/src/prompts/prompt-kit.ts: -------------------------------------------------------------------------------- 1 | import * as Context from "effect/Context"; 2 | import * as HashMap from "effect/HashMap"; 3 | import * as Inspectable from "effect/Inspectable"; 4 | import * as Layer from "effect/Layer"; 5 | import { pipeArguments, type Pipeable } from "effect/Pipeable"; 6 | import * as Prompt from "./prompt.js"; 7 | 8 | export const TypeId: unique symbol = Symbol.for("@effect-mcp/server/PromptKit"); 9 | 10 | export type TypeId = typeof TypeId; 11 | 12 | export class Registry extends Context.Tag( 13 | "@effect-mcp/server/PromptKit/Registry" 14 | )>() { 15 | static readonly Live: Layer.Layer = Layer.sync(Registry, () => 16 | HashMap.empty() 17 | ); 18 | } 19 | 20 | export interface PromptKit 21 | extends Inspectable.Inspectable, 22 | Pipeable { 23 | readonly [TypeId]: TypeId; 24 | readonly prompts: HashMap.HashMap; 25 | readonly add: ( 26 | prompt: S 27 | ) => PromptKit>; 28 | readonly addAll: >( 29 | ...prompts: ToAdd 30 | ) => PromptKit< 31 | Prompts | ToAdd[number], 32 | R | Prompt.Prompt.Context 33 | >; 34 | readonly concat:

( 35 | that: PromptKit 36 | ) => PromptKit>; 37 | readonly finalize: () => Layer.Layer; 38 | } 39 | 40 | class PromptKitImpl 41 | implements PromptKit 42 | { 43 | readonly [TypeId]: TypeId; 44 | constructor(readonly prompts: HashMap.HashMap) { 45 | this[TypeId] = TypeId; 46 | } 47 | 48 | toJSON(): unknown { 49 | return { 50 | _id: "@effect-mcp/server/PromptKit", 51 | prompts: [...HashMap.values(this.prompts)].map((prompt) => prompt.name), 52 | }; 53 | } 54 | toString(): string { 55 | return Inspectable.format(this); 56 | } 57 | [Inspectable.NodeInspectSymbol](): string { 58 | return Inspectable.format(this); 59 | } 60 | 61 | pipe() { 62 | return pipeArguments(this, arguments); 63 | } 64 | 65 | add( 66 | prompt: S 67 | ): PromptKit> { 68 | return new PromptKitImpl>( 69 | HashMap.set(this.prompts, prompt.name, prompt) 70 | ); 71 | } 72 | 73 | addAll>( 74 | ...prompts: ToAdd 75 | ): PromptKit< 76 | Prompts | ToAdd[number], 77 | R | Prompt.Prompt.Context 78 | > { 79 | let map = this.prompts; 80 | 81 | for (const prompt of prompts) { 82 | map = HashMap.set(map, prompt.name, prompt); 83 | } 84 | 85 | return new PromptKitImpl< 86 | Prompts | ToAdd[number], 87 | R | Prompt.Prompt.Context 88 | >(map); 89 | } 90 | 91 | concat

( 92 | that: PromptKit 93 | ): PromptKit> { 94 | return new PromptKitImpl>( 95 | HashMap.union(this.prompts, that.prompts) 96 | ); 97 | } 98 | 99 | finalize(): Layer.Layer { 100 | return Layer.succeed(Registry, this.prompts); 101 | } 102 | } 103 | 104 | export const empty: PromptKit = new PromptKitImpl( 105 | HashMap.empty() 106 | ); 107 | -------------------------------------------------------------------------------- /packages/server/src/prompts/prompt.ts: -------------------------------------------------------------------------------- 1 | import type { JsonRpcError, PromptMessage } from "@effect-mcp/shared"; 2 | import * as Effect from "effect/Effect"; 3 | import * as Schema from "effect/Schema"; 4 | 5 | /** 6 | * @since 1.0.0 7 | * @category type ids 8 | */ 9 | export const TypeId: unique symbol = Symbol.for("@effect-mcp/server/Prompt"); 10 | 11 | /** 12 | * @since 1.0.0 13 | * @category type ids 14 | */ 15 | export type TypeId = typeof TypeId; 16 | 17 | export type Prompt = PromptEffect< 18 | Args, 19 | R 20 | >; // TODO: Add others like dynamic, etc. 21 | 22 | export interface PromptEffect 23 | extends Prompt.Proto { 24 | readonly _tag: "Effect"; 25 | readonly handler: ( 26 | args: Schema.Struct.Type 27 | ) => Effect.Effect; 28 | } 29 | 30 | /** 31 | * Initial idea on how to handle dynamic prompts, like pulling from a database, etc. 32 | */ 33 | interface PromptDynamic { 34 | readonly _tag: "Dynamic"; 35 | readonly populate: (cursor?: string | undefined) => Effect.Effect< 36 | { 37 | prompts: ReadonlyArray>; 38 | nextCursor?: string | undefined; 39 | }, 40 | any, 41 | R 42 | >; 43 | readonly handler: ( 44 | args: any 45 | ) => Effect.Effect; 46 | } 47 | 48 | export namespace Prompt { 49 | export interface Proto { 50 | readonly [TypeId]: TypeId; 51 | readonly _tag: string; 52 | readonly arguments: Args; 53 | readonly description: string; 54 | readonly name: string; 55 | } 56 | 57 | export interface Any extends Prompt {} 58 | 59 | export type Context

> = P extends Prompt< 60 | infer _Args, 61 | infer R 62 | > 63 | ? R 64 | : never; 65 | } 66 | 67 | export const effect = ( 68 | params: { name: string; description: string; arguments: Args }, 69 | handler: ( 70 | args: Schema.Struct.Type 71 | ) => Effect.Effect 72 | ): Prompt => ({ 73 | _tag: "Effect", 74 | handler, 75 | arguments: params.arguments, 76 | description: params.description, 77 | name: params.name, 78 | [TypeId]: TypeId, 79 | }); 80 | -------------------------------------------------------------------------------- /packages/server/src/server.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallToolRequest, 3 | CallToolResult, 4 | ClientNotification, 5 | ClientRequest, 6 | ClientResult, 7 | CompleteRequest, 8 | GetPromptRequest, 9 | GetPromptResult, 10 | JsonRpcError, 11 | JSONRPCError, 12 | JSONRPCNotification, 13 | JSONRPCRequest, 14 | JSONRPCResponse, 15 | LATEST_PROTOCOL_VERSION, 16 | ListPromptsRequest, 17 | ListPromptsResult, 18 | ListResourcesRequest, 19 | ListResourceTemplatesRequest, 20 | ListRootsResult, 21 | ListToolsRequest, 22 | ListToolsResult, 23 | MCP, 24 | Prompt, 25 | PromptMessage, 26 | ReadResourceRequest, 27 | SetLevelRequest, 28 | SubscribeRequest, 29 | Tool, 30 | UnsubscribeRequest, 31 | type CreateMessageResult, 32 | type Implementation, 33 | type InitializeRequest, 34 | type InitializeResult, 35 | type PingRequest, 36 | type RequestId, 37 | } from "@effect-mcp/shared"; 38 | import * as AiToolkit from "@effect/ai/AiToolkit"; 39 | import * as Effect from "effect/Effect"; 40 | import * as HashMap from "effect/HashMap"; 41 | import * as JsonSchema from "effect/JSONSchema"; 42 | import * as Layer from "effect/Layer"; 43 | import * as Match from "effect/Match"; 44 | import * as Option from "effect/Option"; 45 | import * as Predicate from "effect/Predicate"; 46 | import * as Schema from "effect/Schema"; 47 | import * as AST from "effect/SchemaAST"; 48 | import * as Scope from "effect/Scope"; 49 | import { Messenger } from "./messenger.js"; 50 | import * as PromptKit from "./prompts/prompt-kit.js"; 51 | 52 | export const make = ( 53 | config: Implementation 54 | ): Effect.Effect< 55 | MCP.MCP.Service, 56 | never, 57 | Scope.Scope | Messenger | AiToolkit.Registry 58 | > => 59 | Effect.gen(function* () { 60 | const messenger = yield* Messenger; 61 | const toolkit = yield* Effect.serviceOption(AiToolkit.Registry); 62 | const promptkit = yield* Effect.serviceOption(PromptKit.Registry); 63 | // TODO: Add tools, prompts, resources, etc. 64 | 65 | const _notImplemented = (...args: any[]) => 66 | Effect.gen(function* () { 67 | yield* Effect.logDebug(`Not implemented`, args); 68 | }); 69 | 70 | const _handlePing = Effect.fn("HandlePing")(function* ( 71 | id: RequestId, 72 | message: PingRequest 73 | ) { 74 | yield* messenger.sendResult(id, { 75 | _meta: { 76 | pong: true, 77 | }, 78 | }); 79 | }); 80 | 81 | const _handleInitialize = Effect.fn("HandleInitialize")(function* ( 82 | id: RequestId, 83 | message: InitializeRequest 84 | ) { 85 | // TODO: Validate client info 86 | yield* Effect.log(`Initializing server with client info:`, message); 87 | 88 | const response: InitializeResult = { 89 | protocolVersion: LATEST_PROTOCOL_VERSION, 90 | // TODO: Get from tools, etc. 91 | capabilities: { 92 | tools: {}, 93 | prompts: {}, 94 | resources: {}, 95 | resourceTemplates: {}, 96 | }, 97 | serverInfo: config, 98 | }; 99 | 100 | yield* messenger.sendResult(id, response); 101 | }); 102 | 103 | const _handleComplete = Effect.fn("HandleComplete")(function* ( 104 | id: RequestId, 105 | message: CompleteRequest 106 | ) { 107 | // TODO: Implement 108 | }); 109 | 110 | const _handleSetLevel = Effect.fn("HandleSetLevel")(function* ( 111 | id: RequestId, 112 | message: SetLevelRequest 113 | ) { 114 | // TODO: Implement 115 | }); 116 | 117 | const _handleGetPrompt = Effect.fn("HandleGetPrompt")(function* ( 118 | id: RequestId, 119 | message: GetPromptRequest 120 | ) { 121 | if (Option.isNone(promptkit)) { 122 | return yield* messenger.sendError( 123 | id, 124 | JsonRpcError.fromCode("InvalidParams", "No prompts available") 125 | ); 126 | } 127 | 128 | const prompt = HashMap.get(promptkit.value, message.params.name); 129 | if (Option.isNone(prompt)) { 130 | return yield* messenger.sendError( 131 | id, 132 | JsonRpcError.fromCode("InvalidParams", "Prompt not found") 133 | ); 134 | } 135 | 136 | const decodeArgs = Schema.decodeUnknown( 137 | Schema.Struct(prompt.value.arguments) 138 | ); 139 | const encodeSuccess = Schema.encode(Schema.Array(PromptMessage)); 140 | 141 | const args = yield* decodeArgs(message.params.arguments).pipe( 142 | Effect.mapError((err) => 143 | JsonRpcError.fromCode("ParseError", err.message, err.issue) 144 | ), 145 | Effect.flatMap(prompt.value.handler), 146 | Effect.mapError((err) => 147 | JsonRpcError.fromCode("InternalError", "Error calling prompt", err) 148 | ), 149 | Effect.flatMap((messages) => 150 | encodeSuccess(messages).pipe( 151 | Effect.mapError((err) => 152 | JsonRpcError.fromCode("ParseError", err.message, err.issue) 153 | ) 154 | ) 155 | ) 156 | ) as Effect.Effect; 157 | 158 | const result: GetPromptResult = { 159 | messages: args, 160 | }; 161 | 162 | yield* messenger.sendResult(id, result); 163 | }); 164 | 165 | const _handleListPrompts = Effect.fn("HandleListPrompts")(function* ( 166 | id: RequestId, 167 | message: ListPromptsRequest 168 | ) { 169 | const prompts: Prompt[] = []; 170 | 171 | if (Option.isNone(promptkit)) { 172 | const data: ListPromptsResult = { 173 | prompts, 174 | }; 175 | return yield* messenger.sendResult(id, data); 176 | } 177 | 178 | for (const prompt of HashMap.values(promptkit.value)) { 179 | const ast = Schema.Struct(prompt.arguments).ast; 180 | const propertySigs = AST.getPropertySignatures(ast); 181 | const args = propertySigs.map((prop) => ({ 182 | name: prop.name.toString(), 183 | description: (prop.annotations.description ?? "") as string, 184 | required: !prop.isOptional, 185 | })); 186 | 187 | prompts.push({ 188 | name: prompt.name, 189 | description: prompt.description, 190 | arguments: args, 191 | }); 192 | } 193 | 194 | const data: ListPromptsResult = { 195 | prompts, 196 | }; 197 | return yield* messenger.sendResult(id, data); 198 | }); 199 | 200 | const _handleListTools = Effect.fn("HandleListTools")(function* ( 201 | id: RequestId, 202 | message: ListToolsRequest 203 | ) { 204 | const tools: Tool[] = []; 205 | 206 | if (Option.isNone(toolkit)) { 207 | const data: ListToolsResult = { 208 | tools, 209 | }; 210 | return yield* messenger.sendResult(id, data); 211 | } 212 | 213 | yield* Effect.forEach(toolkit.value.keys(), (schema) => 214 | Effect.gen(function* () { 215 | const ast = (schema as any).ast; 216 | 217 | tools.push({ 218 | name: schema._tag, 219 | inputSchema: makeJsonSchema(ast) as any, 220 | description: getDescription(ast), 221 | }); 222 | }) 223 | ); 224 | 225 | const data: ListToolsResult = { 226 | tools, 227 | }; 228 | return yield* messenger.sendResult(id, data); 229 | }); 230 | 231 | const _handleCallTool = Effect.fn("HandleCallTool")(function* ( 232 | id: RequestId, 233 | message: CallToolRequest 234 | ) { 235 | // TODO: Implement 236 | if (Option.isNone(toolkit)) { 237 | return yield* messenger.sendError( 238 | id, 239 | JsonRpcError.fromCode( 240 | "InvalidParams", 241 | "The tool does not exist / is not available." 242 | ) 243 | ); 244 | } 245 | 246 | const tool = Array.from(toolkit.value.entries()).find( 247 | ([schema, _]) => schema._tag === message.params.name 248 | ); 249 | if (!tool) { 250 | return yield* messenger.sendError( 251 | id, 252 | JsonRpcError.fromCode( 253 | "InvalidParams", 254 | "The tool does not exist / is not available." 255 | ) 256 | ); 257 | } 258 | const decodeArgs = Schema.decodeUnknown(tool[0] as any); 259 | const encodeSuccess = Schema.encode(tool[0].success); 260 | 261 | const output = yield* decodeArgs( 262 | injectTag(message.params.arguments, message.params.name) 263 | ).pipe( 264 | Effect.mapError((err) => 265 | JsonRpcError.fromCode("ParseError", err.message, err.issue) 266 | ), 267 | Effect.flatMap(tool[1]), 268 | Effect.mapError((err) => 269 | JsonRpcError.fromCode("InternalError", "Error calling tool", err) 270 | ), 271 | Effect.flatMap((res) => 272 | encodeSuccess(res).pipe( 273 | Effect.mapError((err) => 274 | JsonRpcError.fromCode( 275 | "ParseError", 276 | "Error encoding tool result", 277 | err 278 | ) 279 | ) 280 | ) 281 | ) 282 | ) as Effect.Effect; 283 | 284 | const result: CallToolResult = { 285 | content: [ 286 | { 287 | type: "text", 288 | text: JSON.stringify(output), 289 | }, 290 | ], 291 | }; 292 | 293 | yield* messenger.sendResult(id, result); 294 | }); 295 | 296 | const _handleListResources = Effect.fn("HandleListResources")(function* ( 297 | id: RequestId, 298 | message: ListResourcesRequest 299 | ) { 300 | // TODO: Implement 301 | }); 302 | 303 | const _handleReadResource = Effect.fn("HandleReadResource")(function* ( 304 | id: RequestId, 305 | message: ReadResourceRequest 306 | ) { 307 | // TODO: Implement 308 | }); 309 | 310 | const _handleSubscribeToResourceList = Effect.fn( 311 | "HandleSubscribeToResourceList" 312 | )(function* (id: RequestId, message: SubscribeRequest) { 313 | // TODO: Implement 314 | }); 315 | 316 | const _handleUnsubscribeFromResourceList = Effect.fn( 317 | "HandleUnsubscribeFromResourceList" 318 | )(function* (id: RequestId, message: UnsubscribeRequest) { 319 | // TODO: Implement 320 | }); 321 | 322 | const _handleListResourceTemplates = Effect.fn( 323 | "HandleListResourceTemplates" 324 | )(function* (id: RequestId, message: ListResourceTemplatesRequest) { 325 | // TODO: Implement 326 | }); 327 | 328 | const handleRequest = Effect.fn("HandleRequest")(function* ( 329 | rawMessage: JSONRPCRequest 330 | ) { 331 | return yield* Effect.gen(function* () { 332 | const message = yield* Schema.decodeUnknown(ClientRequest)({ 333 | method: rawMessage.method, 334 | params: rawMessage.params, 335 | }).pipe( 336 | Effect.mapError((error) => 337 | JsonRpcError.fromCode("ParseError", error.message, error.issue) 338 | ) 339 | ); 340 | 341 | yield* Effect.log(`Handling request:`, message); 342 | 343 | yield* Match.value(message).pipe( 344 | Match.when({ method: "ping" }, (msg) => 345 | _handlePing(rawMessage.id, msg) 346 | ), 347 | Match.when({ method: "initialize" }, (msg) => 348 | _handleInitialize(rawMessage.id, msg) 349 | ), 350 | Match.when({ method: "completion/complete" }, (msg) => 351 | _handleComplete(rawMessage.id, msg) 352 | ), 353 | Match.when({ method: "logging/setLevel" }, (msg) => 354 | _handleSetLevel(rawMessage.id, msg) 355 | ), 356 | Match.when({ method: "prompts/get" }, (msg) => 357 | _handleGetPrompt(rawMessage.id, msg) 358 | ), 359 | Match.when({ method: "prompts/list" }, (msg) => 360 | _handleListPrompts(rawMessage.id, msg) 361 | ), 362 | Match.when({ method: "resources/list" }, (msg) => 363 | _handleListResources(rawMessage.id, msg) 364 | ), 365 | Match.when({ method: "resources/read" }, (msg) => 366 | _handleReadResource(rawMessage.id, msg) 367 | ), 368 | Match.when({ method: "resources/subscribe" }, (msg) => 369 | _handleSubscribeToResourceList(rawMessage.id, msg) 370 | ), 371 | Match.when({ method: "resources/unsubscribe" }, (msg) => 372 | _handleUnsubscribeFromResourceList(rawMessage.id, msg) 373 | ), 374 | Match.when({ method: "resources/templates/list" }, (msg) => 375 | _handleListResourceTemplates(rawMessage.id, msg) 376 | ), 377 | Match.when({ method: "tools/list" }, (msg) => 378 | _handleListTools(rawMessage.id, msg) 379 | ), 380 | Match.when({ method: "tools/call" }, (msg) => 381 | _handleCallTool(rawMessage.id, msg) 382 | ), 383 | Match.exhaustive 384 | ); 385 | }).pipe( 386 | Effect.tapError((err) => 387 | Effect.logError(`Error handling request: ${err.message}`) 388 | ), 389 | Effect.catchTag("JsonRpcError", (err) => 390 | messenger.sendError(rawMessage.id, err) 391 | ) 392 | ); 393 | }); 394 | 395 | const handleError = Effect.fn("HandleError")(function* ( 396 | message: JSONRPCError 397 | ) { 398 | // TODO: Implement 399 | }); 400 | 401 | const handleResponse = Effect.fn("HandleResponse")(function* ( 402 | message: JSONRPCResponse 403 | ) { 404 | return yield* Effect.gen(function* () { 405 | const response = yield* Schema.decodeUnknown(ClientResult)( 406 | message.result 407 | ).pipe( 408 | Effect.mapError((error) => 409 | JsonRpcError.fromCode("ParseError", error.message, error.issue) 410 | ) 411 | ); 412 | 413 | Match.value(response).pipe( 414 | Match.when( 415 | (message): message is CreateMessageResult => "model" in message, 416 | _notImplemented 417 | ), 418 | Match.when( 419 | (message): message is ListRootsResult => "roots" in message, 420 | _notImplemented 421 | ), 422 | // Empty response 423 | Match.orElse(_notImplemented) 424 | ); 425 | }).pipe( 426 | Effect.catchTag("JsonRpcError", (err) => 427 | messenger.sendError(message.id, err) 428 | ) 429 | ); 430 | }); 431 | 432 | const handleNotification = Effect.fn("HandleNotification")(function* ( 433 | message: JSONRPCNotification 434 | ) { 435 | return yield* Effect.gen(function* () { 436 | const notification = yield* Schema.decodeUnknown(ClientNotification)({ 437 | method: message.method, 438 | params: message.params, 439 | }).pipe( 440 | Effect.mapError((error) => 441 | JsonRpcError.fromCode("ParseError", error.message, error.issue) 442 | ) 443 | ); 444 | 445 | yield* Match.value(notification).pipe( 446 | Match.when({ method: "notifications/cancelled" }, (msg) => 447 | _notImplemented(msg) 448 | ), 449 | Match.when({ method: "notifications/progress" }, (msg) => 450 | _notImplemented(msg) 451 | ), 452 | Match.when({ method: "notifications/initialized" }, (msg) => 453 | _notImplemented(msg) 454 | ), 455 | Match.when({ method: "notifications/roots/list_changed" }, (msg) => 456 | _notImplemented(msg) 457 | ), 458 | Match.exhaustive 459 | ); 460 | }).pipe( 461 | Effect.catchTag("JsonRpcError", (err) => 462 | Effect.logError(`Error handling notification: ${err.message}`) 463 | ) 464 | ); 465 | }); 466 | 467 | return { 468 | handleRequest, 469 | handleError, 470 | handleResponse, 471 | handleNotification, 472 | } satisfies MCP.MCP.Service; 473 | }); 474 | 475 | export const layer = (config: Implementation) => 476 | Layer.effect(MCP.MCP, make(config)).pipe( 477 | Layer.provideMerge(Messenger.Default) 478 | ); 479 | 480 | /** 481 | * 482 | * Internal utilities copied from `@effect/ai` since they are not exported. 483 | */ 484 | 485 | /** 486 | * 487 | * @param ast 488 | * @returns 489 | */ 490 | 491 | const makeJsonSchema = (ast: AST.AST): JsonSchema.JsonSchema7 => { 492 | const $defs = {}; 493 | const schema = JsonSchema.fromAST(ast, { 494 | definitions: $defs, 495 | topLevelReferenceStrategy: "skip", 496 | }); 497 | if (Object.keys($defs).length === 0) return schema; 498 | (schema as any).$defs = $defs; 499 | return schema; 500 | }; 501 | 502 | const getDescription = (ast: AST.AST): string => { 503 | const annotations = 504 | ast._tag === "Transformation" 505 | ? { 506 | ...ast.to.annotations, 507 | ...ast.annotations, 508 | } 509 | : ast.annotations; 510 | return AST.DescriptionAnnotationId in annotations 511 | ? (annotations[AST.DescriptionAnnotationId] as string) 512 | : ""; 513 | }; 514 | 515 | /** 516 | * Certain providers (i.e. Anthropic) do not do a great job returning the 517 | * `_tag` enum with the parameters for a tool call. This method ensures that 518 | * the `_tag` is injected into the tool call parameters to avoid issues when 519 | * decoding. 520 | */ 521 | function injectTag(params: unknown, tag: string) { 522 | // If for some reason we do not receive an object back for the tool call 523 | // input parameters, just return them unchanged 524 | if (!Predicate.isObject(params)) { 525 | return params; 526 | } 527 | // If the tool's `_tag` is already present in input parameters, return them 528 | // unchanged 529 | if (Predicate.hasProperty(params, "_tag")) { 530 | return params; 531 | } 532 | // Otherwise inject the tool's `_tag` into the input parameters 533 | return { ...params, _tag: tag }; 534 | } 535 | -------------------------------------------------------------------------------- /packages/server/src/transport/sse.ts: -------------------------------------------------------------------------------- 1 | // SSE Api Route 2 | 3 | import { JSONRPCMessage, handleMessage } from "@effect-mcp/shared"; 4 | import * as HttpHeaders from "@effect/platform/Headers"; 5 | import * as HttpRouter from "@effect/platform/HttpRouter"; 6 | import * as HttpServerRequest from "@effect/platform/HttpServerRequest"; 7 | import * as HttpServerResponse from "@effect/platform/HttpServerResponse"; 8 | import * as Effect from "effect/Effect"; 9 | import { pipe } from "effect/Function"; 10 | import * as Schema from "effect/Schema"; 11 | import * as Stream from "effect/Stream"; 12 | import { Messenger } from "../messenger.js"; 13 | 14 | export const SSERoute = (msgEndpoint: string) => 15 | HttpRouter.makeRoute( 16 | "GET", 17 | "/sse", 18 | Effect.gen(function* () { 19 | const sessionId = crypto.randomUUID(); 20 | const messenger = yield* Messenger; 21 | yield* Effect.log(`New SSE session: ${sessionId}`); 22 | // TODO: Filter by sessionId or make new stream per session 23 | const outboundStream = Stream.fromPubSub(messenger.outbound).pipe( 24 | Stream.flatMap((message) => Schema.encode(JSONRPCMessage)(message)), 25 | Stream.map( 26 | (message) => `event: message\ndata: ${JSON.stringify(message)}\n\n` 27 | ) 28 | ); 29 | 30 | const endpointMsg = Stream.succeed( 31 | `event: endpoint\ndata: ${msgEndpoint}?sessionId=${sessionId}\n\n` 32 | ); 33 | 34 | const encoder = new TextEncoder(); 35 | 36 | const stream = pipe( 37 | endpointMsg, 38 | Stream.merge(outboundStream), 39 | Stream.tap((msg) => Effect.log(`Stream message: ${msg}`)), 40 | Stream.map((msg) => encoder.encode(msg)) 41 | ); 42 | 43 | const headers = HttpHeaders.fromInput({ 44 | "content-type": "text/event-stream", 45 | "cache-control": "no-cache", 46 | "x-accel-buffering": "no", 47 | connection: "keep-alive", // if (req.httpVersion !== "2.0") 48 | }); 49 | 50 | // TODO: Store session id in cache 51 | 52 | return HttpServerResponse.stream(stream, { 53 | contentType: "text/event-stream", 54 | headers, 55 | }); 56 | }) 57 | ); 58 | 59 | // Post Message Route 60 | 61 | export const MessageRoute = () => 62 | HttpRouter.makeRoute( 63 | "POST", 64 | "/messages", 65 | Effect.gen(function* () { 66 | // TODO: Check for session id 67 | 68 | const message = yield* HttpServerRequest.schemaBodyJson(JSONRPCMessage); 69 | 70 | yield* Effect.log(`Received message:`, message); 71 | 72 | yield* handleMessage(message).pipe(Effect.forkDaemon); 73 | 74 | return yield* HttpServerResponse.text("Accepted", { status: 202 }); 75 | }) 76 | ); 77 | -------------------------------------------------------------------------------- /packages/server/src/transport/stdio.ts: -------------------------------------------------------------------------------- 1 | import { handleMessage, JSONRPCMessage } from "@effect-mcp/shared"; 2 | import * as Terminal from "@effect/platform/Terminal"; 3 | import * as Context from "effect/Context"; 4 | import * as Effect from "effect/Effect"; 5 | import * as Layer from "effect/Layer"; 6 | import * as Logger from "effect/Logger"; 7 | import * as LogLevel from "effect/LogLevel"; 8 | import * as Queue from "effect/Queue"; 9 | import * as Schedule from "effect/Schedule"; 10 | import * as Schema from "effect/Schema"; 11 | import { Messenger } from "../messenger.js"; 12 | 13 | export class StdioServerTransport extends Context.Tag( 14 | "@effect-mcp/server/StdioServerTransport" 15 | )() {} 16 | 17 | export const make = Effect.gen(function* () { 18 | const messenger = yield* Messenger; 19 | const terminal = yield* Terminal.Terminal; 20 | 21 | const outbound = yield* messenger.outbound.subscribe; 22 | 23 | // Start listening for messages to send via stdout 24 | yield* Effect.gen(function* () { 25 | const response = yield* Queue.take(outbound); 26 | yield* Effect.log(`Sending message:`, response); 27 | 28 | const encoded = yield* Schema.encode(JSONRPCMessage)(response); 29 | 30 | yield* terminal.display(JSON.stringify(encoded) + "\n"); 31 | }).pipe(Effect.repeat(Schedule.forever), Effect.fork); 32 | 33 | // Start listening for messages via stdin 34 | yield* Effect.gen(function* () { 35 | const input = yield* terminal.readLine; 36 | 37 | yield* Effect.log(`Received message: ${input}`); 38 | 39 | if (input) { 40 | const parsed = yield* Schema.decodeUnknown(JSONRPCMessage)( 41 | JSON.parse(input) 42 | ); 43 | 44 | yield* handleMessage(parsed).pipe(Effect.fork); 45 | } 46 | }).pipe( 47 | Effect.catchTag("ParseError", (err) => 48 | Effect.logError(`Error parsing message: ${err.message}`) 49 | ), 50 | Effect.repeat(Schedule.forever) 51 | ); 52 | }).pipe( 53 | // Hiding all logs so they don't interfere with the stdio stream 54 | // Currently can't figure out how to remove default logger from an effect. Only on the top level program. 55 | Logger.withMinimumLogLevel(LogLevel.None) 56 | ); 57 | 58 | export const layer = Layer.effect(StdioServerTransport, make); 59 | -------------------------------------------------------------------------------- /packages/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "references": [ 4 | { 5 | "path": "../shared" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /packages/server/tsup.config.bundled_g47gx86k3cq.mjs: -------------------------------------------------------------------------------- 1 | // tsup.config.ts 2 | import { defineConfig } from "tsup"; 3 | var tsup_config_default = defineConfig({ 4 | format: ["esm", "cjs"], 5 | entry: ["src/index.ts"], 6 | clean: true, 7 | target: "esnext", 8 | treeshake: false, 9 | keepNames: true, 10 | tsconfig: "tsconfig.json", 11 | external: ["effect"] 12 | }); 13 | export { 14 | tsup_config_default as default 15 | }; 16 | //# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidHN1cC5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9faW5qZWN0ZWRfZmlsZW5hbWVfXyA9IFwiL1VzZXJzL2dhcnJldHQvRG9jdW1lbnRzL2dpdGh1Yi9leHBlcmltZW50cy9lZmZlY3QtbWNwL3BhY2thZ2VzL3NlcnZlci90c3VwLmNvbmZpZy50c1wiO2NvbnN0IF9faW5qZWN0ZWRfZGlybmFtZV9fID0gXCIvVXNlcnMvZ2FycmV0dC9Eb2N1bWVudHMvZ2l0aHViL2V4cGVyaW1lbnRzL2VmZmVjdC1tY3AvcGFja2FnZXMvc2VydmVyXCI7Y29uc3QgX19pbmplY3RlZF9pbXBvcnRfbWV0YV91cmxfXyA9IFwiZmlsZTovLy9Vc2Vycy9nYXJyZXR0L0RvY3VtZW50cy9naXRodWIvZXhwZXJpbWVudHMvZWZmZWN0LW1jcC9wYWNrYWdlcy9zZXJ2ZXIvdHN1cC5jb25maWcudHNcIjsvLyB0c3VwLmNvbmZpZy50c1xuaW1wb3J0IHsgZGVmaW5lQ29uZmlnIH0gZnJvbSBcInRzdXBcIjtcblxuZXhwb3J0IGRlZmF1bHQgZGVmaW5lQ29uZmlnKHtcbiAgZm9ybWF0OiBbXCJlc21cIiwgXCJjanNcIl0sXG4gIGVudHJ5OiBbXCJzcmMvaW5kZXgudHNcIl0sXG4gIGNsZWFuOiB0cnVlLFxuICB0YXJnZXQ6IFwiZXNuZXh0XCIsXG4gIHRyZWVzaGFrZTogZmFsc2UsXG4gIGtlZXBOYW1lczogdHJ1ZSxcbiAgdHNjb25maWc6IFwidHNjb25maWcuanNvblwiLFxuICBleHRlcm5hbDogW1wiZWZmZWN0XCJdLFxufSk7XG4iXSwKICAibWFwcGluZ3MiOiAiO0FBQ0EsU0FBUyxvQkFBb0I7QUFFN0IsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsUUFBUSxDQUFDLE9BQU8sS0FBSztBQUFBLEVBQ3JCLE9BQU8sQ0FBQyxjQUFjO0FBQUEsRUFDdEIsT0FBTztBQUFBLEVBQ1AsUUFBUTtBQUFBLEVBQ1IsV0FBVztBQUFBLEVBQ1gsV0FBVztBQUFBLEVBQ1gsVUFBVTtBQUFBLEVBQ1YsVUFBVSxDQUFDLFFBQVE7QUFDckIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K 17 | -------------------------------------------------------------------------------- /packages/server/tsup.config.ts: -------------------------------------------------------------------------------- 1 | // tsup.config.ts 2 | import { defineConfig } from "tsup"; 3 | 4 | export default defineConfig({ 5 | format: ["esm", "cjs"], 6 | entry: ["src/index.ts"], 7 | clean: true, 8 | target: "esnext", 9 | treeshake: false, 10 | keepNames: true, 11 | tsconfig: "tsconfig.json", 12 | external: ["effect"], 13 | }); 14 | -------------------------------------------------------------------------------- /packages/shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@effect-mcp/shared", 3 | "version": "0.0.1", 4 | "private": true, 5 | "description": "Effect MCP Shared", 6 | "type": "module", 7 | "scripts": { 8 | "build": "pnpm run build:code && pnpm run build:types", 9 | "build:code": "tsup", 10 | "build:types": "rm -f tsconfig.tsbuildinfo && tsc --emitDeclarationOnly" 11 | }, 12 | "keywords": [ 13 | "effect", 14 | "mcp", 15 | "modelcontextprotocol" 16 | ], 17 | "author": "Garrett Hardin", 18 | "license": "MIT", 19 | "files": [ 20 | "dist", 21 | "src" 22 | ], 23 | "exports": { 24 | ".": { 25 | "import": { 26 | "@effect-mcp/dev": "./src/index.ts", 27 | "types": "./dist/index.d.ts", 28 | "default": "./dist/index.js" 29 | }, 30 | "require": { 31 | "@effect-mcp/dev": "./src/index.ts", 32 | "types": "./dist/index.d.ts", 33 | "default": "./dist/index.cjs" 34 | } 35 | }, 36 | "./mcp": { 37 | "import": { 38 | "@effect-mcp/dev": "./src/mcp/mcp.ts", 39 | "types": "./dist/mcp/mcp.d.ts", 40 | "default": "./dist/mcp/mcp.js" 41 | }, 42 | "require": { 43 | "@effect-mcp/dev": "./src/mcp/mcp.ts", 44 | "types": "./dist/mcp/mcp.d.ts", 45 | "default": "./dist/mcp/mcp.cjs" 46 | } 47 | }, 48 | "./schema": { 49 | "import": { 50 | "@effect-mcp/dev": "./src/schema/schema.ts", 51 | "types": "./dist/schema/schema.d.ts", 52 | "default": "./dist/schema/schema.js" 53 | }, 54 | "require": { 55 | "@effect-mcp/dev": "./src/schema/schema.ts", 56 | "types": "./dist/schema/schema.d.ts", 57 | "default": "./dist/schema/schema.cjs" 58 | } 59 | } 60 | }, 61 | "dependencies": {}, 62 | "peerDependencies": { 63 | "effect": "^3.13.2", 64 | "typescript": "^5.7.3" 65 | }, 66 | "devDependencies": { 67 | "tsup": "^8.3.6" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/shared/src/error.ts: -------------------------------------------------------------------------------- 1 | import * as Schema from "effect/Schema"; 2 | 3 | export const JsonRpcErrorCode = { 4 | // SDK error codes 5 | ConnectionClosed: -32000, 6 | RequestTimeout: -32001, 7 | 8 | // Standard JSON-RPC error codes 9 | ParseError: -32700, 10 | InvalidRequest: -32600, 11 | MethodNotFound: -32601, 12 | InvalidParams: -32602, 13 | InternalError: -32603, 14 | } as const; 15 | 16 | export class JsonRpcError extends Schema.TaggedError()( 17 | "JsonRpcError", 18 | { 19 | code: Schema.Number, 20 | message: Schema.String, 21 | data: Schema.optional(Schema.Unknown), 22 | } 23 | ) { 24 | static readonly fromCode = ( 25 | cause: keyof typeof JsonRpcErrorCode, 26 | message: string, 27 | data?: unknown 28 | ) => 29 | new JsonRpcError({ 30 | code: JsonRpcErrorCode[cause], 31 | message: message, 32 | data: data, 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /packages/shared/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./error.js"; 2 | export * from "./mcp.js"; 3 | export * as MCP from "./mcp.js"; 4 | export * from "./schema.js"; 5 | -------------------------------------------------------------------------------- /packages/shared/src/mcp.ts: -------------------------------------------------------------------------------- 1 | import * as Context from "effect/Context"; 2 | import * as Effect from "effect/Effect"; 3 | import { pipe } from "effect/Function"; 4 | import * as Match from "effect/Match"; 5 | import type { 6 | JSONRPCError, 7 | JSONRPCMessage, 8 | JSONRPCNotification, 9 | JSONRPCRequest, 10 | JSONRPCResponse, 11 | } from "./schema.js"; 12 | 13 | export namespace MCP { 14 | export interface Service { 15 | handleError: (message: JSONRPCError) => Effect.Effect; 16 | handleResponse: (message: JSONRPCResponse) => Effect.Effect; 17 | handleNotification: (message: JSONRPCNotification) => Effect.Effect; 18 | handleRequest: (message: JSONRPCRequest) => Effect.Effect; 19 | } 20 | } 21 | 22 | export class MCP extends Context.Tag("MCP")() {} 23 | 24 | export const handleMessage = (message: JSONRPCMessage) => 25 | pipe( 26 | MCP, 27 | Effect.flatMap((mcp) => 28 | Match.value(message).pipe( 29 | Match.when( 30 | (message): message is JSONRPCError => 31 | "error" in message && typeof message.error === "object", 32 | (msg) => mcp.handleError(msg) 33 | ), 34 | Match.when( 35 | (message): message is JSONRPCResponse => 36 | "result" in message && typeof message.result === "object", 37 | (msg) => mcp.handleResponse(msg) 38 | ), 39 | Match.when( 40 | (message): message is JSONRPCRequest => "id" in message, 41 | (msg) => mcp.handleRequest(msg) 42 | ), 43 | 44 | Match.orElse((msg) => mcp.handleNotification(msg)) 45 | ) 46 | ) 47 | ); 48 | -------------------------------------------------------------------------------- /packages/shared/src/schema.ts: -------------------------------------------------------------------------------- 1 | import * as Schema from "effect/Schema"; 2 | 3 | export const LATEST_PROTOCOL_VERSION = "2024-11-05"; 4 | export const SUPPORTED_PROTOCOL_VERSIONS = [ 5 | LATEST_PROTOCOL_VERSION, 6 | "2024-10-07", 7 | ]; 8 | 9 | export const JSONRPC_VERSION = "2.0"; 10 | 11 | // Basic schemas 12 | export const ProgressToken = Schema.Union( 13 | Schema.String, 14 | Schema.Number.pipe(Schema.int()) 15 | ); 16 | export type ProgressToken = Schema.Schema.Type; 17 | 18 | export const Cursor = Schema.String; 19 | export type Cursor = Schema.Schema.Type; 20 | 21 | const UnknownStruct = Schema.Record({ 22 | key: Schema.String, 23 | value: Schema.Unknown, 24 | }); 25 | 26 | const BaseRequestParams = Schema.Struct( 27 | Schema.Struct({ 28 | _meta: Schema.Struct( 29 | Schema.Struct({ 30 | progressToken: Schema.optional(ProgressToken), 31 | }).fields, 32 | UnknownStruct 33 | ).pipe(Schema.optional), 34 | }).fields, 35 | UnknownStruct 36 | ); 37 | 38 | export const Request = Schema.Struct({ 39 | method: Schema.String, 40 | params: Schema.optional(BaseRequestParams), 41 | }); 42 | export type Request = Schema.Schema.Type; 43 | 44 | const BaseNotificationParams = Schema.Struct( 45 | Schema.Struct({ 46 | _meta: Schema.Object.pipe(Schema.optional), 47 | }).fields, 48 | UnknownStruct 49 | ); 50 | 51 | export const Notification = Schema.Struct({ 52 | method: Schema.String, 53 | params: Schema.optional(BaseNotificationParams), 54 | }); 55 | export type Notification = Schema.Schema.Type; 56 | 57 | export const Result = Schema.Struct( 58 | Schema.Struct({ 59 | _meta: Schema.Object.pipe(Schema.optional), 60 | }).fields, 61 | UnknownStruct 62 | ); 63 | export type Result = Schema.Schema.Type; 64 | 65 | export const RequestId = Schema.Union( 66 | Schema.String, 67 | Schema.Number.pipe(Schema.int()) 68 | ).pipe(Schema.brand("RequestId")); 69 | export type RequestId = Schema.Schema.Type; 70 | 71 | // JSON-RPC schemas 72 | export const JSONRPCRequest = Schema.Struct({ 73 | ...Request.fields, 74 | jsonrpc: Schema.Literal(JSONRPC_VERSION), 75 | id: RequestId, 76 | }); 77 | export type JSONRPCRequest = Schema.Schema.Type; 78 | 79 | export const JSONRPCNotification = Schema.Struct({ 80 | ...Notification.fields, 81 | jsonrpc: Schema.Literal(JSONRPC_VERSION), 82 | }); 83 | export type JSONRPCNotification = Schema.Schema.Type< 84 | typeof JSONRPCNotification 85 | >; 86 | 87 | export const JSONRPCResponse = Schema.Struct({ 88 | jsonrpc: Schema.Literal(JSONRPC_VERSION), 89 | id: RequestId, 90 | result: Result, 91 | }); 92 | export type JSONRPCResponse = Schema.Schema.Type; 93 | 94 | export const JSONRPCError = Schema.Struct({ 95 | jsonrpc: Schema.Literal(JSONRPC_VERSION), 96 | id: RequestId, 97 | error: Schema.Struct({ 98 | code: Schema.Number.pipe(Schema.int()), 99 | message: Schema.String, 100 | data: Schema.optional(Schema.Unknown), 101 | }), 102 | }); 103 | export type JSONRPCError = Schema.Schema.Type; 104 | 105 | export const JSONRPCMessage = Schema.Union( 106 | JSONRPCRequest, 107 | JSONRPCNotification, 108 | JSONRPCResponse, 109 | JSONRPCError 110 | ); 111 | export type JSONRPCMessage = Schema.Schema.Type; 112 | 113 | export const EmptyResult = Result.annotations({ 114 | parseOptions: { onExcessProperty: "ignore" }, 115 | }); 116 | export type EmptyResult = Schema.Schema.Type; 117 | 118 | export const CancelledNotification = Schema.Struct({ 119 | ...Notification.fields, 120 | method: Schema.Literal("notifications/cancelled"), 121 | params: Schema.Struct({ 122 | ...BaseNotificationParams.fields, 123 | requestId: RequestId, 124 | reason: Schema.String.pipe(Schema.optional), 125 | }), 126 | }); 127 | export type CancelledNotification = Schema.Schema.Type< 128 | typeof CancelledNotification 129 | >; 130 | 131 | export const Implementation = Schema.Struct({ 132 | name: Schema.String, 133 | version: Schema.String, 134 | }).annotations({ 135 | parseOptions: { 136 | onExcessProperty: "preserve", 137 | }, 138 | }); 139 | export type Implementation = Schema.Schema.Type; 140 | 141 | /** 142 | * Capabilities a client may support 143 | */ 144 | export const ClientCapabilities = Schema.Struct( 145 | Schema.Struct({ 146 | experimental: Schema.optional( 147 | Schema.Record({ key: Schema.String, value: Schema.Unknown }) 148 | ), 149 | sampling: Schema.optional( 150 | Schema.Record({ key: Schema.String, value: Schema.Unknown }) 151 | ), 152 | roots: Schema.optional( 153 | Schema.Struct( 154 | Schema.Struct({ 155 | listChanged: Schema.optional(Schema.Boolean), 156 | }).fields, 157 | UnknownStruct 158 | ) 159 | ), 160 | }).fields, 161 | UnknownStruct 162 | ); 163 | export type ClientCapabilities = Schema.Schema.Type; 164 | 165 | /** 166 | * Initialize request schema 167 | */ 168 | export const InitializeRequest = Schema.Struct({ 169 | ...Request.fields, 170 | method: Schema.Literal("initialize"), 171 | params: Schema.Struct({ 172 | ...BaseRequestParams.fields, 173 | protocolVersion: Schema.String, 174 | capabilities: ClientCapabilities, 175 | clientInfo: Implementation, 176 | }), 177 | }); 178 | export type InitializeRequest = Schema.Schema.Type; 179 | 180 | /** 181 | * Server capabilities schema 182 | */ 183 | export const ServerCapabilities = Schema.Struct( 184 | Schema.Struct({ 185 | experimental: Schema.optional( 186 | Schema.Record({ key: Schema.String, value: Schema.Unknown }) 187 | ), 188 | logging: Schema.optional( 189 | Schema.Record({ key: Schema.String, value: Schema.Unknown }) 190 | ), 191 | prompts: Schema.optional( 192 | Schema.Struct( 193 | Schema.Struct({ 194 | listChanged: Schema.optional(Schema.Boolean), 195 | }).fields, 196 | UnknownStruct 197 | ) 198 | ), 199 | resources: Schema.optional( 200 | Schema.Struct( 201 | Schema.Struct({ 202 | subscribe: Schema.optional(Schema.Boolean), 203 | listChanged: Schema.optional(Schema.Boolean), 204 | }).fields, 205 | UnknownStruct 206 | ) 207 | ), 208 | tools: Schema.optional( 209 | Schema.Struct( 210 | Schema.Struct({ 211 | listChanged: Schema.optional(Schema.Boolean), 212 | }).fields, 213 | UnknownStruct 214 | ) 215 | ), 216 | }).fields, 217 | UnknownStruct 218 | ); 219 | export type ServerCapabilities = Schema.Schema.Type; 220 | 221 | /** 222 | * After receiving an initialize request from the client, the server sends this response. 223 | */ 224 | export const InitializeResult = Schema.Struct({ 225 | ...Result.fields, 226 | /** 227 | * The version of the Model Context Protocol that the server wants to use. This may not match the version that the client requested. If the client cannot support this version, it MUST disconnect. 228 | */ 229 | protocolVersion: Schema.String, 230 | capabilities: ServerCapabilities, 231 | serverInfo: Implementation, 232 | /** 233 | * Instructions describing how to use the server and its features. 234 | * 235 | * This can be used by clients to improve the LLM's understanding of available tools, resources, etc. It can be thought of like a "hint" to the model. For example, this information MAY be added to the system prompt. 236 | */ 237 | instructions: Schema.optional(Schema.String), 238 | }); 239 | export type InitializeResult = Schema.Schema.Type; 240 | 241 | /** 242 | * This notification is sent from the client to the server after initialization has finished. 243 | */ 244 | export const InitializedNotification = Schema.Struct({ 245 | ...Notification.fields, 246 | method: Schema.Literal("notifications/initialized"), 247 | }); 248 | export type InitializedNotification = Schema.Schema.Type< 249 | typeof InitializedNotification 250 | >; 251 | 252 | /* Ping */ 253 | /** 254 | * A ping, issued by either the server or the client, to check that the other party is still alive. The receiver must promptly respond, or else may be disconnected. 255 | */ 256 | export const PingRequest = Schema.Struct({ 257 | ...Request.fields, 258 | method: Schema.Literal("ping"), 259 | }); 260 | export type PingRequest = Schema.Schema.Type; 261 | 262 | /* Progress notifications */ 263 | export const Progress = Schema.Struct( 264 | Schema.Struct({ 265 | /** 266 | * The progress thus far. This should increase every time progress is made, even if the total is unknown. 267 | */ 268 | progress: Schema.Number, 269 | /** 270 | * Total number of items to process (or total progress required), if known. 271 | */ 272 | total: Schema.optional(Schema.Number), 273 | }).fields, 274 | UnknownStruct 275 | ); 276 | export type Progress = Schema.Schema.Type; 277 | 278 | /** 279 | * An out-of-band notification used to inform the receiver of a progress update for a long-running request. 280 | */ 281 | export const ProgressNotification = Schema.Struct({ 282 | ...Notification.fields, 283 | method: Schema.Literal("notifications/progress"), 284 | params: Schema.Struct({ 285 | ...BaseNotificationParams.fields, 286 | ...Progress.fields, 287 | /** 288 | * The progress token which was given in the initial request, used to associate this notification with the request that is proceeding. 289 | */ 290 | progressToken: ProgressToken, 291 | }), 292 | }); 293 | export type ProgressNotification = Schema.Schema.Type< 294 | typeof ProgressNotification 295 | >; 296 | 297 | /* Pagination */ 298 | export const PaginatedRequest = Schema.Struct({ 299 | ...Request.fields, 300 | params: Schema.optional( 301 | Schema.Struct({ 302 | ...BaseRequestParams.fields, 303 | /** 304 | * An opaque token representing the current pagination position. 305 | * If provided, the server should return results starting after this cursor. 306 | */ 307 | cursor: Schema.optional(Cursor), 308 | }) 309 | ), 310 | }); 311 | export type PaginatedRequest = Schema.Schema.Type; 312 | 313 | export const PaginatedResult = Schema.Struct({ 314 | ...Result.fields, 315 | /** 316 | * An opaque token representing the pagination position after the last returned result. 317 | * If present, there may be more results available. 318 | */ 319 | nextCursor: Schema.optional(Cursor), 320 | }); 321 | export type PaginatedResult = Schema.Schema.Type; 322 | 323 | /* Resources */ 324 | /** 325 | * The contents of a specific resource or sub-resource. 326 | */ 327 | export const ResourceContents = Schema.Struct( 328 | Schema.Struct({ 329 | /** 330 | * The URI of this resource. 331 | */ 332 | uri: Schema.String, 333 | /** 334 | * The MIME type of this resource, if known. 335 | */ 336 | mimeType: Schema.optional(Schema.String), 337 | }).fields, 338 | UnknownStruct 339 | ); 340 | export type ResourceContents = Schema.Schema.Type; 341 | 342 | export const TextResourceContents = Schema.Struct({ 343 | ...ResourceContents.fields, 344 | /** 345 | * The text of the item. This must only be set if the item can actually be represented as text (not binary data). 346 | */ 347 | text: Schema.String, 348 | }); 349 | export type TextResourceContents = Schema.Schema.Type< 350 | typeof TextResourceContents 351 | >; 352 | 353 | export const BlobResourceContents = Schema.Struct({ 354 | ...ResourceContents.fields, 355 | /** 356 | * A base64-encoded string representing the binary data of the item. 357 | */ 358 | blob: Schema.String.pipe(Schema.pattern(/^[A-Za-z0-9+/]*={0,2}$/)), // base64 pattern 359 | }); 360 | export type BlobResourceContents = Schema.Schema.Type< 361 | typeof BlobResourceContents 362 | >; 363 | 364 | /** 365 | * A known resource that the server is capable of reading. 366 | */ 367 | export const Resource = Schema.Struct( 368 | Schema.Struct({ 369 | /** 370 | * The URI of this resource. 371 | */ 372 | uri: Schema.String, 373 | 374 | /** 375 | * A human-readable name for this resource. 376 | * 377 | * This can be used by clients to populate UI elements. 378 | */ 379 | name: Schema.String, 380 | 381 | /** 382 | * A description of what this resource represents. 383 | * 384 | * This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model. 385 | */ 386 | description: Schema.optional(Schema.String), 387 | 388 | /** 389 | * The MIME type of this resource, if known. 390 | */ 391 | mimeType: Schema.optional(Schema.String), 392 | }).fields, 393 | UnknownStruct 394 | ); 395 | export type Resource = Schema.Schema.Type; 396 | 397 | /** 398 | * A template description for resources available on the server. 399 | */ 400 | export const ResourceTemplate = Schema.Struct( 401 | Schema.Struct({ 402 | /** 403 | * A URI template (according to RFC 6570) that can be used to construct resource URIs. 404 | */ 405 | uriTemplate: Schema.String, 406 | 407 | /** 408 | * A human-readable name for the type of resource this template refers to. 409 | * 410 | * This can be used by clients to populate UI elements. 411 | */ 412 | name: Schema.String, 413 | 414 | /** 415 | * A description of what this template is for. 416 | * 417 | * This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model. 418 | */ 419 | description: Schema.optional(Schema.String), 420 | 421 | /** 422 | * The MIME type for all resources that match this template. This should only be included if all resources matching this template have the same type. 423 | */ 424 | mimeType: Schema.optional(Schema.String), 425 | }).fields, 426 | UnknownStruct 427 | ); 428 | export type ResourceTemplate = Schema.Schema.Type; 429 | 430 | /** 431 | * Sent from the client to request a list of resources the server has. 432 | */ 433 | export const ListResourcesRequest = Schema.Struct({ 434 | ...PaginatedRequest.fields, 435 | method: Schema.Literal("resources/list"), 436 | }); 437 | export type ListResourcesRequest = Schema.Schema.Type< 438 | typeof ListResourcesRequest 439 | >; 440 | 441 | /** 442 | * The server's response to a resources/list request from the client. 443 | */ 444 | export const ListResourcesResult = Schema.Struct({ 445 | ...PaginatedResult.fields, 446 | resources: Schema.Array(Resource), 447 | }); 448 | export type ListResourcesResult = Schema.Schema.Type< 449 | typeof ListResourcesResult 450 | >; 451 | 452 | /** 453 | * Sent from the client to request a list of resource templates the server has. 454 | */ 455 | export const ListResourceTemplatesRequest = Schema.Struct({ 456 | ...PaginatedRequest.fields, 457 | method: Schema.Literal("resources/templates/list"), 458 | }); 459 | export type ListResourceTemplatesRequest = Schema.Schema.Type< 460 | typeof ListResourceTemplatesRequest 461 | >; 462 | 463 | /** 464 | * The server's response to a resources/templates/list request from the client. 465 | */ 466 | export const ListResourceTemplatesResult = Schema.Struct({ 467 | ...PaginatedResult.fields, 468 | resourceTemplates: Schema.Array(ResourceTemplate), 469 | }); 470 | export type ListResourceTemplatesResult = Schema.Schema.Type< 471 | typeof ListResourceTemplatesResult 472 | >; 473 | 474 | /** 475 | * Sent from the client to the server, to read a specific resource URI. 476 | */ 477 | export const ReadResourceRequest = Schema.Struct({ 478 | ...Request.fields, 479 | method: Schema.Literal("resources/read"), 480 | params: Schema.Struct({ 481 | ...BaseRequestParams.fields, 482 | /** 483 | * The URI of the resource to read. The URI can use any protocol; it is up to the server how to interpret it. 484 | */ 485 | uri: Schema.String, 486 | }), 487 | }); 488 | export type ReadResourceRequest = Schema.Schema.Type< 489 | typeof ReadResourceRequest 490 | >; 491 | 492 | /** 493 | * The server's response to a resources/read request from the client. 494 | */ 495 | export const ReadResourceResult = Schema.Struct({ 496 | ...Result.fields, 497 | contents: Schema.Array( 498 | Schema.Union(TextResourceContents, BlobResourceContents) 499 | ), 500 | }); 501 | export type ReadResourceResult = Schema.Schema.Type; 502 | 503 | /** 504 | * An optional notification from the server to the client, informing it that the list of resources it can read from has changed. This may be issued by servers without any previous subscription from the client. 505 | */ 506 | export const ResourceListChangedNotification = Schema.Struct({ 507 | ...Notification.fields, 508 | method: Schema.Literal("notifications/resources/list_changed"), 509 | }); 510 | export type ResourceListChangedNotification = Schema.Schema.Type< 511 | typeof ResourceListChangedNotification 512 | >; 513 | 514 | /** 515 | * Sent from the client to request resources/updated notifications from the server whenever a particular resource changes. 516 | */ 517 | export const SubscribeRequest = Schema.Struct({ 518 | ...Request.fields, 519 | method: Schema.Literal("resources/subscribe"), 520 | params: Schema.Struct({ 521 | ...BaseRequestParams.fields, 522 | /** 523 | * The URI of the resource to subscribe to. The URI can use any protocol; it is up to the server how to interpret it. 524 | */ 525 | uri: Schema.String, 526 | }), 527 | }); 528 | export type SubscribeRequest = Schema.Schema.Type; 529 | 530 | /** 531 | * Sent from the client to request cancellation of resources/updated notifications from the server. This should follow a previous resources/subscribe request. 532 | */ 533 | export const UnsubscribeRequest = Schema.Struct({ 534 | ...Request.fields, 535 | method: Schema.Literal("resources/unsubscribe"), 536 | params: Schema.Struct({ 537 | ...BaseRequestParams.fields, 538 | /** 539 | * The URI of the resource to unsubscribe from. 540 | */ 541 | uri: Schema.String, 542 | }), 543 | }); 544 | export type UnsubscribeRequest = Schema.Schema.Type; 545 | 546 | /** 547 | * A notification from the server to the client, informing it that a resource has changed and may need to be read again. This should only be sent if the client previously sent a resources/subscribe request. 548 | */ 549 | export const ResourceUpdatedNotification = Schema.Struct({ 550 | ...Notification.fields, 551 | method: Schema.Literal("notifications/resources/updated"), 552 | params: Schema.Struct({ 553 | ...BaseNotificationParams.fields, 554 | /** 555 | * The URI of the resource that has been updated. This might be a sub-resource of the one that the client actually subscribed to. 556 | */ 557 | uri: Schema.String, 558 | }), 559 | }); 560 | export type ResourceUpdatedNotification = Schema.Schema.Type< 561 | typeof ResourceUpdatedNotification 562 | >; 563 | 564 | /* Prompts */ 565 | /** 566 | * Describes an argument that a prompt can accept. 567 | */ 568 | export const PromptArgument = Schema.Struct( 569 | Schema.Struct({ 570 | /** 571 | * The name of the argument. 572 | */ 573 | name: Schema.String, 574 | /** 575 | * A human-readable description of the argument. 576 | */ 577 | description: Schema.optional(Schema.String), 578 | /** 579 | * Whether this argument must be provided. 580 | */ 581 | required: Schema.optional(Schema.Boolean), 582 | }).fields, 583 | UnknownStruct 584 | ); 585 | export type PromptArgument = Schema.Schema.Type; 586 | 587 | /** 588 | * A prompt or prompt template that the server offers. 589 | */ 590 | export const Prompt = Schema.Struct( 591 | Schema.Struct({ 592 | /** 593 | * The name of the prompt or prompt template. 594 | */ 595 | name: Schema.String, 596 | /** 597 | * An optional description of what this prompt provides 598 | */ 599 | description: Schema.optional(Schema.String), 600 | /** 601 | * A list of arguments to use for templating the prompt. 602 | */ 603 | arguments: Schema.optional(Schema.Array(PromptArgument)), 604 | }).fields, 605 | UnknownStruct 606 | ); 607 | export type Prompt = Schema.Schema.Type; 608 | 609 | /** 610 | * Sent from the client to request a list of prompts and prompt templates the server has. 611 | */ 612 | export const ListPromptsRequest = Schema.Struct({ 613 | ...PaginatedRequest.fields, 614 | method: Schema.Literal("prompts/list"), 615 | }); 616 | export type ListPromptsRequest = Schema.Schema.Type; 617 | 618 | /** 619 | * The server's response to a prompts/list request from the client. 620 | */ 621 | export const ListPromptsResult = Schema.Struct({ 622 | ...PaginatedResult.fields, 623 | prompts: Schema.Array(Prompt), 624 | }); 625 | export type ListPromptsResult = Schema.Schema.Type; 626 | 627 | /** 628 | * Used by the client to get a prompt provided by the server. 629 | */ 630 | export const GetPromptRequest = Schema.Struct({ 631 | ...Request.fields, 632 | method: Schema.Literal("prompts/get"), 633 | params: Schema.Struct({ 634 | ...BaseRequestParams.fields, 635 | /** 636 | * The name of the prompt or prompt template. 637 | */ 638 | name: Schema.String, 639 | /** 640 | * Arguments to use for templating the prompt. 641 | */ 642 | arguments: Schema.optional( 643 | Schema.Record({ key: Schema.String, value: Schema.String }) 644 | ), 645 | }), 646 | }); 647 | export type GetPromptRequest = Schema.Schema.Type; 648 | 649 | /** 650 | * Text provided to or from an LLM. 651 | */ 652 | export const TextContent = Schema.Struct( 653 | Schema.Struct({ 654 | type: Schema.Literal("text"), 655 | /** 656 | * The text content of the message. 657 | */ 658 | text: Schema.String, 659 | }).fields, 660 | UnknownStruct 661 | ); 662 | export type TextContent = Schema.Schema.Type; 663 | 664 | /** 665 | * An image provided to or from an LLM. 666 | */ 667 | export const ImageContent = Schema.Struct( 668 | Schema.Struct({ 669 | type: Schema.Literal("image"), 670 | /** 671 | * The base64-encoded image data. 672 | */ 673 | data: Schema.String.pipe(Schema.pattern(/^[A-Za-z0-9+/]*={0,2}$/)), // base64 pattern 674 | /** 675 | * The MIME type of the image. Different providers may support different image types. 676 | */ 677 | mimeType: Schema.String, 678 | }).fields, 679 | UnknownStruct 680 | ); 681 | export type ImageContent = Schema.Schema.Type; 682 | 683 | /** 684 | * The contents of a resource, embedded into a prompt or tool call result. 685 | */ 686 | export const EmbeddedResource = Schema.Struct( 687 | Schema.Struct({ 688 | type: Schema.Literal("resource"), 689 | resource: Schema.Union(TextResourceContents, BlobResourceContents), 690 | }).fields, 691 | UnknownStruct 692 | ); 693 | export type EmbeddedResource = Schema.Schema.Type; 694 | 695 | /** 696 | * Describes a message returned as part of a prompt. 697 | */ 698 | export const PromptMessage = Schema.Struct( 699 | Schema.Struct({ 700 | role: Schema.Union(Schema.Literal("user"), Schema.Literal("assistant")), 701 | content: Schema.Union(TextContent, ImageContent, EmbeddedResource), 702 | }).fields, 703 | UnknownStruct 704 | ); 705 | export type PromptMessage = Schema.Schema.Type; 706 | 707 | /** 708 | * The server's response to a prompts/get request from the client. 709 | */ 710 | export const GetPromptResult = Schema.Struct({ 711 | ...Result.fields, 712 | /** 713 | * An optional description for the prompt. 714 | */ 715 | description: Schema.optional(Schema.String), 716 | messages: Schema.Array(PromptMessage), 717 | }); 718 | export type GetPromptResult = Schema.Schema.Type; 719 | 720 | /** 721 | * An optional notification from the server to the client, informing it that the list of prompts it offers has changed. This may be issued by servers without any previous subscription from the client. 722 | */ 723 | export const PromptListChangedNotification = Schema.Struct({ 724 | ...Notification.fields, 725 | method: Schema.Literal("notifications/prompts/list_changed"), 726 | }); 727 | export type PromptListChangedNotification = Schema.Schema.Type< 728 | typeof PromptListChangedNotification 729 | >; 730 | 731 | /* Tools */ 732 | /** 733 | * Definition for a tool the client can call. 734 | */ 735 | export const Tool = Schema.Struct( 736 | Schema.Struct({ 737 | /** 738 | * The name of the tool. 739 | */ 740 | name: Schema.String, 741 | /** 742 | * A human-readable description of the tool. 743 | */ 744 | description: Schema.optional(Schema.String), 745 | /** 746 | * A JSON Schema object defining the expected parameters for the tool. 747 | */ 748 | inputSchema: Schema.Struct( 749 | Schema.Struct({ 750 | type: Schema.Literal("object"), 751 | properties: Schema.optional( 752 | Schema.Record({ key: Schema.String, value: Schema.Unknown }) 753 | ), 754 | }).fields, 755 | UnknownStruct 756 | ), 757 | }).fields, 758 | UnknownStruct 759 | ); 760 | export type Tool = Schema.Schema.Type; 761 | 762 | /** 763 | * Sent from the client to request a list of tools the server has. 764 | */ 765 | export const ListToolsRequest = Schema.Struct({ 766 | ...PaginatedRequest.fields, 767 | method: Schema.Literal("tools/list"), 768 | }); 769 | export type ListToolsRequest = Schema.Schema.Type; 770 | 771 | /** 772 | * The server's response to a tools/list request from the client. 773 | */ 774 | export const ListToolsResult = Schema.Struct({ 775 | ...PaginatedResult.fields, 776 | tools: Schema.Array(Tool), 777 | }); 778 | export type ListToolsResult = Schema.Schema.Type; 779 | 780 | /** 781 | * The server's response to a tool call. 782 | */ 783 | export const CallToolResult = Schema.Struct({ 784 | ...Result.fields, 785 | content: Schema.Array( 786 | Schema.Union(TextContent, ImageContent, EmbeddedResource) 787 | ), 788 | isError: Schema.optional(Schema.Boolean), 789 | }); 790 | export type CallToolResult = Schema.Schema.Type; 791 | 792 | /** 793 | * CallToolResultSchema extended with backwards compatibility to protocol version 2024-10-07. 794 | */ 795 | export const CompatibilityCallToolResult = Schema.Union( 796 | CallToolResult, 797 | Schema.Struct({ 798 | ...Result.fields, 799 | toolResult: Schema.Unknown, 800 | }) 801 | ); 802 | export type CompatibilityCallToolResult = Schema.Schema.Type< 803 | typeof CompatibilityCallToolResult 804 | >; 805 | 806 | /** 807 | * Used by the client to invoke a tool provided by the server. 808 | */ 809 | export const CallToolRequest = Schema.Struct({ 810 | ...Request.fields, 811 | method: Schema.Literal("tools/call"), 812 | params: Schema.Struct({ 813 | ...BaseRequestParams.fields, 814 | name: Schema.String, 815 | arguments: Schema.optional( 816 | Schema.Record({ key: Schema.String, value: Schema.Unknown }) 817 | ), 818 | }), 819 | }); 820 | export type CallToolRequest = Schema.Schema.Type; 821 | 822 | /** 823 | * An optional notification from the server to the client, informing it that the list of tools it offers has changed. This may be issued by servers without any previous subscription from the client. 824 | */ 825 | export const ToolListChangedNotification = Schema.Struct({ 826 | ...Notification.fields, 827 | method: Schema.Literal("notifications/tools/list_changed"), 828 | }); 829 | export type ToolListChangedNotification = Schema.Schema.Type< 830 | typeof ToolListChangedNotification 831 | >; 832 | 833 | /* Logging */ 834 | /** 835 | * The severity of a log message. 836 | */ 837 | export const LoggingLevel = Schema.Literal( 838 | "debug", 839 | "info", 840 | "notice", 841 | "warning", 842 | "error", 843 | "critical", 844 | "alert", 845 | "emergency" 846 | ); 847 | export type LoggingLevel = Schema.Schema.Type; 848 | 849 | /** 850 | * A request from the client to the server, to enable or adjust logging. 851 | */ 852 | export const SetLevelRequest = Schema.Struct({ 853 | ...Request.fields, 854 | method: Schema.Literal("logging/setLevel"), 855 | params: Schema.Struct({ 856 | ...BaseRequestParams.fields, 857 | /** 858 | * The level of logging that the client wants to receive from the server. The server should send all logs at this level and higher (i.e., more severe) to the client as notifications/logging/message. 859 | */ 860 | level: LoggingLevel, 861 | }), 862 | }); 863 | export type SetLevelRequest = Schema.Schema.Type; 864 | 865 | /** 866 | * Notification of a log message passed from server to client. If no logging/setLevel request has been sent from the client, the server MAY decide which messages to send automatically. 867 | */ 868 | export const LoggingMessageNotification = Schema.Struct({ 869 | ...Notification.fields, 870 | method: Schema.Literal("notifications/message"), 871 | params: Schema.Struct({ 872 | ...BaseNotificationParams.fields, 873 | /** 874 | * The severity of this log message. 875 | */ 876 | level: LoggingLevel, 877 | /** 878 | * An optional name of the logger issuing this message. 879 | */ 880 | logger: Schema.optional(Schema.String), 881 | /** 882 | * The data to be logged, such as a string message or an object. Any JSON serializable type is allowed here. 883 | */ 884 | data: Schema.Unknown, 885 | }), 886 | }); 887 | export type LoggingMessageNotification = Schema.Schema.Type< 888 | typeof LoggingMessageNotification 889 | >; 890 | 891 | /* Sampling */ 892 | /** 893 | * Hints to use for model selection. 894 | */ 895 | export const ModelHint = Schema.Struct( 896 | Schema.Struct({ 897 | /** 898 | * A hint for a model name. 899 | */ 900 | name: Schema.optional(Schema.String), 901 | }).fields, 902 | UnknownStruct 903 | ); 904 | export type ModelHint = Schema.Schema.Type; 905 | 906 | /** 907 | * The server's preferences for model selection, requested of the client during sampling. 908 | */ 909 | export const ModelPreferences = Schema.Struct( 910 | Schema.Struct({ 911 | /** 912 | * Optional hints to use for model selection. 913 | */ 914 | hints: Schema.optional(Schema.Array(ModelHint)), 915 | /** 916 | * How much to prioritize cost when selecting a model. 917 | */ 918 | costPriority: Schema.optional(Schema.Number.pipe(Schema.between(0, 1))), 919 | /** 920 | * How much to prioritize sampling speed (latency) when selecting a model. 921 | */ 922 | speedPriority: Schema.optional(Schema.Number.pipe(Schema.between(0, 1))), 923 | /** 924 | * How much to prioritize intelligence and capabilities when selecting a model. 925 | */ 926 | intelligencePriority: Schema.optional( 927 | Schema.Number.pipe(Schema.between(0, 1)) 928 | ), 929 | }).fields, 930 | UnknownStruct 931 | ); 932 | export type ModelPreferences = Schema.Schema.Type; 933 | 934 | /** 935 | * Describes a message issued to or received from an LLM API. 936 | */ 937 | export const SamplingMessage = Schema.Struct( 938 | Schema.Struct({ 939 | role: Schema.Union(Schema.Literal("user"), Schema.Literal("assistant")), 940 | content: Schema.Union(TextContent, ImageContent), 941 | }).fields, 942 | UnknownStruct 943 | ); 944 | export type SamplingMessage = Schema.Schema.Type; 945 | 946 | /** 947 | * A request from the server to sample an LLM via the client. The client has full discretion over which model to select. The client should also inform the user before beginning sampling, to allow them to inspect the request (human in the loop) and decide whether to approve it. 948 | */ 949 | export const CreateMessageRequest = Schema.Struct({ 950 | ...Request.fields, 951 | method: Schema.Literal("sampling/createMessage"), 952 | params: Schema.Struct({ 953 | ...BaseRequestParams.fields, 954 | messages: Schema.Array(SamplingMessage), 955 | /** 956 | * An optional system prompt the server wants to use for sampling. The client MAY modify or omit this prompt. 957 | */ 958 | systemPrompt: Schema.optional(Schema.String), 959 | /** 960 | * A request to include context from one or more MCP servers (including the caller), to be attached to the prompt. The client MAY ignore this request. 961 | */ 962 | includeContext: Schema.optional( 963 | Schema.Union( 964 | Schema.Literal("none"), 965 | Schema.Literal("thisServer"), 966 | Schema.Literal("allServers") 967 | ) 968 | ), 969 | temperature: Schema.optional(Schema.Number), 970 | /** 971 | * The maximum number of tokens to sample, as requested by the server. The client MAY choose to sample fewer tokens than requested. 972 | */ 973 | maxTokens: Schema.Number.pipe(Schema.int()), 974 | stopSequences: Schema.optional(Schema.Array(Schema.String)), 975 | /** 976 | * Optional metadata to pass through to the LLM provider. The format of this metadata is provider-specific. 977 | */ 978 | metadata: Schema.optional( 979 | Schema.Record({ key: Schema.String, value: Schema.Unknown }) 980 | ), 981 | /** 982 | * The server's preferences for which model to select. 983 | */ 984 | modelPreferences: Schema.optional(ModelPreferences), 985 | }), 986 | }); 987 | export type CreateMessageRequest = Schema.Schema.Type< 988 | typeof CreateMessageRequest 989 | >; 990 | 991 | /** 992 | * The client's response to a sampling/create_message request from the server. The client should inform the user before returning the sampled message, to allow them to inspect the response (human in the loop) and decide whether to allow the server to see it. 993 | */ 994 | export const CreateMessageResult = Schema.Struct({ 995 | ...Result.fields, 996 | /** 997 | * The name of the model that generated the message. 998 | */ 999 | model: Schema.String, 1000 | /** 1001 | * The reason why sampling stopped. 1002 | */ 1003 | stopReason: Schema.optional( 1004 | Schema.Union( 1005 | Schema.Literal("endTurn"), 1006 | Schema.Literal("stopSequence"), 1007 | Schema.Literal("maxTokens"), 1008 | Schema.String 1009 | ) 1010 | ), 1011 | role: Schema.Union(Schema.Literal("user"), Schema.Literal("assistant")), 1012 | content: Schema.Union(TextContent, ImageContent), 1013 | }); 1014 | export type CreateMessageResult = Schema.Schema.Type< 1015 | typeof CreateMessageResult 1016 | >; 1017 | 1018 | /* Autocomplete */ 1019 | /** 1020 | * A reference to a resource or resource template definition. 1021 | */ 1022 | export const ResourceReference = Schema.Struct( 1023 | Schema.Struct({ 1024 | type: Schema.Literal("ref/resource"), 1025 | /** 1026 | * The URI or URI template of the resource. 1027 | */ 1028 | uri: Schema.String, 1029 | }).fields, 1030 | UnknownStruct 1031 | ); 1032 | export type ResourceReference = Schema.Schema.Type; 1033 | 1034 | /** 1035 | * Identifies a prompt. 1036 | */ 1037 | export const PromptReference = Schema.Struct( 1038 | Schema.Struct({ 1039 | type: Schema.Literal("ref/prompt"), 1040 | /** 1041 | * The name of the prompt or prompt template 1042 | */ 1043 | name: Schema.String, 1044 | }).fields, 1045 | UnknownStruct 1046 | ); 1047 | export type PromptReference = Schema.Schema.Type; 1048 | 1049 | /** 1050 | * A request from the client to the server, to ask for completion options. 1051 | */ 1052 | export const CompleteRequest = Schema.Struct({ 1053 | ...Request.fields, 1054 | method: Schema.Literal("completion/complete"), 1055 | params: Schema.Struct({ 1056 | ...BaseRequestParams.fields, 1057 | ref: Schema.Union(PromptReference, ResourceReference), 1058 | /** 1059 | * The argument's information 1060 | */ 1061 | argument: Schema.Struct( 1062 | Schema.Struct({ 1063 | /** 1064 | * The name of the argument 1065 | */ 1066 | name: Schema.String, 1067 | /** 1068 | * The value of the argument to use for completion matching. 1069 | */ 1070 | value: Schema.String, 1071 | }).fields, 1072 | UnknownStruct 1073 | ), 1074 | }), 1075 | }); 1076 | export type CompleteRequest = Schema.Schema.Type; 1077 | 1078 | /** 1079 | * The server's response to a completion/complete request 1080 | */ 1081 | export const CompleteResult = Schema.Struct({ 1082 | ...Result.fields, 1083 | completion: Schema.Struct( 1084 | Schema.Struct({ 1085 | /** 1086 | * An array of completion values. Must not exceed 100 items. 1087 | */ 1088 | values: Schema.Array(Schema.String).pipe(Schema.maxItems(100)), 1089 | /** 1090 | * The total number of completion options available. This can exceed the number of values actually sent in the response. 1091 | */ 1092 | total: Schema.optional(Schema.Number.pipe(Schema.int())), 1093 | /** 1094 | * Indicates whether there are additional completion options beyond those provided in the current response, even if the exact total is unknown. 1095 | */ 1096 | hasMore: Schema.optional(Schema.Boolean), 1097 | }).fields, 1098 | UnknownStruct 1099 | ), 1100 | }); 1101 | export type CompleteResult = Schema.Schema.Type; 1102 | 1103 | /* Roots */ 1104 | /** 1105 | * Represents a root directory or file that the server can operate on. 1106 | */ 1107 | export const Root = Schema.Struct( 1108 | Schema.Struct({ 1109 | /** 1110 | * The URI identifying the root. This *must* start with file:// for now. 1111 | */ 1112 | uri: Schema.String.pipe(Schema.startsWith("file://")), 1113 | /** 1114 | * An optional name for the root. 1115 | */ 1116 | name: Schema.optional(Schema.String), 1117 | }).fields, 1118 | UnknownStruct 1119 | ); 1120 | 1121 | /** 1122 | * Sent from the server to request a list of root URIs from the client. 1123 | */ 1124 | export const ListRootsRequest = Schema.Struct({ 1125 | ...Request.fields, 1126 | method: Schema.Literal("roots/list"), 1127 | }); 1128 | export type ListRootsRequest = Schema.Schema.Type; 1129 | 1130 | /** 1131 | * The client's response to a roots/list request from the server. 1132 | */ 1133 | export const ListRootsResult = Schema.Struct({ 1134 | ...Result.fields, 1135 | roots: Schema.Array(Root), 1136 | }); 1137 | export type ListRootsResult = Schema.Schema.Type; 1138 | 1139 | /** 1140 | * A notification from the client to the server, informing it that the list of roots has changed. 1141 | */ 1142 | export const RootsListChangedNotification = Schema.Struct({ 1143 | ...Notification.fields, 1144 | method: Schema.Literal("notifications/roots/list_changed"), 1145 | }); 1146 | export type RootsListChangedNotification = Schema.Schema.Type< 1147 | typeof RootsListChangedNotification 1148 | >; 1149 | 1150 | /* Client messages */ 1151 | export const ClientRequest = Schema.Union( 1152 | PingRequest, 1153 | InitializeRequest, 1154 | CompleteRequest, 1155 | SetLevelRequest, 1156 | GetPromptRequest, 1157 | ListPromptsRequest, 1158 | ListResourcesRequest, 1159 | ListResourceTemplatesRequest, 1160 | ReadResourceRequest, 1161 | SubscribeRequest, 1162 | UnsubscribeRequest, 1163 | CallToolRequest, 1164 | ListToolsRequest 1165 | ); 1166 | export type ClientRequest = Schema.Schema.Type; 1167 | 1168 | export const ClientNotification = Schema.Union( 1169 | CancelledNotification, 1170 | ProgressNotification, 1171 | InitializedNotification, 1172 | RootsListChangedNotification 1173 | ); 1174 | export type ClientNotification = Schema.Schema.Type; 1175 | 1176 | export const ClientResult = Schema.Union( 1177 | EmptyResult, 1178 | CreateMessageResult, 1179 | ListRootsResult 1180 | ); 1181 | export type ClientResult = Schema.Schema.Type; 1182 | 1183 | /* Server messages */ 1184 | export const ServerRequest = Schema.Union( 1185 | PingRequest, 1186 | CreateMessageRequest, 1187 | ListRootsRequest 1188 | ); 1189 | export type ServerRequest = Schema.Schema.Type; 1190 | 1191 | export const ServerNotification = Schema.Union( 1192 | CancelledNotification, 1193 | ProgressNotification, 1194 | LoggingMessageNotification, 1195 | ResourceUpdatedNotification, 1196 | ResourceListChangedNotification, 1197 | ToolListChangedNotification, 1198 | PromptListChangedNotification 1199 | ); 1200 | export type ServerNotification = Schema.Schema.Type; 1201 | 1202 | export const ServerResult = Schema.Union( 1203 | EmptyResult, 1204 | InitializeResult, 1205 | CompleteResult, 1206 | GetPromptResult, 1207 | ListPromptsResult, 1208 | ListResourcesResult, 1209 | ListResourceTemplatesResult, 1210 | ReadResourceResult, 1211 | CallToolResult, 1212 | ListToolsResult 1213 | ); 1214 | export type ServerResult = Schema.Schema.Type; 1215 | -------------------------------------------------------------------------------- /packages/shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { "extends": "../../tsconfig.base.json" } -------------------------------------------------------------------------------- /packages/shared/tsup.config.ts: -------------------------------------------------------------------------------- 1 | // tsup.config.ts 2 | import { defineConfig } from "tsup"; 3 | 4 | export default defineConfig({ 5 | format: ["esm", "cjs"], 6 | entry: ["src/**/*.ts"], 7 | clean: true, 8 | target: "esnext", 9 | treeshake: false, 10 | keepNames: true, 11 | tsconfig: "tsconfig.json", 12 | external: ["effect"], 13 | }); 14 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | - 'examples/*' -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Base Options: */ 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "target": "es2022", 9 | "allowJs": true, 10 | "resolveJsonModule": true, 11 | "moduleDetection": "force", 12 | "isolatedModules": true, 13 | "verbatimModuleSyntax": true, 14 | 15 | /* Strictness */ 16 | "strict": true, 17 | "noUncheckedIndexedAccess": true, 18 | "noImplicitOverride": true, 19 | 20 | /* If transpiling with TypeScript: */ 21 | "module": "NodeNext", 22 | "sourceMap": true, 23 | "emitDeclarationOnly": true, 24 | 25 | /* AND if you're building for a library: */ 26 | "declaration": true, 27 | 28 | /* AND if you're building for a library in a monorepo: */ 29 | "composite": true, 30 | "incremental": true, 31 | "declarationMap": true, 32 | 33 | /* If your code doesn't run in the DOM: */ 34 | "lib": ["es2022"], 35 | 36 | "rootDir": "${configDir}/src", 37 | "outDir": "${configDir}/dist", 38 | "customConditions": ["@effect-mcp/dev"], 39 | "paths": { 40 | "@effect-mcp/shared": ["./packages/shared/src/index.ts"], 41 | "@effect-mcp/shared/*": ["./packages/shared/src/*"] 42 | } 43 | }, 44 | "include": ["${configDir}/src"] 45 | } 46 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json" 3 | } 4 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "tasks": { 4 | "build": { 5 | "outputs": ["dist/**"], 6 | "dependsOn": ["^build"] 7 | }, 8 | "dev": { 9 | "persistent": true, 10 | "cache": false 11 | } 12 | } 13 | } 14 | --------------------------------------------------------------------------------