MCP endpoints
Several features on one URL, the identity the app declares once, and the case for more than one endpoint.
Several features on one endpoint
Section titled “Several features on one endpoint”Every shipped MCP client config points at a single URL, so a real product
usually wants all its domains behind one path. It gets that without giving up
one mcp/ adapter per feature: hosts that write the same path aggregate onto
one endpoint, and a bare #[mcp] is how a feature says “the app’s endpoint”
without repeating the URL.
A #[mcp] path is not a namespace the host owns the way a #[controller]’s
is — nothing nests under it. It names the one endpoint the host joins, which is
exactly why peers share it.
#[module(imports = [WeatherModule, AuthzMcpModule], providers = [WeatherTool])]pub struct WeatherMcpModule; // #[mcp]
#[module(imports = [HelloModule, AuthzMcpModule], providers = [HelloTool])]pub struct HelloMcpModule; // #[mcp]
#[module(imports = [ HttpModule::for_root(None), WeatherMcpModule, HelloMcpModule,])]pub struct AssistantModule;One client, one URL, both features’ tools:
INFO nest_rs::routes: mounted endpoint kind="mcp" path="/mcp"INFO nest_rs::routes: mounted mcp host kind="mcp" path="/mcp" host="WeatherTool" tools="current_weather"INFO nest_rs::routes: mounted mcp host kind="mcp" path="/mcp" host="HelloTool" tools="hello"Nothing in a host changes: it stays a plain ServerHandler and never learns
that it shares. tools/list, prompts/list and resources/list are the union
of every host on the path; a tools/call is routed to the host that declares
the name; the endpoint’s advertised capabilities are the union of theirs.
A single host on a path is served verbatim — the merge only engages beyond
one.
The one rule the merge adds: two hosts on one path may not serve the same tool name. MCP addresses a tool by bare name inside an endpoint, so the loser would be unreachable — it is a boot error naming the tool and both hosts:
duplicate MCP tool name on "/mcp": search (WeatherTool and HelloTool) — a tool isaddressed by bare name within an endpoint, so rename one of them or give eachhost its own pathName your endpoint
Section titled “Name your endpoint”An MCP endpoint is one server to every client that reaches it. The
protocol carries one serverInfo and one instructions, on initialize
and on server/discover alike — the same shape new McpServer({ name, version }) builds in the TypeScript SDK, and the one a FastMCP parent keeps
when it mounts children. Two declarations answer for it, each owned by whoever can know it.
The app says who it is and how its server is used, once, for every endpoint it exposes:
use nest_rs::core::module;use nest_rs::mcp::{McpIdentity, McpModule, McpOptions};
#[module(imports = [ HttpModule::for_root(None), McpModule::for_root(McpOptions { server: Some( McpIdentity::new("acme-assistant", env!("CARGO_PKG_VERSION")) .title("Acme assistant") .instructions("Ask before writing anything. Every result is scoped to your token."), ), ..Default::default() }), WeatherMcpModule, HelloMcpModule,])]pub struct AssistantModule;Both parts have to come from here. The version, because
env!("CARGO_PKG_VERSION") written in a shared feature library is the library’s
version rather than the deployment’s. The instructions, because they describe
the server — a client may fold them into the model’s system prompt — and on
an endpoint several features share, no single host can see the whole.
Keep them general: what a caller must know to use the surface at all. What each
tool does is already carried by its own #[tool(description = "…")], which is
what the model reads when it chooses between them.
A host says which endpoint stands apart, on the file that serves it:
#[mcp(path = "/mcp/weather", name = "acme-weather")]#[derive(Clone)]pub struct WeatherTool { /* … */ }name and title are optional and each overrides the app’s per field, so the
host above renames its endpoint while still inheriting the app’s version, title
and instructions. Two hosts on one path both declaring is a boot error naming
both — otherwise the endpoint’s name would depend on imports = [..] order.
path, name and title are the whole list. Every other identity field —
version, description, website_url, icons, instructions — describes the
server, and writing one on a host is a compile error naming the seam that takes
it. A feature library knows neither the binary’s version nor, on a shared
endpoint, the whole surface, so those have one owner: the app’s
McpModule::for_root. To version the address, write it into the path —
#[mcp(path = "/mcp/v1")].
Identity is declared; capabilities are observed. A declaration replaces what
it states and can never claim a capability no host serves. Declare no
instructions at all and the hosts’ own get_info blurbs are joined instead of
dropped — a fallback, not a second way to write them.
Multiple servers on one app
Section titled “Multiple servers on one app”The other direction: a second server is a second path on a host. Each mounts
on the HTTP transport independently — different tools, different injected
dependencies, different authorization policy if you want one.
Namespacing is the URL: /mcp for the app’s own endpoint, whatever you write
for the second. The MCP spec already namespaces tools per endpoint, so a second
endpoint is the namespace — no extra primitive to learn, and no prefix the
second path has to sit under.
Going further
Section titled “Going further”- MCP — the host each of these mounts.
- Authorization — resolved once per path, not per host.