From 1c0799031d6e6e539f04136cc6075897d4bc1fa1 Mon Sep 17 00:00:00 2001 From: Hemant Jadhav Date: Tue, 8 Sep 2026 18:15:54 +0530 Subject: [PATCH] feat(skills): improve and complete Python SDK skills documentation --- skills/auth-security/SKILL.md | 194 ++++++++++++++++-- skills/mcp-app-architecture/SKILL.md | 159 ++++++++++++--- skills/middleware-pipeline/SKILL.md | 248 +++++++++++++++++++++--- skills/tools-resources-prompts/SKILL.md | 245 ++++++++++++++++++----- skills/ui-widgets/SKILL.md | 123 +++++++++--- 5 files changed, 823 insertions(+), 146 deletions(-) diff --git a/skills/auth-security/SKILL.md b/skills/auth-security/SKILL.md index 2a0ea9a..796d0e6 100644 --- a/skills/auth-security/SKILL.md +++ b/skills/auth-security/SKILL.md @@ -1,16 +1,150 @@ --- name: nitrostack-python-auth-security -description: OAuth 2.1, OAuthModule, OAuthGuard, and scope guards in NitroStack Python MCP servers. +description: Best practices for implementing JWT, API Keys, OAuth 2.1, RBAC, and Scope Guards in NitroStack Python MCP servers. --- ## When to Use +Use this skill when configuring security modules, implementing user authentication, protecting Python tools/resources with guards, verifying JWTs or API keys, or securing endpoints with OAuth 2.1 and PKCE. -Protecting Python NitroStack tools (oauth template, Auth0/introspection). Not the TypeScript `OAuthModule` Nest package. +--- + +## 1. JSON Web Tokens (JWT) + +Use `JWTModule` for stateless token-based authentication. + +### Register `JWTModule` on `AppModule`: +```python +from nitrostack import module, ConfigModule, JWTModule +import os + +@module( + name="app", + imports=[ + ConfigModule.for_root(env_file_path=".env"), + JWTModule.for_root( + secret_env_var="JWT_SECRET", # Reads secret from os.environ["JWT_SECRET"] + expires_in="24h", # Token expiration window (e.g., '1h', '24h', '7d') + audience=os.environ.get("TOKEN_AUDIENCE"), + issuer=os.environ.get("TOKEN_ISSUER"), + ), + UserModule, + ], +) +class AppModule: + pass +``` + +### Protect Tools with `JwtGuard`: +`JwtGuard` automatically extracts the `Bearer ` from metadata/headers, verifies the signature, and populates `context.auth` (`AuthContext` containing `subject`, `scopes`, `client_id`, `claims`, etc.). + +```python +from nitrostack import injectable, tool, use_guards, JwtGuard, ExecutionContext +from pydantic import BaseModel, Field + +class UserProfileInput(BaseModel): + include_preferences: bool = Field(default=False) + +@injectable() +class UserTools: + @tool( + name="get_my_profile", + description="Fetch the authenticated user profile", + input_schema=UserProfileInput, + ) + @use_guards(JwtGuard) + async def get_my_profile(self, input: UserProfileInput, context: ExecutionContext) -> dict: + user_id = context.auth.subject + roles = context.auth.claims.get("roles", []) + return {"user_id": user_id, "roles": roles} +``` -## Wire OAuth on the root module +--- + +## 2. API Key Authentication -The `python-oauth` template imports `OAuthModule.for_root(...)` next to `ConfigModule` and the feature module. Values come from `.env` (`RESOURCE_URI`, `AUTH_SERVER_URL`, introspection client, audience, issuer). Follow `OAUTH_SETUP.md` in the generated project. +Use `ApiKeyModule` for service-to-service validation. +### Register `ApiKeyModule`: +```python +from nitrostack import module, ApiKeyModule + +@module( + name="app", + imports=[ + ApiKeyModule.for_root( + keys_env_prefix="API_KEY", # Reads API_KEY, API_KEY_1, API_KEY_2, etc. + header_name="x-api-key", # Header to inspect in request metadata + hashed=False, # Set True if stored keys are SHA-256 hashes + ), + SystemModule, + ], +) +class AppModule: + pass +``` + +### Protect Tools with `ApiKeyGuard`: +`ApiKeyGuard` checks for the API key in `context.metadata["x-api-key"]` or `context.metadata["headers"]["x-api-key"]` and validates it against `ApiKeyService`. + +```python +from nitrostack import injectable, tool, use_guards, ApiKeyGuard, ExecutionContext +from pydantic import BaseModel + +class MetricsInput(BaseModel): + category: str + +@injectable() +class SystemTools: + @tool( + name="fetch_system_metrics", + description="Fetch internal system metrics (API Key required)", + input_schema=MetricsInput, + ) + @use_guards(ApiKeyGuard) + async def fetch_metrics(self, input: MetricsInput, context: ExecutionContext) -> dict: + return {"status": "ok", "cpu_load": 0.42} +``` + +--- + +## 3. Role-Based Access Control (RBAC) & Custom Guards + +Custom guards implement the `Guard` protocol with `async def can_activate(self, context: ExecutionContext) -> bool`. Chain authentication guards before authorization guards using `@use_guards(...)`. + +```python +from nitrostack import injectable, tool, use_guards, JwtGuard, ExecutionContext +from pydantic import BaseModel + +@injectable() +class AdminGuard: + async def can_activate(self, context: ExecutionContext) -> bool: + # Requires JwtGuard / OAuthGuard to have populated context.auth first + if not context.auth: + return False + return context.auth.claims.get("role") == "admin" + +class ResetDbInput(BaseModel): + confirm: bool + +@injectable() +class AdminTools: + @tool( + name="reset_database", + description="Wipes database. Admin only.", + input_schema=ResetDbInput, + ) + @use_guards(JwtGuard, AdminGuard) # First authenticate with JWT, then check Admin role + async def reset_database(self, input: ResetDbInput, context: ExecutionContext) -> dict: + return {"success": True, "initiated_by": context.auth.subject} +``` + +--- + +## 4. OAuth 2.1 & Scopes + +Use `OAuthModule` for OAuth 2.1 protected resource metadata discovery and token introspection. + +### Wire `OAuthModule` on `AppModule`: ```python from nitrostack import module, ConfigModule, OAuthModule import os @@ -31,32 +165,52 @@ import os ), FlightsModule, ], - providers=[SystemHealthCheck], ) class AppModule: pass ``` -## Protect tools - +### Scope Protection with `require_scopes` or Custom Scope Guards: ```python -from nitrostack import use_guards, OAuthGuard -from guards.oauth_guard import create_scope_guard +from nitrostack import injectable, tool, use_guards, OAuthGuard, require_scopes, ExecutionContext +from pydantic import BaseModel + +class FlightSearchInput(BaseModel): + origin: str + destination: str -@use_guards(OAuthGuard, create_scope_guard(["read"])) +@injectable() +class FlightTools: + @tool( + name="search_flights", + description="Search available flights", + input_schema=FlightSearchInput, + ) + @use_guards(OAuthGuard) + @require_scopes("read") + async def search_flights(self, input: FlightSearchInput, context: ExecutionContext) -> dict: + return {"flights": []} ``` -`OAuthGuard`: +### Behavior of `OAuthGuard`: +- Reads Bearer token from `context.metadata` (`authorization`, `headers`, or `_oauth`). +- If **no token** and `OAUTH_REQUIRED` is not `true` → allows the call (useful for local development in Studio/Inspector). +- If **no token** and `OAUTH_REQUIRED=true` → raises `PermissionError`. +- If token present → validates token via `OAuthService.introspect_token` and populates `context.auth` (`subject`, `scopes`, `aud` as a normalized `list`, `claims`). -- Reads Bearer token from metadata/headers (HTTP `Authorization` is forwarded into execution metadata). -- If **no token** and `OAUTH_REQUIRED` is not true → allow (local Studio/Inspector). -- If **no token** and `OAUTH_REQUIRED=true` → `PermissionError`. -- If token present → `OAuthService.introspect_token`; populate `context.auth` (`subject`, `scopes`, `aud` as a list, claims). +--- + +## 5. Scope & PKCE Utility Functions -`create_scope_guard(["read"])` no-ops when oauth is not required; otherwise requires those scopes on `context.auth.scopes`. +NitroStack provides built-in helper functions in `nitrostack`: +- `has_scope(context.auth, "read")` -> `bool` +- `has_all_scopes(context.auth, ["read", "write"])` -> `bool` +- `has_any_scope(context.auth, ["admin", "write"])` -> `bool` +- `generate_code_verifier()`, `generate_code_challenge(verifier)`, `verify_pkce(verifier, challenge)` -## Do not +--- -- Copy TypeScript `OAuthGuard` / Passport strategies. -- Check `"aud" in context.auth.aud` assuming `aud` is a string — it is normalized to a list. -- Put secrets in skill files or commit `.env`. +## Do Not +- Do not copy TypeScript `@nitrostack/core` decorator syntax or Passport strategies. +- Do not assume `context.auth.aud` is a string; NitroStack normalizes `aud` to a `list` to ensure safe membership checks (`"aud" in context.auth.aud`). +- Do not commit `.env` or hardcode secrets into source files. diff --git a/skills/mcp-app-architecture/SKILL.md b/skills/mcp-app-architecture/SKILL.md index 67fcc2f..7b9e531 100644 --- a/skills/mcp-app-architecture/SKILL.md +++ b/skills/mcp-app-architecture/SKILL.md @@ -1,16 +1,18 @@ --- name: nitrostack-python-mcp-app-architecture -description: Bootstrapping a NitroStack Python MCP server — AppModule, modules, DI, and McpApplicationFactory. +description: Bootstrapping a NitroStack Python MCP server — AppModule, modules, DI, events, lifecycles, and McpApplicationFactory. --- ## When to Use +Use this skill when creating, structuring, or refactoring a NitroStack **Python** MCP server: configuring the root module, organizing feature modules, managing dependencies via DI, emitting/handling events, registering health checks, or configuring server transports. -Use this skill when creating or changing a NitroStack **Python** MCP server: root module, feature modules, providers, or process startup. Do not use TypeScript `@McpApp`, `@Module`, or Zod. +--- -## Bootstrapping +## 1. Bootstrapping an Application -Generated apps start from `main.py` with `McpApplicationFactory.create` and the root `AppModule`. There is no required `@mcp_app` class in the starter templates. +NitroStack Python applications can be started using `McpApplicationFactory.create(...)` with the root module or `@mcp_app` class. +### Standard Entrypoint (`main.py`): ```python import asyncio from nitrostack import McpApplicationFactory @@ -24,9 +26,38 @@ if __name__ == "__main__": asyncio.run(main()) ``` -Optional: `@mcp_app(module=AppModule, server=ServerConfig(name="...", version="1.0.0"))` exists on a class, but factory-from-module is the template path. +### With ServerConfig & Transport Options (`@mcp_app`): +```python +import asyncio +from nitrostack import mcp_app, McpApplicationFactory, ServerConfig +from app_module import AppModule + +@mcp_app( + module=AppModule, + server=ServerConfig( + name="my-mcp-server", + version="1.0.0", + transport_type="stdio", # Options: "stdio", "http", or "dual" (runs stdio + http together) + max_sessions=100, # Max concurrent HTTP sessions + session_timeout_ms=1800000, # 30 min idle timeout + ), +) +class App: + pass + +async def main(): + app = await McpApplicationFactory.create(App) + await app.start() + +if __name__ == "__main__": + asyncio.run(main()) +``` + +--- + +## 2. Root AppModule & Configuration -## Root AppModule +The root module aggregates imported feature modules, global services, and configuration. ```python from nitrostack import module, ConfigModule @@ -45,45 +76,127 @@ class AppModule: pass ``` -`@module(...)` takes `name`, `controllers`, `providers`, `imports`, `exports`. Feature tools go on **controllers**. Injectable services go on **providers**. `imports` pull in other modules (and `ConfigModule` / `OAuthModule`). +--- -## Feature modules +## 3. Feature Modules & Separation of Concerns -Put `@tool` / `@resource` / `@prompt` classes in `controllers`. Put services the controllers construct in `providers` and `exports` if other modules need them. +Modules organize tools and services into isolated domain boundaries. +- **`controllers`**: Classes containing `@tool`, `@resource`, or `@prompt` definitions. +- **`providers`**: Services, repositories, or helpers managed by the DI container. +- **`imports`**: Other modules whose exported providers are needed here. +- **`exports`**: Providers from this module made available to other modules that import it. ```python from nitrostack import module from modules.calculator.calculator_tools import CalculatorTools +from modules.calculator.calculator_service import CalculatorService @module( name="calculator", controllers=[CalculatorTools], - providers=[], - exports=[], + providers=[CalculatorService], + exports=[CalculatorService], ) class CalculatorModule: pass ``` -## Dependency injection +--- + +## 4. Dependency Injection (DI) -Mark classes `@injectable()` or `@injectable(deps=[SomeService])`. Constructor args must match `deps`. The container instantiates controllers and providers; do not `SomeService()` by hand inside a tool class. +Mark classes with `@injectable()` or `@injectable(deps=[...])`. Constructor parameters must match the declared dependencies in `deps`. ```python from nitrostack import injectable -@injectable(deps=[PizzazService]) -class PizzazTools: - def __init__(self, service: PizzazService): - self.service = service +@injectable() +class DatabaseService: + def query(self, sql: str) -> list: + return [] + +@injectable(deps=[DatabaseService]) +class UserService: + def __init__(self, db: DatabaseService): + self.db = db + +@injectable(deps=[UserService]) +class UserTools: + def __init__(self, user_service: UserService): + self.user_service = user_service ``` -## Health checks +> [!NOTE] +> The DI container automatically resolves and instantiates singletons for all controllers and providers registered in active modules. Do not instantiate `@injectable` services manually inside tool methods. -Register a provider with `@health_check("system")` on a method that returns a bool (see `health/system_health.py`). +--- + +## 5. Event System (`EventEmitter` and `@on_event`) + +NitroStack includes an asynchronous internal event system to decouple services and modules. + +### Emitting Events: +```python +from nitrostack import injectable, EventEmitter + +@injectable() +class OrderService: + async def place_order(self, order_id: str, amount: float): + # Process order logic... + + # Emit event to all registered listeners + await EventEmitter.get_instance().emit( + "order.placed", + {"order_id": order_id, "amount": amount} + ) +``` + +### Listening to Events with `@on_event`: +Decorate any method inside an `@injectable` provider or controller with `@on_event("event_name")`: + +```python +from nitrostack import injectable, on_event + +@injectable() +class NotificationService: + @on_event("order.placed") + async def on_order_placed(self, payload: dict): + order_id = payload.get("order_id") + print(f"Sending confirmation email for order {order_id}") +``` + +--- + +## 6. Health Checks (`@health_check`) + +Register health checks on providers or controllers to expose system status on the `/mcp/health` endpoint: + +```python +from nitrostack import injectable, health_check + +@injectable() +class SystemHealthCheck: + @health_check("database") + async def check_database(self) -> bool: + # Return True for healthy, False for unhealthy + return True +``` -## Do not +--- + +## 7. CLI Lifecycle (`nitrostack-py`) + +- `nitrostack-py init [name] [--template python-starter|python-pizzaz|python-oauth]` +- `nitrostack-py dev [--port 3000] [--widget 3001]` (hot reload) +- `nitrostack-py start` (production runner) +- `nitrostack-py generate tool|module|guard|pipe|interceptor|filter|service ` +- `nitrostack-py pack` (builds deployable wheel in `dist/`) +- `nitrostack-py validate` (lints imports, modules, and dependencies) +- `nitrostack-py register --name my-server --file app.py` (registers with Claude Desktop) + +--- -- Import `@nitrostack/core` or write TypeScript decorators. -- Put `@tool` methods on a module class that is only a `@module` config holder. -- Skip `ConfigModule.for_root` if the app reads `.env`. +## Do Not +- Do not import `@nitrostack/core` or use TypeScript `@Module` syntax. +- Do not define `@tool` methods directly on module configuration classes. Put them on controller classes. +- Do not instantiate services manually with `SomeService()`; use constructor DI via `@injectable(deps=[...])`. diff --git a/skills/middleware-pipeline/SKILL.md b/skills/middleware-pipeline/SKILL.md index 65606ad..dd5a4b2 100644 --- a/skills/middleware-pipeline/SKILL.md +++ b/skills/middleware-pipeline/SKILL.md @@ -1,49 +1,243 @@ --- name: nitrostack-python-middleware-pipeline -description: Guards, middleware, interceptors, pipes, and exception filters on NitroStack Python tools. +description: Best practices for implementing and applying Guards, Interceptors, Middleware, Pipes, and Exception Filters in NitroStack Python MCP servers. --- ## When to Use +Use this skill when implementing request authorization, input transformation, execution timing, error handling, or logging on NitroStack Python tools, resources, and prompts. -Authorizing or transforming a tool/resource/prompt call in the **Python** SDK (`nitrostack.core.pipeline`). +--- + +## Pipeline Execution Order + +When an MCP client invokes a tool handler, NitroStack executes the pipeline in the following sequence: +1. **Guards** (`@use_guards`): Verify authentication/authorization (`can_activate`). +2. **Pipes** (`@use_pipes`): Transform/validate raw inputs (`transform`). +3. **Middleware** (`@use_middleware`): Wrap execution before/after handler (`use`). +4. **Interceptors** (`@use_interceptors`): Intercept or transform results (`intercept`). +5. **Handler**: The actual `@tool` async method execution. +6. **Exception Filters** (`@use_filters`): Catch and map any uncaught exceptions into user-friendly responses (`catch`). + +--- + +## 1. Guards (`Guard` and `@use_guards`) + +Guards control access before reaching pipes or handlers. + +### Protocol: +```python +from nitrostack import ExecutionContext + +class Guard: + async def can_activate(self, context: ExecutionContext) -> bool: + ... +``` + +### Example: +```python +from nitrostack import injectable, ExecutionContext + +@injectable() +class RolesGuard: + async def can_activate(self, context: ExecutionContext) -> bool: + user_roles = context.auth.claims.get("roles", []) if context.auth else [] + return "admin" in user_roles +``` + +### Usage on Tools: +```python +from nitrostack import injectable, tool, use_guards, ExecutionContext +from pydantic import BaseModel + +class PurgeLogsInput(BaseModel): + before_date: str + +@injectable() +class LogTools: + @tool(name="purge_logs", description="Purge system logs", input_schema=PurgeLogsInput) + @use_guards(RolesGuard) + async def purge_logs(self, input: PurgeLogsInput, context: ExecutionContext) -> dict: + return {"purged": True} +``` + +--- + +## 2. Interceptors (`Interceptor` and `@use_interceptors`) + +Interceptors wrap handler execution to measure duration, modify results, or log operations. + +### Protocol: +```python +from typing import Callable, Any +from nitrostack import ExecutionContext + +class Interceptor: + async def intercept(self, context: ExecutionContext, next_fn: Callable[[], Any]) -> Any: + ... +``` + +### Example: +```python +import time +from nitrostack import injectable, ExecutionContext + +@injectable() +class TimingInterceptor: + async def intercept(self, context: ExecutionContext, next_fn): + start = time.time() + result = await next_fn() + duration_ms = (time.time() - start) * 1000 + context.logger.info(f"Tool {context.tool_name} executed in {duration_ms:.2f}ms") + if isinstance(result, dict): + result["_meta"] = {"duration_ms": duration_ms} + return result +``` + +--- -## Stacking on a handler +## 3. Exception Filters (`ExceptionFilter` and `@use_filters`) -Decorators attach lists on the function: `_mcp_guards`, `_mcp_middleware`, `_mcp_interceptors`, `_mcp_pipes`, `_mcp_filters`. +Exception filters catch uncaught errors thrown anywhere in the execution chain and format clean error responses. +### Protocol: ```python -from nitrostack import tool, use_guards, OAuthGuard, ExecutionContext -from guards.oauth_guard import create_scope_guard +from nitrostack import ExecutionContext -@tool(name="search_flights", description="...", input_schema=SearchFlightsInput) -@use_guards(OAuthGuard, create_scope_guard(["read"])) -async def search_flights(self, input: SearchFlightsInput, context: ExecutionContext) -> dict: - ... +class ExceptionFilter: + async def catch(self, error: Exception, context: ExecutionContext) -> Any: + ... ``` -Same pattern: `use_middleware`, `use_interceptors`, `use_pipes`, `use_filters`. +### Example: +```python +from nitrostack import injectable, ExecutionContext + +@injectable() +class CustomExceptionFilter: + async def catch(self, error: Exception, context: ExecutionContext) -> dict: + context.logger.error(f"Captured exception: {error}") + return { + "error": True, + "message": str(error), + "tool": context.tool_name, + } +``` + +--- + +## 4. Middleware (`Middleware` and `@use_middleware`) + +Middleware executes before and after the handler by calling `await next_fn()`. + +### Protocol: +```python +from typing import Callable, Any +from nitrostack import ExecutionContext -## Protocols +class Middleware: + async def use(self, context: ExecutionContext, next_fn: Callable[[], Any]) -> Any: + ... +``` -Implement these as classes (often returned from a factory): +### Example: +```python +from nitrostack import injectable, ExecutionContext + +@injectable() +class LoggingMiddleware: + async def use(self, context: ExecutionContext, next_fn): + context.logger.info(f"--> Entering tool: {context.tool_name}") + try: + result = await next_fn() + context.logger.info(f"<-- Exiting tool: {context.tool_name}") + return result + except Exception as exc: + context.logger.error(f"Error in tool {context.tool_name}: {exc}") + raise exc +``` -- **Guard** — `async def can_activate(self, context: ExecutionContext) -> bool` -- **Middleware** — `async def use(self, context, next_fn)` -- **Interceptor** — `async def intercept(self, context, next_fn)` -- **Pipe** — `async def transform(self, value, metadata: PipeMetadata)` -- **ExceptionFilter** — `async def catch(self, error, context)` +--- -Scaffold with `nitrostack-py generate guard|pipe|interceptor|filter `. +## 5. Pipes (`Pipe` and `@use_pipes`) -## Built-ins +Pipes transform or validate tool input arguments before they reach the handler. -- `ApiKeyGuard` — `x-api-key` in metadata/headers; `ApiKeyService` or `API_KEY` env. -- `JwtGuard` — `Authorization: Bearer`; fills `context.auth`. -- `OAuthGuard` — introspects the access token; see the auth-security skill. +### Protocol: +```python +from typing import Any +from nitrostack.core.pipeline import PipeMetadata -Read tokens from `context.metadata` (`authorization`, `headers`, `_oauth`). Do not assume Express/Nest request objects. +class Pipe: + async def transform(self, value: Any, metadata: PipeMetadata) -> Any: + ... +``` -## Do not +### Example: +```python +from nitrostack import injectable +from nitrostack.core.pipeline import PipeMetadata + +@injectable() +class TrimStringPipe: + async def transform(self, value: Any, metadata: PipeMetadata) -> Any: + if isinstance(value, str): + return value.strip() + if hasattr(value, "__dict__"): + for k, v in value.__dict__.items(): + if isinstance(v, str): + setattr(value, k, v.strip()) + return value +``` + +--- + +## Stacking Multiple Pipeline Decorators + +```python +from nitrostack import ( + injectable, + tool, + use_guards, + use_pipes, + use_middleware, + use_interceptors, + use_filters, + ExecutionContext, +) + +@injectable() +class SecureOperationTools: + @tool(name="execute_task", description="Executes sensitive task", input_schema=TaskInput) + @use_guards(RolesGuard) + @use_pipes(TrimStringPipe) + @use_middleware(LoggingMiddleware) + @use_interceptors(TimingInterceptor) + @use_filters(CustomExceptionFilter) + async def execute_task(self, input: TaskInput, context: ExecutionContext) -> dict: + return {"status": "completed"} +``` + +--- + +## Built-in Guards +- **`ApiKeyGuard`**: Validates `x-api-key` metadata/headers with `ApiKeyService`. +- **`JwtGuard`**: Validates Bearer JWTs and populates `context.auth`. +- **`OAuthGuard`**: Introspects OAuth 2.1 access tokens and populates `context.auth`. + +--- + +## CLI Generators +Generate boilerplate classes matching the Python SDK signatures: +```bash +nitrostack-py generate guard AdminGuard +nitrostack-py generate pipe SanitizeInput +nitrostack-py generate interceptor Metrics +nitrostack-py generate filter GlobalException +``` + +--- -- Use TypeScript `UseGuards()` / Nest middleware. -- Fail closed on missing OAuth when `OAUTH_REQUIRED` is unset — `OAuthGuard` allows no-token when oauth is not required (Studio mock flights). +## Do Not +- Do not use TypeScript NestJS decorator names (`@UseGuards()` in camelCase). Use snake_case `use_guards`, `use_middleware`, etc. +- Do not forget `await next_fn()` in middleware and interceptors. +- Do not access raw HTTP `Request` objects; use `context.metadata` and `context.auth`. diff --git a/skills/tools-resources-prompts/SKILL.md b/skills/tools-resources-prompts/SKILL.md index 08d4826..651bb73 100644 --- a/skills/tools-resources-prompts/SKILL.md +++ b/skills/tools-resources-prompts/SKILL.md @@ -1,83 +1,236 @@ --- name: nitrostack-python-tools-resources-prompts -description: Define MCP tools, resources, and prompts in NitroStack Python with Pydantic schemas and ExecutionContext. +description: Guidelines and patterns for defining Tools, Resources, Prompts, Caching, Rate Limiting, Tasks, and File Uploads in NitroStack Python MCP servers. --- ## When to Use +Use this skill when defining, validating, or optimizing Tools (`@tool`), Resources (`@resource`), and Prompts (`@prompt`) on a NitroStack Python MCP server using Pydantic schemas, execution context, caching, rate limiting, and background tasks. -Defining, editing, or validating tools, resources, or prompts on a **Python** NitroStack server. Use Pydantic `BaseModel`, not Zod. +--- + +## 1. Defining Tools (`@tool`) -## Tools +An MCP tool exposes a callable function to AI clients. Decorate async methods on an `@injectable` controller class with `@tool`. -Decorate **async methods on an `@injectable` class** (listed in the module’s `controllers`). Signature is `(self, input: Model, context: ExecutionContext)`. +### Tool Options: +* `name` (required): Unique snake_case string identifier. +* `description` (required): Clear description guiding the LLM when to invoke the tool. +* `input_schema` (required): Pydantic `BaseModel` class defining input schema. +* `output_schema` (optional): Pydantic model validating output payload structure. +* `annotations` (optional): `ToolAnnotations(destructive_hint=..., read_only_hint=..., idempotent_hint=..., open_world_hint=...)`. +* `task_support` (optional): `"forbidden"`, `"optional"`, or `"required"` for long-running task processing. +* `visibility` (optional): `"visible"` or `"hidden"`. +### Example: ```python -from nitrostack import injectable, tool, widget, ExecutionContext +from nitrostack import injectable, tool, initial_tool, ToolAnnotations, ExecutionContext from pydantic import BaseModel, Field from typing import Literal -class CalculateInput(BaseModel): - operation: Literal["add", "subtract", "multiply", "divide"] = Field(description="The operation to perform") - a: float = Field(description="First number") - b: float = Field(description="Second number") +class WeatherInput(BaseModel): + city: str = Field(description="City name, e.g., San Francisco") + unit: Literal["celsius", "fahrenheit"] = Field(default="celsius", description="Temperature unit") + +class WeatherOutput(BaseModel): + temperature: float + condition: str + +@injectable() +class WeatherTools: + @tool( + name="get_current_weather", + description="Get current weather conditions for a given city.", + input_schema=WeatherInput, + output_schema=WeatherOutput, + annotations=ToolAnnotations(read_only_hint=True), + ) + @initial_tool # Optional: Auto-invoked when client initializes/connects + async def get_weather(self, input: WeatherInput, context: ExecutionContext) -> dict: + context.logger.info(f"Fetching weather for {input.city}") + return {"temperature": 21.5, "condition": "Sunny"} +``` + +--- + +## 2. Tool Policies: Caching (`@cache`) & Rate Limiting (`@rate_limit`) + +Control performance and throttle requests with method decorators from `nitrostack`. + +### Caching (`@cache`): +Caches method outputs for a given TTL in seconds. Skips `ExecutionContext` when building cache keys. +```python +from nitrostack import injectable, tool, cache, ExecutionContext +from pydantic import BaseModel + +class StatusInput(BaseModel): + system_id: str + +@injectable() +class DiagnosticTools: + @tool(name="get_system_status", description="Get diagnostic metrics", input_schema=StatusInput) + @cache(ttl=60) # Caches result for 60 seconds + async def get_status(self, input: StatusInput, context: ExecutionContext) -> dict: + return {"system_id": input.system_id, "healthy": True} +``` + +### Rate Limiting (`@rate_limit`): +Restricts tool execution frequency to a maximum number of calls within a time window (in seconds). +```python +from nitrostack import injectable, tool, rate_limit, ExecutionContext +from pydantic import BaseModel + +class DiagnosticInput(BaseModel): + deep_scan: bool = False + +@injectable() +class MaintenanceTools: + @tool(name="run_diagnostics", description="Runs heavy diagnostics", input_schema=DiagnosticInput) + @rate_limit(max=5, window=60) # Maximum 5 calls per 60 seconds + async def run_diagnostics(self, input: DiagnosticInput, context: ExecutionContext) -> dict: + return {"scan_complete": True} +``` + +--- + +## 3. Asynchronous Background Tasks + +For long-running tools, set `task_support="optional"` or `"required"` and use `context.task` for progress reporting and cancellation checks. + +```python +import asyncio +from nitrostack import injectable, tool, ExecutionContext +from pydantic import BaseModel, Field + +class DataProcessingInput(BaseModel): + dataset_url: str = Field(description="URL of dataset to process") @injectable() -class CalculatorTools: +class DataPipelineTools: @tool( - name="calculate", - description="Perform basic arithmetic calculations", - input_schema=CalculateInput, - output_schema=CalculateOutput, # optional Pydantic model + name="process_large_dataset", + description="Processes large dataset asynchronously", + input_schema=DataProcessingInput, + task_support="optional", # or "required" ) - @widget("calculator-result") - async def calculate(self, input: CalculateInput, context: ExecutionContext) -> dict: - context.logger.info(f"{input.operation} {input.a} {input.b}") - return {"result": input.a + input.b, ...} + async def process_dataset(self, input: DataProcessingInput, context: ExecutionContext) -> dict: + total_steps = 5 + for step in range(1, total_steps + 1): + # Check if client cancelled the task + if context.task: + context.task.throw_if_cancelled() + context.task.update_progress(f"Processing step {step}/{total_steps}...") + + await asyncio.sleep(1) # Simulate processing chunk + + return {"status": "completed", "dataset": input.dataset_url} ``` -`@tool` keyword args: `name`, `description`, `input_schema` (required), plus optional `title`, `output_schema`, `annotations`, `task_support`, `visibility`, `examples` (`ToolExamples`), `invocation`, `metadata`. +--- + +## 4. Handling File Uploads in Tools (Base64) -Stack `@initial_tool` (before or after `@tool`) to auto-invoke on client connect. +NitroStack tools receive client file uploads as base64-encoded strings within Pydantic schemas. -Inspector sends empty strings for unused optionals; use Pydantic `field_validator(..., mode="before")` to coerce `""` to defaults when needed. +### 1. Define Input Schema: +```python +from pydantic import BaseModel, Field -## Resources +class FileUploadInput(BaseModel): + file_name: str = Field(description="Original file name, e.g., document.pdf") + file_type: str = Field(description="MIME type, e.g., application/pdf") + file_content: str = Field(description="Base64 encoded file content") +``` +### 2. Universal Base64 Decoder & Secure File Storage: ```python -from nitrostack import injectable, resource, ExecutionContext +import base64 +import os +import re +from pathlib import Path +from nitrostack import injectable, tool, ExecutionContext + +UPLOAD_DIR = Path(os.getcwd()) / "uploads" + +def decode_base64_file(content: str) -> bytes: + """Decodes Data URL or raw base64 string into bytes.""" + data_url_match = re.match(r"^data:[^;]+;base64,(.+)$", content) + raw_data = data_url_match.group(1) if data_url_match else content + return base64.b64decode(raw_data) + +@injectable() +class FileTools: + @tool(name="upload_document", description="Save uploaded file securely", input_schema=FileUploadInput) + async def upload_document(self, input: FileUploadInput, context: ExecutionContext) -> dict: + UPLOAD_DIR.mkdir(parents=True, exist_ok=True) + + # Prevent directory traversal attacks + safe_filename = Path(input.file_name).name + destination = (UPLOAD_DIR / safe_filename).resolve() + if not str(destination).startswith(str(UPLOAD_DIR.resolve())): + raise ValueError("Invalid file path (path traversal detected).") + + file_bytes = decode_base64_file(input.file_content) + destination.write_bytes(file_bytes) + + context.logger.info(f"Saved file {safe_filename} ({len(file_bytes)} bytes)") + return {"success": True, "saved_path": str(destination), "bytes": len(file_bytes)} +``` + +--- -@injectable(deps=[DuffelService]) -class FlightResources: - def __init__(self, service: DuffelService): - self.service = service +## 5. Defining Resources (`@resource`) + +Resources expose data or files at custom URI schemes that AI clients can inspect. + +```python +from nitrostack import injectable, resource, ResourceAnnotations, ExecutionContext +@injectable() +class ServerResources: @resource( - uri="flight://popular-routes", - name="Popular Flight Routes", - description="Information about popular routes and pricing", + uri="server://config", + name="Server Configuration", + description="System configuration parameters", mime_type="application/json", + annotations=ResourceAnnotations(audience=["assistant"], priority=1.0), ) - async def popular_routes(self, context: ExecutionContext) -> dict: - return {"routes": [...]} + async def get_config(self, context: ExecutionContext) -> dict: + return {"env": "production", "debug": False} ``` -List the class on the feature module `controllers`. +--- + +## 6. Defining Prompts (`@prompt`) -## Prompts +Prompts expose parameterized prompt templates to guide LLM interactions. ```python -from nitrostack import injectable, prompt, ExecutionContext - -@prompt( - name="flight_search_assistant", - description="Help users search for flights and book holds.", -) -async def flight_search_assistant(self, args: dict, context: ExecutionContext) -> str: - return "Use search_flights and search_airports, then create an order hold." +from nitrostack import injectable, prompt, PromptArgument, ExecutionContext + +@injectable() +class AssistantPrompts: + @prompt( + name="code_review", + description="Review code according to security and style guidelines", + arguments=[ + PromptArgument(name="language", description="Target language (e.g. Python)", required=True), + PromptArgument(name="code", description="Source code snippet", required=True), + ], + ) + async def code_review_prompt(self, arguments: dict, context: ExecutionContext) -> list: + lang = arguments.get("language", "Python") + code = arguments.get("code", "") + return [ + { + "role": "user", + "content": f"You are a senior {lang} engineer. Review this code for bugs and security:\n\n{code}", + } + ] ``` -## Do not +--- -- Use `z.object` / Zod `inputSchema`. -- Register a bare function as a tool without a controller class unless you are matching an existing example in this repo. -- Invent Nest-style `@Controller('prefix')` — Python tools use the `name=` string as the MCP tool name. +## Do Not +- Do not use Zod (`z.object`) — use Pydantic `BaseModel`. +- Do not forget `context: ExecutionContext` parameter on `@tool`, `@resource`, and `@prompt` methods. +- Do not write unvalidated file paths directly to disk without path traversal checks. diff --git a/skills/ui-widgets/SKILL.md b/skills/ui-widgets/SKILL.md index 3a9ac41..f37d430 100644 --- a/skills/ui-widgets/SKILL.md +++ b/skills/ui-widgets/SKILL.md @@ -1,52 +1,115 @@ --- name: nitrostack-python-ui-widgets -description: Python NitroStack MCP Apps widgets — @widget, widgets/out HTML, WidgetOptions, and preview. +description: Best practices for linking Python MCP tools to UI widgets, WidgetOptions, CSP, application modes, and preview. --- ## When to Use +Use this skill when attaching UI widgets to Python MCP tool outputs (e.g. interactive cards, map views, flight cards, or forms) and configuring widget routes, CSP, and preview rendering. -Attaching UI to a Python tool result (Calculator, Pizzaz, flight-booking). Not `@nitrostack/ui` npm widgets. +--- + +## 1. Associating a Widget with a Tool (`@widget`) -## Associate a widget with a tool +Decorate any `@tool` method with `@widget("route-name")` or with `@widget(WidgetOptions(...))` to bind frontend HTML/React components to tool results. +### Simple Route Binding: ```python -from nitrostack import tool, widget, WidgetOptions, WidgetCsp +from nitrostack import injectable, tool, widget, ExecutionContext +from pydantic import BaseModel -@tool(name="calculate", description="...", input_schema=CalculateInput) -@widget("calculator-result") -async def calculate(self, input: CalculateInput, context: ExecutionContext) -> dict: - return {"result": ..., "expression": ...} -``` +class ProductInput(BaseModel): + product_id: str -Or with CSP / border: +@injectable() +class StoreTools: + @tool(name="get_product", description="Fetch product card", input_schema=ProductInput) + @widget("product-card") # Maps to widgets/out/product-card.html + async def get_product(self, input: ProductInput, context: ExecutionContext) -> dict: + return { + "name": "Super Nitro Coffee", + "price": 4.99, + "stock": 42, + } +``` +### Advanced WidgetOptions with CSP & Border: ```python -@widget(WidgetOptions( - route="pizza-map", - prefers_border=True, - csp=WidgetCsp( - resource_domains=["https://api.mapbox.com", ...], - connect_domains=["https://api.mapbox.com"], - ), -)) +from nitrostack import injectable, tool, widget, WidgetOptions, WidgetCsp, ExecutionContext +from pydantic import BaseModel + +class LocationInput(BaseModel): + city: str + +@injectable() +class MapTools: + @tool(name="show_store_map", description="Show interactive map of stores", input_schema=LocationInput) + @widget( + WidgetOptions( + route="store-map", + prefers_border=True, + csp=WidgetCsp( + resource_domains=["https://api.mapbox.com", "https://events.mapbox.com"], + connect_domains=["https://api.mapbox.com"], + ), + ) + ) + async def show_map(self, input: LocationInput, context: ExecutionContext) -> dict: + return {"city": input.city, "stores": [{"name": "Downtown", "lat": 37.77, "lng": -122.41}]} ``` -`nitrostack-py init` and `ensure_python_widgets` write `widgets/out/{route}.html` plus `widgets/preview.html`. Routes are scraped from `@widget("...")` and `WidgetOptions(route="...")`. +--- + +## 2. Application Modes (`NITROSTACK_APP_MODE`) + +NitroStack supports mode-gated metadata to serve OpenAI and MCP Apps clients simultaneously via the `NITROSTACK_APP_MODE` environment variable (default: `universal`): + +| Mode | Tool `_meta` | Resource MIME Type | +|---|---|---| +| `universal` (default) | Populates both OpenAI (`openai/outputTemplate`) and MCP Apps (`_meta.ui`) | `text/html;profile=mcp-app` | +| `mcp-app` | Standard MCP Apps UI metadata (`resourceUri`, `visibility`, CSP) | `text/html;profile=mcp-app` | +| `openai` | OpenAI template metadata (`ui/template`, `outputTemplate`) | `text/html` | + +--- + +## 3. Widget Files & Directory Layout + +- **`widgets/out/{route}.html`**: The production HTML bundle rendered inside the MCP host's webview. NitroStack automatically exposes this file as an MCP resource with URI `ui://widget/{route}.html`. +- **`widgets/preview.html`**: Static preview page for inspecting widget templates during development. +- **`src/widgets/`** (optional): Next.js or React frontend source app when using a multi-package setup. -Return JSON/`dict` from the tool; the widget HTML reads `structuredContent` / injected tool data. Do not return a React tree from Python. +--- + +## 4. Widget Data Flow Protocol -## Files +1. **Python Tool Execution**: The `@tool` method executes and returns standard Python `dict` or Pydantic `BaseModel` data. +2. **Host Delivery**: NitroStack embeds the serialized data as `structuredContent` in the MCP JSON-RPC response metadata. +3. **Webview Rendering**: The widget HTML bundle loads inside the client iframe and reads the injected tool data via the standard MCP Apps bridge (`window.addEventListener('message', ...)`). -- `widgets/out/.html` — page the MCP host loads (`ui://widget/.html`). -- `widgets/preview.html` — static preview. -- Optional `src/widgets/` Next app in some templates for local widget `npm run dev` (port `WIDGETS_DEV_PORT`, default 3001). MCP server port is `PORT` (default 3000). +> [!IMPORTANT] +> Always return clean domain data (`dict` or Pydantic models) from your Python `@tool` methods. Do not return raw React or HTML trees from Python handlers. -## Host / Inspector +--- -`NITROSTACK_APP_MODE=universal` (set by init). Streamable HTTP: `MCP_TRANSPORT_TYPE=http` (and `MCP_STATELESS=true` when using the inspector flow documented in init next-steps). +## 5. Development & Testing Workflow -## Do not +### Running Dev Server with Hot-Reload: +```bash +nitrostack-py dev --port 3000 --widget 3001 +``` + +### Testing in MCP Inspector: +For MCP Inspector over HTTP, run in stateless mode: +```bash +MCP_TRANSPORT_TYPE=http MCP_STATELESS=true NITROSTACK_APP_MODE=universal python main.py +``` +- Connect MCP Inspector to `http://localhost:3000/mcp` (Streamable HTTP, no trailing slash). +- Turn **Authentication off** in Inspector (unless actively testing OAuth endpoints). +- Navigate to the **Apps** tab to view live interactive widget rendering. +- Live widget preview is also accessible directly at: `http://localhost:3000/widgets/preview`. + +--- -- Scaffold a TypeScript widget package as the source of truth for Python tools. -- Use a widget route that is not a simple name (`calculator-result`, `flight-search-results`). -- Point `_meta.ui.resourceUri` at a non-`ui://` URI. +## Do Not +- Do not set `_meta.ui.resourceUri` to anything other than a `ui://widget/...` URI. +- Do not use complex nested route paths; use clean kebab-case names (e.g. `product-card`, `flight-results`). +- Do not attempt to run TypeScript `@nitrostack/widgets` hooks directly in Python runtime code.