## Introduction The `McpServer.ts` module provides an implementation of an [MCP (Model Context Protocol)](https://modelcontextprotocol.io/docs/getting-started/intro) server using the [Effect](https://effect.website) eco system. ## Getting Started It's important to understand the architecture of the Effect MCP server. Here is an example of a MCP server implementation: ```typescript import { NodeRuntime, NodeSink, NodeStream } from "@effect/platform-node" import { Effect, Layer, Logger } from "effect" import { Schema } from "effect/schema" import { McpServer, Tool, Toolkit } from "effect/unstable/ai" // Define a simple tool const DemoTool = Tool.make("DemoTool", { description: "A demo tool that echoes back the input", parameters: { message: Schema.String }, success: Schema.String }) const MyToolkit = Toolkit.make(DemoTool) const DemoResource = McpServer.resource({ uri: "file:///demo.txt", name: "Demo Resource", content: Effect.succeed("# Demo Content\nThis is a demo resource.") }) const DemoPrompt = McpServer.prompt({ name: "Demo Prompt", description: "A demo prompt", parameters: { topic: Schema.String }, completion: { topic: () => Effect.succeed(["AI", "programming", "Effect"]) }, content: ({ topic }) => Effect.succeed(`Tell me about ${topic}`) }) const ServerLayer = Layer.mergeAll( DemoResource, DemoPrompt, McpServer.toolkit(MyToolkit).pipe( Layer.provideMerge( MyToolkit.toLayer({ DemoTool: ({ message }) => Effect.succeed(`Echo: ${message}`) }) ) ) ).pipe( Layer.provide( McpServer.layerStdio({ name: "Demo MCP Server", version: "1.0.0", stdin: NodeStream.stdin, stdout: NodeSink.stdout }) ), Layer.provide(Logger.layer([Logger.consolePretty({ stderr: true })])) ) Layer.launch(ServerLayer).pipe(NodeRuntime.runMain) ``` The server exposes three main parts: - **`Resource`**, which represents a readable MCP resource such as a file accessible to the client - **`Prompt`**, which defines a prompt template that can be used by the client and should not be confused with `Prompt.ts` - **`ToolkitLayer`**, which contains the definitions of all tools the server exposes, provided with their implementations through `ToolImplLayer`. The part layers are merged into one layer that has a MCP server implementation as dependency. `McpServer.layerStdio` is used to create a standard I/O–based MCP server identified by its name and version. Because of the layer architecture the server implementation can be easily exchanged with an HTTP based implementation with `McpServer.layerHttp`. Finally, a logging layer is added with `Logger.layer([Logger.consolePretty({ stderr: true })])`, ensuring logs are written to `stderr`. This is essential when using stdio, as any output to `stdout` would interfere with the protocol communication. ## Resources Resources in the MCP server represent files or data that can be accessed by an MCP client. Each resource is defined as a template that specifies its location, behavior, and metadata. The `McpServer.resource` helper allows you to declaratively define such resources with dynamic parameters, completions, and content generation. ```typescript import { Effect } from "effect" import { Schema } from "effect/schema" import { McpSchema, McpServer } from "effect/unstable/ai" const SimpleResource = McpServer.resource({ uri: "file:///demo.txt", name: "Demo Resource", description: "A simple demo resource", mimeType: "text/plain", content: Effect.succeed("This is demo content") }) const idParam = McpSchema.param("id", Schema.NumberFromString) const TemplateResource = McpServer.resource`file://path/to/file/${idParam}`({ name: "Demo Resource Template", description: "A parameterized resource template", completion: { id: (_: string) => Effect.succeed([1, 2, 3, 4, 5]) }, content: Effect.fn(function*(_uri, id) { return `# MCP Server Demo - ID: ${id}` }), mimeType: "text/x-markdown", audience: ["assistant", "user"] }) ``` In this example, the resource is parameterized by an `id` that forms part of the URI. The `completion` function enables clients to request valid parameter values dynamically. The `content` function defines how the resource's data is generated at runtime—in this case, returning a Markdown string containing the provided `id`. The `mimeType` specifies the format of the resource, while the `audience` property determines who can access it (either `"assistant"` and/or `"user"`). ## Prompts Prompts define reusable templates that an MCP client can invoke with parameters. They serve as structured, parameterized instructions or messages that the client can send to the server. Using `McpServer.prompt`, you can describe the prompt's schema, auto-completion behavior, and content generation logic in a declarative way. ```typescript import { Effect } from "effect" import { Schema } from "effect/schema" import { McpServer } from "effect/unstable/ai" const DemoPrompt = McpServer.prompt({ name: "Demo Prompt", description: "A demo prompt to demonstrate MCP server capabilities", parameters: { name: Schema.String }, completion: { name: () => Effect.succeed(["Tom", "Tim", "Jerry"]) }, content: ({ name }) => Effect.succeed(`Use the greetings tool to write a greeting for ${name}.`) }) ``` In this example, the prompt defines a single parameter, `name`. The `completion` property provides an auto-completion mechanism, allowing the client to suggest or autofill common names. The `content` function then generates the actual prompt text dynamically based on the provided parameter. ## Tools and Toolkit Tools define executable capabilities that the MCP server exposes to clients. Each tool describes a contract while the actual logic is provided separately through an implementation layer. Tools are grouped into toolkits, which can be combined and converted into layers. ```typescript import { Effect, Layer } from "effect" import { Schema } from "effect/schema" import { McpServer, Tool, Toolkit } from "effect/unstable/ai" const DemoTool = Tool.make("DemoTool", { description: "This is a demo tool for the documentation", parameters: { demoId: Schema.Number, demoName: Schema.String }, success: Schema.String }) const OtherDemoTool = Tool.make("OtherDemoTool", { description: "Another demo tool", parameters: { value: Schema.Number }, success: Schema.String }) const MyToolkit = Toolkit.make(DemoTool, OtherDemoTool) const ToolkitLayer = McpServer.toolkit(MyToolkit).pipe( Layer.provideMerge( MyToolkit.toLayer({ DemoTool: ({ demoId, demoName }) => Effect.succeed(`Processed ${demoName} with ID ${demoId}`), OtherDemoTool: ({ value }) => Effect.succeed(`Other tool result: ${value * 2}`) }) ) ) ``` In this example, `Tool.make` defines new tools with typed parameters and result schemas for success outcomes. Multiple tools can be grouped into a single `Toolkit` using `Toolkit.make`. The toolkit is then transformed into a layer defining the interface of the tools using `McpServer.toolkit()`. The corresponding implementations are attached using `.toLayer`, which binds each tool definition to its concrete logic. Finally, the completed toolkit layer can be merged with other layers to create the MCP server. ## Elicitation requests Elicitation requests are used to request additional input directly from the user. An elicitation defines both the message shown to the user and the expected response schema, ensuring structured and validated user input. ```typescript import { Effect } from "effect" import { Schema } from "effect/schema" import { McpServer } from "effect/unstable/ai" const DemoElicitation = McpServer.elicit({ message: `Please answer the question ("yes" | "no") (default "no"):`, schema: Schema.Struct({ answer: Schema.Union([Schema.Literal("yes"), Schema.Literal("no")]) }) }).pipe( Effect.catchTag("ElicitationDeclined", (_error) => { return Effect.succeed({ answer: "no" }) }) ) ``` In this example, the server poses a simple yes/no question to the user. The input is validated against the defined schema, ensuring that only `"yes"` or `"no"` responses are accepted. If the user declines to answer or the elicitation fails, a fallback value is provided—here, the default answer is `"no"`. ## Complete Working Example Here's a complete, copy/pastable MCP server example that combines all the concepts: ```typescript import { NodeRuntime, NodeStdio } from "@effect/platform-node" import { Effect, Layer, Logger, Schema } from "effect" import { McpSchema, McpServer, Tool, Toolkit } from "effect/unstable/ai" // Define tools const GreetTool = Tool.make("GreetTool", { description: "Generate a greeting message", parameters: Schema.Struct({ name: Schema.String, style: Schema.Union([Schema.Literal("formal"), Schema.Literal("casual")]) }), success: Schema.String }) const CalculatorTool = Tool.make("CalculatorTool", { description: "Perform basic arithmetic operations", parameters: Schema.Struct({ operation: Schema.Union([ Schema.Literal("add"), Schema.Literal("subtract"), Schema.Literal("multiply"), Schema.Literal("divide") ]), a: Schema.Number, b: Schema.Number }), success: Schema.Number }) // Create toolkit const MyToolkit = Toolkit.make(GreetTool, CalculatorTool) // Define a resource const ReadmeResource = McpServer.resource({ uri: "file:///README.md", name: "README", description: "Project README file", mimeType: "text/markdown", content: Effect.succeed("# MCP Server Demo\n\nThis is a demo MCP server built with Effect.") }) // Define a parameterized resource const idParam = McpSchema.param("id", Schema.NumberFromString) const UserResource = McpServer.resource`file://users/${idParam}.json`({ name: "User Data", description: "User information by ID", completion: { id: (_: string) => Effect.succeed([1, 2, 3, 4, 5]) }, content: Effect.fn(function*(_uri, id) { return JSON.stringify( { id, name: `User ${id}`, email: `user${id}@example.com` }, null, 2 ) }), mimeType: "application/json" }) // Define a prompt const AnalysisPrompt = McpServer.prompt({ name: "Analyze Data", description: "Analyze data and provide insights", parameters: { dataType: Schema.String, focus: Schema.Union([Schema.Literal("summary"), Schema.Literal("details")]) }, completion: { dataType: () => Effect.succeed(["sales", "users", "metrics"]), focus: () => Effect.succeed(["summary" as const, "details" as const]) }, content: ({ dataType, focus }) => Effect.succeed( `Please analyze the ${dataType} data and provide a ${focus} analysis. Use available tools to gather information.` ) }) // Create the server layer const ServerLayer = Layer.mergeAll( ReadmeResource, UserResource, AnalysisPrompt, McpServer.toolkit(MyToolkit).pipe( Layer.provideMerge( MyToolkit.toLayer({ GreetTool: ({ name, style }) => { const greeting = style === "formal" ? `Good day, ${name}. It is a pleasure to meet you.` : `Hey ${name}! What's up?` return Effect.succeed(greeting) }, CalculatorTool: ({ operation, a, b }) => { let result: number switch (operation) { case "add": result = a + b break case "subtract": result = a - b break case "multiply": result = a * b break case "divide": result = a / b break } return Effect.succeed(result) } }) ) ) ).pipe( Layer.provide( McpServer.layerStdio({ name: "Demo MCP Server", version: "1.0.0" }) ), Layer.provide(NodeStdio.layer), Layer.provide(Layer.succeed(Logger.LogToStderr)(true)) ) // Run the server Layer.launch(ServerLayer).pipe(NodeRuntime.runMain) ```