MCP Server
The Model Context Protocol (MCP) is a JSON-RPC protocol that lets an MCP
client — Claude Desktop, an IDE assistant, or any other host — discover and use
capabilities you expose: tools the model can call, resources it can read,
prompts it can fill in, and completions for argument values. Effect’s
McpServer module turns your toolkits, resources, and prompts into a fully
wired MCP server that speaks the protocol over stdio or HTTP.
The two modules involved:
McpServer— the service and layers that run a server and the registration helpers (registerToolkit,resource,prompt,elicit, …).McpSchema— the protocol vocabulary: every request, notification, result, and error modeled as an EffectSchema/Rpc. You rarely touch most of it directly, but it is there when you need to construct or match a protocol value.
Both live under effect/unstable/ai.
import { McpServer, McpSchema } from "effect/unstable/ai"A complete stdio server
Section titled “A complete stdio server”Here is a full server that registers a toolkit, a resource template, and a prompt, then runs as a stdio entrypoint. This is the shape of a real MCP server you would point Claude Desktop at.
import { Effect, Layer, Logger, Schema } from "effect"import { McpServer, Tool, Toolkit } from "effect/unstable/ai"import { NodeRuntime, NodeStdio } from "@effect/platform-node"
// 1. A tool the model can call. Tools come from the AI `Tool`/`Toolkit` API.const GetWeather = Tool.make("GetWeather", { description: "Get the current weather for a city", parameters: Schema.Struct({ city: Schema.String }), success: Schema.Struct({ tempC: Schema.Number, summary: Schema.String })})
const WeatherToolkit = Toolkit.make(GetWeather)
const ToolkitLayer = McpServer.toolkit(WeatherToolkit).pipe( Layer.provide( WeatherToolkit.toLayer({ GetWeather: ({ city }) => Effect.succeed({ tempC: city === "London" ? 12 : 24, summary: "Clear" }) }) ))
// 2. A resource template. `McpSchema.param` names a URL parameter and gives it// a schema used to decode the value out of the matched URI.const idParam = McpSchema.param("id", Schema.NumberFromString)
const ReadmeResource = McpServer.resource`file://readme/${idParam}`({ name: "Project README", description: "Returns a generated README for the given project id", // Optional per-parameter completion: suggest candidate ids to the client. completion: { id: () => Effect.succeed([1, 2, 3]) }, // Returning a string becomes a single text resource at the requested uri. content: (_uri, id) => Effect.succeed(`# Project ${id}\n\nGenerated docs.`)})
// 3. A prompt the client can render and send to the model.const SummarizePrompt = McpServer.prompt({ name: "summarize", description: "Summarize a document in a chosen tone", parameters: { tone: Schema.Literals(["concise", "playful"]) }, completion: { tone: () => Effect.succeed(["concise", "playful"]) }, content: ({ tone }) => Effect.succeed(`Summarize the attached document in a ${tone} tone.`)})
// 4. Merge all registrations, then provide the stdio transport.const ServerLayer = Layer.mergeAll( ToolkitLayer, ReadmeResource, SummarizePrompt).pipe( Layer.provide( McpServer.layerStdio({ name: "Demo Server", version: "1.0.0" }) ), Layer.provide(NodeStdio.layer), // MCP uses stdout for the JSON-RPC stream, so logs must go to stderr. Layer.provide(Layer.succeed(Logger.LogToStderr)(true)))
Layer.launch(ServerLayer).pipe(NodeRuntime.runMain)A few things to note:
- Each registration helper (
toolkit,resource,prompt) returns aLayer. You merge them andLayer.providethe transport underneath. - The transport layer (
layerStdio) provides theMcpServerservice that the registration layers depend on. - Over stdio, stdout is the protocol channel. Route your logs to stderr with
Logger.LogToStderr, or the client will choke on the mixed output.
stdio vs HTTP
Section titled “stdio vs HTTP”Both transports run the same server; only the wiring differs.
layerStdio installs the NDJSON-RPC serialization and reads/writes the current
Stdio service. This is the transport local desktop clients launch as a
subprocess.
import { Layer, Logger } from "effect"import { McpServer } from "effect/unstable/ai"import { NodeStdio } from "@effect/platform-node"
const transport = McpServer.layerStdio({ name: "Demo Server", version: "1.0.0"}).pipe( Layer.provide(NodeStdio.layer), Layer.provide(Layer.succeed(Logger.LogToStderr)(true)))layerHttp registers a JSON-RPC POST route on an existing HttpRouter at the
given path. HTTP clients must complete MCP initialization first; the server
tracks each session with an Mcp-Session-Id header.
import { Layer } from "effect"import { McpServer } from "effect/unstable/ai"import { HttpRouter } from "effect/unstable/http"
const transport = McpServer.layerHttp({ name: "Demo Server", version: "1.0.0", path: "/mcp"}).pipe(Layer.provide(HttpRouter.layer))Compose the registration layers on top exactly as in the stdio example, then
serve the HttpRouter with your platform’s HTTP server layer.
If you already have a custom RpcServer.Protocol, use the lower-level
layer (no transport) or call run directly inside your own
layer graph.
McpServer reference
Section titled “McpServer reference”McpServer
Section titled “McpServer”The Context.Service that stores registered tools, resources, prompts, and
completions and resolves incoming MCP requests. You normally obtain it via one
of the layers rather than constructing it yourself; the registration helpers
require it in their environment.
import { Effect } from "effect"import { McpServer } from "effect/unstable/ai"
const program = Effect.gen(function* () { const server = yield* McpServer // server.tools, server.resources, server.prompts, ...})McpServer.layer provides both McpServer and McpServerClient but installs
no transport.
Runs an MCP server over the ambient RpcServer.Protocol. It performs
initialization and session handling, serves registered capabilities, and
forwards queued server notifications. Returns an Effect<never> you fork inside
a scope.
import { Effect } from "effect"import { McpServer } from "effect/unstable/ai"
// Requires McpServer | RpcServer.Protocol in the environment.const main = McpServer.run({ name: "Demo Server", version: "1.0.0" })// => Effect<never, never, McpServer | RpcServer.Protocol>Base layer that forks run and provides McpServer + McpServerClient, but
does not install a transport — the surrounding graph must supply
RpcServer.Protocol. Use this for custom transports. The extensions option
advertises non-standard capabilities (keys must be vendor/name).
import { McpServer } from "effect/unstable/ai"
const base = McpServer.layer({ name: "Demo Server", version: "1.0.0", extensions: { "acme/telemetry": { enabled: true } }})// => Layer<McpServer | McpServerClient, never, RpcServer.Protocol>layerStdio
Section titled “layerStdio”Runs the server over stdio. Composes layer with the stdio RPC protocol and
NDJSON-RPC serialization; requires the Stdio service (e.g. NodeStdio.layer).
| Option | Type | Description |
|---|---|---|
name | string | Server name reported on initialize. |
version | string | Server version reported on initialize. |
extensions | Record<`${string}/${string}`, unknown>? | Optional non-standard capabilities. |
import { McpServer } from "effect/unstable/ai"
const transport = McpServer.layerStdio({ name: "Demo Server", version: "1.0.0"})// => Layer<McpServer | McpServerClient, never, Stdio>layerHttp
Section titled “layerHttp”Registers an HTTP POST JSON-RPC route at path on the current HttpRouter.
Same options as layerStdio plus a required path.
import { McpServer } from "effect/unstable/ai"
const transport = McpServer.layerHttp({ name: "Demo Server", version: "1.0.0", path: "/mcp"})// => Layer<McpServer | McpServerClient, never, HttpRouter>registerToolkit
Section titled “registerToolkit”Effect that registers every tool in a Toolkit as an MCP tool and
wires tools/call to the toolkit’s handlers. Tool descriptions, JSON schemas,
and hint annotations (read-only / destructive / idempotent / open-world) are
derived from the tool definitions. Requires McpServer and the toolkit handlers
in its environment.
import { Effect } from "effect"import { McpServer, Tool, Toolkit } from "effect/unstable/ai"import { Schema } from "effect"
const Echo = Tool.make("Echo", { description: "Echo a message back", parameters: Schema.Struct({ message: Schema.String }), success: Schema.String})
const kit = Toolkit.make(Echo)
const register = Effect.gen(function* () { yield* McpServer.registerToolkit(kit) // => void; tools/list now reports "Echo"})toolkit
Section titled “toolkit”The layer form of registerToolkit. Registers the toolkit into the server when
the layer is built; provide the toolkit’s handler layer underneath.
import { Layer } from "effect"import { McpServer } from "effect/unstable/ai"
// const ToolkitLayer = McpServer.toolkit(kit).pipe(Layer.provide(kit.toLayer({ ... })))// => Layer<never, never, Tool.HandlersFor<...>>registerResource
Section titled “registerResource”Registers an MCP resource (or resource template) from an Effect program. It has two forms.
Static resource — pass a fixed uri and an Effect that produces the
content. Returning a string yields a text resource, a Uint8Array yields a
blob, or return a full ReadResourceResult for fine control.
import { Effect } from "effect"import { McpServer } from "effect/unstable/ai"
const register = McpServer.registerResource({ uri: "config://app", name: "App Config", mimeType: "application/json", content: Effect.succeed(JSON.stringify({ theme: "dark" }))})// => Effect<void, never, McpServer>Resource template — call it as a tagged template with McpSchema.param
interpolations. Each param’s schema decodes the matched URI segment, and your
content function receives the decoded params in order.
import { Effect, Schema } from "effect"import { McpServer, McpSchema } from "effect/unstable/ai"
const userId = McpSchema.param("userId", Schema.NumberFromString)
const register = McpServer.registerResource`user://${userId}/profile`({ name: "User Profile", content: (uri, id) => Effect.succeed({ contents: [{ uri, text: `Profile for user #${id}` }] })})// reading user://42/profile => "Profile for user #42"resource
Section titled “resource”The layer form of registerResource, with the same two overloads (static URI or
tagged template). Use it to compose resource registration into a server layer.
import { Effect } from "effect"import { McpServer } from "effect/unstable/ai"
const ConfigResource = McpServer.resource({ uri: "config://app", name: "App Config", content: Effect.succeed("theme=dark")})// => Layer<never, never, never>ResourceCompletions
Section titled “ResourceCompletions”Utility type that maps a resource template’s parameter schemas to a record of
completion handlers — keyed by the explicit param name (or paramN when
unnamed). Each handler takes the partial input string and returns candidate
values of the parameter’s type. You use it implicitly through a template’s
completion option.
import { Effect, Schema } from "effect"import { McpServer, McpSchema } from "effect/unstable/ai"
const repo = McpSchema.param("repo", Schema.String)
const register = McpServer.registerResource`repo://${repo}`({ name: "Repo", // ResourceCompletions<[Param<"repo">]> = { repo: (input: string) => Effect<string[]> } completion: { repo: (input) => Effect.succeed(["effect", "effect-smol"].filter((r) => r.startsWith(input))) }, content: (uri, name) => Effect.succeed({ contents: [{ uri, text: name }] })})ValidateCompletions
Section titled “ValidateCompletions”Utility type that validates a completion-handler record against the allowed
parameter keys: any key not in the parameter set resolves to never, producing
a type error at the call site. It backs the completion option on resources and
prompts, so a typo in a completion key is caught at compile time.
// type Valid = ValidateCompletions<{ id: (s: string) => any }, "id"> // ok// type Bad = ValidateCompletions<{ xyz: (s: string) => any }, "id"> // xyz: never => errorregisterPrompt
Section titled “registerPrompt”Registers an MCP prompt from an Effect program. parameters is a Schema
fields object that is decoded from the client’s arguments; content returns
either a string (turned into a single user text message) or an array of
PromptMessage. completion suggests values per argument.
import { Effect, Schema } from "effect"import { McpServer } from "effect/unstable/ai"
const register = McpServer.registerPrompt({ name: "translate", description: "Translate text to a target language", parameters: { language: Schema.String, text: Schema.String }, completion: { language: () => Effect.succeed(["French", "German", "Japanese"]) }, content: ({ language, text }) => Effect.succeed(`Translate the following into ${language}:\n\n${text}`)})// => Effect<void, never, McpServer>prompt
Section titled “prompt”The layer form of registerPrompt. Returns a Layer you merge into the server.
import { Effect, Schema } from "effect"import { McpServer } from "effect/unstable/ai"
const GreetPrompt = McpServer.prompt({ name: "greet", parameters: { name: Schema.String }, content: ({ name }) => Effect.succeed(`Say hello to ${name}.`)})// => Layer<never, never, never>elicit
Section titled “elicit”Asks the connected client to collect structured input from the user, decoding
the accepted response with the supplied schema. A declined request fails with
ElicitationDeclined; a canceled request interrupts the fiber. Only usable
inside a handler where McpServerClient is available (i.e. while serving a
request).
import { Effect, Schema } from "effect"import { McpServer } from "effect/unstable/ai"
const askForName = McpServer.elicit({ message: "What name should I use?", schema: Schema.Struct({ name: Schema.String })})// => Effect<{ name: string }, ElicitationDeclined, McpServerClient>clientCapabilities
Section titled “clientCapabilities”Reads the current client’s advertised capabilities (sampling, elicitation, roots, …) from its initialize payload. Useful for branching on what the connected client supports.
import { Effect } from "effect"import { McpServer } from "effect/unstable/ai"
const canElicit = McpServer.clientCapabilities.pipe( Effect.map((caps) => caps.elicitation !== undefined))// => Effect<boolean, never, McpServerClient>McpSchema reference
Section titled “McpSchema reference”McpSchema models the entire MCP wire protocol with Effect Schema and Rpc
definitions. Most servers never construct these directly — the registration
helpers above build them for you — but they are the source of truth for the
protocol and are useful when writing clients, middleware, or matching raw
messages.
Schema helpers
Section titled “Schema helpers”MCP distinguishes an absent field from a field present with value undefined.
These helpers make that distinction encode correctly.
optional
Section titled “optional”Marks a struct field optional such that explicit undefined is omitted
during encoding (rather than serialized as undefined).
import { Schema } from "effect"import { McpSchema } from "effect/unstable/ai"
const S = Schema.Struct({ note: McpSchema.optional(Schema.String) })// decode {} => {}// encode { note: undefined } => {} (field omitted, not serialized)optionalWithDefault
Section titled “optionalWithDefault”Marks a field optional and supplies a default when it is absent, both when decoding and when constructing.
import { Schema } from "effect"import { McpSchema } from "effect/unstable/ai"
const S = Schema.Struct({ retries: McpSchema.optionalWithDefault(Schema.Number, () => 3)})// decode {} => { retries: 3 }IDs and metadata
Section titled “IDs and metadata”| Schema | Shape |
|---|---|
RequestId | string | number — JSON-RPC request id. |
ProgressToken | string | number — links progress to a request. |
Cursor | string — opaque pagination token. |
RequestMeta | { _meta?: { progressToken? } } |
ResultMeta | { _meta?: Record<string, Json> } |
NotificationMeta | { _meta?: Record<string, Json> } |
PaginatedRequestMeta | RequestMeta + { cursor? } |
PaginatedResultMeta | ResultMeta + { nextCursor? } |
import { McpSchema } from "effect/unstable/ai"
const meta = McpSchema.PaginatedRequestMeta.make({ cursor: "page-2" })// => { cursor: "page-2" }Capabilities and implementation
Section titled “Capabilities and implementation”Implementation
Section titled “Implementation”Name + version (and optional title) of a client or server implementation, exchanged during initialize.
import { McpSchema } from "effect/unstable/ai"
const impl = McpSchema.Implementation.make({ name: "Demo Server", version: "1.0.0" })// => { name: "Demo Server", version: "1.0.0" }ClientCapabilities
Section titled “ClientCapabilities”What an MCP client supports: roots, sampling, elicitation, plus open
experimental / extensions records. This is the value clientCapabilities
returns.
import { McpSchema } from "effect/unstable/ai"
const caps = McpSchema.ClientCapabilities.make({ elicitation: {} })// => { elicitation: {} } (client can be elicited from)ServerCapabilities
Section titled “ServerCapabilities”What the server advertises: tools, resources, prompts, completions,
logging, each optionally signalling listChanged / subscribe. The server
fills this in automatically based on what you registered.
import { McpSchema } from "effect/unstable/ai"
const caps = McpSchema.ServerCapabilities.make({ tools: { listChanged: true }, completions: {}})Conversation role literal, "user" or "assistant".
import { McpSchema } from "effect/unstable/ai"
const role: McpSchema.Role = "assistant"Annotations
Section titled “Annotations”Client-facing hints on an object: intended audience (roles) and a priority
between 0 and 1.
import { McpSchema } from "effect/unstable/ai"
const ann = McpSchema.Annotations.make({ audience: ["user"], priority: 0.8 })// => { audience: ["user"], priority: 0.8 }Errors and codes
Section titled “Errors and codes”All MCP errors extend McpErrorBase ({ code, message, data? }) and are tagged
Schema errors with a fixed numeric code. McpError is the union of all of
them. Servers raise InvalidParams / InternalError from handlers; the
framework maps failures to JSON-RPC error responses.
| Error / value | Code | Meaning |
|---|---|---|
ParseError | -32700 | JSON could not be parsed. |
InvalidRequest | -32600 | Not a valid request object. |
MethodNotFound | -32601 | Unknown / unavailable method. |
InvalidParams | -32602 | Parameters don’t match the schema. |
InternalError | -32603 | Unexpected server-side failure. |
McpErrorBase | any | Base shape for a custom error. |
The matching constants are PARSE_ERROR_CODE, INVALID_REQUEST_ERROR_CODE,
METHOD_NOT_FOUND_ERROR_CODE, INVALID_PARAMS_ERROR_CODE, and
INTERNAL_ERROR_CODE.
import { McpSchema } from "effect/unstable/ai"
const err = new McpSchema.InvalidParams({ message: "Unknown tool 'frobnicate'" })err.code // => -32602McpSchema.INVALID_PARAMS_ERROR_CODE // => -32602
// A ready-made "not implemented" internal error:McpSchema.InternalError.notImplemented // => InternalError { message: "Not implemented" }Requests, notifications, and results
Section titled “Requests, notifications, and results”These are the Rpc definitions and result schemas that make up the protocol.
Each Rpc.make entry carries the method name, payload, success, and error
schemas; the server’s RPC groups (below) bundle them. The registration helpers
produce these results for you — this table is the reference for matching or
constructing them by hand.
| Method / schema | Kind | Notes |
|---|---|---|
Ping | request | "ping" liveness check; empty result. |
Initialize / InitializeResult | request | Handshake; exchanges capabilities + protocol version. |
InitializedNotification | notification | "notifications/initialized" after handshake. |
CancelledNotification | notification | "notifications/cancelled" cancels a request. |
ProgressNotification | notification | "notifications/progress" for long operations. |
ResourceListChangedNotification | notification | Tells clients to re-list resources. |
Resource | schema | A readable resource descriptor. |
ResourceTemplate | schema | RFC 6570 URI-template resource. |
ResourceContents | schema | Base contents (uri, mimeType?). |
TextResourceContents | schema | Contents + text. |
BlobResourceContents | schema | Contents + blob (Uint8Array). |
ListResources / ListResourcesResult | request | "resources/list". |
ListResourceTemplates / ListResourceTemplatesResult | request | "resources/templates/list". |
ReadResource / ReadResourceResult | request | "resources/read". |
Initialize / InitializeResult
Section titled “Initialize / InitializeResult”The first request a client sends. The server replies with its capabilities,
serverInfo, and the negotiated protocolVersion.
import { McpSchema } from "effect/unstable/ai"
const result = McpSchema.InitializeResult.make({ protocolVersion: "2025-06-18", capabilities: McpSchema.ServerCapabilities.make({ tools: { listChanged: true } }), serverInfo: McpSchema.Implementation.make({ name: "Demo", version: "1.0.0" })})Resource
Section titled “Resource”A concrete resource the server can read. Built automatically by
registerResource’s static form.
import { McpSchema } from "effect/unstable/ai"
const res = new McpSchema.Resource({ uri: "config://app", name: "App Config", mimeType: "application/json"})// => Resource { uri: "config://app", name: "App Config", ... }ReadResourceResult
Section titled “ReadResourceResult”The reply to resources/read: a list of TextResourceContents or
BlobResourceContents. This is the full-control return type for a resource’s
content.
import { McpSchema } from "effect/unstable/ai"
const reply = McpSchema.ReadResourceResult.make({ contents: [{ uri: "config://app", text: "theme=dark" }]})// => { contents: [{ uri: "config://app", text: "theme=dark" }] }RPC groups
Section titled “RPC groups”McpSchema bundles the protocol into RPC groups used by the server transport:
ClientRequestRpcs— requests clients send (initialize, list/read/call, …), withMcpServerClientMiddlewareinstalled.ClientNotificationRpcs— client notifications (cancelled, progress, initialized, roots list changed).ClientRpcs— the merge of the two; this is what the server handles.ServerRequestRpcs— requests the server can send back to a client (ping, samplingCreateMessage,ListRoots,Elicit).ServerNotificationRpcs— server notifications (logging, list-changed, resource-updated, progress, cancelled).
import { McpSchema } from "effect/unstable/ai"
// The methods the server answers:McpSchema.ClientRpcs.requests.has("tools/call") // => trueThere are matching *Encoded types (ClientRequestEncoded,
ServerNotificationEncoded, FromClientEncoded, FromServerEncoded, …) that
describe the wire shapes, derived via RequestEncoded, NotificationEncoded,
SuccessEncoded, and FailureEncoded.
Parameters and conditional enablement
Section titled “Parameters and conditional enablement”Wraps a schema with a name so it can be used as a resource URI-template
parameter. The wrapped schema decodes the matched segment; the name drives
template compilation and completion lookup. isParam tests for it.
import { Schema } from "effect"import { McpSchema } from "effect/unstable/ai"
const id = McpSchema.param("id", Schema.NumberFromString)McpSchema.isParam(id) // => true// Used in: McpServer.resource`thing://${id}`({ ... })EnabledWhen
Section titled “EnabledWhen”A context annotation that conditionally shows a tool, resource, or prompt based
on the connecting client’s initialize payload. Attach it via the annotations
option; the server filters list results per client.
import { Context } from "effect"import { McpSchema } from "effect/unstable/ai"
// Only expose to clients that support elicitation:const annotations = Context.make( McpSchema.EnabledWhen, (init) => init.capabilities.elicitation !== undefined)// pass as: McpServer.prompt({ ..., annotations })