mcp
Introduction
Section titled “Introduction”The McpServer.ts module provides an implementation of an MCP (Model Context Protocol) server using the
Effect eco system.
Getting Started
Section titled “Getting Started”It’s important to understand the architecture of the Effect MCP server. Here is an example of a MCP server implementation:
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 toolconst 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 clientPrompt, which defines a prompt template that can be used by the client and should not be confused withPrompt.tsToolkitLayer, which contains the definitions of all tools the server exposes, provided with their implementations throughToolImplLayer.
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
Section titled “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.
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
Section titled “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.
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
Section titled “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.
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
Section titled “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.
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
Section titled “Complete Working Example”Here’s a complete, copy/pastable MCP server example that combines all the concepts:
import { NodeRuntime, NodeStdio } from "@effect/platform-node"import { Effect, Layer, Logger, Schema } from "effect"import { McpSchema, McpServer, Tool, Toolkit } from "effect/unstable/ai"
// Define toolsconst 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 toolkitconst MyToolkit = Toolkit.make(GreetTool, CalculatorTool)
// Define a resourceconst 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 resourceconst 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 promptconst 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 layerconst 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 serverLayer.launch(ServerLayer).pipe(NodeRuntime.runMain)