McpTest unstable
In-memory test client for McpServer registrations.
McpTest.makeClient runs the MCP server over an in-memory
RpcServer.Protocol, performs the handshake for the selected protocol
version, and returns a typed client. Requests travel through the same
JSON-RPC encoding, routing, middleware, and response encoding as the stdio
and HTTP transports, so tests reach protocol encoding, list methods,
sessions, and notifications without opening a transport.
Constructors
makeClient
unstableCreates an in-memory MCP client for a registration layer.
Details
The server is built from McpServer.layer with the supplied protocols
and registrations, over an in-memory RpcServer.Protocol. The client
speaks options.protocolVersion, by default the first adapter's version:
stateful versions initialize and send notifications/initialized before
makeClient returns; the stateless version attaches request metadata to
every request instead.
Because requests reach handlers through the server runner, the transport
built-ins are discharged the same way as on stdio: McpRequestContext on
every request, and McpServerClient only when every adapter in protocols
is stateful (see McpServer.ProvidedServices). Any other McpRequest
marker in registrations stays in the requirements and must be met by MCP
middleware; providing the service directly does not discharge it.
The client and server live in the surrounding Scope.
See
- Client for the request methods and notifications
- McpServer.middleware for discharging request-level dependencies
Signature
declare const makeClient: <Protocols extends Arr.NonEmptyReadonlyArray<McpProtocol.ProtocolAdapter>, RA = never, RE = never, RR = never>(registrations: Layer.Layer<RA, RE, RR>, options: { readonly clientCapabilities?: McpSchema.ClientCapabilities; readonly clientInfo?: McpSchema.Implementation; readonly extensions?: NonNullable<McpSchema.ServerCapabilities["extensions"]>; readonly instructions?: string; readonly name?: string; readonly protocols: Protocols; readonly protocolVersion?: Protocols[number]["protocolVersion"]; readonly version?: string;}) => Effect.Effect<Client, Cause.IllegalArgumentError | RE, Scope.Scope | Exclude<McpServer.McpRequest.Without<RR>, McpServer.McpServer> | McpServer.McpRequest.From<"Requires", Exclude<McpServer.McpRequest.Only<"Requires", RR>, McpServer.ProvidedServices<Protocols>>>>Example Reading a resource in a test
import { Effect } from "effect"import { McpProtocol, McpServer, McpTest } from "effect/ai"
const registrations = McpServer.resource({ uri: "test://greeting", name: "greeting", content: Effect.succeed("hello")})
const program = Effect.gen(function*() { const client = yield* McpTest.makeClient(registrations, { protocols: [McpProtocol.v2025_11_25] }) const result = yield* client.readResource({ uri: "test://greeting" }) return result.contents.map((content) => content.uri)}).pipe(Effect.scoped)
const actual = await Effect.runPromise(program)actual // => ["test://greeting"]Models
A typed in-memory MCP client connected to a running McpServer.
Details
Each request method mirrors one entry of McpSchema.ClientRequestRpcs,
except initialize, which makeClient sends during the handshake. Requests
are encoded with the client schemas and decoded with the server's selected
protocol adapter, so results and errors match what a wire client observes.
Server-to-client notifications are queued on notifications in their wire
encoding, in delivery order. Server-to-client requests (elicitation, sampling, roots) are
rejected with a method-not-found error.
Signature
interface Client { readonly callTool: (request: { readonly _meta?: { [key: string]: unknown; readonly progressToken?: string | number; }; readonly arguments?: { [key: string]: any; }; readonly name: string; }) => Effect<CallToolResult, ParseError | McpErrorBase | InvalidRequest | MethodNotFound | InvalidParams | InternalError>; readonly complete: (request: { readonly argument: { readonly name: string; readonly value: string; }; readonly context?: { readonly arguments?: { [key: string]: string; }; }; readonly ref: ResourceReference | PromptReference; }) => Effect<CompleteResult, ParseError | McpErrorBase | InvalidRequest | MethodNotFound | InvalidParams | InternalError>; readonly getPrompt: (request: { readonly _meta?: { [key: string]: unknown; readonly progressToken?: string | number; }; readonly arguments?: { [key: string]: string; }; readonly name: string; readonly title?: string; }) => Effect<GetPromptResult, ParseError | McpErrorBase | InvalidRequest | MethodNotFound | InvalidParams | InternalError>; readonly listPrompts: (request?: PaginatedRequestMeta) => Effect<ListPromptsResult, ParseError | McpErrorBase | InvalidRequest | MethodNotFound | InvalidParams | InternalError>; readonly listResources: (request?: PaginatedRequestMeta) => Effect<ListResourcesResult, ParseError | McpErrorBase | InvalidRequest | MethodNotFound | InvalidParams | InternalError>; readonly listResourceTemplates: (request?: PaginatedRequestMeta) => Effect<ListResourceTemplatesResult, ParseError | McpErrorBase | InvalidRequest | MethodNotFound | InvalidParams | InternalError>; readonly listTools: (request?: PaginatedRequestMeta) => Effect<ListToolsResult, ParseError | McpErrorBase | InvalidRequest | MethodNotFound | InvalidParams | InternalError>; readonly notifications: Dequeue<{ readonly _tag: "Notification"; readonly method: "notifications/cancelled"; readonly payload: { readonly _meta?: { [key: string]: unknown; }; readonly reason?: string; readonly requestId: string | number; }; } | { readonly _tag: "Notification"; readonly method: "notifications/progress"; readonly payload: { readonly _meta?: { [key: string]: unknown; }; readonly message?: string; readonly progress: number; readonly progressToken: string | number; readonly total?: number; }; } | { readonly _tag: "Notification"; readonly method: "notifications/resources/list_changed"; readonly payload: { readonly _meta?: { [key: string]: unknown; }; } | undefined; } | { readonly _tag: "Notification"; readonly method: "notifications/resources/updated"; readonly payload: { readonly _meta?: { [key: string]: unknown; }; readonly uri: string; }; } | { readonly _tag: "Notification"; readonly method: "notifications/prompts/list_changed"; readonly payload: { readonly _meta?: { [key: string]: unknown; }; } | undefined; } | { readonly _tag: "Notification"; readonly method: "notifications/tools/list_changed"; readonly payload: { readonly _meta?: { [key: string]: unknown; }; } | undefined; } | { readonly _tag: "Notification"; readonly method: "notifications/message"; readonly payload: { readonly _meta?: { [key: string]: unknown; }; readonly data: any; readonly level: "error" | "debug" | "info" | "notice" | "warning" | "critical" | "alert" | "emergency"; readonly logger?: string; }; } | { readonly _tag: "Notification"; readonly method: "notifications/elicitation/complete"; readonly payload: { readonly elicitationId: string; }; }>; readonly ping: Effect<void, ParseError | McpErrorBase | InvalidRequest | MethodNotFound | InvalidParams | InternalError>; readonly protocolVersion: ProtocolVersion; readonly readResource: (request: { readonly _meta?: { [key: string]: unknown; readonly progressToken?: string | number; }; readonly uri: string; }) => Effect<ReadResourceResult, ParseError | McpErrorBase | InvalidRequest | MethodNotFound | InvalidParams | InternalError>; readonly server: { readonly addPrompt: (options: { readonly annotations: Context<never>; readonly completions: Record<string, (input: string, context: CompletionContext) => Effect.Effect<CompleteResult, InternalError, McpRequestContext>>; readonly handle: (params: Record<string, string>) => Effect<GetPromptResult | InputRequired, InvalidParams | InternalError, McpRequestContext>; readonly prompt: Prompt; readonly services?: Context<never>; }) => Effect<void>; readonly addResource: (options: { readonly annotations: Context<never>; readonly handle: Effect<ReadResourceResult, InternalError, McpRequestContext>; readonly resource: Resource; readonly services?: Context<never>; }) => Effect<void>; readonly addResourceTemplate: (options: { readonly annotations: Context<never>; readonly completions: Record<string, (input: string, context: CompletionContext) => Effect.Effect<CompleteResult, InternalError>>; readonly handle: (uri: string, params: Array<string>) => Effect<ReadResourceResult, InvalidParams | InternalError, McpRequestContext>; readonly routerPath: string; readonly services?: Context<never>; readonly template: ResourceTemplate; }) => Effect<void>; readonly addTool: (options: { readonly annotations: Context<never>; readonly handle: (payload: any) => Effect<CallToolResult | InputRequired, InvalidParams | InternalError, McpRequestContext>; readonly services?: Context<never>; readonly tool: Tool; }) => Effect<void>; readonly callTool: (requests: { readonly _meta?: { [key: string]: unknown; readonly progressToken?: string | number; }; readonly arguments?: { [key: string]: any; }; readonly name: string; }) => Effect<CallToolResult, McpErrorBase | InvalidParams | InternalError, McpServerClient>; readonly completion: (complete: { readonly argument: { readonly name: string; readonly value: string; }; readonly context?: { readonly arguments?: { [key: string]: string; }; }; readonly ref: ResourceReference | PromptReference; }) => Effect<CompleteResult, McpErrorBase | InvalidParams | InternalError, McpServerClient>; readonly findResource: (uri: string) => Effect<ReadResourceResult, McpErrorBase | InvalidParams | InternalError, McpServerClient>; readonly getPromptResult: (request: { readonly _meta?: { [key: string]: unknown; readonly progressToken?: string | number; }; readonly arguments?: { [key: string]: string; }; readonly name: string; readonly title?: string; }) => Effect<GetPromptResult, McpErrorBase | InvalidParams | InternalError, McpServerClient>; readonly notifications: { "notifications/cancelled": <AsQueue extends boolean = false, Discard = false>(input: { readonly _meta?: { [key: string]: unknown; }; readonly reason?: string; readonly requestId: string | number; }, options?: { readonly context?: Context<never>; readonly discard?: Discard; readonly headers?: Input; }) => Effect<Discard extends true ? void : void, Discard extends true ? never : never, never>; "notifications/message": <AsQueue extends boolean = false, Discard = false>(input: { readonly _meta?: { [key: string]: unknown; }; readonly data: any; readonly level: "error" | "debug" | "info" | "notice" | "warning" | "critical" | "alert" | "emergency"; readonly logger?: string; }, options?: { readonly context?: Context<never>; readonly discard?: Discard; readonly headers?: Input; }) => Effect<Discard extends true ? void : void, Discard extends true ? never : never, never>; "notifications/progress": <AsQueue extends boolean = false, Discard = false>(input: { readonly _meta?: { [key: string]: unknown; }; readonly message?: string; readonly progress: number; readonly progressToken: string | number; readonly total?: number; }, options?: { readonly context?: Context<never>; readonly discard?: Discard; readonly headers?: Input; }) => Effect<Discard extends true ? void : void, Discard extends true ? never : never, never>; "notifications/prompts/list_changed": <AsQueue extends boolean = false, Discard = false>(input: { readonly _meta?: { [key: string]: unknown; }; } | undefined, options?: { readonly context?: Context<never>; readonly discard?: Discard; readonly headers?: Input; }) => Effect<Discard extends true ? void : void, Discard extends true ? never : never, never>; "notifications/resources/list_changed": <AsQueue extends boolean = false, Discard = false>(input: { readonly _meta?: { [key: string]: unknown; }; } | undefined, options?: { readonly context?: Context<never>; readonly discard?: Discard; readonly headers?: Input; }) => Effect<Discard extends true ? void : void, Discard extends true ? never : never, never>; "notifications/resources/updated": <AsQueue extends boolean = false, Discard = false>(input: { readonly _meta?: { [key: string]: unknown; }; readonly uri: string; }, options?: { readonly context?: Context<never>; readonly discard?: Discard; readonly headers?: Input; }) => Effect<Discard extends true ? void : void, Discard extends true ? never : never, never>; "notifications/tools/list_changed": <AsQueue extends boolean = false, Discard = false>(input: { readonly _meta?: { [key: string]: unknown; }; } | undefined, options?: { readonly context?: Context<never>; readonly discard?: Discard; readonly headers?: Input; }) => Effect<Discard extends true ? void : void, Discard extends true ? never : never, never>; }; readonly notifyElicitationComplete: (options: { readonly clientId: number; readonly elicitationId: string; }) => Effect<void>; readonly prompts: readonly Array<{ readonly annotations: Context<never>; readonly prompt: Prompt; }>; readonly resources: readonly Array<{ readonly annotations: Context<never>; readonly resource: Resource; }>; readonly resourceTemplates: readonly Array<{ readonly annotations: Context<never>; readonly template: ResourceTemplate; }>; readonly tools: readonly Array<{ readonly annotations: Context<never>; readonly tool: Tool; }>; }; readonly setLevel: (request: { readonly _meta?: { [key: string]: unknown; readonly progressToken?: string | number; }; readonly level: "error" | "debug" | "info" | "notice" | "warning" | "critical" | "alert" | "emergency"; }) => Effect<void, ParseError | McpErrorBase | InvalidRequest | MethodNotFound | InvalidParams | InternalError>; readonly subscribe: (request: { readonly _meta?: { [key: string]: unknown; readonly progressToken?: string | number; }; readonly uri: string; }) => Effect<void, ParseError | McpErrorBase | InvalidRequest | MethodNotFound | InvalidParams | InternalError>; readonly unsubscribe: (request: { readonly _meta?: { [key: string]: unknown; readonly progressToken?: string | number; }; readonly uri: string; }) => Effect<void, ParseError | McpErrorBase | InvalidRequest | MethodNotFound | InvalidParams | InternalError>;}