NitroStack
OfficialREADME.md
# NitroStack Python SDK
A Python-idiomatic port of the **NitroStack** Model Context Protocol (MCP) framework, enabling NestJS-like modular architecture, dependency injection, execution pipelines, background task processing, built-in authentication modules, and a diagnostic testing harness.
---
## Features
- **Nested Modular Architecture**: Group components cleanly with `@module`.
- **Dependency Injection**: Explicit class constructor DI with `DIContainer` and `@injectable(deps=[...])`.
- **Pipeline Interceptors**: Build guards, middleware, interceptors, pipes, and exception filters for tool execution.
- **Asynchronous Background Tasks**: Spawn background workers automatically for long-running tools.
- **Built-in Authentication**: Modules for API Keys, JWT verification, and OAuth 2.1 (featuring Protected Resource Metadata discovery servers).
- **In-Process Testing Harness**: Run unit and integration tests against modules without managing subprocesses or real network transports.
- **CLI Tooling (`nitrostack-py`)**: Scaffold apps (`init`), generate components (`generate`), pack deployable wheels (`pack`), upgrade/install dependencies, validate projects, auto-register servers with Claude (`register`), and run hot-reload development servers (`dev`).
---
## Installation
```bash
pip install nitrostack
```
To install local developer or test dependencies:
```bash
pip install -e .
```
---
## Scaffolding a New Project (Recommended)
You can quickly scaffold a new project template using the interactive CLI tool:
```bash
nitrostack-py init
```
*(Or via Python: `python -m nitrostack.cli.main init`)*
The project name is optional on the command line. If omitted, the CLI asks for it next:
```bash
nitrostack-py init my-server --template python-starter
```
This launches an interactive prompt where you can:
1. **Project name** (if not passed as an argument). Default: `my-mcp-server`.
2. **Choose a template** by explicit name:
- **python-starter**: A simple calculator server.
- **python-pizzaz**: A pizza shop finder with maps and widgets.
- **python-oauth**: A flight booking server demonstrating OAuth 2.1 authentication and guarded routes.
3. **Provide metadata**: Specify a custom description and author name.
4. **Install dependencies**: `Install dependencies: (Y/n)` — Enter or `Y` runs `npm install` in `src/widgets`; `n` skips it. `--skip-install` skips the prompt.
Optional port flags override the defaults (**3000** MCP, **3001** widgets):
```bash
nitrostack-py init my-server --template python-starter --port 4000 --widget 4001
nitrostack-py dev --port 4000 --widget 4001
nitrostack-py start --port 4000 --widget 4001
```
Once scaffolded, follow the next steps printed by the CLI to run your server, configure environment variables, and try it out.
---
## CLI (`nitrostack-py`)
The CLI is installed with the SDK (`nitrostack-py`, or `python -m nitrostack.cli.main`). Run `nitrostack-py --help` to list commands.
### Project lifecycle
```bash
nitrostack-py init my-server
nitrostack-py dev # hot-reload development server
nitrostack-py start # production server (no reload)
nitrostack-py register --name my-mcp-server --file app.py
```
### Generate components
Existing `tool` and `module` generators are unchanged. Additional generators create pipeline and service stubs that follow the current Python decorator/protocol APIs:
```bash
nitrostack-py generate tool add_numbers
nitrostack-py generate module payments
nitrostack-py generate guard MyGuard
nitrostack-py generate pipe Validation
nitrostack-py generate interceptor Transform
nitrostack-py generate filter HttpException
nitrostack-py generate service Email
```
Generated files:
| Command | Output |
|---|---|
| `generate tool <name>` | `{name}_tool.py` in the current directory |
| `generate module <name>` | `{name}_module.py` in the current directory |
| `generate guard <Name>` | `guards/<name>.py` |
| `generate pipe <Name>` | `pipes/<name>.py` |
| `generate interceptor <Name>` | `interceptors/<name>.py` |
| `generate filter <Name>` | `filters/<name>.py` |
| `generate service <Name>` | `services/<name>.py` |
Attach generated pipeline classes with `@use_guards`, `@use_pipes`, `@use_interceptors`, or `@use_filters`. Register services in a module's `providers` list.
### Pack a deployable wheel
```bash
nitrostack-py pack --dry-run # list files; does not write an artifact
nitrostack-py pack # write dist/*.whl
```
`pack` builds a wheel with setuptools (the same backend as this SDK), refreshes `requirements.txt` from `pyproject.toml` when possible, and always includes `.env.example`. The real `.env` file and other secrets are never packed. Temporary build directories are deleted afterwards.
### Upgrade, install, validate
```bash
nitrostack-py upgrade # latest nitrostack on PyPI (writes nitrostack>=latest)
nitrostack-py upgrade --version 0.3.2 # pin exactly this version (writes nitrostack==0.3.2)
nitrostack-py upgrade --dry-run # print the change; do not edit files
nitrostack-py upgrade --allow-downgrade --version 0.1.0 # required to pin an older version
nitrostack-py install # install project + development dependencies
nitrostack-py install --production # skip optional extras and requirements-dev.txt
nitrostack-py validate # lint deps, @mcp_app imports, and @module() refs
```
`init` writes `pyproject.toml`, `.python-version`, and `uv.toml`, then runs `uv lock` when `uv` is on PATH. `install` prefers `uv sync` in that case. If `requirements.txt` pins nitrostack as a local path (`-e /path/to/nitrostack-python-sdk`), install uses `pip install -r requirements.txt` instead so unpublished SDK testing still works. The uv equivalent is:
```toml
[tool.uv.sources]
nitrostack = { path = "/path/to/nitrostack-python-sdk", editable = true }
```
then `uv lock` / `uv sync`. Without `uv`, install falls back to `.venv` + `pip`.
`upgrade` updates the `nitrostack` dependency spec in `pyproject.toml` in place (and `requirements.txt` when it already pins nitrostack). `--version X` writes `nitrostack==X`. Without `--version`, the latest PyPI release is written as `nitrostack>=latest`. A target older than the currently declared version is rejected unless `--allow-downgrade` is passed. `validate` reports missing/conflicting dependencies, `@mcp_app` modules that fail to import, and `@module()` `imports`/`exports` that are not real classes.
---
## Quick Start
### 1. Write your First Server
Create a file named `app.py`:
```python
import asyncio
from pydantic import BaseModel, Field
from nitrostack import (
tool,
resource,
injectable,
module,
mcp_app,
McpApplicationFactory,
ServerConfig,
ExecutionContext,
)
# 1. Input Validation Schema
class AddInput(BaseModel):
a: float = Field(description="First number")
b: float = Field(description="Second number")
# 2. Injected Provider Service
@injectable(deps=[])
class CalculatorService:
def add(self, a: float, b: float) -> float:
return a + b
# 3. Controller
@injectable(deps=[CalculatorService])
class CalculatorController:
def __init__(self, service: CalculatorService):
self.service = service
@tool(
name="add",
description="Add two numbers together",
input_schema=AddInput
)
async def add(self, input: AddInput, context: ExecutionContext) -> float:
context.logger.info(f"Adding {input.a} and {input.b}")
return self.service.add(input.a, input.b)
@resource(
uri="calc://info",
name="Calculator Info",
description="Metadata about this calculator"
)
async def get_info(self, context: ExecutionContext) -> str:
return "Simple Add Calculator v1.0.0"
# 4. Modules
@module(
name="calculator",
controllers=[CalculatorController],
providers=[CalculatorService]
)
class CalculatorModule:
pass
@module(
name="app",
imports=[CalculatorModule]
)
class AppModule:
pass
# 5. Application Entrypoint
@mcp_app(
module=AppModule,
server=ServerConfig(name="math-server", version="1.0.0")
)
class App:
pass
async def main():
app = await McpApplicationFactory.create(App)
await app.start()
if __name__ == "__main__":
asyncio.run(main())
```
### 2. Configure Environment Variables
The SDK reads standard settings from the environment or `.env` files:
| Environment Variable | Description |
|---|---|
| `HOST` | Bind address for HTTP/SSE (default: `127.0.0.1`). Set `0.0.0.0` to listen on all interfaces (containers). |
| `TRUSTED_PROXIES` / `MCP_TRUSTED_PROXIES` | Comma-separated IPs, CIDRs, or hostnames allowed to send `X-Forwarded-Host` / `X-Forwarded-Proto`. Unset means those headers are ignored. `X-Forwarded-For` is never used to decide trust. |
| `PORT` / `MCP_SERVER_PORT` | The port to bind for HTTP/SSE transport (default: `3000`). Overridden by `nitrostack-py --port`. |
| `WIDGETS_DEV_PORT` | Widget Next.js port (default: `3001`). Overridden by `nitrostack-py --widget`. |
| `MCP_TRANSPORT_TYPE` | Transport selection: `stdio`, `http`, or `dual` (combining stdio + HTTP/SSE). |
| `NODE_ENV` | If set to `production`, defaults to `dual` transport. Otherwise defaults to `stdio`. |
| `MCP_MAX_SESSIONS` | Cap on concurrent Streamable HTTP sessions; new sessions beyond the cap get an HTTP `429`. Unset = unlimited. |
| `MCP_SESSION_TIMEOUT_MS` | Idle timeout (ms) for stateful HTTP sessions; sessions with no activity for this long are terminated automatically. Unset = no timeout. |
| `MCP_GRACEFUL_SHUTDOWN_TIMEOUT_MS` | How long (ms) the HTTP transport waits for in-flight requests to finish when shutting down (default: `10000`). |
| `NITRO_MCP_PROTOCOL_VERSION` | Protocol era (case-insensitive): `auto` / `both` / `dual` / `dual-spec` (default when unset or unknown), `modern` / `latest` / `2026` / `2026-07-28`, or `legacy` / `2025` / `2025-06-18` / `2025-11-25`. `auto` is not the same as `modern`. Wins over `ServerConfig.protocol_era`. |
| `MCP_STATELESS` | Explicit override: `true` forces `modern` (stateless HTTP), `false` forces `legacy` (sessionful). Wins over `NITRO_MCP_PROTOCOL_VERSION` and `ServerConfig.protocol_era`. |
| `MCP_ALLOWED_HOSTS` / `MCP_ALLOWED_ORIGINS` | Comma-separated allow-lists for DNS-rebinding protection, used only when CORS is disabled. |
| `NITROSTACK_LOG_FILE` | Destination file for logs (default: `nitrostack.log`). |
| `NITROSTACK_LOG_LEVEL` | Log level (`DEBUG`, `INFO`, `WARNING`, `ERROR`). |
| `NITROSTACK_LOG_TO_STDOUT` | Set to `true` to allow logging to stdout under stdio transport (Caution: may corrupt protocol stream). |
| `NITROSTACK_HTTP_DEBUG` | Set to `1` to log every HTTP request (method, path, headers, body) and response status to stderr. Use when diagnosing a client that fails to connect: uvicorn's access log shows neither headers nor timestamps. |
---
## Transport Options
NitroStack apps can run over three transports, selected via `MCP_TRANSPORT_TYPE` (or `ServerConfig(transport_type=...)`):
- **`stdio`** (default outside production): JSON-RPC over stdin/stdout — the standard mode for desktop MCP clients (Claude Desktop, Cursor, etc.).
- **`http`**: Streamable HTTP + legacy SSE over a real network port, for cloud/remote deployments. Exposes:
- `POST/GET/DELETE /mcp` — Streamable HTTP (session-based JSON-RPC + SSE streaming). `/mcp` and `/mcp/` are equivalent; the server does not 307 between them (MCP Inspector needs the no-slash URL for its SSE GET).
- `GET /sse` + `POST /mcp/messages/` — legacy HTTP+SSE for older clients (trailing slash required so messages aren't swallowed by the Streamable HTTP `/mcp` mount)
- `GET /mcp/health` — health check (`status`, active session count, uptime)
- Per-session isolation, idle-session timeouts, and DNS-rebinding protection are provided by the underlying `mcp` SDK's `StreamableHTTPSessionManager`; NitroStack adds CORS, a concurrent-session cap, and the health endpoint on top.
- Task-mode tools that call `context.task.update_progress(...)` push a live `notifications/progress` event over the session's SSE stream (in addition to always being pollable via `tasks/get`) whenever the client sends a `_meta.progressToken` on the `tools/call` request.
- **`dual`** (default in production): runs `stdio` and `http` concurrently as `asyncio` tasks in the same process/event loop — not separate threads — so both share the same `DIContainer` singletons, and uvicorn's signal-based graceful shutdown works correctly (it only installs signal handlers on the main thread). Shutdown is coordinated: either transport stopping (STDIO hitting EOF, or HTTP receiving a termination signal) cleanly stops the other.
Example:
```python
server = ServerConfig(name="my-server", transport_type="http", max_sessions=100, session_timeout_ms=1_800_000)
```
`ServerConfig.protocol_era` is used only when `MCP_STATELESS` and `NITRO_MCP_PROTOCOL_VERSION` are both unset. Unknown tokens become `auto`, same as an unknown env value.
---
## Developing & Testing
### Auto-Registering with Claude Desktop
To automatically configure your server script with Claude Desktop without any manual editing:
```bash
nitrostack-py register --name my-mcp-server --file app.py
```
*(If your scripts folder is not in PATH, use: `python -m nitrostack.cli.main register --name my-mcp-server --file app.py`)*
This detects all standard and Windows Store installation directories, sets up virtualenv executables, and writes the JSON configuration. Once registered, simply restart Claude Desktop.
### Widgets (UI tools)
Bind a static HTML template to a tool with `@widget` and return domain JSON from the handler:
```python
from nitrostack import tool, widget, ExecutionContext
@tool(name="show_card", description="Product card", input_schema=CardInput)
@widget("card")
async def show_card(self, input: CardInput, context: ExecutionContext) -> dict:
return {"name": "Widget", "price": 9.99}
```
Place HTML at `widgets/out/{route}.html` (e.g. `widgets/out/card.html`). The SDK registers
`ui://widget/card.html` as an MCP resource and sets mode-gated `_meta` on `tools/list` and
`tools/call` results.
**`NITROSTACK_APP_MODE`** (default `universal`):
| Mode | Tool `_meta` | Resource MIME |
|------|----------------|---------------|
| `universal` (default) | Both OpenAI and MCP Apps keys | `text/html;profile=mcp-app` |
| `openai` | `openai/outputTemplate`, `ui/template` | `text/html` |
| `mcp-app` | `_meta.ui` (`resourceUri`, `visibility`, CSP) | `text/html;profile=mcp-app` |
Object form for CSP and border options:
```python
from nitrostack import WidgetOptions, WidgetCsp, widget
@widget(WidgetOptions(
route="chart",
prefers_border=True,
csp=WidgetCsp(connect_domains=["https://api.example.com"]),
))
```
`nitrostack-py init` copies `widgets/out/{route}.html` for every `@widget`. Widget HTML
is generated in Python from the tool's `structuredContent` (one iframe, N cards).
MCP Inspector: use **HTTP + stateless**, then the **Apps** tab. `tools/call` also embeds
the data-filled HTML. Do not use `widgets/preview.html` as the live result — that file
is a static helper. Live preview: `http://localhost:3000/widgets/preview`.
Turn **Authentication off** in Inspector. Pizzaz/starter have no OAuth. If Auth is on,
Inspector POSTs `/register` and you will see `Cannot POST /register` / `Unexpected token '<'`.
Connect Streamable HTTP to `http://localhost:3000/mcp` (no trailing slash).
For **open pizza shops only**, call `show_pizza_list` with `{"openNow": true}` or
`show_pizza_map` with `{"filter": "open_now"}`. Omitting those fields returns every shop,
including closed ones (Pizzeria Delfina).
**NitroStudio:** folder-connect looks for a TypeScript project (`package.json` with
`@nitrostack/core` and `src/index.ts`). A Python server will not detect. Keep using
MCP Inspector over HTTP, or point Studio at a custom Streamable HTTP URL if the build
supports it. Do not enable OAuth against this server.
Example server: `examples/widgets_example.py` with templates in `examples/widgets/out/`.
For MCP Inspector over HTTP, use stateless mode:
```bash
cd examples
MCP_TRANSPORT_TYPE=http MCP_STATELESS=true NITROSTACK_APP_MODE=universal python widgets_example.py
```
### Running Tests
To run the automated test suite, execute:
```bash
python tests/test_basic.py
python tests/test_tasks.py
python tests/test_initial_tool.py
python tests/test_transports.py
python tests/test_widgets.py
python tests/test_widget_metadata.py
python tests/test_pizzaz_widgets.py
python tests/test_template_widgets.py
python tests/test_cli.py
pytest tests/test_cli.py -v
python tests/test_tool_input_schema.py
```
### Testing Harness
Write in-process unit tests using the harness:
```python
import asyncio
from nitrostack.testing import NitroTestingModule
from app import AppModule
async def test_add():
harness = await NitroTestingModule.create(AppModule)
result = await harness.call_tool("add", {"input": {"a": 5, "b": 10}})
assert result == 15.0
print("Test passed!")
if __name__ == "__main__":
asyncio.run(test_add())
```
This server cannot be deployed
Maintenance
ActivityActive
ResponsivenessUnresponsive