Skip to main content
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-level rmcp ServerHandler that the nestrs-mcp binary 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 the authz feature, per-tool-call authorization. Its own ToolRouter stays empty (rmcp routers are invariant in the handler type); list_tools / get_tool / call_tool dispatch 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 complete ServerHandler with its own #[tool_router], so each can be served on its own transport. NestrsMcpServer::sub_servers() returns all four bundled in a SubServers struct.
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:
Register the server in your client config exactly as shown in Connect from a client (the 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, …).
NestrsMcpServer::sub_servers() returns all four handlers in one value — sub_servers.introspection, sub_servers.runtime, sub_servers.scaffold, sub_servers.docs — so you don’t have to import each module by hand.
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).
As a library dependency, the same features apply — 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, …).
Codex’s config.toml may show unrelated diff hunks on the first run — that’s toml::to_string_pretty re-formatting the existing file. Commit the new file once and you’re set; subsequent runs produce no diff.

Run

The HTTP transport is a StreamableHttpService mounted on an axum::Router at /mcp.

Security

The HTTP transport has no built-in auth. nestrs-mcp --transport http binds to the address you give it and serves /mcp to anyone who can reach it. Every tool — including the destructive scaffolding actions (new_project, create_module, create_resource, create_dto, generate_crud) — is reachable. This is intentional for v1 (the recommended path is stdio with a local subprocess), but it is a real gap before exposing :7777 to anything beyond localhost.
Before you expose the HTTP transport to a non-loopback address:
  1. 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).
  2. Bind the listener to 127.0.0.1, never 0.0.0.0, so the proxy is the only way in.
  3. 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.
OAuth PKCE and a first-class bearer-token middleware are tracked as follow-ups; the live admin port on the app side already supports bearer auth (see 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). The client launches nestrs-mcp on demand and pipes JSON-RPC through its stdin/stdout — no ports, no auth, no leftover processes.
.mcp.json in your project root (or ~/.claude.json for a global install):
After saving, restart the client (or click “Refresh” in the MCP servers panel). The handshake succeeds and the stock binary’s tool list includes every area tool.

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):
Then point the client at http://<host>:7777/mcp:
.mcp.json:

Verifying the connection

From the shell, a quick sanity check that the HTTP transport is alive:
A 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)

The get_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:
and the binary needs:
The sidecar exposes:
  • GET /__nestrs/health — { status, uptime_ms, version }
  • GET /__nestrs/providers — Vec<{ type_name, scope }>
  • GET /__nestrs/routes — Vec<RouteInfo> from the RouteRegistry
  • GET /__nestrs/openapi.json — proxy of the OpenAPI doc
If a token is configured, requests must carry 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

The NestrsMcpServer 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(...):
Matching rules worth knowing:
  • resources/read checks 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 common nestrs://docs/{name} shape.
  • The resources.subscribe capability is advertised only when at least one URI is marked subscribable; on protocol ≥ 2026-07-28 the subscriptions/listen flow is used, and the server keeps the legacy subscribe/unsubscribe methods working for older peers.
  • Cache hints travel as top-level ttlMs / cacheScope fields and are suppressed for peers on protocol versions older than 2026-07-28. CacheScope::Private restricts 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 returns CallToolResponse::InputRequired instead of an error, using the elicit_input helper; on the retry the client echoes the answer back, and input_responses reads it:
The 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 the tools/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/cancel are rejected with -32601 unless .with_task_support() advertised the io.modelcontextprotocol/tasks extension.
  • An operation can request input mid-flight via ctx.request_input(key, request); the client answers through tasks/update and the future resumes — the same MRTR loop as tools, applied to tasks.
  • Cancellation is cooperative: tasks/cancel acknowledges immediately; the operation observes ctx.cancelled() and exits with TaskExit::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:
On every tools/call the override:
  1. Opens a TransactionSlot on the configured pool (if any) and installs it into the request scope — tool bodies read it via current_mcp_transaction().
  2. Installs the ability and principal — current_mcp_ability() / current_mcp_principal().
  3. Dispatches the tool.
  4. 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).
  5. 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 return Result<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("module x not found")).
  • Success: a Json<T> payload — the typed value lands in structuredContent (the #[tool] macro stamps an outputSchema from 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_host const 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]
The parser is strictly additive: missing or unknown attributes are reported as “unrecognized attr” but do not fail the parse. New macros added to nestrs-macros will show up as unrecognized in nestrs-mcp until the parser is updated — that is the intended maintenance surface.

See also