mcp
js/ai/mcp.ts
fino:ai/mcp — Model Context Protocol client and server adapters for agents.
Useful references:
- Model Context Protocol: https://modelcontextprotocol.io/
- Lifecycle: https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle
- Streamable HTTP transport: https://modelcontextprotocol.io/specification/2025-06-18/basic/transports
- Tools: https://modelcontextprotocol.io/specification/2025-06-18/server/tools
- Resources: https://modelcontextprotocol.io/specification/2025-06-18/server/resources
- Prompts: https://modelcontextprotocol.io/specification/2025-06-18/server/prompts
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 DELETE —
mountMcp() 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.