@lousho/build-ai-agent/mcp subpath, and
@modelcontextprotocol/sdk is an optional peer: install it to use either
direction.
connectMcp, loadMcpTools and serveMcp are exported from
@lousho/build-ai-agent/mcp; the package root and @lousho/build-ai-agent/tools
export them too. createAgent({ mcpServers }) needs no import from the subpath.
Use MCP servers in an agent
createAgent({ mcpServers }) takes the same map. The servers connect on
await agent.ready() or, automatically, on the first send() / stream();
each server’s tools are added as <server>__<tool> (e.g. docs__search).
A server that cannot connect fails that call, and the next call tries again.
agent.close() disconnects them (stops stdio processes); a later tool call
reconnects. Without mcpServers, ready() and close() do nothing. The
vendor/ model prefix chooses the provider; with OpenRouter use
openrouter/<vendor>/<model> (e.g. openrouter/openai/gpt-4o-mini).
command and args; env is added to the
default environment (PATH and the like), not a replacement for it. HTTP
entries use the streamable HTTP transport and send the static headers on every
request. An HTTP entry can also sign in with OAuth, as the MCP authorization
spec describes: add oauth: { redirectUri } and an operator signs the agent in
once; see MCP servers with OAuth.
The map is the same one a spec file declares; see
Configuration (the mcpServers section) for the YAML form.
Share servers with connectMcp()
To share servers between agents, or to choose how failures are handled, call
connectMcp(servers, options?) and pass its tools yourself. It connects every
server and lists its tools before it resolves, so tools are known up front.
Options:
onError:'throw'(default) rejects when a server cannot connect, after closing the others;'skip'leaves that server out and warns throughlogger.lazy(defaulttrue): afterclose()or a dropped connection, the next tool call reconnects. Withfalsethat call fails instead. Listing tools needs a connection, solazynever delays the first connect.logger: receives skipped-server and skipped-tool warnings (default: none).tokens: the token store (AgentStore.tokens) of servers withoauth; required when any server hasoauth.
{ tools, close(), status() }; status() maps each server to
'idle', 'connected', 'failed' or 'needs-auth' (an oauth server the
app is not signed in to).
tools is a Record<string, ToolDescriptor> keyed <server>__<tool> — a
map, not the array defineTool() results make. createAgent({ tools }) takes
both forms, and they combine: tools: [myTool, mcp.tools] registers the array
entries under their own names and the record’s entries under their keys, so
own tools and MCP tools can sit in one list. (Spreading into one record,
tools: { ...mcp.tools, my_tool: myTool }, works too.)
mcp.tools is a record, and tools also takes an array, so the two shapes
mix: tools: [mcp.tools, weatherTool] or tools: [...createFsTools(workspace), ...Object.values(mcp.tools)] (each MCP descriptor carries its
<server>__<tool> name, so Object.values works too).
Approval for MCP tools
MCP servers describe each tool with annotations (readOnlyHint,
destructiveHint, idempotentHint, openWorldHint and a title). They are
hints, but the SDK uses them as the default for approvals:
a tool with readOnlyHint: true runs; a tool with destructiveHint: true, or
one that sends no destructiveHint (the MCP spec’s default is destructive),
pauses the run until a human approves; destructiveHint: false runs. A tool
without annotations therefore asks. The raw annotations stay on
descriptor.metadata.mcp.annotations, and title becomes the displayName.
Set approval on a server entry (mcpServers, createAgent, connectMcp()) or
in loadMcpTools(client, name, { approval }):
'annotations'(default): as above.'always'/'never': ask for every tool / none of them.- A function
({ name, annotations }) => booleandecides per tool (nameis the bare tool name;annotationsis{}when the server sent none). Only in code; a spec file takes the three strings.
allow, deny or ask.
Tools from a client you connected yourself
Connect aClient from @modelcontextprotocol/sdk yourself and load
its tools with loadMcpTools(), then pass the result to createAgent() (or
register it on a ToolRegistry):
loadMcpTools is also available from the package root and from
@lousho/build-ai-agent/tools.
Serve an agent over MCP
serveMcp() is the reverse of loadMcpTools(): it exposes an agent (and,
optionally, some of its tools) as an MCP server, so Claude Code, Cursor and
other MCP clients can call it.
- Stateless. Every call to the agent tool is a fresh conversation.
- Cancellation. Cancelling the MCP request aborts the agent run
(
agent.send(message, { signal })). - Errors. An agent failure comes back as an MCP result with
isError: true. - Approvals. Approval-gated tools cannot be approved over MCP. A run that
pauses for approval returns
isError: truewith a message saying so. Tools flaggedneedsApprovalare not exposed directly unless you passallowApprovalTools: true; if you do, clients run them with no human gate. - stdio. Nothing but the MCP protocol is written to stdout; warnings go to stderr.
- HTTP. Binds
127.0.0.1by default. Addauth: { type: 'bearer', token }to require anAuthorization: Bearerheader; binding a non-loopback host withoutauthlogs a warning.
Annotations
tools/list carries MCP annotations so clients can tell what a tool does. A tool
that needs approval is advertised readOnlyHint: false, destructiveHint: true;
state hints yourself with annotations (sent verbatim). A tool with no hints sends
none, so a Lousho agent consuming this server keeps asking before it runs (see
approval on connectMcp()); readOnlyHint: true runs without asking. A tool that
needs approval is never advertised read-only. The built-in read_file, list_dir,
glob, grep, todo_read, current_date and day_name tools are read-only.
From the command line
lousho mcp serves an agent spec file (stdio by default):
.mcp.json for Claude Code):
Limits
- Approval-gated tools cannot be approved over MCP when you serve an agent: a run that pauses for approval returns
isError: true, and tools flaggedneedsApprovalare not exposed unlessallowApprovalToolsis set. - HTTP clients send static
headerson every request. OAuth (oauth) is app-owned: one grant per server for the whole agent, signed in by an operator; there are no per-user MCP grants yet. serveMcp()calls are stateless: each call to the agent tool is a fresh conversation, and the HTTP transport acceptsPOSTonly.