Bridging an MCP server¶
The mcp module connects to Model
Context Protocol servers and exposes their
tools to the model as user-defined tools. require('mcp') yields the module;
mcp.connect_stdio launches a
server as a child process and talks to it over its stdin/stdout, while
mcp.connect_http reaches one
over streamable HTTP. Each returns an
McpClient handle whose
list_tools
enumerates what the server advertises (each already shaped for
registry:add) and
whose call dispatches
a tool.
Both discovery and reconfiguration run once per config load (at startup and on
each /reload), with reconfigure firing first. So connect the server in a
reconfigure handler and keep the handle in a global; the later
discover_tools handler then reads
that handle and registers the server's tools. The handle's
connected
method reports whether the session is still live, so the tools can stop being
offered (with a nudge to /reload) once a stdio server exits, since nothing
respawns it:
-- mcp_time.lua -- expose the mcp-server-time MCP server's tools to the model.
--
-- Drop this beside your config.lua and `require('mcp_time')`.
local mcp = require 'mcp'
-- `reconfigure` runs before `discover_tools`, so connect here and stash the
-- handle and tool list in globals for the discovery handler to read.
global mcp_client
global mcp_tools
agent.on('reconfigure', function()
-- pcall so a missing or broken server just disables these tools rather than
-- aborting the whole config load.
local ok, client = pcall(mcp.connect_stdio, {
command = { 'uvx', 'mcp-server-time' },
})
if not ok then
print('mcp: time server unavailable: ' .. tostring(client))
mcp_client, mcp_tools = nil, nil
return
end
-- Reassigning drops any handle left from a previous /reload; its stdio child
-- is killed as the old session closes.
mcp_client = client
mcp_tools = client:list_tools()
end)
agent.on('discover_tools', function(registry)
if not mcp_client then return end
-- A stdio server that has died stays non-nil but closed: nothing respawns it.
-- Stop offering its tools and tell the user how to recover.
if not mcp_client:connected() then
print('mcp: time server has exited; run /reload to reconnect')
return
end
for _, tool in ipairs(mcp_tools) do
registry:add {
name = 'time__' .. tool.name,
description = tool.description,
parameters = tool.parameters,
run = function(args, ctx)
return mcp_client:call(tool.name, args)
end,
}
end
end)
A tool's result comes back as { content, is_error }, which is exactly what a
run callback may return, so a server-reported failure surfaces to the model
instead of raising; only a transport or protocol failure raises. For several
servers, make the globals a list of { prefix, handle, tools } and loop the
same way, namespacing each server's tools by its prefix.
For a server reached over HTTP the shape is the same, only the connect_*
call changes. For example, DeepWiki is a free,
public, no-authentication server whose tools answer questions about GitHub
repositories:
A server behind a token takes an Authorization header (and any others) via
headers = { Authorization = 'Bearer ' .. token }.
An HTTP connection recovers on its own: it replays the handshake when the
server reports an expired session, and reconnects a dropped event stream with
exponential backoff (indefinitely by default). Pass max_retries to
connect_http to bound that so a
server that stays down eventually surfaces a failure rather than retrying
forever. A stdio child has no such recovery, so /reload is the way back.