mcp

js/ai/mcp.ts

fino:ai/mcp — Model Context Protocol client and server adapters for agents.

Useful references:

MCPClient connects to an MCP server, performs the initialize handshake, lists remote tools and resources, and converts remote MCP tools into Fino Tool instances that can be passed directly to an Agent. MCPServer exposes local Fino tools and resources to MCP-compatible clients over stdio, custom transports, or a Streamable HTTP endpoint.

Protocol model

Transports are intentionally small: a transport only sends JSON-RPC strings, receives JSON-RPC strings, and closes. The bundled transports cover stdio server processes and HTTP/SSE endpoints; applications can provide custom transports for embedded servers, test peers, or nonstandard deployment environments.

The server API is endpoint-first. Mount server.httpHandler() in an application router when you need auth, CORS, logging, sessions, or other middleware around the MCP endpoint. listen() is only a convenience wrapper for local tools and tests; it should not replace the application router in a composed service.

Current protocol coverage

The implementation covers MCP 2025-06-18 initialization, tools, resources, resource templates, prompts, stdio-style transports, Streamable HTTP POST and GET SSE, client-side roots, sampling, elicitation handlers, cursor-aware list methods, list-change notifications, and resource subscriptions. OAuth and authorization policy stay with application middleware around the mounted endpoint.

import { App } from 'fino:net/http/app';
import { mcpServer, mountMcp } from 'fino:ai/mcp';
import { tool } from 'fino:ai/tool';
import { v } from 'fino:validate';

const server = mcpServer({
  name: 'support-tools',
  tools: [tool({
    name: 'lookup_ticket',
    description: 'Look up a support ticket by id.',
    parameters: v.object({ id: v.string().describe('Support ticket id') }),
    execute: async ({ id }: { id: string }) => `ticket:${id}`,
  })],
});

const app = new App();
mountMcp(app, '/mcp', server);

Interfaces

interface StdioTransportOptions {

Options for launching an MCP server over stdio.

The command is spawned as a child process and JSON-RPC messages travel as newline-delimited JSON over its stdin/stdout, matching the MCP stdio transport convention.

import { stdioTransport } from 'fino:ai/mcp';

const transport = stdioTransport({
  command: 'mcp-filesystem',
  args: ['--root', '/srv/data'],
  env: { LOG_LEVEL: 'warn' },
  cwd: '/srv',
});

Properties

command: string

Executable to spawn.

args?: string[]

Arguments passed to the executable.

env?: Record<string, string>

Environment variables for the child process.

cwd?: string

Working directory for the child process.

interface HttpTransportOptions {

Options for connecting to an MCP server over HTTP.

import { httpTransport } from 'fino:ai/mcp';

const transport = httpTransport({
  url: 'https://tools.example.com/mcp',
  headers: { authorization: 'Bearer <token>' },
  sseChannel: true,
});

Properties

url: string

MCP endpoint URL; every JSON-RPC message is POSTed here.

headers?: Record<string, string>

Extra headers sent on every request — typically authorization.

sseChannel?: boolean

Open a GET SSE channel to receive server-initiated messages.

The channel opens only after the server issues an Mcp-Session-Id, and resumes with Last-Event-ID when the stream drops.

interface MCPClientOptions {

Options for MCPClient.

Only transport is required. Providing roots, sampling, or elicitation also advertises the matching client capability during the initialize handshake so the server knows it may call back; a server that calls an unconfigured handler receives a JSON-RPC error. The on* callbacks observe server notifications after the client has already scheduled a refresh of its retained list signals.

import { MCPClient, httpTransport } from 'fino:ai/mcp';

const client = new MCPClient({
  transport: httpTransport({ url: 'https://tools.example.com/mcp' }),
  roots: [{ uri: 'file:///srv/project', name: 'project' }],
  elicitation: async ({ message }) => ({ action: 'decline' }),
  onToolsChanged: () => console.log('remote tool list changed'),
});

Properties

transport: Transport

Transport used to exchange JSON-RPC messages with the server.

roots?: McpRoot[] | (() => McpRoot[] | Promise<McpRoot[]>)

Root URIs, or a provider for them, answered to server roots/list calls.

sampling?: (params: McpSamplingRequest) => McpSamplingResult | Promise<McpSamplingResult>

Handler for server-initiated sampling/createMessage requests.

elicitation?: ( params: McpElicitationRequest ) => McpElicitationResult | Promise<McpElicitationResult>

Handler for server-initiated elicitation/create requests.

onToolsChanged?: () => void | Promise<void>

Called after the server signals that its tool list changed.

onResourcesChanged?: () => void | Promise<void>

Called after the server signals that its resource list changed.

onPromptsChanged?: () => void | Promise<void>

Called after the server signals that its prompt list changed.

onResourceUpdated?: (uri: string) => void | Promise<void>

Called when a resource URI subscribed via subscribeResource() updates.

interface McpResource {

Resource descriptor returned by an MCP server.

Descriptors identify what a server can serve; pass the uri to MCPClient.readResource() to fetch the content itself.

const resources = await client.listResources();
for (const res of resources) {
  console.log(res.uri, res.mimeType ?? 'unknown type');
}

Properties

uri: string

Unique URI identifying the resource.

name?: string

Human-readable resource name.

description?: string

Description of what the resource contains.

mimeType?: string

MIME type of the resource content.

interface McpResourceTemplate {

Resource template descriptor returned by an MCP server.

Templates describe parameterized URIs (RFC 6570) that clients expand themselves before calling readResource().

const templates = await client.listResourceTemplates();
const uri = templates[0].uriTemplate.replace('{name}', 'report');
const contents = await client.readResource(uri);

Properties

uriTemplate: string

RFC 6570 URI template, e.g. file:///data/{name}.txt.

name?: string

Human-readable template name.

description?: string

Description of the resources the template expands to.

mimeType?: string

MIME type shared by resources matching the template.

interface McpResourceContent {

Resource content returned from readResource().

A content object carries either text (for textual resources) or blob (base64-encoded binary), not both.

const [content] = await client.readResource('file:///data/report.txt');
if (content.text !== undefined) console.log(content.text);

Properties

uri: string

URI of the resource this content belongs to.

mimeType?: string

MIME type of the content.

text?: string

Textual content, when the resource is text.

blob?: string

Base64-encoded binary content, when the resource is binary.

interface McpPromptArgument {

Prompt argument descriptor returned by an MCP server.

const [prompt] = await client.listPrompts();
const required = (prompt.arguments ?? []).filter((arg) => arg.required);

Properties

name: string

Argument name, used as a key in the getPrompt() args object.

description?: string

Description of what the argument controls.

required?: boolean

Whether the argument must be supplied to getPrompt().

interface McpPrompt {

Prompt descriptor returned by an MCP server.

Descriptors come from listPrompts(); render one into messages with getPrompt().

const prompts = await client.listPrompts();
const summarize = prompts.find((p) => p.name === 'summarize');

Properties

name: string

Prompt name passed to getPrompt().

description?: string

Description of what the prompt produces.

arguments?: McpPromptArgument[]

Arguments the prompt accepts.

interface McpPromptMessage {

Prompt message returned by getPrompt().

const { messages } = await client.getPrompt('summarize', { text: '...' });
for (const msg of messages) {
  if (msg.content.type === 'text') console.log(msg.role, msg.content.text);
}

Properties

role: 'user' | 'assistant'

Conversation role the message belongs to.

content: McpContent

Message content part.

interface McpPromptResult {

Prompt content returned by getPrompt().

const result = await client.getPrompt('summarize', { text: 'long text' });
console.log(result.description, result.messages.length);

Properties

description?: string

Description of the rendered prompt.

messages: McpPromptMessage[]

Rendered conversation messages, ready to feed to a model.

interface McpRoot {

Root URI exposed by an MCP client.

Roots tell a server which locations the client considers in scope. They are served to the peer when it calls roots/list.

const client = new MCPClient({
  transport,
  roots: [{ uri: 'file:///srv/project', name: 'project' }],
});

Properties

uri: string

Root URI, typically a file:// URL.

name?: string

Human-readable root name.

interface McpSamplingRequest {

Server-initiated sampling request delivered to an MCP client.

Sampling lets a server borrow the client's model access: the server sends a conversation and generation parameters, and the client's sampling handler decides whether and how to run the completion.

const client = new MCPClient({
  transport,
  sampling: async (req) => {
    const answer = await runModel(req.messages, { maxTokens: req.maxTokens });
    return { role: 'assistant', content: { type: 'text', text: answer } };
  },
});

Properties

messages: McpPromptMessage[]

Conversation to complete.

maxTokens?: number

Maximum tokens the server wants generated.

systemPrompt?: string

System prompt requested by the server.

includeContext?: string

How much MCP context to include, per the MCP sampling spec.

temperature?: number

Requested sampling temperature.

stopSequences?: string[]

Sequences that should stop generation.

metadata?: Record<string, unknown>

Provider-specific metadata passed through unchanged.

modelPreferences?: Record<string, unknown>

Model selection hints from the server.

interface McpSamplingResult {

Sampling result returned by an MCP client.

const result: McpSamplingResult = {
  role: 'assistant',
  content: { type: 'text', text: 'Paris is the capital of France.' },
  model: 'gpt-4o',
  stopReason: 'endTurn',
};

Properties

role: 'assistant' | 'user'

Role of the generated message.

content: McpContent

Generated content.

model?: string

Name of the model that produced the completion.

stopReason?: string

Why generation stopped.

interface McpElicitationRequest {

Server-initiated elicitation request delivered to an MCP client.

Elicitation lets a server ask the user for structured input mid-operation. The client's elicitation handler surfaces the message, collects a response, and reports whether the user accepted, declined, or cancelled.

const client = new MCPClient({
  transport,
  elicitation: async ({ message }) => {
    const email = await askUser(message);
    if (email === null) return { action: 'cancel' };
    return { action: 'accept', content: { email } };
  },
});

Properties

message: string

Human-readable request to show the user.

requestedSchema?: Record<string, unknown>

JSON schema the accepted content should conform to.

metadata?: Record<string, unknown>

Provider-specific metadata passed through unchanged.

interface McpElicitationResult {

Elicitation result returned by an MCP client.

const accepted: McpElicitationResult = {
  action: 'accept',
  content: { email: 'user@example.com' },
};

Properties

action: 'accept' | 'decline' | 'cancel'

Whether the user accepted, declined, or cancelled the request.

content?: Record<string, unknown>

User-provided values when the action is accept.

interface McpListParams {

Cursor accepted by MCP list methods.

Pass the nextCursor from a previous McpListPage to fetch the following page; omit it to start from the beginning.

let cursor: string | undefined;
do {
  const page = await client.listToolsPage(cursor ? { cursor } : {});
  use(page.items);
  cursor = page.nextCursor;
} while (cursor !== undefined);

Properties

cursor?: string

Opaque cursor from a previous page's nextCursor.

interface McpListPage<T> {

One page of an MCP list result.

nextCursor is present only when more items are available; feed it back as McpListParams.cursor to continue. Server option callbacks (tools, resources, prompts, ...) may also return this shape to serve paginated lists.

const page = await client.listResourcesPage();
if (page.nextCursor !== undefined) {
  const more = await client.listResourcesPage({ cursor: page.nextCursor });
}

Properties

items: T[]

Items in this page.

nextCursor?: string

Cursor for the next page, absent on the final page.

interface MCPServerContext {

Context passed to MCP server resource readers and prompt renderers.

const server = mcpServer({
  readResource: async (uri, ctx) => {
    const text = await loadDocument(uri, ctx.signal);
    return { uri, mimeType: 'text/plain', text };
  },
});

Properties

signal: AbortSignal

Abort signal for the current MCP request.

sessionId?: string

Streamable HTTP session id when the request arrived over HTTP.

Custom and stdio transports do not create HTTP sessions, so this is absent for those calls.

interface MCPServerOptions {

Options for exposing local Fino capabilities as an MCP server.

Every capability is optional; the server only advertises what is configured. tools, resources, resourceTemplates, and prompts accept either a static array or a callback receiving McpListParams, which allows cursor-based pagination by returning an McpListPage.

import { mcpServer } from 'fino:ai/mcp';
import { tool } from 'fino:ai/tool';
import { v } from 'fino:validate';

const server = mcpServer({
  name: 'docs',
  instructions: 'Read-only access to project documentation.',
  tools: [tool({
    name: 'search_docs',
    description: 'Search documentation by keyword.',
    parameters: v.object({ query: v.string().describe('Search query') }),
    execute: async ({ query }: { query: string }) => searchDocs(query),
  })],
  resources: [{ uri: 'doc://readme', name: 'README', mimeType: 'text/markdown' }],
  readResource: (uri) => ({ uri, mimeType: 'text/markdown', text: loadDoc(uri) }),
  listChanged: { tools: true, resources: true },
});

Properties

name?: string

Implementation name returned from initialize.

version?: string

Implementation version returned from initialize.

instructions?: string

Optional human-readable guidance returned from initialize.

tools?: Task[] | ( ( params: McpListParams ) => Task[] | McpListPage<Task> | Promise<Task[] | McpListPage<Task>> )

Local Fino tools exposed through MCP tools/list and tools/call.

resources?: McpResource[] | ( ( params: McpListParams ) => McpResource[] | McpListPage<McpResource> | Promise<McpResource[] | McpListPage<McpResource>> )

Static or lazy resource descriptors returned from resources/list.

resourceTemplates?: McpResourceTemplate[] | ( ( params: McpListParams ) => McpResourceTemplate[] | McpListPage<McpResourceTemplate> | Promise<McpResourceTemplate[] | McpListPage<McpResourceTemplate>> )

Static or lazy resource templates returned from resources/templates/list.

readResource?: ( uri: string, ctx: MCPServerContext ) => McpResourceContent | McpResourceContent[] | Promise<McpResourceContent | McpResourceContent[]>

Reader used by resources/read.

Throw JsonRpcError for protocol failures, or return one or more content objects for the requested URI.

prompts?: McpPrompt[] | ( ( params: McpListParams ) => McpPrompt[] | McpListPage<McpPrompt> | Promise<McpPrompt[] | McpListPage<McpPrompt>> )

Static or lazy prompt descriptors returned from prompts/list.

getPrompt?: ( name: string, args: Record<string, unknown>, ctx: MCPServerContext ) => McpPromptResult | Promise<McpPromptResult>

Prompt renderer used by prompts/get.

allowedOrigins?: string[] | ((origin: string, request: Request) => boolean | Promise<boolean>)

Streamable HTTP Origin policy.

By default requests with no Origin are allowed and requests with an Origin must match the request URL origin. Pass an allow-list or predicate when mounting behind a trusted cross-origin gateway.

listChanged?: { tools?: boolean; resources?: boolean; prompts?: boolean; }

Advertise and emit MCP list-changed notifications.

interface MCPRouteTarget {

Minimal route target accepted by mountMcp().

App and Router from fino:net/http/app both satisfy this shape, so MCP endpoints can be mounted at the application root or inside a nested router with its own middleware stack.

import { App } from 'fino:net/http/app';
import { mcpServer, mountMcp } from 'fino:ai/mcp';

const app = new App();
mountMcp(app, '/mcp', mcpServer({ name: 'tools' }));

Methods

get(path: string): MCPRouteMethod

Start a GET method branch for the MCP endpoint.

post(path: string): MCPRouteMethod

Start a POST method branch for the MCP endpoint.

delete(path: string): MCPRouteMethod

Start a DELETE method branch for the MCP endpoint.

interface MCPRouteMethod {

Method branch returned by an MCPRouteTarget verb; handle() registers.

This mirrors the fluent app.get(path).handle(fn) route-registration shape of fino:net/http/app, so mountMcp() can register the same handler for each HTTP method without depending on the full router API.

const handler = server.httpHandler();
app.post('/mcp').handle((ctx) => handler(ctx.request));

Methods

handle(handler: (ctx: { request: Request; }) => Response | Promise<Response>): unknown

Register the MCP endpoint handler for this method.

Functions

function stdioTransport(opts: StdioTransportOptions): Transport

Create a stdio transport for an MCP server process.

The process is spawned immediately. Each send() writes one newline-terminated JSON-RPC message to the child's stdin, and receive() yields non-empty lines from its stdout. close() closes the child's stdin — the conventional shutdown signal for stdio MCP servers, which are expected to exit once their input ends.

import { MCPClient, stdioTransport } from 'fino:ai/mcp';

const client = new MCPClient({
  transport: stdioTransport({
    command: 'npx',
    args: ['-y', '@modelcontextprotocol/server-filesystem', '/srv/data'],
  }),
});
await client.connect();
const tools = await client.listTools();

function httpTransport(opts: HttpTransportOptions): Transport

Create a Streamable HTTP transport for an MCP server.

Every outgoing message is POSTed to the endpoint with the MCP protocol version header. When a response carries an Mcp-Session-Id header the transport pins that session id on all subsequent requests and, if sseChannel is set, opens a GET text/event-stream channel for server-initiated messages. Servers may answer a POST with either a JSON body or an SSE stream; both are folded into the single receive() iterable.

Throws JsonRpcError from send() when the endpoint responds with a non-2xx status.

import { MCPClient, httpTransport } from 'fino:ai/mcp';

const client = new MCPClient({
  transport: httpTransport({
    url: 'https://tools.example.com/mcp',
    headers: { authorization: 'Bearer <token>' },
    sseChannel: true,
  }),
});
await client.connect();

function mcpServer(opts: MCPServerOptions = {}): MCPServer

Create an MCP server from local Fino tools and resource readers.

Convenience wrapper around new MCPServer(opts).

import { mcpServer } from 'fino:ai/mcp';
import { tool } from 'fino:ai/tool';
import { v } from 'fino:validate';

const server = mcpServer({
  name: 'calculator',
  tools: [tool({
    name: 'add',
    description: 'Add two numbers.',
    parameters: v.object({ a: v.number(), b: v.number() }),
    execute: ({ a, b }: { a: number; b: number }) => String(a + b),
  })],
});

function mountMcp(target: MCPRouteTarget, path: string, server: MCPServer): MCPRouteTarget

Mount an MCP server on a Fino App or Router.

Registers the server's Streamable HTTP handler for GET, POST, and DELETE on the given path and returns the target for chaining. The target's middleware remains responsible for authentication, authorization, logging, and rate limiting around the MCP endpoint.

import { App } from 'fino:net/http/app';
import { mcpServer, mountMcp } from 'fino:ai/mcp';

const app = new App();
app.use(requireBearerToken);
mountMcp(app, '/mcp', mcpServer({ name: 'internal-tools' }));
app.listen({ port: 8080 });

function mcpClient(opts: MCPClientOptions): MCPClient

Create an MCPClient.

Convenience wrapper around new MCPClient(opts); the returned client still needs connect() before use.

import { mcpClient, httpTransport } from 'fino:ai/mcp';

const client = mcpClient({
  transport: httpTransport({ url: 'https://tools.example.com/mcp' }),
});
await client.connect();
const tools = await client.listTools();

Types

type McpContent = { type: 'text'; text: string; } | { type: 'image'; data: string; mimeType: string; } | { type: 'resource'; resource: McpResourceContent; }

Content part used by MCP prompts, sampling, and tool/resource responses.

text carries plain text, image carries base64-encoded data with a MIME type, and resource embeds a full McpResourceContent object inline.

Classes

class MCPServer {

MCP server backed by local Fino tools and resources.

Use serve() for stdio or custom transports, httpHandler() for a Streamable HTTP route, or listen() for small standalone tools. HTTP sessions are tracked in memory and are scoped to this server instance, so a load-balanced deployment must pin clients to the instance that created their session.

Tool calls dispatch to Tool.invoke() with a synthetic run context whose suspend() throws — MCP has no suspension protocol, so suspendable tools fail the call instead of pausing. Tool results are converted from Fino content parts to MCP content (text, image, and document-as-resource parts).

import { MCPServer } from 'fino:ai/mcp';
import { tool } from 'fino:ai/tool';
import { v } from 'fino:validate';

const server = new MCPServer({
  name: 'support-tools',
  tools: [tool({
    name: 'lookup_ticket',
    description: 'Look up a support ticket by id.',
    parameters: v.object({ id: v.string().describe('Ticket id') }),
    execute: async ({ id }: { id: string }) => `ticket:${id}`,
  })],
});

const handle = server.listen({ port: 8080, path: '/mcp' });
await handle.ready;

Constructors

constructor(opts: MCPServerOptions = {})

Create an MCP server from the given capabilities.

The advertised MCP capabilities are derived from which options are present: configuring tools advertises tools, any of resources / resourceTemplates / readResource advertises resources, and prompts / getPrompt advertises prompts. listChanged flags add the corresponding listChanged (and, for resources, subscribe) capability markers.

Methods

async notifyToolsChanged(): Promise<void>

Notify clients that the server tool list changed.

Broadcasts notifications/tools/list_changed to every connected peer and HTTP session. No-op unless listChanged.tools was enabled — the capability must be advertised before clients can rely on the signal.

const server = mcpServer({ tools: () => currentTools, listChanged: { tools: true } });
currentTools.push(newTool);
await server.notifyToolsChanged();
async notifyResourcesChanged(): Promise<void>

Notify clients that the server resource list changed.

Broadcasts notifications/resources/list_changed to every connected peer and HTTP session. No-op unless listChanged.resources was enabled.

async notifyPromptsChanged(): Promise<void>

Notify clients that the server prompt list changed.

Broadcasts notifications/prompts/list_changed to every connected peer and HTTP session. No-op unless listChanged.prompts was enabled.

async notifyResourceUpdated(uri: string): Promise<void>

Notify subscribed clients that a resource URI was updated.

Sends notifications/resources/updated only to connections that subscribed to this exact URI via resources/subscribe; unlike the list change notifications it is not gated on a listChanged flag.

await saveDocument('doc://readme', updated);
await server.notifyResourceUpdated('doc://readme');
async serve(transport: Transport): Promise<void>

Serve this MCP endpoint over any JSON-RPC string transport.

Handles one peer per call and resolves when the transport ends. Use this to run the server over a stdio pair, an in-memory duplex in tests, or any custom transport; server-initiated notifications flow back through the same peer.

const [clientSide, serverSide] = inMemoryTransportPair();
void server.serve(serverSide);

const client = new MCPClient({ transport: clientSide });
await client.connect();
httpHandler(): (request: Request) => Promise<Response>

Create a Streamable HTTP handler for this MCP endpoint.

Mount the returned handler on one path for GET, POST, and DELETEmountMcp() does exactly that. POST carries client JSON-RPC traffic: an initialize request creates a session whose id is returned in the Mcp-Session-Id response header, and every later request must echo that header (missing ids get 400, unknown ids 404). GET opens the standalone SSE channel for server-initiated notifications and replays missed events when a client reconnects with Last-Event-ID (the last 32 events per session are retained). DELETE terminates the session.

Requests failing the allowedOrigins policy are rejected with 403; other methods get 405.

import { App } from 'fino:net/http/app';

const app = new App();
const handler = server.httpHandler();
app.get('/mcp').handle((ctx) => handler(ctx.request));
app.post('/mcp').handle((ctx) => handler(ctx.request));
app.delete('/mcp').handle((ctx) => handler(ctx.request));
listen(opts: ListenOptions): ServerHandle

Start a small standalone HTTP MCP server.

Binds a bare HTTP server and dispatches only the configured path (default /); anything else responds 404. Prefer mounting httpHandler() into fino:net/http/app for production services so application middleware controls authentication and policy.

const handle = server.listen({ port: 0, path: '/mcp' });
await handle.ready;
console.log(`listening on ${handle.port}`);
handle.close();

class MCPClient {

Client for MCP tools and resources.

Construct with a transport, call connect() to perform the initialize handshake, then use the list and read methods. Remote MCP tools are converted into Fino Tool instances whose execute proxies tools/call over the transport, so they can be passed directly to an Agent.

The client also answers server-initiated requests: roots/list when roots is configured, sampling/createMessage when sampling is configured, and elicitation/create when elicitation is configured. List-changed notifications from the server refresh the retained tools, resources, resourceTemplates, and prompts signals automatically.

Every request method throws if called before connect().

import { MCPClient, stdioTransport } from 'fino:ai/mcp';
import { agent, streamText } from 'fino:ai/agent';
import { openai } from 'fino:ai/model';

const mcp = new MCPClient({
  transport: stdioTransport({ command: 'mcp-filesystem', args: ['/srv/data'] }),
});
await mcp.connect();

const bot = agent({
  model: openai({ model: 'gpt-4o' }),
  tools: await mcp.listTools(),
});
const stream = bot.stream('Summarize the monthly report.');
for await (const text of streamText(stream)) console.log(text);
await mcp.close();

Constructors

constructor(opts: MCPClientOptions)

Create a client on the given transport.

The JSON-RPC peer starts reading from the transport immediately so server-initiated requests and notifications can be answered, but no MCP traffic is sent until connect() runs the initialize handshake.

Getters

get tools(): ReadonlySignal<Tool[]>

Retained first-page list of remote tools.

The signal starts empty, fetches lazily on first observation, and refreshes automatically when the server emits notifications/tools/list_changed. Use listTools() / listToolsPage() for an explicit fetch or for pagination.

import { effect } from 'fino:signals';

effect(() => {
  console.log('tools:', mcp.tools.value.map((t) => t.name));
});
get resources(): ReadonlySignal<McpResource[]>

Retained first-page list of remote resources.

Fetches lazily on first observation and refreshes when the server emits notifications/resources/list_changed.

get resourceTemplates(): ReadonlySignal<McpResourceTemplate[]>

Retained first-page list of remote resource templates.

Fetches lazily on first observation and refreshes when the server emits notifications/resources/list_changed.

get prompts(): ReadonlySignal<McpPrompt[]>

Retained first-page list of remote prompts.

Fetches lazily on first observation and refreshes when the server emits notifications/prompts/list_changed.

Methods

async connect(): Promise<void>

Perform the MCP initialize handshake.

Advertises the client capabilities implied by the configured options (roots, sampling, elicitation), then sends notifications/initialized. Must complete before any list, read, or subscribe method is used — those throw until the client is connected.

async listTools(params: McpListParams = {}): Promise<Tool[]>

List remote tools as Fino Tool instances.

Returns one tools/list page without cursor metadata — the convenience form for servers that fit their tool list in a single page. Each returned Tool proxies its execution through tools/call on this client; text content parts of the result are concatenated, and a result flagged isError is surfaced as a tool error rather than a thrown exception.

Throws if called before connect().

const tools = await mcp.listTools();
const bot = agent({ model, tools });
async listToolsPage(params: McpListParams = {}): Promise<McpListPage<Tool>>

Return one MCP tools/list page with the server-provided cursor metadata.

Use this when a remote server may paginate large tool lists. listTools() remains the compatibility helper for one-page servers and returns only the current page's Tool instances.

Throws if called before connect().

const all: Tool[] = [];
let cursor: string | undefined;
do {
  const page = await mcp.listToolsPage(cursor ? { cursor } : {});
  all.push(...page.items);
  cursor = page.nextCursor;
} while (cursor !== undefined);
async listResources(params: McpListParams = {}): Promise<McpResource[]>

List remote resource descriptors.

Returns one resources/list page without cursor metadata; use listResourcesPage() when the server paginates. Throws if called before connect().

async listResourcesPage(params: McpListParams = {}): Promise<McpListPage<McpResource>>

Return one MCP resources/list page with cursor metadata.

Throws if called before connect().

async listResourceTemplates(params: McpListParams = {}): Promise<McpResourceTemplate[]>

List remote resource templates.

Returns one resources/templates/list page without cursor metadata; use listResourceTemplatesPage() when the server paginates. Throws if called before connect().

async listResourceTemplatesPage( params: McpListParams = { } ): Promise<McpListPage<McpResourceTemplate>>

Return one MCP resources/templates/list page with cursor metadata.

Throws if called before connect().

async readResource(uri: string): Promise<McpResourceContent[]>

Read the content of a resource by URI.

Returns the server's content list — usually one entry, but servers may return several (e.g. a directory URI expanding to its files). Throws if called before connect(); unknown URIs surface as JsonRpcError from the server.

const [content] = await mcp.readResource('file:///data/report.txt');
console.log(content.text);
async listPrompts(params: McpListParams = {}): Promise<McpPrompt[]>

List remote prompt descriptors.

Returns one prompts/list page without cursor metadata; use listPromptsPage() when the server paginates. Throws if called before connect().

async listPromptsPage(params: McpListParams = {}): Promise<McpListPage<McpPrompt>>

Return one MCP prompts/list page with cursor metadata.

Throws if called before connect().

async getPrompt(name: string, args: Record<string, unknown> = {}): Promise<McpPromptResult>

Render a remote prompt into conversation messages.

Calls prompts/get with the given argument values. Throws if called before connect(); unknown prompt names surface as JsonRpcError from the server.

const { messages } = await mcp.getPrompt('summarize', { text: longText });
async subscribeResource(uri: string): Promise<void>

Subscribe to update notifications for a resource URI.

Updates arrive through the onResourceUpdated callback configured on the client. Throws if called before connect().

const mcp = new MCPClient({
  transport,
  onResourceUpdated: (uri) => console.log('updated:', uri),
});
await mcp.connect();
await mcp.subscribeResource('file:///data/report.txt');
async unsubscribeResource(uri: string): Promise<void>

Cancel a resource subscription made with subscribeResource().

Throws if called before connect().

async close(): Promise<void>

Close the underlying peer and transport.

For stdio transports this closes the child's stdin so the server process can exit. The client cannot be reused after closing.