Skip to content

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 Effect Schema / 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"

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 a Layer. You merge them and Layer.provide the transport underneath.
  • The transport layer (layerStdio) provides the McpServer service 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.

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))
)

If you already have a custom RpcServer.Protocol, use the lower-level layer (no transport) or call run directly inside your own layer graph.


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>

Runs the server over stdio. Composes layer with the stdio RPC protocol and NDJSON-RPC serialization; requires the Stdio service (e.g. NodeStdio.layer).

OptionTypeDescription
namestringServer name reported on initialize.
versionstringServer version reported on initialize.
extensionsRecord<`${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>

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>

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"
})

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<...>>

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"

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>

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 }] })
})

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 => error

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>

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>

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>

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 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.

MCP distinguishes an absent field from a field present with value undefined. These helpers make that distinction encode correctly.

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)

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 }
SchemaShape
RequestIdstring | number — JSON-RPC request id.
ProgressTokenstring | number — links progress to a request.
Cursorstring — opaque pagination token.
RequestMeta{ _meta?: { progressToken? } }
ResultMeta{ _meta?: Record<string, Json> }
NotificationMeta{ _meta?: Record<string, Json> }
PaginatedRequestMetaRequestMeta + { cursor? }
PaginatedResultMetaResultMeta + { nextCursor? }
import { McpSchema } from "effect/unstable/ai"
const meta = McpSchema.PaginatedRequestMeta.make({ cursor: "page-2" })
// => { cursor: "page-2" }

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" }

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)

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"

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 }

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 / valueCodeMeaning
ParseError-32700JSON could not be parsed.
InvalidRequest-32600Not a valid request object.
MethodNotFound-32601Unknown / unavailable method.
InvalidParams-32602Parameters don’t match the schema.
InternalError-32603Unexpected server-side failure.
McpErrorBaseanyBase 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 // => -32602
McpSchema.INVALID_PARAMS_ERROR_CODE // => -32602
// A ready-made "not implemented" internal error:
McpSchema.InternalError.notImplemented // => InternalError { message: "Not implemented" }

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 / schemaKindNotes
Pingrequest"ping" liveness check; empty result.
Initialize / InitializeResultrequestHandshake; exchanges capabilities + protocol version.
InitializedNotificationnotification"notifications/initialized" after handshake.
CancelledNotificationnotification"notifications/cancelled" cancels a request.
ProgressNotificationnotification"notifications/progress" for long operations.
ResourceListChangedNotificationnotificationTells clients to re-list resources.
ResourceschemaA readable resource descriptor.
ResourceTemplateschemaRFC 6570 URI-template resource.
ResourceContentsschemaBase contents (uri, mimeType?).
TextResourceContentsschemaContents + text.
BlobResourceContentsschemaContents + blob (Uint8Array).
ListResources / ListResourcesResultrequest"resources/list".
ListResourceTemplates / ListResourceTemplatesResultrequest"resources/templates/list".
ReadResource / ReadResourceResultrequest"resources/read".

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" })
})

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", ... }

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" }] }

McpSchema bundles the protocol into RPC groups used by the server transport:

  • ClientRequestRpcs — requests clients send (initialize, list/read/call, …), with McpServerClientMiddleware installed.
  • 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, sampling CreateMessage, 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") // => true

There are matching *Encoded types (ClientRequestEncoded, ServerNotificationEncoded, FromClientEncoded, FromServerEncoded, …) that describe the wire shapes, derived via RequestEncoded, NotificationEncoded, SuccessEncoded, and FailureEncoded.

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}`({ ... })

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 })
  • Tools — define the Tool / Toolkit values you expose via registerToolkit.
  • RPC — the request/response machinery an MCP server is built on.