This is the shortest path from a fresh Elixir app to a working MCP server.
1. Define a server module
Use FastestMCP.ServerModule when the server belongs to your application:
defmodule MyApp.MCPServer do
use FastestMCP.ServerModule,
http: [port: 4100, allowed_hosts: :localhost]
alias FastestMCP.Context
def server(opts) do
base_server(opts)
|> FastestMCP.add_tool("sum", fn %{"a" => a, "b" => b}, _ctx -> a + b end)
|> FastestMCP.add_tool("visit", fn _arguments, ctx ->
visits = Context.get_state(ctx, :visits, 0) + 1
:ok = Context.set_state(ctx, :visits, visits)
%{visits: visits, server: ctx.server_name}
end)
end
endbase_server/1 keeps the builder DSL intact while making the module name the
server identity automatically.
If you need to advertise protocol extension capabilities during
initialization, pass experimental_capabilities: when constructing the server:
FastestMCP.server("capability-demo",
experimental_capabilities: %{
"acme.widgets" => %{"version" => 1}
}
)The map is exposed under initialize capabilities as "experimental".
2. Start it under your supervision tree
defmodule MyApp.Application do
use Application
def start(_type, _args) do
children = [
MyApp.MCPServer
]
Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
end
end3. Call it in process
FastestMCP.call_tool(MyApp.MCPServer, "sum", %{"a" => 20, "b" => 22})
# => 42
FastestMCP.call_tool(MyApp.MCPServer, "visit", %{}, session_id: "docs-session")
# => %{visits: 1, server: "Elixir.MyApp.MCPServer"}Reusing the same session_id keeps session state attached to the same client
conversation.
4. Serve it over streamable HTTP
The http: option in use FastestMCP.ServerModule starts the transport for
you. If you want to embed it inside an existing Plug or Phoenix stack, use the
shared HTTP app directly:
children = [
{Bandit, plug: FastestMCP.http_app(MyApp.MCPServer, allowed_hosts: :localhost), port: 4100}
]Phoenix forwarding works the same way:
forward "/mcp", FastestMCP.Transport.HTTPApp,
server_name: MyApp.MCPServer,
path: "/mcp",
allowed_hosts: :any5. Connect with a client
client =
FastestMCP.Client.connect!("http://127.0.0.1:4100/mcp",
client_info: %{"name" => "docs-client", "version" => "1.0.0"},
session_stream: true
)
%{items: tools} = FastestMCP.Client.list_tools(client)
FastestMCP.Client.call_tool(client, "sum", %{"a" => 20, "b" => 22})
FastestMCP.Client.complete(
client,
%{"type" => "ref/prompt", "name" => "draft_release"},
%{"name" => "environment", "value" => "pr"}
)From here, branch into the focused guides:
- Components
- Context
- Dependency Injection
- Lifespan
- Transports
- Client
- Sampling and Interaction
- Background Tasks
- Middleware
- Providers and Mounting
Why This Shape
FastestMCP treats the server as application infrastructure, not a separate app framework hiding inside your codebase. Module-owned startup makes server identity explicit, fits normal OTP supervision, and keeps the same builder API available for dynamic or generated cases.