nestrs-mcp is a Model Context Protocol server for nestrs. MCP-aware clients (Claude Code, Cursor, VS Code, Codex CLI, anything that speaks the protocol) gain a structured view of your project — modules, controllers, providers, routes, DTOs, schedules, event handlers — plus live runtime queries against a running app and a set of scaffolding actions.
The intended outcome: instead of re-parsing the source tree on every turn, the model calls list_routes, get_app_health, create_resource, or search_docs and gets a typed answer back. How much of that you get out of the box — and what you serve yourself — is covered in Server architecture.
What it exposes
Introspection tools read the workspace’s
src/ tree via syn (mirroring the attribute shapes from nestrs-macros); the live-runtime tools query a running app’s admin port instead. Both live on separate handlers — see below.
Server architecture
nestrs-mcp ships two layers:
NestrsMcpServer(nestrs_mcp::server) — the top-levelrmcpServerHandlerthat thenestrs-mcpbinary serves. It carries the protocol surfaces beyond tools — prompts, resources, resource templates, argument completion, resource subscriptions (SEP-1696), cache hints (SEP-2549), elicitation (SEP-1034), and the task store (SEP-2663) — plus, under theauthzfeature, per-tool-call authorization. Its ownToolRouterstays empty (rmcp routers are invariant in the handler type);list_tools/get_tool/call_tooldispatch to the four area handlers so the stock binary advertises all 24 tools.- Four area handlers (
nestrs_mcp::tools) —IntrospectionTools,RuntimeTools,ScaffoldTools,DocsTools. Each is a completeServerHandlerwith its own#[tool_router], so each can be served on its own transport.NestrsMcpServer::sub_servers()returns all four bundled in aSubServersstruct.
The stock
nestrs-mcp binary lists every area tool (list_modules, get_app_health, create_resource, search_docs, …). Embedding a single area handler is still supported when you want a narrower surface.Serving the tools yourself
nestrs-mcp is a library as well as a binary. Each area handler is a standalone ServerHandler, so you can serve any subset from your own binary:
command is now your binary instead of nestrs-mcp). To serve more than one area, spawn one serve_server per handler on separate transports and register each under its own name (nestrs-introspection, nestrs-runtime, …).
To serve the non-tool surfaces (prompts, resources, elicitation, tasks) plus the authz pipeline instead, serve NestrsMcpServer and register what you need — see Protocol surfaces beyond tools and the sections after it. Serving a NestrsMcpServer and one or more area handlers side by side is fine; they’re independent handlers.
Install
The admin-port tools (
get_app_health, get_app_routes, get_app_providers) and the AdminClient behind them are compiled in unconditionally — no feature needed on nestrs-mcp. The admin feature only re-exports nestrs::admin::{AdminHandle, AdminOptions} for embedders. The feature that matters for the runtime tools is admin on the nestrs crate of the target app (see Live runtime).nestrs-mcp = { version = "1.5.0", features = ["admin"] } etc. The full feature list (stdio, http, admin, authz, authz-row-level, elicitation) is on the API reference.
Setup wizard
cargo install is half the story — the client still needs to know about the server. The init subcommand (alias: setup) detects installed editors by checking the well-known config paths, asks which ones to configure, and writes the right MCP server entry into each one — idempotently preserving everything else.
Detection rules: an editor is “detected” if its config file or its parent directory exists. So a fresh checkout with
.vscode/ but no mcp.json still gets offered the option to create the file.
Merge behavior: all four formats (mcpServers for Claude Code / Cursor, servers for VS Code, [mcp_servers] for Codex) are merged round-trip — the wizard preserves every unrelated key and every other server entry. A second run is a no-op (the file is byte-identical, nothing is rewritten).
After the wizard finishes, restart your editor (or click Refresh in the MCP servers panel) and the nestrs server appears in the MCP panel with a green handshake. The stock binary advertises every area tool (list_modules, get_app_health, create_resource, search_docs, …).
Run
StreamableHttpService mounted on an axum::Router at /mcp.
Security
Before you expose the HTTP transport to a non-loopback address:- Put a reverse proxy in front of it that terminates TLS and enforces auth. Any of these work and are well-trodden:
- Caddy:
reverse_proxy 127.0.0.1:7777 { basicauth { ... } } - nginx:
auth_basic "nestrs-mcp"; auth_basic_user_file ...; - Cloudflare Tunnel + Cloudflare Access (zero-trust JWTs in front of the local listener).
- Caddy:
- Bind the listener to
127.0.0.1, never0.0.0.0, so the proxy is the only way in. - Set
--http-addr 127.0.0.1:<port>explicitly; the default is already loopback, but spell it out so a later refactor can’t widen the bind by accident.
nestrs::admin::AdminOptions { token: ... }), and per-tool-call authorization on the MCP side itself is available today via the authz feature (see Per-tool-call authorization).
Connect from a client
nestrs-mcp speaks the standard Model Context Protocol. The two patterns are stdio (the client spawns the binary as a subprocess) and Streamable HTTP (the client connects to a running server).
stdio (local, recommended)
The client launchesnestrs-mcp on demand and pipes JSON-RPC through its stdin/stdout — no ports, no auth, no leftover processes.
- Claude Code
- Cursor
- VS Code (Copilot Chat)
- Codex CLI
.mcp.json in your project root (or ~/.claude.json for a global install):Streamable HTTP (networked / hosted)
Useful when the binary runs on a host the client can’t shell into, or when several clients should share one server. Start the server (it stays in the foreground; run it under your process supervisor of choice):http://<host>:7777/mcp:
- Claude Code
- Cursor
- VS Code (Copilot Chat)
- Codex CLI
.mcp.json:Verifying the connection
From the shell, a quick sanity check that the HTTP transport is alive:200 OK with an mcp-session-id header and a JSON result block means the handshake succeeded and the client can call tools.
Talking to a running nestrs app (live runtime)
Theget_app_health, get_app_routes, and get_app_providers tools hit a localhost-only sidecar exposed by NestApplication::use_admin(AdminOptions) in the nestrs crate’s admin feature. To enable it, the app’s Cargo.toml needs:
GET /__nestrs/health—{ status, uptime_ms, version }GET /__nestrs/providers—Vec<{ type_name, scope }>GET /__nestrs/routes—Vec<RouteInfo>from theRouteRegistryGET /__nestrs/openapi.json— proxy of the OpenAPI doc
Authorization: Bearer <token>. Without a token the listener refuses to bind to anything but 127.0.0.1 and responds 401 to all routes.
The MCP get_app_health / get_app_routes / get_app_providers tools take base_url + optional token per call, so the model can target a running app on the user’s machine without restarting the server.
Protocol surfaces beyond tools
TheNestrsMcpServer wrapper (through its McpSurfaces bundle) implements the MCP surfaces that aren’t tools: prompts, resources, resource templates, argument completion, resource subscriptions, and cache hints. Build a McpSurfaces value, register what you need, and attach it with .with_surfaces(...):
resources/readchecks the static resource map first — a static resource always wins over a template that happens to match the same URI.- Template matching is a static-prefix match (everything before the first
{, with a trailing/trimmed) — enough for the commonnestrs://docs/{name}shape. - The
resources.subscribecapability is advertised only when at least one URI is marked subscribable; on protocol ≥ 2026-07-28 thesubscriptions/listenflow is used, and the server keeps the legacysubscribe/unsubscribemethods working for older peers. - Cache hints travel as top-level
ttlMs/cacheScopefields and are suppressed for peers on protocol versions older than 2026-07-28.CacheScope::Privaterestricts caching to the requesting user’s client;Public(the default) allows intermediaries.
Elicitation: asking the user mid-flight
Two mechanisms, both opt-in: MRTR for tools (SEP-2322) — no Cargo feature needed. A tool that needs more input returnsCallToolResponse::InputRequired instead of an error, using the elicit_input helper; on the retry the client echoes the answer back, and input_responses reads it:
request_state argument (Option<String>) carries opaque server-side state across rounds if the tool needs it.
Server→client elicitation (SEP-1034) — behind the elicitation feature (it adds rmcp’s elicitation support, including the url crate for URL-based elicitations). Register one handler on the surfaces bundle, then call NestrsMcpServer::elicit from a tool body:
NestrsMcpServer::elicit probes the client’s capability first and returns a Cancel result (rather than an error) if the client didn’t advertise elicitation — the kinder shape for the client’s UI.
Long-running tasks (SEP-2663)
A tool that kicks off slow work can hand the client a task handle instead of blocking thetools/call response. The task store always exists; advertise the extension with .with_task_support() so clients know they can poll:
tasks/get,tasks/update,tasks/cancelare rejected with-32601unless.with_task_support()advertised theio.modelcontextprotocol/tasksextension.- An operation can request input mid-flight via
ctx.request_input(key, request); the client answers throughtasks/updateand the future resumes — the same MRTR loop as tools, applied to tasks. - Cancellation is cooperative:
tasks/cancelacknowledges immediately; the operation observesctx.cancelled()and exits withTaskExit::Cancelled.
Per-tool-call authorization (authz)
Behind the authz feature, the wrapper’s ServerHandler::call_tool override runs every tool call inside the same task-local scopes the HTTP, WebSocket, and GraphQL transports use:
tools/call the override:
- Opens a
TransactionSloton the configured pool (if any) and installs it into the request scope — tool bodies read it viacurrent_mcp_transaction(). - Installs the ability and principal —
current_mcp_ability()/current_mcp_principal(). - Dispatches the tool.
- Post-masks the response with
nestrs::mask_value— the same masking walker HTTP/WS/GraphQL use, so the “what gets stripped” rules are identical across transports. Tool-error results are never masked (stripping fields would hide the diagnostic the caller needs). - Commits the transaction on success, rolls back on a tool-level error.
authz-row-level additionally wires the row-level predicates (it mirrors the main crate’s authz-row-level feature). See Authorization for the Ability/masking model itself.
Tool error conventions
Tool bodies returnResult<Json<...>, rmcp::ErrorData>:
- Operational failure (operation ran but failed — file not found, parse error, app not reachable, scaffold failure): the tool returns
Err(ErrorData::internal_error(msg)). The message is visible to the model, which can read it and recover or retry. - Bad parameters (unknown module / controller / provider / DTO / route name):
Err(ErrorData::invalid_params("modulexnot found")). - Success: a
Json<T>payload — the typed value lands instructuredContent(the#[tool]macro stamps anoutputSchemafrom the return type) plus a text fallback block for clients that only render text.
Source parser
nestrs-mcp re-implements the attribute parser in introspection::source using syn directly. It does not depend on nestrs-macros (it is proc-macro only and would create a build-time circular dep). The parser recognizes:
#[module(...)]—imports,controllers,providers,microservices,exports,re_exports#[controller("/path"[, version, host])]— emits__nestrs_prefix/__nestrs_version/__nestrs_hostconst fns#[routes(state, controller_guards)]impls with their per-fn attributes:#[get/post/put/patch/delete/options/head/all(...)],#[ver(...)],#[use_guards(...)],#[use_interceptors(...)],#[use_pipes(...)],#[use_filters(...)],#[set_metadata(...)],#[roles(...)],#[param::body/query/param/req/headers/ip],#[openapi(...)]#[injectable(scope = "singleton|transient|request")]#[dto(...)]and its field-attr translation table (IsString,IsEmail,IsNotEmpty,IsUuid,MinLength,MaxLength,Min,Max,IsUrl,ValidateNested, etc.)#[ws_gateway(path = "/ws")],#[ws_routes],#[micro_routes],#[event_routes],#[schedule_routes]
nestrs-macros will show up as unrecognized in nestrs-mcp until the parser is updated — that is the intended maintenance surface.
See also
- API reference: nestrs-mcp — exact signatures for the server, surfaces, tasks, authz, and client types
- Authorization — the
Ability/ masking model the MCPauthzfeature reuses - CLI overview —
nestrs-cli new,nestrs-cli generate resource - Observability — metrics, tracing, and request logging
- API security — guards, interceptors, CORS, CSRF
- API version control —
#[ver(...)]and module-level versioning