Serving an agent over MCP
View SourceLegion.MCP.Server exposes one of your agents as an
MCP server. The harness's model writes
Lua, your app runs it in the agent's sandbox against your tools, and the
result goes back to the harness. The agent never calls an LLM itself, so it
needs no API key.
1. Install
The server is built on anubis_mcp, an
optional dependency of Legion: without it in your deps, Legion.MCP.Server
does not exist. Add it next to Legion:
# mix.exs
{:legion, "~> 0.6"},
{:anubis_mcp, "~> 2.0"}Serving over HTTP through Legion.MCP.Plug also needs :plug, which every
Phoenix app already has. If Legion was compiled before you added
:anubis_mcp, recompile it with mix deps.compile legion --force.
2. Write the agent
Any agent on the Lua sandbox, the default, can be served, including one you
already chat with. If it lists Legion.Tools.AgentTool or
Legion.Tools.HumanTool, those are left out over MCP. This agent serves a
shop's catalogue: CatalogTool wraps your product search, and CatalogAgent
answers questions with it.
defmodule MyApp.Tools.CatalogTool do
@moduledoc "Searches the product catalogue by free text."
use Legion.Tool
@doc "Products matching the query, with name, price and stock"
def search(query), do: MyApp.Catalog.search(query)
end
defmodule MyApp.CatalogAgent do
@moduledoc """
Answers questions about the shop's catalogue: products, prices and stock.
"""
use Legion.Agent
def tools, do: [MyApp.Tools.CatalogTool]
endOn connect, the server sends the harness a short introduction: the agent's
@moduledoc plus the first sentence of each tool's @moduledoc. The model
reads a tool in full only when it is about to use it, so keep the agent's
doc short and let each tool's first sentence say what the tool is for.
3. Create the server
# lib/my_app/mcp.ex
defmodule MyApp.MCP do
use Legion.MCP.Server, agent: MyApp.CatalogAgent, name: "my_app", version: "1.0.0"
endThe harness gets two tools: help returns a tool's source and docs, and
repl runs code in the agent's sandbox and returns the result.
4. Serve it
In a Phoenix app, start the server after Legion and forward a path to
Legion.MCP.Plug:
# lib/my_app/application.ex
children = [
Legion,
{MyApp.MCP, transport: {:streamable_http, start: true}},
MyAppWeb.Endpoint
]
# lib/my_app_web/router.ex
scope "/mcp" do
forward "/", Legion.MCP.Plug, server: MyApp.MCP
endOutside Phoenix, a Plug.Router takes
forward "/mcp", to: Legion.MCP.Plug, init_opts: [server: MyApp.MCP].
start: true starts the server even when Phoenix isn't serving, as in
mix test. Leave the scope without a pipeline: plug :accepts, ["json"]
would reject the event stream that harnesses open.
5. Connect and try it
Run mix phx.server and add http://localhost:4000/mcp to your harness's
MCP settings as an HTTP server. Ask it which green hoodies are in stock. Its
model calls help with CatalogTool to learn what search returns, then
sends code like this to repl:
local hoodies = CatalogTool.search("hoodie")
local green = {}
for _, product in ipairs(hoodies) do
if string.find(string.lower(product.name), "green") and product.stock > 0 then
table.insert(green, product.name)
end
end
return greenrepl returns the matching names, such as ["Green Hoodie"], and the model
answers you. Globals set by the code persist across repl calls.
On the first request the server also logs a warning: anyone who can reach
the endpoint can run code in your sandbox. Signing users in closes that.
Web pages are kept out already: Legion.MCP.Plug refuses a browser
request from any origin but localhost, so a site you have open cannot
reach your local server. Hosts that send no Origin, as CLI and desktop
ones do, are served; list a web client's origin in allowed_origins:.
Next steps
- Sign-in - pass
authorization:touse Legion.MCP.Serverand mountAnubis.Server.Transport.WellKnownat the site root (Anubis's sign-in guide).session/1then gives each user their own agent and vault. Keep it one agent per user: whoever reaches an agent shares its history and variables. - Rate limits - return
rate_limit:rules fromsession/1. Everyreplcall counts towards:max_evals, which the limiter reads from the store, so it needs aLegion.Store.Postgresstore too;helpis free.Legion.RateLimitercovers setting up the limiter. - Custom MCP tools - add them with
componentin the server module (Anubis's server guide). They bypass the agent, so they get no rate limiting, saved history or vault. Use aLegion.Toolwhen you need those. - Other options - request timeouts, trimming the introduction, telemetry
and the stdio transport are covered in the
Legion.MCP.Serverdocs.