| 1 | ## Introduction |
| 2 | |
| 3 | The `McpServer.ts` module provides an implementation of an [MCP (Model Context Protocol)](https://modelcontextprotocol.io/docs/getting-started/intro) server using the |
| 4 | [Effect](https://effect.website) eco system. |
| 5 | |
| 6 | ## Getting Started |
| 7 | |
| 8 | It's important to understand the architecture of the Effect MCP server. |
| 9 | Here is an example of a MCP server implementation: |
| 10 | |
| 11 | ```typescript |
| 12 | import { NodeRuntime, NodeStdio } from "@effect/platform-node" |
| 13 | import { Effect, Layer, Logger, Schema } from "effect" |
| 14 | import { McpProtocol, McpServer, Tool, Toolkit } from "effect/ai" |
| 15 | |
| 16 | // Define a simple tool |
| 17 | const DemoTool = Tool.make("DemoTool", { |
| 18 | description: "A demo tool that echoes back the input", |
| 19 | parameters: Schema.Struct({ |
| 20 | message: Schema.String |
| 21 | }), |
| 22 | success: Schema.String |
| 23 | }) |
| 24 | |
| 25 | const MyToolkit = Toolkit.make(DemoTool) |
| 26 | |
| 27 | const DemoResource = McpServer.resource({ |
| 28 | uri: "file:///demo.txt", |
| 29 | name: "Demo Resource", |
| 30 | content: Effect.succeed("# Demo Content\nThis is a demo resource.") |
| 31 | }) |
| 32 | |
| 33 | const DemoPrompt = McpServer.prompt({ |
| 34 | name: "Demo Prompt", |
| 35 | description: "A demo prompt", |
| 36 | parameters: { |
| 37 | topic: Schema.String |
| 38 | }, |
| 39 | completion: { |
| 40 | topic: () => Effect.succeed(["AI", "programming", "Effect"]) |
| 41 | }, |
| 42 | content: ({ topic }) => Effect.succeed(`Tell me about ${topic}`) |
| 43 | }) |
| 44 | |
| 45 | const ServerLayer = Layer.mergeAll( |
| 46 | DemoResource, |
| 47 | DemoPrompt, |
| 48 | McpServer.toolkit(MyToolkit).pipe( |
| 49 | Layer.provideMerge( |
| 50 | MyToolkit.toLayer({ |
| 51 | DemoTool: ({ message }) => Effect.succeed(`Echo: ${message}`) |
| 52 | }) |
| 53 | ) |
| 54 | ) |
| 55 | ).pipe( |
| 56 | Layer.provide( |
| 57 | McpServer.layerStdio({ |
| 58 | name: "Demo MCP Server", |
| 59 | version: "1.0.0", |
| 60 | protocols: [McpProtocol.v2025_06_18] |
| 61 | }) |
| 62 | ), |
| 63 | Layer.provide(NodeStdio.layer), |
| 64 | Layer.provide(Logger.layer([Logger.consolePretty()])), |
| 65 | Layer.provideMerge(Layer.succeed(Logger.LogToStderr, true)) |
| 66 | ) |
| 67 | |
| 68 | Layer.launch(ServerLayer).pipe(NodeRuntime.runMain) |
| 69 | ``` |
| 70 | |
| 71 | The server exposes three main parts: |
| 72 | |
| 73 | - **`Resource`**, which represents a readable MCP resource such as a file accessible to the client |
| 74 | - **`Prompt`**, which defines a prompt template that can be used by the client and should not be |
| 75 | confused with `Prompt.ts` |
| 76 | - **`ToolkitLayer`**, which contains the definitions of all tools the server exposes, provided with |
| 77 | their implementations through `ToolImplLayer`. |
| 78 | |
| 79 | The part layers are merged into one layer that has a MCP server implementation as dependency. |
| 80 | `McpServer.layerStdio` is used to create a standard I/O–based MCP server identified by its name and |
| 81 | version. Its ordered, non-empty `protocols` declaration names implemented protocol adapters rather |
| 82 | than arbitrary version strings. This release supports `McpProtocol.v2024_11_05`, |
| 83 | `McpProtocol.v2025_03_26`, and `McpProtocol.v2025_06_18`. The `v2024_11_05` adapter implements that |
| 84 | revision's RPC schemas and stdio framing, including its batch policy. It does not implement the |
| 85 | historical two-endpoint HTTP+SSE transport. `McpServer.layerHttp` instead offers the 2024 RPC schema |
| 86 | through the same single-endpoint HTTP compatibility transport used by the 2025 adapters. Because of |
| 87 | the layer architecture the server implementation can be easily exchanged with this HTTP-based |
| 88 | implementation. Finally, a logging layer is added with |
| 89 | `Logger.layer([Logger.consolePretty({ stderr: true })])`, ensuring logs are written to `stderr`. |
| 90 | This is essential when using stdio, as any output to `stdout` would interfere with the protocol |
| 91 | communication. |
| 92 | |
| 93 | ## Resources |
| 94 | |
| 95 | Resources in the MCP server represent files or data that can be accessed by an MCP client. Each |
| 96 | resource is defined as a template that specifies its location, behavior, and metadata. The |
| 97 | `McpServer.resource` helper allows you to declaratively define such resources with dynamic |
| 98 | parameters, completions, and content generation. |
| 99 | |
| 100 | ```typescript |
| 101 | import { Effect, Schema } from "effect" |
| 102 | import { McpSchema, McpServer } from "effect/ai" |
| 103 | |
| 104 | const SimpleResource = McpServer.resource({ |
| 105 | uri: "file:///demo.txt", |
| 106 | name: "Demo Resource", |
| 107 | description: "A simple demo resource", |
| 108 | mimeType: "text/plain", |
| 109 | content: Effect.succeed("This is demo content") |
| 110 | }) |
| 111 | |
| 112 | const idParam = McpSchema.param("id", Schema.NumberFromString) |
| 113 | |
| 114 | const TemplateResource = McpServer.resource`file://path/to/file/${idParam}`({ |
| 115 | name: "Demo Resource Template", |
| 116 | description: "A parameterized resource template", |
| 117 | completion: { |
| 118 | id: (_: string) => Effect.succeed([1, 2, 3, 4, 5]) |
| 119 | }, |
| 120 | content: Effect.fn(function*(_uri, id) { |
| 121 | return `# MCP Server Demo - ID: ${id}` |
| 122 | }), |
| 123 | mimeType: "text/x-markdown", |
| 124 | audience: ["assistant", "user"] |
| 125 | }) |
| 126 | ``` |
| 127 | |
| 128 | In this example, the resource is parameterized by an `id` that forms part of the URI. The |
| 129 | `completion` function enables clients to request valid parameter values dynamically. The `content` |
| 130 | function defines how the resource's data is generated at runtime—in this case, returning a Markdown |
| 131 | string containing the provided `id`. The `mimeType` specifies the format of the resource, while the |
| 132 | `audience` property determines who can access it (either `"assistant"` and/or `"user"`). |
| 133 | |
| 134 | ## Prompts |
| 135 | |
| 136 | Prompts define reusable templates that an MCP client can invoke with parameters. They serve as |
| 137 | structured, parameterized instructions or messages that the client can send to the server. Using |
| 138 | `McpServer.prompt`, you can describe the prompt's schema, auto-completion behavior, and content |
| 139 | generation logic in a declarative way. |
| 140 | |
| 141 | ```typescript |
| 142 | import { Effect, Schema } from "effect" |
| 143 | import { McpServer } from "effect/ai" |
| 144 | |
| 145 | const DemoPrompt = McpServer.prompt({ |
| 146 | name: "Demo Prompt", |
| 147 | description: "A demo prompt to demonstrate MCP server capabilities", |
| 148 | parameters: { |
| 149 | name: Schema.String |
| 150 | }, |
| 151 | completion: { |
| 152 | name: () => Effect.succeed(["Tom", "Tim", "Jerry"]) |
| 153 | }, |
| 154 | content: ({ name }) => Effect.succeed(`Use the greetings tool to write a greeting for ${name}.`) |
| 155 | }) |
| 156 | ``` |
| 157 | |
| 158 | In this example, the prompt defines a single parameter, `name`. The `completion` property provides |
| 159 | an auto-completion mechanism, allowing the client to suggest or autofill common names. The `content` |
| 160 | function then generates the actual prompt text dynamically based on the provided parameter. |
| 161 | |
| 162 | ## Tools and Toolkit |
| 163 | |
| 164 | Tools define executable capabilities that the MCP server exposes to clients. Each tool describes a |
| 165 | contract while the actual logic is provided separately through an implementation layer. Tools are |
| 166 | grouped into toolkits, which can be combined and converted into layers. |
| 167 | |
| 168 | ```typescript |
| 169 | import { Effect, Layer, Schema } from "effect" |
| 170 | import { McpServer, Tool, Toolkit } from "effect/ai" |
| 171 | |
| 172 | const DemoTool = Tool.make("DemoTool", { |
| 173 | description: "This is a demo tool for the documentation", |
| 174 | parameters: Schema.Struct({ |
| 175 | demoId: Schema.Number, |
| 176 | demoName: Schema.String |
| 177 | }), |
| 178 | success: Schema.String |
| 179 | }) |
| 180 | |
| 181 | const OtherDemoTool = Tool.make("OtherDemoTool", { |
| 182 | description: "Another demo tool", |
| 183 | parameters: Schema.Struct({ |
| 184 | value: Schema.Number |
| 185 | }), |
| 186 | success: Schema.String |
| 187 | }) |
| 188 | |
| 189 | const MyToolkit = Toolkit.make(DemoTool, OtherDemoTool) |
| 190 | |
| 191 | const ToolkitLayer = McpServer.toolkit(MyToolkit).pipe( |
| 192 | Layer.provideMerge( |
| 193 | MyToolkit.toLayer({ |
| 194 | DemoTool: ({ demoId, demoName }) => Effect.succeed(`Processed ${demoName} with ID ${demoId}`), |
| 195 | OtherDemoTool: ({ value }) => Effect.succeed(`Other tool result: ${value * 2}`) |
| 196 | }) |
| 197 | ) |
| 198 | ) |
| 199 | ``` |
| 200 | |
| 201 | In this example, `Tool.make` defines new tools with typed parameters and result schemas for success |
| 202 | outcomes. Multiple tools can be grouped into a single `Toolkit` using `Toolkit.make`. |
| 203 | |
| 204 | The toolkit is then transformed into a layer defining the interface of the tools using |
| 205 | `McpServer.toolkit()`. The corresponding implementations are attached using `.toLayer`, which binds |
| 206 | each tool definition to its concrete logic. Finally, the completed toolkit layer can be merged with |
| 207 | other layers to create the MCP server. |
| 208 | |
| 209 | ## Elicitation requests |
| 210 | |
| 211 | Elicitation requests are used to request additional input directly from the user. An elicitation |
| 212 | defines both the message shown to the user and the expected response schema, ensuring structured and |
| 213 | validated user input. |
| 214 | |
| 215 | ```typescript |
| 216 | import { Effect, Schema } from "effect" |
| 217 | import { McpServer } from "effect/ai" |
| 218 | |
| 219 | const DemoElicitation = McpServer.elicit({ |
| 220 | message: `Please answer the question ("yes" | "no") (default "no"):`, |
| 221 | schema: Schema.Struct({ |
| 222 | answer: Schema.Union([Schema.Literal("yes"), Schema.Literal("no")]) |
| 223 | }) |
| 224 | }).pipe( |
| 225 | Effect.catchTag("ElicitationDeclined", (_error) => { |
| 226 | return Effect.succeed({ answer: "no" }) |
| 227 | }) |
| 228 | ) |
| 229 | ``` |
| 230 | |
| 231 | In this example, the server poses a simple yes/no question to the user. The input is validated |
| 232 | against the defined schema, ensuring that only `"yes"` or `"no"` responses are accepted. If the user |
| 233 | declines to answer or the elicitation fails, a fallback value is provided—here, the default answer |
| 234 | is `"no"`. |
| 235 | |
| 236 | ## Complete Working Example |
| 237 | |
| 238 | Here's a complete, copy/pastable MCP server example that combines all the concepts: |
| 239 | |
| 240 | ```typescript |
| 241 | import { NodeRuntime, NodeStdio } from "@effect/platform-node" |
| 242 | import { Effect, Layer, Logger, Schema } from "effect" |
| 243 | import { McpProtocol, McpSchema, McpServer, Tool, Toolkit } from "effect/ai" |
| 244 | |
| 245 | // Define tools |
| 246 | const GreetTool = Tool.make("GreetTool", { |
| 247 | description: "Generate a greeting message", |
| 248 | parameters: Schema.Struct({ |
| 249 | name: Schema.String, |
| 250 | style: Schema.Union([Schema.Literal("formal"), Schema.Literal("casual")]) |
| 251 | }), |
| 252 | success: Schema.String |
| 253 | }) |
| 254 | |
| 255 | const CalculatorTool = Tool.make("CalculatorTool", { |
| 256 | description: "Perform basic arithmetic operations", |
| 257 | parameters: Schema.Struct({ |
| 258 | operation: Schema.Union([ |
| 259 | Schema.Literal("add"), |
| 260 | Schema.Literal("subtract"), |
| 261 | Schema.Literal("multiply"), |
| 262 | Schema.Literal("divide") |
| 263 | ]), |
| 264 | a: Schema.Number, |
| 265 | b: Schema.Number |
| 266 | }), |
| 267 | success: Schema.Number |
| 268 | }) |
| 269 | |
| 270 | // Create toolkit |
| 271 | const MyToolkit = Toolkit.make(GreetTool, CalculatorTool) |
| 272 | |
| 273 | // Define a resource |
| 274 | const ReadmeResource = McpServer.resource({ |
| 275 | uri: "file:///README.md", |
| 276 | name: "README", |
| 277 | description: "Project README file", |
| 278 | mimeType: "text/markdown", |
| 279 | content: Effect.succeed("# MCP Server Demo\n\nThis is a demo MCP server built with Effect.") |
| 280 | }) |
| 281 | |
| 282 | // Define a parameterized resource |
| 283 | const idParam = McpSchema.param("id", Schema.NumberFromString) |
| 284 | |
| 285 | const UserResource = McpServer.resource`file://users/${idParam}.json`({ |
| 286 | name: "User Data", |
| 287 | description: "User information by ID", |
| 288 | completion: { |
| 289 | id: (_: string) => Effect.succeed([1, 2, 3, 4, 5]) |
| 290 | }, |
| 291 | content: Effect.fn(function*(_uri, id) { |
| 292 | return JSON.stringify( |
| 293 | { |
| 294 | id, |
| 295 | name: `User ${id}`, |
| 296 | email: `user${id}@example.com` |
| 297 | }, |
| 298 | null, |
| 299 | 2 |
| 300 | ) |
| 301 | }), |
| 302 | mimeType: "application/json" |
| 303 | }) |
| 304 | |
| 305 | // Define a prompt |
| 306 | const AnalysisPrompt = McpServer.prompt({ |
| 307 | name: "Analyze Data", |
| 308 | description: "Analyze data and provide insights", |
| 309 | parameters: { |
| 310 | dataType: Schema.String, |
| 311 | focus: Schema.Union([Schema.Literal("summary"), Schema.Literal("details")]) |
| 312 | }, |
| 313 | completion: { |
| 314 | dataType: () => Effect.succeed(["sales", "users", "metrics"]), |
| 315 | focus: () => Effect.succeed(["summary" as const, "details" as const]) |
| 316 | }, |
| 317 | content: ({ dataType, focus }) => |
| 318 | Effect.succeed( |
| 319 | `Please analyze the ${dataType} data and provide a ${focus} analysis. Use available tools to gather information.` |
| 320 | ) |
| 321 | }) |
| 322 | |
| 323 | // Create the server layer |
| 324 | const ServerLayer = Layer.mergeAll( |
| 325 | ReadmeResource, |
| 326 | UserResource, |
| 327 | AnalysisPrompt, |
| 328 | McpServer.toolkit(MyToolkit).pipe( |
| 329 | Layer.provideMerge( |
| 330 | MyToolkit.toLayer({ |
| 331 | GreetTool: ({ name, style }) => { |
| 332 | const greeting = style === "formal" |
| 333 | ? `Good day, ${name}. It is a pleasure to meet you.` |
| 334 | : `Hey ${name}! What's up?` |
| 335 | return Effect.succeed(greeting) |
| 336 | }, |
| 337 | CalculatorTool: ({ operation, a, b }) => { |
| 338 | let result: number |
| 339 | switch (operation) { |
| 340 | case "add": |
| 341 | result = a + b |
| 342 | break |
| 343 | case "subtract": |
| 344 | result = a - b |
| 345 | break |
| 346 | case "multiply": |
| 347 | result = a * b |
| 348 | break |
| 349 | case "divide": |
| 350 | result = a / b |
| 351 | break |
| 352 | } |
| 353 | return Effect.succeed(result) |
| 354 | } |
| 355 | }) |
| 356 | ) |
| 357 | ) |
| 358 | ).pipe( |
| 359 | Layer.provide( |
| 360 | McpServer.layerStdio({ |
| 361 | name: "Demo MCP Server", |
| 362 | version: "1.0.0", |
| 363 | protocols: [McpProtocol.v2025_06_18] |
| 364 | }) |
| 365 | ), |
| 366 | Layer.provide(NodeStdio.layer), |
| 367 | Layer.provideMerge(Layer.succeed(Logger.LogToStderr, true)) |
| 368 | ) |
| 369 | |
| 370 | // Run the server |
| 371 | Layer.launch(ServerLayer).pipe(NodeRuntime.runMain) |
| 372 | ``` |