From f99a6ecb22a98f2ae9179fbc9963869837560d79 Mon Sep 17 00:00:00 2001 From: Gautam-7-7 <140533905+Gautam-7-7@users.noreply.github.com> Date: Mon, 29 Jun 2026 15:43:31 +0530 Subject: [PATCH] feat: achieve full parity with typescript sdk templates and widgets --- PYTHON_SDK_SPEC.md | 711 -------------- __pycache__/nitrostudio.cpython-312.pyc | Bin 41622 -> 0 bytes dist/nitrostack-0.3.1-py3-none-any.whl | Bin 64133 -> 0 bytes dist/nitrostack-0.3.1.tar.gz | Bin 65120 -> 0 bytes .../calculator_server.cpython-312.pyc | Bin 10909 -> 10920 bytes examples/flight_booking_server.py | 584 ++++++++++-- .../__pycache__/app_module.cpython-312.pyc | Bin 574 -> 0 bytes .../calculator_module.cpython-312.pyc | Bin 707 -> 0 bytes .../calculator_service.cpython-312.pyc | Bin 657 -> 0 bytes .../calculator_tools.cpython-312.pyc | Bin 1705 -> 0 bytes nitrostack.egg-info/PKG-INFO | 216 ----- nitrostack.egg-info/SOURCES.txt | 35 - nitrostack.egg-info/dependency_links.txt | 1 - nitrostack.egg-info/entry_points.txt | 3 - nitrostack.egg-info/requires.txt | 5 - nitrostack/__init__.py | 2 + .../__pycache__/__init__.cpython-312.pyc | Bin 2081 -> 2119 bytes nitrostack/__pycache__/studio.cpython-312.pyc | Bin 42653 -> 0 bytes .../auth/__pycache__/api_key.cpython-312.pyc | Bin 4140 -> 4151 bytes .../auth/__pycache__/config.cpython-312.pyc | Bin 4048 -> 4059 bytes .../auth/__pycache__/jwt.cpython-312.pyc | Bin 6823 -> 6834 bytes .../auth/__pycache__/oauth.cpython-312.pyc | Bin 11211 -> 11222 bytes .../cli/__pycache__/main.cpython-312.pyc | Bin 38517 -> 0 bytes nitrostack/cli/main.py | 893 ++++++++++++++---- .../additional_decorators.cpython-312.pyc | Bin 6885 -> 6896 bytes .../core/__pycache__/app.cpython-312.pyc | Bin 33126 -> 33340 bytes .../core/__pycache__/context.cpython-312.pyc | Bin 8553 -> 8564 bytes .../__pycache__/decorators.cpython-312.pyc | Bin 7258 -> 8239 bytes .../core/__pycache__/di.cpython-312.pyc | Bin 3723 -> 3734 bytes .../core/__pycache__/errors.cpython-312.pyc | Bin 1655 -> 1666 bytes .../core/__pycache__/module.cpython-312.pyc | Bin 1723 -> 1734 bytes .../core/__pycache__/pipeline.cpython-312.pyc | Bin 14130 -> 14141 bytes .../core/__pycache__/task.cpython-312.pyc | Bin 5293 -> 5304 bytes nitrostack/core/app.py | 6 + nitrostack/core/decorators.py | 20 + .../__pycache__/event_emitter.cpython-312.pyc | Bin 3561 -> 3572 bytes nitrostack/templates/flight-booking/.env | 7 + .../templates/flight-booking/.env.example | 7 + .../templates/flight-booking/OAUTH_SETUP.md | 26 + .../templates/flight-booking/app_module.py | 30 + .../flight-booking/guards/oauth_guard.py | 14 + .../flight-booking/health/system_health.py | 11 + .../templates/flight-booking}/main.py | 0 .../modules/flights/booking_tools.py | 96 ++ .../modules/flights/flights_module.py | 15 + .../modules/flights/flights_prompts.py | 21 + .../modules/flights/flights_resources.py | 30 + .../modules/flights/flights_tools.py | 77 ++ .../flight-booking}/requirements.txt | 0 .../flight-booking/services/duffel_service.py | 282 ++++++ .../src/widgets/app/airport-search/page.tsx | 270 ++++++ .../src/widgets/app/flight-details/page.tsx | 261 +++++ .../app/flight-search-results/page.tsx | 378 ++++++++ .../src/widgets/app/globals.css | 167 ++++ .../flight-booking/src/widgets/app/layout.tsx | 18 + .../widgets/app/order-cancellation/page.tsx | 207 ++++ .../src/widgets/app/order-summary/page.tsx | 245 +++++ .../widgets/app/payment-confirmation/page.tsx | 152 +++ .../src/widgets/app/seat-selection/page.tsx | 486 ++++++++++ .../flight-booking/src/widgets/next-env.d.ts | 5 + .../flight-booking/src/widgets/next.config.js | 45 + .../flight-booking/src/widgets/package.json | 25 + .../flight-booking/src/widgets/tsconfig.json | 28 + .../src/widgets/widget-manifest.json | 395 ++++++++ .../templates/pizzaz}/.env | 0 nitrostack/templates/pizzaz/app_module.py | 14 + .../templates/pizzaz/health/system_health.py | 11 + .../templates/pizzaz}/main.py | 0 .../pizzaz/modules/pizzaz/pizzaz_data.py | 85 ++ .../pizzaz/modules/pizzaz/pizzaz_module.py | 12 + .../pizzaz/modules/pizzaz/pizzaz_service.py | 33 + .../pizzaz/modules/pizzaz/pizzaz_tools.py | 83 ++ .../templates/pizzaz}/requirements.txt | 0 .../pizzaz/src/widgets/app/layout.tsx | 18 + .../src/widgets/app/pizza-list/page.tsx | 272 ++++++ .../pizzaz/src/widgets/app/pizza-map/page.tsx | 216 +++++ .../src/widgets/app/pizza-shop/page.tsx | 374 ++++++++ .../widgets/components/CompactShopCard.tsx | 144 +++ .../src/widgets/components/PizzaCard.tsx | 191 ++++ .../pizzaz/src/widgets/next.config.js | 45 + .../templates/pizzaz/src/widgets/package.json | 31 + .../pizzaz/src/widgets/tsconfig.json | 28 + .../pizzaz/src/widgets/widget-manifest.json | 253 +++++ .../templates/starter}/.env | 0 nitrostack/templates/starter/app_module.py | 14 + .../templates/starter/health/system_health.py | 11 + nitrostack/templates/starter/main.py | 10 + .../modules/calculator/calculator_module.py | 11 + .../modules/calculator/calculator_tools.py | 117 +++ .../templates/starter/requirements.txt | 0 .../widgets/app/calculator-result/page.tsx | 180 ++++ .../starter/src/widgets/app/layout.tsx | 18 + .../starter/src/widgets/next.config.js | 45 + .../starter/src/widgets/package.json | 25 + .../starter/src/widgets/tsconfig.json | 28 + .../starter/src/widgets/widget-manifest.json | 48 + .../__pycache__/__init__.cpython-312.pyc | Bin 4422 -> 4812 bytes .../__pycache__/stdio.cpython-312.pyc | Bin 2181 -> 0 bytes pyproject.toml | 4 +- .../__pycache__/app_module.cpython-312.pyc | Bin 956 -> 0 bytes scratch/test-flight/app_module.py | 22 - .../flight_booking_module.cpython-312.pyc | Bin 749 -> 0 bytes .../flight_booking_service.cpython-312.pyc | Bin 2127 -> 0 bytes .../flight_booking_tools.cpython-312.pyc | Bin 3822 -> 0 bytes .../flight_booking/flight_booking_module.py | 13 - .../flight_booking/flight_booking_service.py | 35 - .../flight_booking/flight_booking_tools.py | 49 - .../__pycache__/app_module.cpython-312.pyc | Bin 585 -> 0 bytes scratch/test-food/app_module.py | 12 - .../food_delivery_module.cpython-312.pyc | Bin 737 -> 0 bytes .../food_delivery_service.cpython-312.pyc | Bin 2293 -> 0 bytes .../food_delivery_tools.cpython-312.pyc | Bin 3571 -> 0 bytes .../food_delivery/food_delivery_module.py | 13 - .../food_delivery/food_delivery_service.py | 41 - .../food_delivery/food_delivery_tools.py | 45 - scratch/test_mcp_init_direct.py | 13 - test_custom_file.log | 0 .../test_basic.cpython-312-pytest-9.0.2.pyc | Bin 11781 -> 11792 bytes tests/__pycache__/test_basic.cpython-312.pyc | Bin 4072 -> 0 bytes ..._initial_tool.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 4757 bytes .../test_oauth.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 9410 bytes ...st_production.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 18648 bytes .../test_tasks.cpython-312-pytest-9.0.2.pyc | Bin 16232 -> 16390 bytes tests/__pycache__/test_tasks.cpython-312.pyc | Bin 9456 -> 0 bytes ...dget_metadata.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 6602 bytes tests/test_initial_tool.py | 7 +- tests/test_oauth.py | 7 +- tests/test_tasks.py | 7 +- tests/test_widget_metadata.py | 69 ++ 129 files changed, 6907 insertions(+), 1521 deletions(-) delete mode 100644 PYTHON_SDK_SPEC.md delete mode 100644 __pycache__/nitrostudio.cpython-312.pyc delete mode 100644 dist/nitrostack-0.3.1-py3-none-any.whl delete mode 100644 dist/nitrostack-0.3.1.tar.gz delete mode 100644 my-test-server/__pycache__/app_module.cpython-312.pyc delete mode 100644 my-test-server/modules/calculator/__pycache__/calculator_module.cpython-312.pyc delete mode 100644 my-test-server/modules/calculator/__pycache__/calculator_service.cpython-312.pyc delete mode 100644 my-test-server/modules/calculator/__pycache__/calculator_tools.cpython-312.pyc delete mode 100644 nitrostack.egg-info/PKG-INFO delete mode 100644 nitrostack.egg-info/SOURCES.txt delete mode 100644 nitrostack.egg-info/dependency_links.txt delete mode 100644 nitrostack.egg-info/entry_points.txt delete mode 100644 nitrostack.egg-info/requires.txt delete mode 100644 nitrostack/__pycache__/studio.cpython-312.pyc delete mode 100644 nitrostack/cli/__pycache__/main.cpython-312.pyc create mode 100644 nitrostack/templates/flight-booking/.env create mode 100644 nitrostack/templates/flight-booking/.env.example create mode 100644 nitrostack/templates/flight-booking/OAUTH_SETUP.md create mode 100644 nitrostack/templates/flight-booking/app_module.py create mode 100644 nitrostack/templates/flight-booking/guards/oauth_guard.py create mode 100644 nitrostack/templates/flight-booking/health/system_health.py rename {scratch/test-flight => nitrostack/templates/flight-booking}/main.py (100%) create mode 100644 nitrostack/templates/flight-booking/modules/flights/booking_tools.py create mode 100644 nitrostack/templates/flight-booking/modules/flights/flights_module.py create mode 100644 nitrostack/templates/flight-booking/modules/flights/flights_prompts.py create mode 100644 nitrostack/templates/flight-booking/modules/flights/flights_resources.py create mode 100644 nitrostack/templates/flight-booking/modules/flights/flights_tools.py rename {scratch/test-flight => nitrostack/templates/flight-booking}/requirements.txt (100%) create mode 100644 nitrostack/templates/flight-booking/services/duffel_service.py create mode 100644 nitrostack/templates/flight-booking/src/widgets/app/airport-search/page.tsx create mode 100644 nitrostack/templates/flight-booking/src/widgets/app/flight-details/page.tsx create mode 100644 nitrostack/templates/flight-booking/src/widgets/app/flight-search-results/page.tsx create mode 100644 nitrostack/templates/flight-booking/src/widgets/app/globals.css create mode 100644 nitrostack/templates/flight-booking/src/widgets/app/layout.tsx create mode 100644 nitrostack/templates/flight-booking/src/widgets/app/order-cancellation/page.tsx create mode 100644 nitrostack/templates/flight-booking/src/widgets/app/order-summary/page.tsx create mode 100644 nitrostack/templates/flight-booking/src/widgets/app/payment-confirmation/page.tsx create mode 100644 nitrostack/templates/flight-booking/src/widgets/app/seat-selection/page.tsx create mode 100644 nitrostack/templates/flight-booking/src/widgets/next-env.d.ts create mode 100644 nitrostack/templates/flight-booking/src/widgets/next.config.js create mode 100644 nitrostack/templates/flight-booking/src/widgets/package.json create mode 100644 nitrostack/templates/flight-booking/src/widgets/tsconfig.json create mode 100644 nitrostack/templates/flight-booking/src/widgets/widget-manifest.json rename {scratch/test-flight => nitrostack/templates/pizzaz}/.env (100%) create mode 100644 nitrostack/templates/pizzaz/app_module.py create mode 100644 nitrostack/templates/pizzaz/health/system_health.py rename {scratch/test-food => nitrostack/templates/pizzaz}/main.py (100%) create mode 100644 nitrostack/templates/pizzaz/modules/pizzaz/pizzaz_data.py create mode 100644 nitrostack/templates/pizzaz/modules/pizzaz/pizzaz_module.py create mode 100644 nitrostack/templates/pizzaz/modules/pizzaz/pizzaz_service.py create mode 100644 nitrostack/templates/pizzaz/modules/pizzaz/pizzaz_tools.py rename {scratch/test-food => nitrostack/templates/pizzaz}/requirements.txt (100%) create mode 100644 nitrostack/templates/pizzaz/src/widgets/app/layout.tsx create mode 100644 nitrostack/templates/pizzaz/src/widgets/app/pizza-list/page.tsx create mode 100644 nitrostack/templates/pizzaz/src/widgets/app/pizza-map/page.tsx create mode 100644 nitrostack/templates/pizzaz/src/widgets/app/pizza-shop/page.tsx create mode 100644 nitrostack/templates/pizzaz/src/widgets/components/CompactShopCard.tsx create mode 100644 nitrostack/templates/pizzaz/src/widgets/components/PizzaCard.tsx create mode 100644 nitrostack/templates/pizzaz/src/widgets/next.config.js create mode 100644 nitrostack/templates/pizzaz/src/widgets/package.json create mode 100644 nitrostack/templates/pizzaz/src/widgets/tsconfig.json create mode 100644 nitrostack/templates/pizzaz/src/widgets/widget-manifest.json rename {scratch/test-food => nitrostack/templates/starter}/.env (100%) create mode 100644 nitrostack/templates/starter/app_module.py create mode 100644 nitrostack/templates/starter/health/system_health.py create mode 100644 nitrostack/templates/starter/main.py create mode 100644 nitrostack/templates/starter/modules/calculator/calculator_module.py create mode 100644 nitrostack/templates/starter/modules/calculator/calculator_tools.py rename nitrostack.egg-info/top_level.txt => nitrostack/templates/starter/requirements.txt (100%) create mode 100644 nitrostack/templates/starter/src/widgets/app/calculator-result/page.tsx create mode 100644 nitrostack/templates/starter/src/widgets/app/layout.tsx create mode 100644 nitrostack/templates/starter/src/widgets/next.config.js create mode 100644 nitrostack/templates/starter/src/widgets/package.json create mode 100644 nitrostack/templates/starter/src/widgets/tsconfig.json create mode 100644 nitrostack/templates/starter/src/widgets/widget-manifest.json delete mode 100644 nitrostack/transports/__pycache__/stdio.cpython-312.pyc delete mode 100644 scratch/test-flight/__pycache__/app_module.cpython-312.pyc delete mode 100644 scratch/test-flight/app_module.py delete mode 100644 scratch/test-flight/modules/flight_booking/__pycache__/flight_booking_module.cpython-312.pyc delete mode 100644 scratch/test-flight/modules/flight_booking/__pycache__/flight_booking_service.cpython-312.pyc delete mode 100644 scratch/test-flight/modules/flight_booking/__pycache__/flight_booking_tools.cpython-312.pyc delete mode 100644 scratch/test-flight/modules/flight_booking/flight_booking_module.py delete mode 100644 scratch/test-flight/modules/flight_booking/flight_booking_service.py delete mode 100644 scratch/test-flight/modules/flight_booking/flight_booking_tools.py delete mode 100644 scratch/test-food/__pycache__/app_module.cpython-312.pyc delete mode 100644 scratch/test-food/app_module.py delete mode 100644 scratch/test-food/modules/food_delivery/__pycache__/food_delivery_module.cpython-312.pyc delete mode 100644 scratch/test-food/modules/food_delivery/__pycache__/food_delivery_service.cpython-312.pyc delete mode 100644 scratch/test-food/modules/food_delivery/__pycache__/food_delivery_tools.cpython-312.pyc delete mode 100644 scratch/test-food/modules/food_delivery/food_delivery_module.py delete mode 100644 scratch/test-food/modules/food_delivery/food_delivery_service.py delete mode 100644 scratch/test-food/modules/food_delivery/food_delivery_tools.py delete mode 100644 scratch/test_mcp_init_direct.py delete mode 100644 test_custom_file.log delete mode 100644 tests/__pycache__/test_basic.cpython-312.pyc create mode 100644 tests/__pycache__/test_initial_tool.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/__pycache__/test_oauth.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/__pycache__/test_production.cpython-312-pytest-9.0.2.pyc delete mode 100644 tests/__pycache__/test_tasks.cpython-312.pyc create mode 100644 tests/__pycache__/test_widget_metadata.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/test_widget_metadata.py diff --git a/PYTHON_SDK_SPEC.md b/PYTHON_SDK_SPEC.md deleted file mode 100644 index f87cf57..0000000 --- a/PYTHON_SDK_SPEC.md +++ /dev/null @@ -1,711 +0,0 @@ -# NitroStack Python SDK — Feature Specification - -> Derived from `@nitrostack/core` TypeScript source. -> This document defines what the Python SDK must implement, how each feature maps from TS, and what the Python-idiomatic API should look like. - ---- - -## 1. MCP Primitives - -### 1.1 Tools - -**TS source:** `decorators.ts` → `@Tool()`, `tool.ts` → `Tool` class - -Tools are the primary way an MCP server exposes functionality to AI clients. - -**TS API:** -```typescript -@Tool({ - name: 'login', - title: 'Login', - description: 'Login with email and password', - inputSchema: z.object({ email: z.string(), password: z.string() }), - outputSchema: z.object({ token: z.string() }), - annotations: { readOnlyHint: false, destructiveHint: false }, - taskSupport: 'optional', -}) -async login(input: LoginInput, context: ExecutionContext) { ... } -``` - -**Python API:** -```python -@tool( - name="login", - title="Login", - description="Login with email and password", - input_schema=LoginInput, # Pydantic model - output_schema=LoginOutput, # Pydantic model (optional) - annotations=ToolAnnotations(read_only_hint=False), - task_support="optional", -) -async def login(self, input: LoginInput, context: ExecutionContext): ... -``` - -**Fields to support:** - -| Field | Type | Notes | -|---|---|---| -| `name` | `str` | Required | -| `title` | `str` | Optional, display name | -| `description` | `str` | Required | -| `input_schema` | Pydantic model or dict | Required. Call `.model_json_schema()` for wire format | -| `output_schema` | Pydantic model or dict | Optional | -| `annotations` | `ToolAnnotations` dataclass | Optional | -| `invocation` | `ToolInvocation` dataclass | Optional, for OpenAI Apps SDK UI messages | -| `task_support` | `"forbidden" \| "optional" \| "required"` | Default: `"forbidden"` | -| `visibility` | `"visible" \| "hidden"` | Default: `"visible"`, MCP Apps mode | -| `examples` | `ToolExamples` dataclass | Optional | -| `metadata` | dict | Optional, category/tags/rate_limit | - -**`ToolAnnotations` fields (from `types.ts`):** - -| Field | Default | Meaning | -|---|---|---| -| `destructive_hint` | `True` | Tool may make destructive changes | -| `idempotent_hint` | `False` | Repeated calls have no extra effect | -| `read_only_hint` | `False` | Tool does not modify environment | -| `open_world_hint` | `True` | Tool interacts with external entities | - -**`@InitialTool()`** — marks a tool to be auto-called on client connection. Python equivalent: `@initial_tool` stacked with `@tool`. - ---- - -### 1.2 Resources - -**TS source:** `decorators.ts` → `@Resource()` - -Resources expose readable data to AI clients (files, DB records, API data, etc.). - -**TS API:** -```typescript -@Resource({ - uri: 'db://users/schema', - name: 'User Schema', - description: 'Database schema for users table', - mimeType: 'application/json', -}) -async getUserSchema(context: ExecutionContext) { ... } -``` - -**Python API:** -```python -@resource( - uri="db://users/schema", - name="User Schema", - description="Database schema for users table", - mime_type="application/json", -) -async def get_user_schema(self, context: ExecutionContext): ... -``` - -**Fields to support:** - -| Field | Type | Notes | -|---|---|---| -| `uri` | `str` | Required. Static URI or template (e.g. `notes://note/{id}`) | -| `name` | `str` | Required | -| `title` | `str` | Optional | -| `description` | `str` | Required | -| `mime_type` | `str` | Optional, e.g. `"application/json"`, `"text/plain"` | -| `size` | `int` | Optional, bytes | -| `annotations` | `ResourceAnnotations` dataclass | Optional | -| `metadata.cacheable` | `bool` | Optional | -| `metadata.cache_max_age` | `int` | Optional, seconds | - -**`ResourceAnnotations` fields:** - -| Field | Type | Meaning | -|---|---|---| -| `audience` | `list["user" \| "assistant"]` | Intended consumer | -| `priority` | `float` | 0.0–1.0 importance | -| `last_modified` | `str` | ISO 8601 timestamp | - -**Resource content return types** (discriminated union from `types.ts`): -```python -# Handler must return one of: -{"type": "text", "data": "..."} -{"type": "binary", "data": b"..."} -{"type": "json", "data": {...}} -``` - ---- - -### 1.3 Prompts - -**TS source:** `decorators.ts` → `@Prompt()` - -Prompt templates that AI clients can request to pre-fill conversation context. - -**TS API:** -```typescript -@Prompt({ - name: 'auth-help', - description: 'Help with authentication flows', - arguments: [{ name: 'issue', description: 'The auth issue', required: true }], -}) -async authHelp(args: PromptArgs, context: ExecutionContext) { ... } -``` - -**Python API:** -```python -@prompt( - name="auth-help", - description="Help with authentication flows", - arguments=[PromptArgument(name="issue", description="The auth issue", required=True)], -) -async def auth_help(self, args: dict, context: ExecutionContext): ... -``` - -**`PromptMessage` return type** — handler must return a list of: -```python -PromptMessage(role="user" | "assistant" | "system", content="...") -``` - ---- - -## 2. Execution Pipeline - -**TS source:** `tool.ts` → `Tool.execute()` - -Every tool call runs through this chain in order. Any stage can abort execution. - -``` -ExceptionFilters (wrap everything) - └── Guards (can_activate → bool) - └── Middleware (use(ctx, next)) - └── Interceptors (intercept(ctx, next)) - └── Pipes (transform(value, meta)) - └── Handler (your function) -``` - -### 2.1 Guards - -**TS source:** `guards/guard.interface.ts` - -```python -# Protocol definition -class Guard(Protocol): - async def can_activate(self, context: ExecutionContext) -> bool: ... - -# Usage -@use_guards(ApiKeyGuard, MyCustomGuard) -@tool(...) -async def my_tool(self, input, context): ... -``` - -Return `False` → raises `"Access denied by guard"` error automatically. - -**Built-in guards:** -- `ApiKeyGuard` — validates `x-api-key` from request metadata -- `JwtGuard` — validates Bearer JWT token, populates `context.auth` -- `OAuthGuard` — validates OAuth 2.1 access token with audience binding - ---- - -### 2.2 Middleware - -**TS source:** `middleware/middleware.interface.ts` - -```python -class MyMiddleware: - async def use(self, context: ExecutionContext, next: Callable) -> Any: - # before - result = await next() - # after - return result -``` - ---- - -### 2.3 Interceptors - -**TS source:** `interceptors/interceptor.interface.ts` - -```python -class MyInterceptor: - async def intercept(self, context: ExecutionContext, next: Callable) -> Any: - result = await next() - # transform result - return result -``` - ---- - -### 2.4 Pipes - -**TS source:** `pipes/pipe.interface.ts` - -```python -class MyPipe: - async def transform(self, value: Any, metadata: PipeMetadata) -> Any: - # validate or coerce - return value -``` - ---- - -### 2.5 Exception Filters - -**TS source:** `filters/exception-filter.interface.ts` - -```python -class MyFilter: - async def catch(self, error: Exception, context: ExecutionContext) -> Any: - # handle and return fallback result - return {"error": str(error)} -``` - ---- - -## 3. Module System - -**TS source:** `module.ts`, `app-decorator.ts` - -NestJS-style feature grouping. Each module declares its controllers and providers. - -**TS API:** -```typescript -@Module({ - name: 'calculator', - controllers: [CalculatorController], - providers: [CalculatorService], - imports: [ConfigModule], - exports: [CalculatorService], -}) -export class CalculatorModule {} -``` - -**Python API:** -```python -@module( - name="calculator", - controllers=[CalculatorController], - providers=[CalculatorService], - imports=[ConfigModule], - exports=[CalculatorService], -) -class CalculatorModule: ... -``` - ---- - -## 4. App Entry Point - -**TS source:** `app-decorator.ts` → `@McpApp`, `McpApplicationFactory` - -**TS API:** -```typescript -@McpApp({ module: AppModule, server: { name: 'my-server', version: '1.0.0' } }) -class App {} -const app = await McpApplicationFactory.create(App); -await app.start(); -``` - -**Python API:** -```python -@mcp_app(module=AppModule, server=ServerConfig(name="my-server", version="1.0.0")) -class App: ... - -async def main(): - app = await McpApplicationFactory.create(App) - await app.start() - -asyncio.run(main()) -``` - ---- - -## 5. Dependency Injection - -**TS source:** `di/container.ts`, `di/injectable.decorator.ts` - -Global singleton `DIContainer`. All providers are singleton-scoped by default. - -**TS API:** -```typescript -@Injectable() -export class CalculatorService { - constructor(private config: ConfigService) {} -} -``` - -**Python API (explicit deps — always required, no auto-inference from type hints):** -```python -@injectable(deps=[ConfigService]) -class CalculatorService: - def __init__(self, config: ConfigService): - self.config = config -``` - -**Container usage:** -```python -container = DIContainer.get_instance() -container.register(CalculatorService) -container.register_value("API_KEY", "my-key") -service = container.resolve(CalculatorService) -``` - -> **Note:** Python SDK always uses explicit `deps=[]`. Never attempt to auto-infer from type hints — this breaks under `from __future__ import annotations` (PEP 563). - ---- - -## 6. Built-in Auth Modules - -### 6.1 ApiKeyModule - -**TS source:** `apikey-module.ts` - -```python -ApiKeyModule.for_root( - keys_env_prefix="API_KEY", # reads API_KEY_1, API_KEY_2, etc. - header_name="x-api-key", - hashed=False, # set True if storing SHA-256 hashes -) -``` - -Methods: -- `ApiKeyModule.get_keys()` → list of configured keys -- `ApiKeyModule.validate(key)` → `bool` -- `ApiKeyModule.hash_key(key)` → SHA-256 hex string -- `ApiKeyModule.generate_key(prefix="sk")` → `"sk_"` secure key - ---- - -### 6.2 JWTModule - -**TS source:** `jwt-module.ts` - -```python -JWTModule.for_root( - secret_env_var="JWT_SECRET", - expires_in="24h", - audience="my-app", - issuer="https://auth.example.com", -) -``` - ---- - -### 6.3 OAuthModule - -**TS source:** `oauth-module.ts` - -Full OAuth 2.1 compliance: -- RFC 9728 — Protected Resource Metadata (`/.well-known/oauth-protected-resource`) -- RFC 8414 — Authorization Server Metadata (`/.well-known/oauth-authorization-server`) -- RFC 8707 — Token audience binding -- RFC 7636 — PKCE -- RFC 7662 — Token introspection - -```python -OAuthModule.for_root( - resource_uri=os.environ["RESOURCE_URI"], - authorization_servers=["https://auth.example.com"], - scopes_supported=["mcp:read", "mcp:write", "tools:execute"], - token_introspection_endpoint=os.environ.get("INTROSPECTION_ENDPOINT"), - token_introspection_client_id=os.environ.get("INTROSPECTION_CLIENT_ID"), - token_introspection_client_secret=os.environ.get("INTROSPECTION_CLIENT_SECRET"), -) -``` - -In stdio mode: starts a separate HTTP server on `OAUTH_DISCOVERY_PORT` (default 3005) to serve discovery endpoints, and notifies the client via stderr: -``` -[NITROSTACK_OAUTH]{...}[/NITROSTACK_OAUTH] -``` - ---- - -### 6.4 ConfigModule - -**TS source:** `config-module.ts` - -```python -ConfigModule.for_root( - env_file_path=".env", - ignore_env_file=False, - defaults={"LOG_LEVEL": "info"}, - validate=lambda cfg: "API_KEY" in cfg, -) - -# Usage via injection: -@injectable(deps=[ConfigService]) -class MyService: - def __init__(self, config: ConfigService): - self.api_key = config.get_or_throw("API_KEY") -``` - -> **Critical:** Parse `.env` manually — never use a library that writes to stdout. MCP stdio protocol uses stdout for JSON-RPC; any stray output breaks the connection. - ---- - -## 7. Additional Decorators - -**TS source:** `decorators/cache.decorator.ts`, `decorators/rate-limit.decorator.ts`, `decorators/health-check.decorator.ts` - -### 7.1 Cache - -```python -@cache(ttl=60) # cache output for 60 seconds -@tool(...) -async def my_tool(self, input, context): ... -``` - -### 7.2 Rate Limit - -```python -@rate_limit(max=10, window=60) # 10 calls per 60 seconds per context -@tool(...) -async def my_tool(self, input, context): ... -``` - -### 7.3 Health Check - -```python -@health_check("database") -def check_db(self) -> bool: - return self.db.ping() -``` - -Health checks are exposed as a built-in MCP resource at `health://status`. - ---- - -## 8. MCP Tasks (Async Long-Running Tools) - -**TS source:** `task.ts` - -For tools that take too long to complete synchronously. Server returns `taskId` immediately and runs the tool in the background. - -**Task support levels:** -- `"forbidden"` (default) — tool cannot be invoked as a task -- `"optional"` — can be called normally or as a task -- `"required"` — must always be invoked as a task - -**Progress reporting from inside the handler:** -```python -async def long_tool(self, input, context: ExecutionContext): - if context.task: - context.task.update_progress("Step 1 of 3...") - context.task.throw_if_cancelled() - # ... do work ... - context.task.update_progress("Step 2 of 3...") -``` - -**MCP Task protocol methods exposed:** -- `tasks/get` — get task status -- `tasks/list` — list all tasks -- `tasks/cancel` — cancel a running task -- `tasks/result` — wait for and get task result - ---- - -## 9. Events - -**TS source:** `events/event-emitter.ts`, `events/event.decorator.ts` - -Lightweight pub/sub event bus (singleton). - -```python -@on_event("user.created") -async def handle_user_created(self, payload: dict): ... - -# Emit from anywhere: -event_emitter = EventEmitter.get_instance() -await event_emitter.emit("user.created", {"id": 123}) -``` - ---- - -## 10. Transports - -**TS source:** `server.ts`, `transports/` - -Auto-detected from environment variables. **Never write to stdout** — it carries the JSON-RPC wire protocol in stdio mode. - -| Mode | When | How | -|---|---|---| -| `stdio` | `NODE_ENV` not set or `development` | Default, for Claude Desktop | -| `http` | `MCP_TRANSPORT_TYPE=http` | Streamable HTTP on `PORT` | -| `dual` | `NODE_ENV=production` | stdio + HTTP simultaneously | - -**Env vars:** -- `PORT` / `MCP_SERVER_PORT` — HTTP server port -- `MCP_TRANSPORT_TYPE` — override transport -- `OAUTH_DISCOVERY_PORT` — OAuth discovery HTTP port (default 3005) - -**HTTP endpoints:** -- `POST /mcp` — Streamable HTTP (primary) -- `GET /sse` + `POST /mcp/messages` — Legacy SSE (compatibility) - ---- - -## 11. App Modes - -**TS source:** `app-mode.ts` - -Controlled by `NITROSTACK_APP_MODE` env var: - -| Mode | Value | Use | -|---|---|---| -| Standard MCP | `mcp` (default) | Claude Desktop, standard clients | -| OpenAI Apps SDK | `openai` | OpenAI Assistants / ChatGPT plugins | -| MCP Apps Spec | `mcpapps` | MCP Apps spec compliance | - -In `openai` mode, tool `_meta` uses `openai/*` keys. In `mcpapps` mode, uses `_meta.ui` format. - ---- - -## 12. Execution Context - -**TS source:** `types.ts` → `ExecutionContext` - -Passed to every handler, guard, middleware, interceptor, pipe, and filter. - -```python -@dataclass -class ExecutionContext: - request_id: str # unique request ID for tracing - tool_name: str | None # name of tool being executed - logger: Logger # structured logger (file only, never stdout) - metadata: dict # raw request metadata - auth: AuthContext | None # populated if authenticated - task: TaskContext | None # populated for async task execution -``` - -**`AuthContext` fields:** -```python -@dataclass -class AuthContext: - subject: str | None # user/client identifier - scopes: list[str] # granted permissions - client_id: str | None # machine-to-machine - exp: int | None # expiration timestamp - iat: int | None # issued-at timestamp - iss: str | None # issuer URL - claims: dict # custom claims - token_payload: Any # full decoded token -``` - ---- - -## 13. Logger - -**TS source:** `logger.ts` - -Structured logger — **stdout disabled**, writes to file only. - -```python -class Logger(Protocol): - def debug(self, message: str, meta: dict | None = None) -> None: ... - def info(self, message: str, meta: dict | None = None) -> None: ... - def warn(self, message: str, meta: dict | None = None) -> None: ... - def error(self, message: str, meta: dict | None = None) -> None: ... -``` - -Recommended implementation: `loguru` or `structlog` with file-only sink. - ---- - -## 14. Error Types - -**TS source:** `errors.ts` - -```python -class ToolExecutionError(Exception): ... -class ValidationError(Exception): ... -class ResourceNotFoundError(Exception): ... -class PromptNotFoundError(Exception): ... -``` - -Errors in tool execution are returned as `isError: True` MCP responses, not protocol-level errors. - ---- - -## 15. Testing Module - -**TS source:** `testing/index.ts` - -In-process test harness — no real transport, no subprocess. - -```python -async def test_add_tool(): - module = await NitroTestingModule.create(AppModule) - result = await module.call_tool("add", {"a": 2, "b": 3}) - assert result == 5 - - resource = await module.read_resource("notes://all") - assert resource is not None - - prompt = await module.get_prompt("summarise", {"text": "hello"}) - assert len(prompt.messages) > 0 -``` - ---- - -## 16. CLI (`nitrostack-py`) - -Equivalent of `@nitrostack/cli` for Python projects. - -| Command | What it does | -|---|---| -| `nitrostack-py init my-server` | Scaffold new Python MCP server | -| `nitrostack-py dev` | Hot-reload dev server (uses `watchfiles`) | -| `nitrostack-py start` | Run the server | -| `nitrostack-py generate tool ` | Generate tool boilerplate | -| `nitrostack-py generate module ` | Generate module boilerplate | - -**Starter template structure:** -``` -my-server/ -├── main.py -├── app.module.py -├── modules/ -│ └── calculator/ -│ ├── calculator.module.py -│ ├── calculator.tools.py -│ └── calculator.service.py -├── .env -└── requirements.txt -``` - ---- - -## 17. Python Stack Decisions - -| TypeScript | Python equivalent | Reason | -|---|---|---| -| Zod | Pydantic v2 | Direct equivalent, built-in JSON Schema | -| `reflect-metadata` | Attributes on functions/classes | No runtime metadata system in Python | -| `emitDecoratorMetadata` | Explicit `deps=[]` always | PEP 563 makes type hint inference unreliable | -| Parameter decorators `@Inject()` | `deps=[]` in `@injectable` | Python has no parameter decorators | -| TypeScript interfaces | `typing.Protocol` | Structural subtyping | -| `winston` logger | `loguru` or `structlog` | File-only sink, no stdout | -| `@modelcontextprotocol/sdk` | `mcp` (official Python SDK) | Build on top, don't reimplement transport | -| Next.js widgets | Skip in v1 | Deeply Node.js-specific, not relevant to Python backend | - ---- - -## 18. What Python SDK Does NOT Port (v1 scope) - -| Feature | Reason | -|---|---| -| `@Widget()` and React component serving | Requires Next.js build pipeline — Node.js only | -| `@modelcontextprotocol/sdk` transport internals | The official `mcp` Python package handles this | -| `emitDecoratorMetadata` auto-DI | No Python equivalent; use explicit `deps=[]` | -| Parameter decorators (`@Inject()`) | Python language limitation | -| PostHog analytics in CLI | Nice-to-have, v2 | - ---- - -## 19. Deployment (No Managed Cloud) - -NitroStack has no proprietary hosting. The Python SDK follows the same model — bring your own infrastructure. - -**What the SDK provides for deployment:** -- Environment-based transport switching (`MCP_TRANSPORT_TYPE`) -- OAuth 2.1 for remote authenticated deployments -- Port config via env vars (`PORT`, `MCP_SERVER_PORT`, `OAUTH_DISCOVERY_PORT`) -- Health check endpoints for load balancers -- Structured file logging (no stdout pollution) - -**Tested deployment targets:** Railway, Fly.io, Render, any Docker/container host. diff --git a/__pycache__/nitrostudio.cpython-312.pyc b/__pycache__/nitrostudio.cpython-312.pyc deleted file mode 100644 index 7a2215a84d6231c69f55decc78a11a9efe5966ae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 41622 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 diff --git a/dist/nitrostack-0.3.1-py3-none-any.whl b/dist/nitrostack-0.3.1-py3-none-any.whl deleted file mode 100644 index 68e2d0b0f24a8f23a7560d3687bdd4f1a1cd77b2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 64133 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 diff --git a/examples/__pycache__/calculator_server.cpython-312.pyc b/examples/__pycache__/calculator_server.cpython-312.pyc index 5356d331c08d37d6ba8f21524b7d43ddf877019f..f0de6648f3872011385f996a793af97768d2a3ce 100644 GIT binary patch delta 50 zcmbOmx*~K#E32xLvsFxJacWU!j6h8m} delta 39 ucmZ1xIyZDfE31^bvsFxJacWU bool: + user_scopes = getattr(context.auth, "scopes", []) + missing_scopes = [s for s in required_scopes if s not in user_scopes] + if missing_scopes: + raise ValueError( + f"Insufficient scope. Required: {', '.join(required_scopes)}. " + f"Missing: {', '.join(missing_scopes)}" + ) + return True + return ScopeGuard + +# 2. Input Validation Schemas class SearchFlightsInput(BaseModel): - pass + origin: str = Field(description="Origin airport IATA code (e.g., 'JFK', 'LHR')") + destination: str = Field(description="Destination airport IATA code (e.g., 'LAX', 'CDG')") + departureDate: str = Field(description="Departure date in YYYY-MM-DD format") + returnDate: Optional[str] = Field(default=None, description="Return date in YYYY-MM-DD format for round trip") + adults: int = Field(default=1, description="Number of adult passengers (18+)") + cabinClass: str = Field(default="economy", description="Preferred cabin class (economy, premium_economy, business, first)") + +class FlightDetailsInput(BaseModel): + offerId: str = Field(description="The flight offer ID from search results") + +class AirportSearchInput(BaseModel): + query: str = Field(description="The search query for airports (e.g., 'London', 'New York')") + +class CreateOrderInput(BaseModel): + offerId: str = Field(description="The offer ID to book") + passengers: str = Field(description="JSON string containing array of passenger objects. Each passenger must have: title (mr/ms/mrs/miss/dr), givenName (first name), familyName (last name), gender (M/F), bornOn (YYYY-MM-DD), email, phoneNumber.") + +class OrderDetailsInput(BaseModel): + orderId: str = Field(description="The order ID") + +class SeatMapInput(BaseModel): + offerId: str = Field(description="The offer ID to get seats for") + +class CancelOrderInput(BaseModel): + orderId: str = Field(description="The order ID to cancel") -class BookFlightInput(BaseModel): - flight_id: str = Field(description="The flight identifier (e.g., FL101, FL202, FL303)") - passenger_name: str = Field(description="Passenger's full name") +class GetAirlinesInput(BaseModel): + pass -class BookingInfoInput(BaseModel): - booking_id: str = Field(description="The flight booking ID (e.g., BKG-5001)") -# 2. Flight Booking Business Service -@injectable(deps=[]) -class FlightBookingService: +# 3. Duffel Flight API Service +@injectable() +class DuffelService: def __init__(self): - # Mock flight schedules - self.flights = { - "FL101": {"from": "NYC", "to": "LON", "date": "2026-07-01", "price": 450.0}, - "FL202": {"from": "PAR", "to": "TOK", "date": "2026-07-02", "price": 850.0}, - "FL303": {"from": "LAX", "to": "NYC", "date": "2026-07-03", "price": 200.0} - } - self.bookings = {} - self.booking_counter = 5000 + self.api_key = os.environ.get("DUFFEL_API_KEY") + self.is_mock = not self.api_key or self.api_key.startswith("your-") or len(self.api_key) < 5 - def list_flights(self) -> Dict[str, Any]: - return self.flights + def _request(self, method: str, path: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + if self.is_mock: + raise ValueError("Duffel API key is not configured. Running in mock mode.") + + headers = { + "Authorization": f"Bearer {self.api_key}", + "Duffel-Version": "v1", + "Content-Type": "application/json" + } + url = f"https://api.duffel.com{path}" + req_data = json.dumps({"data": data}).encode("utf-8") if data else None + + req = urllib.request.Request(url, data=req_data, headers=headers, method=method) + try: + with urllib.request.urlopen(req, timeout=10) as response: + res_payload = json.loads(response.read().decode("utf-8")) + return res_payload.get("data", {}) + except Exception as e: + sys.stderr.write(f"Duffel API Request failed: {e}\n") + sys.stderr.flush() + raise e - def book_flight(self, flight_id: str, passenger_name: str) -> Dict[str, Any]: - if flight_id not in self.flights: - raise ValueError(f"Flight {flight_id} not found.") + async def search_flights(self, params: Dict[str, Any]) -> Dict[str, Any]: + if self.is_mock: + return { + "id": "orq_mock123456", + "offers": [ + { + "id": "off_mock123456", + "total_amount": "450.00", + "total_currency": "USD", + "expires_at": "2026-12-31T12:00:00Z", + "slices": [ + { + "origin": {"iata_code": params["origin"], "name": "Origin Airport", "city_name": "Origin City"}, + "destination": {"iata_code": params["destination"], "name": "Dest Airport", "city_name": "Dest City"}, + "duration": "PT6H30M", + "segments": [ + { + "id": "seg_outbound", + "origin": {"iata_code": params["origin"]}, + "destination": {"iata_code": params["destination"]}, + "departing_at": f"{params['departureDate']}T08:00:00Z", + "arriving_at": f"{params['departureDate']}T14:30:00Z", + "marketing_carrier": {"name": "Mock Airlines"}, + "marketing_carrier_flight_number": "MK123", + "aircraft": {"name": "Boeing 787"} + } + ] + } + ] + } + ] + } - self.booking_counter += 1 - booking_id = f"BKG-{self.booking_counter}" + slices = [{"origin": params["origin"], "destination": params["destination"], "departure_date": params["departureDate"]}] + if params.get("returnDate"): + slices.append({"origin": params["destination"], "destination": params["origin"], "departure_date": params["returnDate"]}) + + duffel_params = { + "slices": slices, + "passengers": params.get("passengers", [{"type": "adult"}]), + "cabin_class": params.get("cabinClass", "economy"), + "return_offers": True + } + res = self._request("POST", "/offer_requests", duffel_params) + return { + "id": res.get("id"), + "offers": res.get("offers", []), + "passengers": res.get("passengers", []), + "slices": res.get("slices", []) + } + + async def get_offer(self, offer_id: str) -> Dict[str, Any]: + if self.is_mock: + return { + "id": offer_id, + "total_amount": "450.00", + "total_currency": "USD", + "expires_at": "2026-12-31T12:00:00Z", + "slices": [ + { + "origin": {"iata_code": "JFK", "name": "John F. Kennedy Airport", "city_name": "New York"}, + "destination": {"iata_code": "LAX", "name": "Los Angeles Airport", "city_name": "Los Angeles"}, + "duration": "PT6H30M", + "segments": [ + { + "id": "seg_mock", + "origin": {"iata_code": "JFK"}, + "destination": {"iata_code": "LAX"}, + "departing_at": "2026-07-15T08:00:00Z", + "arriving_at": "2026-07-15T14:30:00Z", + "marketing_carrier": {"name": "Mock Airlines"}, + "marketing_carrier_flight_number": "MK123", + "aircraft": {"name": "Boeing 787"} + } + ] + } + ] + } + return self._request("GET", f"/offers/{offer_id}") + + async def get_seats_for_offer(self, offer_id: str) -> List[Dict[str, Any]]: + if self.is_mock: + return [ + { + "cabin_class": "economy", + "rows": [ + { + "row_number": 10, + "sections": [ + { + "elements": [ + { + "type": "seat", + "id": "seat_10a", + "designator": "10A", + "available_services": [{"total_amount": "25.00", "total_currency": "USD"}], + "disclosures": ["window"] + }, + { + "type": "seat", + "id": "seat_10b", + "designator": "10B", + "available_services": [{"total_amount": "0.00", "total_currency": "USD"}], + "disclosures": ["middle"] + } + ] + } + ] + } + ] + } + ] + res = self._request("GET", f"/seat_maps?offer_id={offer_id}") + return res if isinstance(res, list) else [] + + async def create_order(self, params: Dict[str, Any]) -> Dict[str, Any]: + if self.is_mock: + return { + "id": "ord_mock123456", + "status": "held", + "total_amount": "450.00", + "total_currency": "USD", + "expires_at": "2026-12-31T12:00:00Z", + "booking_reference": "ABCXYZ", + "passengers": [ + { + "id": f"pax_{idx}", + "given_name": p["given_name"], + "family_name": p["family_name"], + "type": "adult" + } for idx, p in enumerate(params["passengers"]) + ], + "slices": [ + { + "origin": {"iata_code": "JFK"}, + "destination": {"iata_code": "LAX"}, + "duration": "PT6H30M", + "segments": [ + { + "departing_at": "2026-07-15T08:00:00Z", + "arriving_at": "2026-07-15T14:30:00Z" + } + ] + } + ] + } - self.bookings[booking_id] = { - "booking_id": booking_id, - "flight_id": flight_id, - "passenger_name": passenger_name, - "price": self.flights[flight_id]["price"], - "status": "Confirmed" + order_payload = { + "selected_offers": params["selectedOffers"], + "passengers": params["passengers"], + "type": "hold" } - return self.bookings[booking_id] + return self._request("POST", "/orders", order_payload) - def get_booking(self, booking_id: str) -> Dict[str, Any]: - if booking_id not in self.bookings: - raise KeyError(f"Booking {booking_id} not found.") - return self.bookings[booking_id] + async def get_order(self, order_id: str) -> Dict[str, Any]: + if self.is_mock: + return { + "id": order_id, + "status": "held", + "total_amount": "450.00", + "total_currency": "USD", + "booking_reference": "ABCXYZ", + "created_at": "2026-06-25T12:00:00Z", + "expires_at": "2026-12-31T12:00:00Z", + "passengers": [ + { + "id": "pax_0", + "given_name": "John", + "family_name": "Doe", + "type": "adult", + "email": "john@example.com", + "phone_number": "+1234567890" + } + ], + "slices": [ + { + "id": "sli_mock", + "origin": {"iata_code": "JFK", "name": "John F. Kennedy Airport", "city_name": "New York"}, + "destination": {"iata_code": "LAX", "name": "Los Angeles Airport", "city_name": "Los Angeles"}, + "duration": "PT6H30M", + "segments": [ + { + "id": "seg_mock", + "origin": {"iata_code": "JFK"}, + "destination": {"iata_code": "LAX"}, + "departing_at": "2026-07-15T08:00:00Z", + "arriving_at": "2026-07-15T14:30:00Z", + "marketing_carrier": {"name": "Mock Airlines"}, + "marketing_carrier_flight_number": "MK123", + "aircraft": {"name": "Boeing 787"} + } + ] + } + ] + } + return self._request("GET", f"/orders/{order_id}") + async def cancel_order(self, order_id: str) -> Dict[str, Any]: + if self.is_mock: + return { + "id": "ocr_mock123456", + "refund_amount": "450.00", + "refund_currency": "USD", + "confirmed_at": "2026-06-25T12:30:00Z" + } + cancel_payload = {"order_id": order_id} + return self._request("POST", "/order_cancellations", cancel_payload) -# 3. Controller exposing tools with OAuth Guards -@injectable(deps=[FlightBookingService]) -class FlightBookingController: - def __init__(self, service: FlightBookingService): + async def get_airlines(self) -> List[Dict[str, Any]]: + if self.is_mock: + return [ + {"iata_code": "AA", "name": "American Airlines"}, + {"iata_code": "DL", "name": "Delta Air Lines"}, + {"iata_code": "UA", "name": "United Airlines"}, + {"iata_code": "BA", "name": "British Airways"} + ] + res = self._request("GET", "/airlines") + return res if isinstance(res, list) else [] + + +# 4. Controllers Exposing Tools +@injectable(deps=[DuffelService]) +class FlightTools: + def __init__(self, service: DuffelService): self.service = service @tool( name="search_flights", title="Search Flights", - description="Search for available flights and schedules (No Auth required)", + description="Search for flight offers based on origin, destination, dates, and preferences.", input_schema=SearchFlightsInput ) - async def search_flights(self, input: SearchFlightsInput, context: ExecutionContext) -> Dict[str, Any]: - context.logger.info("Searching available flights...") - return {"flights": self.service.list_flights()} + @use_guards(OAuthGuard, create_scope_guard(["read"])) + @widget("flight-search-results") + async def search_flights(self, input: SearchFlightsInput, context: ExecutionContext) -> dict: + context.logger.info(f"Searching flights from {input.origin} to {input.destination}") + return await self.service.search_flights(input.model_dump()) @tool( - name="book_flight", - title="Book Flight Ticket", - description="Book a flight ticket. Requires OAuth authentication.", - input_schema=BookFlightInput + name="get_flight_details", + title="Get Flight Details", + description="Get detailed information about a specific flight offer including baggage allowance, conditions.", + input_schema=FlightDetailsInput ) - @use_guards(OAuthGuard) - async def book_flight(self, input: BookFlightInput, context: ExecutionContext) -> Dict[str, Any]: - # OAuthGuard validates access token and populates context.auth - user_id = getattr(context.auth, "subject", "unknown-user") - context.logger.info(f"Booking flight {input.flight_id} for user {user_id}") - try: - result = self.service.book_flight(input.flight_id, input.passenger_name) - return {"status": "success", "booking": result} - except Exception as e: - return {"status": "failed", "error": str(e)} + @use_guards(OAuthGuard, create_scope_guard(["read"])) + @widget("flight-details") + async def get_flight_details(self, input: FlightDetailsInput, context: ExecutionContext) -> dict: + context.logger.info(f"Fetching flight details for offer {input.offerId}") + return await self.service.get_offer(input.offerId) @tool( - name="get_booking_details", - title="Get Booking Details", - description="Get booking receipt and details. Requires OAuth authentication.", - input_schema=BookingInfoInput + name="search_airports", + title="Search Airports", + description="Search for airports by query string.", + input_schema=AirportSearchInput ) - @use_guards(OAuthGuard) - async def get_booking_details(self, input: BookingInfoInput, context: ExecutionContext) -> Dict[str, Any]: - context.logger.info(f"Retrieving details for booking {input.booking_id}") + @use_guards(OAuthGuard, create_scope_guard(["read"])) + @widget("airport-search") + async def search_airports(self, input: AirportSearchInput, context: ExecutionContext) -> dict: + context.logger.info(f"Searching airports for query: {input.query}") + return {"airports": [{"iataCode": "JFK", "name": "John F. Kennedy Airport"}]} + + @tool( + name="get_airlines", + title="Get Airlines", + description="Get list of common airlines.", + input_schema=GetAirlinesInput + ) + @use_guards(OAuthGuard, create_scope_guard(["read"])) + async def get_airlines(self, input: GetAirlinesInput, context: ExecutionContext) -> dict: + context.logger.info("Fetching common airlines") + res = await self.service.get_airlines() + return {"airlines": res} + + +@injectable(deps=[DuffelService]) +class BookingTools: + def __init__(self, service: DuffelService): + self.service = service + + @tool( + name="create_order", + title="Create Order", + description="Create a flight order with hold (no payment required). Collect passenger details first.", + input_schema=CreateOrderInput + ) + @use_guards(OAuthGuard, create_scope_guard(["write"])) + @widget("order-summary") + async def create_order(self, input: CreateOrderInput, context: ExecutionContext) -> dict: + context.logger.info(f"Creating flight order for offer {input.offerId}") try: - result = self.service.get_booking(input.booking_id) - return {"status": "success", "booking": result} - except Exception as e: - return {"status": "failed", "error": str(e)} + passengers_array = json.loads(input.passengers) + except Exception: + raise ValueError("Invalid passengers JSON format") + + passengers = [] + for p in passengers_array: + passengers.append({ + "title": p.get("title", "mr"), + "given_name": p.get("givenName"), + "family_name": p.get("familyName"), + "gender": p.get("gender", "M"), + "born_on": p.get("bornOn"), + "email": p.get("email"), + "phone_number": p.get("phoneNumber") + }) + + order_params = { + "selectedOffers": [input.offerId], + "passengers": passengers + } + return await self.service.create_order(order_params) + + @tool( + name="get_order_details", + title="Get Order Details", + description="Get detailed information about an order.", + input_schema=OrderDetailsInput + ) + @use_guards(OAuthGuard, create_scope_guard(["read"])) + @widget("order-summary") + async def get_order_details(self, input: OrderDetailsInput, context: ExecutionContext) -> dict: + context.logger.info(f"Fetching details for order {input.orderId}") + return await self.service.get_order(input.orderId) + + @tool( + name="get_seat_map", + title="Get Seat Map", + description="Get available seats for a flight offer to allow seat selection.", + input_schema=SeatMapInput + ) + @use_guards(OAuthGuard, create_scope_guard(["read"])) + @widget("seat-selection") + async def get_seat_map(self, input: SeatMapInput, context: ExecutionContext) -> dict: + context.logger.info(f"Fetching seat map for offer {input.offerId}") + res = await self.service.get_seats_for_offer(input.offerId) + return {"offerId": input.offerId, "cabins": res} + + @tool( + name="cancel_order", + title="Cancel Order", + description="Cancel a flight order and request refund if applicable.", + input_schema=CancelOrderInput + ) + @use_guards(OAuthGuard, create_scope_guard(["write"])) + @widget("order-cancellation") + async def cancel_order(self, input: CancelOrderInput, context: ExecutionContext) -> dict: + context.logger.info(f"Cancelling order {input.orderId}") + return await self.service.cancel_order(input.orderId) + + +# 5. Flight Prompts +@injectable(deps=[DuffelService]) +class FlightPrompts: + def __init__(self, service: DuffelService): + self.service = service + + @prompt( + name="flight_search_assistant", + description="An AI assistant specialized in helping users search for flights and make booking decisions." + ) + async def flight_search_assistant(self, args: dict, context: ExecutionContext) -> str: + return "You are a professional flight booking assistant. Help the user search for flights using search_flights, search_airports, and assist in booking holds." + + @prompt( + name="flight_comparison", + description="Compare multiple flight offers and provide recommendations." + ) + async def flight_comparison(self, args: dict, context: ExecutionContext) -> str: + return "Compare the provided flight offer details, check layover times, durations, and pricing, and give the user a summary of best options." + + +# 6. Flight Resources +@injectable(deps=[DuffelService]) +class FlightResources: + def __init__(self, service: DuffelService): + self.service = service + + @resource( + uri="flight://popular-routes", + name="Popular Flight Routes", + description="Information about popular routes and pricing", + mime_type="application/json" + ) + async def popular_routes(self, context: ExecutionContext) -> dict: + return { + "routes": [ + {"route": "JFK -> LAX", "price": "$200-400"}, + {"route": "LHR -> JFK", "price": "$400-800"} + ] + } + + @resource( + uri="flight://booking-guide", + name="Flight Booking Guide", + description="Guide on searching and booking flights", + mime_type="text/markdown" + ) + async def booking_guide(self, context: ExecutionContext) -> str: + return "# Flight Booking Guide\n\n1. Search flights.\n2. Collect passenger info.\n3. Create order hold." + + +# 7. Health Check +class SystemHealthCheck: + def __init__(self): + self.start_time = time.time() + + @health_check("system") + def check_system(self) -> bool: + uptime = time.time() - self.start_time + return uptime >= 0 -# 4. Modules +# 8. Modules @module( - name="flight_booking", - controllers=[FlightBookingController], - providers=[FlightBookingService], - exports=[FlightBookingService] + name="flights", + controllers=[FlightTools, BookingTools, FlightPrompts, FlightResources], + providers=[DuffelService], + exports=[DuffelService] ) -class FlightBookingModule: +class FlightsModule: pass @module( name="app", imports=[ - FlightBookingModule, # Configure OAuth resource protection OAuthModule.for_root( - resource_uri="http://localhost:8000/mcp", - authorization_servers=["http://localhost:3000/oauth"], - scopes_supported=["flight:read", "flight:write"], - token_introspection_endpoint="http://localhost:3000/oauth/introspect" - ) - ] + resource_uri="https://mcplocal", + authorization_servers=["https://dev-5dt0utuk315713tjm.us.auth0.com"], + scopes_supported=["read", "write", "admin"], + token_introspection_endpoint=os.environ.get("INTROSPECTION_ENDPOINT") + ), + FlightsModule + ], + controllers=[], + providers=[SystemHealthCheck], + exports=[] ) class AppModule: pass -# 5. Application Entrypoint +# 9. Application Entrypoint @mcp_app( module=AppModule, - server=ServerConfig(name="flight-booking-server", version="1.0.0") + server=ServerConfig(name="airline-ticketing-server", version="1.0.0") ) class App: pass async def main(): - sys.stderr.write("Starting Flight Booking Server...\n") + sys.stderr.write("🔐 Starting Airline Ticketing MCP Server with OAuth 2.1...\n") sys.stderr.flush() app = await McpApplicationFactory.create(App) await app.start() diff --git a/my-test-server/__pycache__/app_module.cpython-312.pyc b/my-test-server/__pycache__/app_module.cpython-312.pyc deleted file mode 100644 index d73bb8038f3eb6d0c478bdbdce04d490ece64b1a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 574 zcmYjNJ&W5w5S>}smgTP~hLjE|(x|Yj5V#`*jEhup5lABpLP#6qWMwHU*>~#iH^jf; z@colCDaC?|kSd*n1BO(YmE@2W%)Ymyd2eRE_WK>6^W}x}@l5v*O)kf*!RZ}=9fS~~ zfCybgjhSGP;Ti}GIEDr{mbe7{Jv8>ss*=GQ2%C2hw)U)g+1HY8BR8G)Pt6=uttbvR zf)lc5z6dtL&tmzud{c&>Bu-N`)_%m5xr&llW~u51@j8>S5L~9pN@ToUg&Latw)`Qe zeaZ`Ka_SM-0S6aDa9NHHQ`R&|DyO+)sJ7>6AJ0=A&#Rgu-1EH88(&nJdpK#iKQCw3 zi}xvcE|&f#^A~e|^Xx+`sEb9Ek29WTW6Exc(g3yAB*~@jp$rWIsb29N9BpV?^6@1z zr+GW;70s7se(*^elu*yBcG5}kZ$tmV+CXa2%EH#DlpN~v_N%J?wR}QUZzR2n+a diff --git a/my-test-server/modules/calculator/__pycache__/calculator_module.cpython-312.pyc b/my-test-server/modules/calculator/__pycache__/calculator_module.cpython-312.pyc deleted file mode 100644 index e50b56e6e2828908c2968663a549f1a645187883..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 707 zcmaJ;J&)5s5S_K-IL;-xgbz^xhtMIpxT~lTq990AL`aCnva~Yx5(U=Tu_hx42jc31gyKO-G{4ji{2*3|RF3hjN?hS%- z$RHyeauN|@u*03)ja-Xe-pIYkBj5ldSz`>$9TVnN-&b$tRh=Ao4q5XIGJoY%kyR}b zHJ7b0_-R;3bTb#(oHIl>h6zvSJSl`6ifJJQ=S*UJu#dlDaxqDnZl{-}p{E^ZwRXOfdWuCUm` z>$Y+&`~#HrKe4rt0SmFRTZtf8`Q9dq;y}LleQ(~cJm&pi&;vX#AH=7x(S9NE`vfhi z_mC_>f@B&dEMW{dD0Twx_=+_#91cKwbCCXuH=b3SobXu?cfOK}nd^A>ooKPjdV`<&8ShpZkeVW!=qvq4P?MvM`6roR4$mm~iaa zb&M)2GtIrWAQ_sBblpc-!X>-p7sw8Au;E*XJ?C4cN9G#(Oc%jYa1=#9AMZa*kE}8_ zJrPwY(xG}5jS4N5Nhh#+9_)*fT*6WsmGwidTOu-iX4XW<$UY7cn$jpU}=FXz&mo&W#< diff --git a/my-test-server/modules/calculator/__pycache__/calculator_tools.cpython-312.pyc b/my-test-server/modules/calculator/__pycache__/calculator_tools.cpython-312.pyc deleted file mode 100644 index f835e097554b82325234e99ecfa363aed5265779..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1705 zcmZuxO=ufO6rS0C$+8tYNuAbBytGZo9%8UhbxBhk6YNsz9zqTkf@QrkQlzeS-Pu)a z86867LUJfMIuHuZDG5$#a_Y7BUSw*B7zjD^(%$6C(4SM^TS>KJJh0!qH}CDddGDJy zzYGs&35?etI@K2nA-|zA7o9jTUH4DJV6ypi*2@0lNI3;cTEC6Tz!#jT3ZVKH;ni}~jf$j8+tYF zx-#p!wTRUNlyk28b=?d47QGZiULy0Z>xE&IczER4b)S+hp5xyy=07Ulh`ET1Wv`xi z#U;LKm!g1iQLHsCB|J_pf%O)u>Hcxi|3Aq?qKid|r&byw1G}bx^u;-VZ%NzC-B|?K zSQKLeGn7VSd?kLPDkfNQbQb{*s=_E&OA1pqw1NgC>P%%CH=gMOYMH?eW^$8R+$tF? zv!NZLG3yM;QwPi-T_OqsE~JT>0ZV?L`!qDlNKO||x=EU}N!-+c&>#+7a;O*w*-@kx z^PrR}G5%)T;v`H+OC_u%uDgErxVTdT^AI@4Sg+fpJ$~-4_FXP5k5oOz*3SUlcCs4T zVZF8t{MboU=E+Si;5ur^tsG)J_C;j{8I`#TUJW<)Z}OTa^;DCl-@BXA5Im`i&>!47 zh)E3i?!edG1^A3mOjfCad~{R~|J64#+N4$Os3##0p&`M7dmkAO$@kiz?I>xIs&&Kx z)GAiw$T&XE@i7KXe4YHDu2M%`S~sDJm9T8D>$Vp%+cBgOMCCG&uT{cQB=GeS7&T>4 zOn~I5;%!`*M6dy3%-`;lo4Tx@IhI$TZ3O+70DdQb-zJ?L$(*=9a&P3NHL+t&JScDR z$K^-mtuLSO=huE(c(L$&w)O7EyVjL<{^b4Uz2?jO?xGr5U`L zm9#GRHoYP)f*(Hd@h1Qsl~Vee%=3.10 -Description-Content-Type: text/markdown -Requires-Dist: mcp>=1.0.0 -Requires-Dist: pydantic>=2.0.0 -Requires-Dist: starlette>=0.30.0 -Requires-Dist: uvicorn>=0.20.0 -Requires-Dist: watchfiles>=0.18.0 - -# 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 new apps (`init`), generate boilerplates (`generate`), 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 my-server -``` - -*(Or via Python: `python -m nitrostack.cli.main init my-server`)* - -This launches an interactive prompt where you can: -1. **Choose a template**: - - **Starter**: A simple calculator server. - - **Advanced**: A food delivery server with items and order status tracking. - - **OAuth**: A flight booking server demonstrating OAuth 2.1 authentication and guarded routes. -2. **Provide metadata**: Specify a custom description and author name. - -Once scaffolded, follow the next steps printed by the CLI to run your server, configure environment variables, and try it out. - ---- - -## NitroStudio Dashboard - -NitroStudio is an interactive visual developer dashboard for inspecting, graphing, and testing your MCP servers. - -To launch the dashboard, execute: -```bash -nitrostack-studio -``` -If your Python scripts directory is not configured in your system `PATH`, you can run it via Python: -```bash -python -m nitrostack.studio -``` -This launches the interface in your default web browser, allowing you to traverse directories, visualize your dependency graph, test tool execution forms, chat with a local mock LLM, and inspect RPC logs. - ---- - -## 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 | -|---|---| -| `PORT` / `MCP_SERVER_PORT` | The port to bind for HTTP/SSE transport (default: `8000`). | -| `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`. | -| `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). | - ---- - -## 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. - -### 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 -``` - -### 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()) -``` diff --git a/nitrostack.egg-info/SOURCES.txt b/nitrostack.egg-info/SOURCES.txt deleted file mode 100644 index 12a4830..0000000 --- a/nitrostack.egg-info/SOURCES.txt +++ /dev/null @@ -1,35 +0,0 @@ -README.md -pyproject.toml -nitrostack/__init__.py -nitrostack/studio.py -nitrostack.egg-info/PKG-INFO -nitrostack.egg-info/SOURCES.txt -nitrostack.egg-info/dependency_links.txt -nitrostack.egg-info/entry_points.txt -nitrostack.egg-info/requires.txt -nitrostack.egg-info/top_level.txt -nitrostack/auth/api_key.py -nitrostack/auth/config.py -nitrostack/auth/jwt.py -nitrostack/auth/oauth.py -nitrostack/cli/main.py -nitrostack/core/additional_decorators.py -nitrostack/core/app.py -nitrostack/core/context.py -nitrostack/core/decorators.py -nitrostack/core/di.py -nitrostack/core/errors.py -nitrostack/core/module.py -nitrostack/core/pipeline.py -nitrostack/core/task.py -nitrostack/events/event_emitter.py -nitrostack/static/app.js -nitrostack/static/index.html -nitrostack/static/styles.css -nitrostack/testing/__init__.py -nitrostack/transports/stdio.py -tests/test_basic.py -tests/test_initial_tool.py -tests/test_oauth.py -tests/test_production.py -tests/test_tasks.py \ No newline at end of file diff --git a/nitrostack.egg-info/dependency_links.txt b/nitrostack.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789..0000000 --- a/nitrostack.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/nitrostack.egg-info/entry_points.txt b/nitrostack.egg-info/entry_points.txt deleted file mode 100644 index 7862392..0000000 --- a/nitrostack.egg-info/entry_points.txt +++ /dev/null @@ -1,3 +0,0 @@ -[console_scripts] -nitrostack-py = nitrostack.cli.main:main -nitrostack-studio = nitrostack.studio:start_server_cli diff --git a/nitrostack.egg-info/requires.txt b/nitrostack.egg-info/requires.txt deleted file mode 100644 index aecb838..0000000 --- a/nitrostack.egg-info/requires.txt +++ /dev/null @@ -1,5 +0,0 @@ -mcp>=1.0.0 -pydantic>=2.0.0 -starlette>=0.30.0 -uvicorn>=0.20.0 -watchfiles>=0.18.0 diff --git a/nitrostack/__init__.py b/nitrostack/__init__.py index ad8d6f0..c9a3055 100644 --- a/nitrostack/__init__.py +++ b/nitrostack/__init__.py @@ -5,6 +5,7 @@ resource, prompt, initial_tool, + widget, ToolAnnotations, ResourceAnnotations, PromptArgument, @@ -82,6 +83,7 @@ "resource", "prompt", "initial_tool", + "widget", "ToolAnnotations", "ResourceAnnotations", "PromptArgument", diff --git a/nitrostack/__pycache__/__init__.cpython-312.pyc b/nitrostack/__pycache__/__init__.cpython-312.pyc index cd615727e79996648d164b2a4102ae8ccfe1d0c5..d4322cd1c9df9e64ba9cea25f4ea1508f48e2460 100644 GIT binary patch delta 765 zcmZ9IO>YuG7{?h3yB1hjmiM=|qDU#PwzahsJou6rO?vb4grwONq?CDp%x+VAku>oO zkcl@hdiK_{XAgb?N&EoDi=KLQXA-rt$^7zvp7}j9*?F&j*0nF1=5smUC%t__XIhH1 zX2q>3`oT|JU02sl1E_(9K?q_9La2i-`%5&81{gAXXau7W#Tdk7?WJ){Ktg7PCNTvm zOhXzokdZx=W-$jjT!9r_g;iXGHQB#GZ(<(ucnfA3!UTJ#0c#)*-r&Eoc!}-__MW z5NMx=OU8Hc9`2H#4@pr~&Ia9n&%T_IQ#Yw`)zc;+{>1Y(-f$$Q3xov>2M47G{Ds|lkn4C9(%_bhub>ND}4 zbM^4hT`Sr)n9C4QQ#=*}eLdca8 ZAnq&IAN-Oj%`eriukLqWsO+I*{ssgS!RG(~ delta 701 zcmZvY%WD%+7{%wN$&4nMydN`3nzU(Sn%AT?wn+EMm|LJZ@TVR-0$pD>)GR( zZj#Qd+&U$St=JwKz#s%s2OUEY!Z3tozCa@wg{ahC8pAlmF#!qbeKd(FNJ*{IG#X%_ z2_|MBBQuR=F$XzZghk9l9t%*A`7OGH%dm_^DB=pt&XfP1aGO@J3RSE@4eL;sRX=T@ z1r|1;DSd#h;|6R<9i(@$1udy{dJi{Y6SrVX`Vehn2RgV7+tP>W4(`G(QH~Tl@{z#( zlV~OOQ~ZmHf9~ZkN$JabGWC!KH+*f4h1>y)@kidXXjsUIK-5LIvd8~;yZoUqbEu0@ zHm^GK#Pi{ljVIH=!N@uoGsl_^CL_6X9=krWhb$(Jh`X4lu1|4nA5(`V_;;V#PKsVi zAbn%mVQkDM%n%JzAS00F1vSs#c+1>Y*8-~|Dhb@-@6^(2S;#dP^Ynjr500l#t-*_7 zf8?C73jd*+q{=VVtXdQGI?rl3wIQU%_cWu~6#KP)e?V!!|5^F%TIF(mf2a3k;;>2Y z$USb*>pHKN4#(7X7=No3SN7cwao6P3#jgcI$b}jpo(ts)20zzqzZLMj*4P8L^cM$0 BtlR(q diff --git a/nitrostack/__pycache__/studio.cpython-312.pyc b/nitrostack/__pycache__/studio.cpython-312.pyc deleted file mode 100644 index db83b1a0825962b8843dcf52b61971305260abc2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42653 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 b3u(9%Z({5nZQeb>epko_%PizGnErnOcguD~ diff --git a/nitrostack/auth/__pycache__/api_key.cpython-312.pyc b/nitrostack/auth/__pycache__/api_key.cpython-312.pyc index 17d249def62dd48627791ed40914d100cc9f7fc7..f3ae70a906ca138ce22c1d4e619f2a5c658f74ba 100644 GIT binary patch delta 50 zcmZ3Zuw7w83X`h6vsFxJacWU|Fh66gQ` delta 39 ucmdmFy4-X_CXJB3S diff --git a/nitrostack/auth/__pycache__/oauth.cpython-312.pyc b/nitrostack/auth/__pycache__/oauth.cpython-312.pyc index 5f9b4d4e27f4c5a9bca24f28bd603b87e2ae6d8d..f451c9a499d8cf951f752151534af65f0d1e4abc 100644 GIT binary patch delta 50 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 UdRJ5Of!6SicAzQkDuT8D2k<3ILjV8( diff --git a/nitrostack/cli/main.py b/nitrostack/cli/main.py index 6c64773..d673dfe 100644 --- a/nitrostack/cli/main.py +++ b/nitrostack/cli/main.py @@ -195,131 +195,571 @@ async def track_order(self, input: OrderStatusInput, context: ExecutionContext) return self.service.get_order_status(input.order_id) """ -FLIGHT_APP_MODULE_TEMPLATE = """from nitrostack import module -from nitrostack.auth.oauth import OAuthModule -from modules.flight_booking.flight_booking_module import FlightBookingModule +FLIGHT_APP_MODULE_TEMPLATE = """from nitrostack import module, ConfigModule, OAuthModule +from modules.flights.flights_module import FlightsModule +from health.system_health import SystemHealthCheck +import os @module( name="app", imports=[ - FlightBookingModule, + ConfigModule.for_root( + env_file_path=".env", + defaults={"RESOURCE_URI": "https://mcplocal", "PORT": "8000"} + ), # Configure OAuth resource protection OAuthModule.for_root( - resource_uri="http://localhost:8000/mcp", - authorization_servers=["http://localhost:3000/oauth"], - scopes_supported=["flight:read", "flight:write"], - token_introspection_endpoint="http://localhost:3000/oauth/introspect" - ) + resource_uri=os.environ.get("RESOURCE_URI", "https://mcplocal"), + authorization_servers=[os.environ.get("AUTH_SERVER_URL", "https://dev-5dt0utuk315713tjm.us.auth0.com")], + scopes_supported=["read", "write", "admin"], + token_introspection_endpoint=os.environ.get("INTROSPECTION_ENDPOINT"), + token_introspection_client_id=os.environ.get("INTROSPECTION_CLIENT_ID"), + token_introspection_client_secret=os.environ.get("INTROSPECTION_CLIENT_SECRET"), + audience=os.environ.get("TOKEN_AUDIENCE"), + issuer=os.environ.get("TOKEN_ISSUER") + ), + FlightsModule ], controllers=[], - providers=[], + providers=[SystemHealthCheck], exports=[] ) class AppModule: pass """ -FLIGHT_BOOKING_MODULE_TEMPLATE = """from nitrostack import module -from modules.flight_booking.flight_booking_tools import FlightBookingController -from modules.flight_booking.flight_booking_service import FlightBookingService - -@module( - name="flight_booking", - imports=[], - controllers=[FlightBookingController], - providers=[FlightBookingService], - exports=[FlightBookingService] -) -class FlightBookingModule: - pass +FLIGHT_OAUTH_GUARD_TEMPLATE = """from nitrostack import ExecutionContext + +def create_scope_guard(required_scopes: list): + class ScopeGuard: + async def can_activate(self, context: ExecutionContext) -> bool: + user_scopes = getattr(context.auth, "scopes", []) + missing_scopes = [s for s in required_scopes if s not in user_scopes] + if missing_scopes: + raise ValueError( + f"Insufficient scope. Required: {', '.join(required_scopes)}. " + f"Missing: {', '.join(missing_scopes)}" + ) + return True + return ScopeGuard """ -FLIGHT_BOOKING_SERVICE_TEMPLATE = """from nitrostack import injectable +FLIGHT_SYSTEM_HEALTH_TEMPLATE = """import time +from nitrostack import health_check -@injectable(deps=[]) -class FlightBookingService: +class SystemHealthCheck: def __init__(self): - # Mock flight schedule - self.flights = { - "FL101": {"from": "NYC", "to": "LON", "date": "2026-07-01", "price": 450.0}, - "FL202": {"from": "PAR", "to": "TOK", "date": "2026-07-02", "price": 850.0}, - "FL303": {"from": "LAX", "to": "NYC", "date": "2026-07-03", "price": 200.0} - } - self.bookings = {} - self.booking_counter = 5000 + self.start_time = time.time() + + @health_check("system") + def check_system(self) -> bool: + uptime = time.time() - self.start_time + return uptime >= 0 +""" - def list_flights(self) -> dict: - return self.flights +FLIGHT_DUFFEL_SERVICE_TEMPLATE = """import os +import sys +import json +import urllib.request +import urllib.parse +from typing import List, Dict, Any, Optional +from nitrostack import injectable + +@injectable() +class DuffelService: + def __init__(self): + self.api_key = os.environ.get("DUFFEL_API_KEY") + self.is_mock = not self.api_key or self.api_key.startswith("your-") or len(self.api_key) < 5 - def book_flight(self, flight_id: str, passenger_name: str) -> dict: - if flight_id not in self.flights: - return {"status": "error", "message": f"Flight {flight_id} not found."} + def _request(self, method: str, path: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + if self.is_mock: + raise ValueError("Duffel API key is not configured. Running in mock mode.") + + headers = { + "Authorization": f"Bearer {self.api_key}", + "Duffel-Version": "v1", + "Content-Type": "application/json" + } + url = f"https://api.duffel.com{path}" + req_data = json.dumps({"data": data}).encode("utf-8") if data else None - self.booking_counter += 1 - booking_id = f"BKG-{self.booking_counter}" + req = urllib.request.Request(url, data=req_data, headers=headers, method=method) + try: + with urllib.request.urlopen(req, timeout=10) as response: + res_payload = json.loads(response.read().decode("utf-8")) + return res_payload.get("data", {}) + except Exception as e: + sys.stderr.write(f"Duffel API Request failed: {e}\\n") + sys.stderr.flush() + raise e + + async def search_flights(self, params: Dict[str, Any]) -> Dict[str, Any]: + if self.is_mock: + return { + "id": "orq_mock123456", + "offers": [ + { + "id": "off_mock123456", + "total_amount": "450.00", + "total_currency": "USD", + "expires_at": "2026-12-31T12:00:00Z", + "slices": [ + { + "origin": {"iata_code": params["origin"], "name": "Origin Airport", "city_name": "Origin City"}, + "destination": {"iata_code": params["destination"], "name": "Dest Airport", "city_name": "Dest City"}, + "duration": "PT6H30M", + "segments": [ + { + "id": "seg_outbound", + "origin": {"iata_code": params["origin"]}, + "destination": {"iata_code": params["destination"]}, + "departing_at": f"{params['departureDate']}T08:00:00Z", + "arriving_at": f"{params['departureDate']}T14:30:00Z", + "marketing_carrier": {"name": "Mock Airlines"}, + "marketing_carrier_flight_number": "MK123", + "aircraft": {"name": "Boeing 787"} + } + ] + } + ] + } + ] + } - self.bookings[booking_id] = { - "booking_id": booking_id, - "flight_id": flight_id, - "passenger_name": passenger_name, - "price": self.flights[flight_id]["price"], - "status": "Confirmed" + slices = [ + { + "origin": params["origin"], + "destination": params["destination"], + "departure_date": params["departureDate"] + } + ] + if params.get("returnDate"): + slices.append({ + "origin": params["destination"], + "destination": params["origin"], + "departure_date": params["returnDate"] + }) + + duffel_params = { + "slices": slices, + "passengers": params.get("passengers", [{"type": "adult"}]), + "cabin_class": params.get("cabinClass", "economy"), + "return_offers": True + } + res = self._request("POST", "/offer_requests", duffel_params) + return { + "id": res.get("id"), + "offers": res.get("offers", []), + "passengers": res.get("passengers", []), + "slices": res.get("slices", []) } - return self.bookings[booking_id] - def get_booking(self, booking_id: str) -> dict: - return self.bookings.get(booking_id, {"status": "error", "message": f"Booking {booking_id} not found."}) + async def get_offer(self, offer_id: str) -> Dict[str, Any]: + if self.is_mock: + return { + "id": offer_id, + "total_amount": "450.00", + "total_currency": "USD", + "expires_at": "2026-12-31T12:00:00Z", + "slices": [ + { + "origin": {"iata_code": "JFK", "name": "John F. Kennedy Airport", "city_name": "New York"}, + "destination": {"iata_code": "LAX", "name": "Los Angeles Airport", "city_name": "Los Angeles"}, + "duration": "PT6H30M", + "segments": [ + { + "id": "seg_mock", + "origin": {"iata_code": "JFK"}, + "destination": {"iata_code": "LAX"}, + "departing_at": "2026-07-15T08:00:00Z", + "arriving_at": "2026-07-15T14:30:00Z", + "marketing_carrier": {"name": "Mock Airlines"}, + "marketing_carrier_flight_number": "MK123", + "aircraft": {"name": "Boeing 787"} + } + ] + } + ] + } + return self._request("GET", f"/offers/{offer_id}") + + async def get_seats_for_offer(self, offer_id: str) -> List[Dict[str, Any]]: + if self.is_mock: + return [ + { + "cabin_class": "economy", + "rows": [ + { + "row_number": 10, + "sections": [ + { + "elements": [ + { + "type": "seat", + "id": "seat_10a", + "designator": "10A", + "available_services": [{"total_amount": "25.00", "total_currency": "USD"}], + "disclosures": ["window"] + }, + { + "type": "seat", + "id": "seat_10b", + "designator": "10B", + "available_services": [{"total_amount": "0.00", "total_currency": "USD"}], + "disclosures": ["middle"] + } + ] + } + ] + } + ] + } + ] + res = self._request("GET", f"/seat_maps?offer_id={offer_id}") + return res if isinstance(res, list) else [] + + async def create_order(self, params: Dict[str, Any]) -> Dict[str, Any]: + if self.is_mock: + return { + "id": "ord_mock123456", + "status": "held", + "total_amount": "450.00", + "total_currency": "USD", + "expires_at": "2026-12-31T12:00:00Z", + "booking_reference": "ABCXYZ", + "passengers": [ + { + "id": f"pax_{idx}", + "given_name": p["given_name"], + "family_name": p["family_name"], + "type": "adult" + } for idx, p in enumerate(params["passengers"]) + ], + "slices": [ + { + "origin": {"iata_code": "JFK"}, + "destination": {"iata_code": "LAX"}, + "duration": "PT6H30M", + "segments": [ + { + "departing_at": "2026-07-15T08:00:00Z", + "arriving_at": "2026-07-15T14:30:00Z" + } + ] + } + ] + } + + order_payload = { + "selected_offers": params["selectedOffers"], + "passengers": params["passengers"], + "type": "hold" + } + return self._request("POST", "/orders", order_payload) + + async def get_order(self, order_id: str) -> Dict[str, Any]: + if self.is_mock: + return { + "id": order_id, + "status": "held", + "total_amount": "450.00", + "total_currency": "USD", + "booking_reference": "ABCXYZ", + "created_at": "2026-06-25T12:00:00Z", + "expires_at": "2026-12-31T12:00:00Z", + "passengers": [ + { + "id": "pax_0", + "given_name": "John", + "family_name": "Doe", + "type": "adult", + "email": "john@example.com", + "phone_number": "+1234567890" + } + ], + "slices": [ + { + "id": "sli_mock", + "origin": {"iata_code": "JFK", "name": "John F. Kennedy Airport", "city_name": "New York"}, + "destination": {"iata_code": "LAX", "name": "Los Angeles Airport", "city_name": "Los Angeles"}, + "duration": "PT6H30M", + "segments": [ + { + "id": "seg_mock", + "origin": {"iata_code": "JFK"}, + "destination": {"iata_code": "LAX"}, + "departing_at": "2026-07-15T08:00:00Z", + "arriving_at": "2026-07-15T14:30:00Z", + "marketing_carrier": {"name": "Mock Airlines"}, + "marketing_carrier_flight_number": "MK123", + "aircraft": {"name": "Boeing 787"} + } + ] + } + ] + } + return self._request("GET", f"/orders/{order_id}") + + async def cancel_order(self, order_id: str) -> Dict[str, Any]: + if self.is_mock: + return { + "id": "ocr_mock123456", + "refund_amount": "450.00", + "refund_currency": "USD", + "confirmed_at": "2026-06-25T12:30:00Z" + } + cancel_payload = {"order_id": order_id} + return self._request("POST", "/order_cancellations", cancel_payload) + + async def get_airlines(self) -> List[Dict[str, Any]]: + if self.is_mock: + return [ + {"iata_code": "AA", "name": "American Airlines"}, + {"iata_code": "DL", "name": "Delta Air Lines"}, + {"iata_code": "UA", "name": "United Airlines"}, + {"iata_code": "BA", "name": "British Airways"} + ] + res = self._request("GET", "/airlines") + return res if isinstance(res, list) else [] """ -FLIGHT_BOOKING_TOOLS_TEMPLATE = """from nitrostack import injectable, tool, use_guards, OAuthGuard, ExecutionContext -from modules.flight_booking.flight_booking_service import FlightBookingService -from pydantic import BaseModel, Field +FLIGHT_MODULE_TEMPLATE = """from nitrostack import module +from modules.flights.flights_tools import FlightTools +from modules.flights.booking_tools import BookingTools +from modules.flights.flights_prompts import FlightPrompts +from modules.flights.flights_resources import FlightResources +from services.duffel_service import DuffelService -class SearchFlightsInput(BaseModel): +@module( + name="flights", + controllers=[FlightTools, BookingTools, FlightPrompts, FlightResources], + providers=[DuffelService], + exports=[DuffelService] +) +class FlightsModule: pass +""" -class BookFlightInput(BaseModel): - flight_id: str = Field(description="The flight identifier (e.g., FL101, FL202)") - passenger_name: str = Field(description="Passenger's full name") - -class BookingInfoInput(BaseModel): - booking_id: str = Field(description="The flight booking ID (e.g., BKG-5001)") +FLIGHT_TOOLS_TEMPLATE = """from nitrostack import injectable, tool, use_guards, OAuthGuard, ExecutionContext +from services.duffel_service import DuffelService +from guards.oauth_guard import create_scope_guard +from pydantic import BaseModel, Field +from typing import Optional -@injectable(deps=[FlightBookingService]) -class FlightBookingController: - def __init__(self, service: FlightBookingService): +class SearchFlightsInput(BaseModel): + origin: str = Field(description="Origin airport IATA code (e.g., 'JFK', 'LHR')") + destination: str = Field(description="Destination airport IATA code (e.g., 'LAX', 'CDG')") + departureDate: str = Field(description="Departure date in YYYY-MM-DD format") + returnDate: Optional[str] = Field(default=None, description="Return date in YYYY-MM-DD format for round trip") + adults: int = Field(default=1, description="Number of adult passengers (18+)") + cabinClass: str = Field(default="economy", description="Preferred cabin class (economy, premium_economy, business, first)") + +class FlightDetailsInput(BaseModel): + offerId: str = Field(description="The flight offer ID from search results") + +class AirportSearchInput(BaseModel): + query: str = Field(description="The search query for airports (e.g., 'London', 'New York')") + +@injectable(deps=[DuffelService]) +class FlightTools: + def __init__(self, service: DuffelService): self.service = service @tool( name="search_flights", - description="Search for available flights and schedules", + title="Search Flights", + description="Search for flight offers based on origin, destination, dates, and preferences.", input_schema=SearchFlightsInput ) + @use_guards(OAuthGuard, create_scope_guard(["read"])) async def search_flights(self, input: SearchFlightsInput, context: ExecutionContext) -> dict: - context.logger.info("Searching available flights...") - return {"flights": self.service.list_flights()} + context.logger.info(f"Searching flights from {input.origin} to {input.destination}") + res = await self.service.search_flights(input.model_dump()) + return res @tool( - name="book_flight", - description="Book a flight ticket. Requires OAuth authentication.", - input_schema=BookFlightInput + name="get_flight_details", + title="Get Flight Details", + description="Get detailed information about a specific flight offer including baggage allowance, conditions.", + input_schema=FlightDetailsInput ) - @use_guards(OAuthGuard) - async def book_flight(self, input: BookFlightInput, context: ExecutionContext) -> dict: - # OAuthGuard validates access token and populates context.auth - user_id = getattr(context.auth, "subject", "unknown-user") - context.logger.info(f"Booking flight {input.flight_id} for user {user_id}") - return self.service.book_flight(input.flight_id, input.passenger_name) + @use_guards(OAuthGuard, create_scope_guard(["read"])) + async def get_flight_details(self, input: FlightDetailsInput, context: ExecutionContext) -> dict: + context.logger.info(f"Fetching flight details for offer {input.offerId}") + res = await self.service.get_offer(input.offerId) + return res @tool( - name="get_booking_details", - description="Get booking receipt and details. Requires OAuth authentication.", - input_schema=BookingInfoInput + name="search_airports", + title="Search Airports", + description="Search for airports by query string.", + input_schema=AirportSearchInput ) - @use_guards(OAuthGuard) - async def get_booking_details(self, input: BookingInfoInput, context: ExecutionContext) -> dict: - context.logger.info(f"Retrieving details for booking {input.booking_id}") - return self.service.get_booking(input.booking_id) + @use_guards(OAuthGuard, create_scope_guard(["read"])) + async def search_airports(self, input: AirportSearchInput, context: ExecutionContext) -> dict: + context.logger.info(f"Searching airports for query: {input.query}") + return {"airports": [{"iataCode": "JFK", "name": "John F. Kennedy Airport"}]} + + @tool( + name="get_airlines", + title="Get Airlines", + description="Get list of common airlines." + ) + @use_guards(OAuthGuard, create_scope_guard(["read"])) + async def get_airlines(self, context: ExecutionContext) -> dict: + context.logger.info("Fetching common airlines") + res = await self.service.get_airlines() + return {"airlines": res} +""" + +FLIGHT_BOOKING_TOOLS_TEMPLATE = """from nitrostack import injectable, tool, use_guards, OAuthGuard, ExecutionContext +from services.duffel_service import DuffelService +from guards.oauth_guard import create_scope_guard +from pydantic import BaseModel, Field +import json + +class CreateOrderInput(BaseModel): + offerId: str = Field(description="The offer ID to book") + passengers: str = Field(description="JSON string containing array of passenger objects. Each passenger must have: title (mr/ms/mrs/miss/dr), givenName (first name), familyName (last name), gender (M/F), bornOn (YYYY-MM-DD), email, phoneNumber.") + +class OrderDetailsInput(BaseModel): + orderId: str = Field(description="The order ID") + +class SeatMapInput(BaseModel): + offerId: str = Field(description="The offer ID to get seats for") + +class CancelOrderInput(BaseModel): + orderId: str = Field(description="The order ID to cancel") + +@injectable(deps=[DuffelService]) +class BookingTools: + def __init__(self, service: DuffelService): + self.service = service + + @tool( + name="create_order", + title="Create Order", + description="Create a flight order with hold (no payment required). Collect passenger details first.", + input_schema=CreateOrderInput + ) + @use_guards(OAuthGuard, create_scope_guard(["write"])) + async def create_order(self, input: CreateOrderInput, context: ExecutionContext) -> dict: + context.logger.info(f"Creating flight order for offer {input.offerId}") + try: + passengers_array = json.loads(input.passengers) + except Exception: + raise ValueError("Invalid passengers JSON format") + + passengers = [] + for p in passengers_array: + passengers.append({ + "title": p.get("title", "mr"), + "given_name": p.get("givenName"), + "family_name": p.get("familyName"), + "gender": p.get("gender", "M"), + "born_on": p.get("bornOn"), + "email": p.get("email"), + "phone_number": p.get("phoneNumber") + }) + + order_params = { + "selectedOffers": [input.offerId], + "passengers": passengers + } + res = await self.service.create_order(order_params) + return res + + @tool( + name="get_order_details", + title="Get Order Details", + description="Get detailed information about an order.", + input_schema=OrderDetailsInput + ) + @use_guards(OAuthGuard, create_scope_guard(["read"])) + async def get_order_details(self, input: OrderDetailsInput, context: ExecutionContext) -> dict: + context.logger.info(f"Fetching details for order {input.orderId}") + res = await self.service.get_order(input.orderId) + return res + + @tool( + name="get_seat_map", + title="Get Seat Map", + description="Get available seats for a flight offer to allow seat selection.", + input_schema=SeatMapInput + ) + @use_guards(OAuthGuard, create_scope_guard(["read"])) + async def get_seat_map(self, input: SeatMapInput, context: ExecutionContext) -> dict: + context.logger.info(f"Fetching seat map for offer {input.offerId}") + res = await self.service.get_seats_for_offer(input.offerId) + return {"offerId": input.offerId, "cabins": res} + + @tool( + name="cancel_order", + title="Cancel Order", + description="Cancel a flight order and request refund if applicable.", + input_schema=CancelOrderInput + ) + @use_guards(OAuthGuard, create_scope_guard(["write"])) + async def cancel_order(self, input: CancelOrderInput, context: ExecutionContext) -> dict: + context.logger.info(f"Cancelling order {input.orderId}") + res = await self.service.cancel_order(input.orderId) + return res +""" + +FLIGHT_PROMPTS_TEMPLATE = """from nitrostack import injectable, prompt, ExecutionContext +from services.duffel_service import DuffelService +from typing import List + +@injectable(deps=[DuffelService]) +class FlightPrompts: + def __init__(self, service: DuffelService): + self.service = service + + @prompt( + name="flight_search_assistant", + description="An AI assistant specialized in helping users search for flights and make booking decisions." + ) + async def flight_search_assistant(self, args: dict, context: ExecutionContext) -> str: + return "You are a professional flight booking assistant. Help the user search for flights using search_flights, search_airports, and assist in booking holds." + + @prompt( + name="flight_comparison", + description="Compare multiple flight offers and provide recommendations." + ) + async def flight_comparison(self, args: dict, context: ExecutionContext) -> str: + return "Compare the provided flight offer details, check layover times, durations, and pricing, and give the user a summary of best options." +""" + +FLIGHT_RESOURCES_TEMPLATE = """from nitrostack import injectable, resource, ExecutionContext +from services.duffel_service import DuffelService + +@injectable(deps=[DuffelService]) +class FlightResources: + def __init__(self, service: DuffelService): + self.service = service + + @resource( + uri="flight://popular-routes", + name="Popular Flight Routes", + description="Information about popular routes and pricing", + mime_type="application/json" + ) + async def popular_routes(self, context: ExecutionContext) -> dict: + return { + "routes": [ + {"route": "JFK -> LAX", "price": "$200-400"}, + {"route": "LHR -> JFK", "price": "$400-800"} + ] + } + + @resource( + uri="flight://booking-guide", + name="Flight Booking Guide", + description="Guide on searching and booking flights", + mime_type="text/markdown" + ) + async def booking_guide(self, context: ExecutionContext) -> str: + return "# Flight Booking Guide\\n\\n1. Search flights.\\n2. Collect passenger info.\\n3. Create order hold." """ OAUTH_SETUP_TEMPLATE = """# OAuth 2.1 Server Setup Guide @@ -336,16 +776,16 @@ async def get_booking_details(self, input: BookingInfoInput, context: ExecutionC # Or use JWKS (JSON Web Key Sets) to cryptographically verify signatures locally # JWKS_URI=http://localhost:3000/oauth/jwks -# TOKEN_AUDIENCE=flight-booking-service -# TOKEN_ISSUER=http://localhost:3000/oauth +# TOKEN_AUDIENCE=https://mcplocal +# TOKEN_ISSUER=https://dev-5dt0utuk315713tjm.us.auth0.com ``` ## 2. Protected Routes -The tools in this server use the `@use_guards(OAuthGuard)` decorator to automatically protect endpoints: -* **`search_flights`**: Public (no guard). -* **`book_flight`**: Protected (requires valid access token with `flight:write` scope). -* **`get_booking_details`**: Protected (requires valid access token with `flight:read` scope). +The tools in this server use the `@use_guards(OAuthGuard, create_scope_guard([...]))` decorators to automatically protect endpoints: +* **Public**: No guards (or custom public filters). +* **Read-Protected**: Requires valid access token with `read` scope. +* **Write-Protected**: Requires valid access token with `write` scope. When calling protected tools, the client must pass a valid Bearer token in the `Authorization` header. """ @@ -419,8 +859,8 @@ def init_project(name: str, template: str = None): # 2. Select template if not template: print("\033[32m? \033[1;37mChoose a template:\033[0m") - print(" \033[34m1. Starter\033[0m Simple calculator for learning basics") - print(" \033[34m2. Advanced\033[0m Food delivery with items & status tracking") + print(" \033[34m1. Starter\033[0m Simple calculator/converter for learning basics") + print(" \033[34m2. Advanced\033[0m Pizza shop finder with maps & widgets") print(" \033[34m3. OAuth\033[0m Flight booking with OAuth 2.1 auth") while True: @@ -428,23 +868,22 @@ def init_project(name: str, template: str = None): sys.stdout.flush() choice = sys.stdin.readline().strip() if not choice or choice == "1": - template = "calculator" + template = "starter" break elif choice == "2": - template = "food-delivery" + template = "pizzaz" break elif choice == "3": template = "flight-booking" break - else: - print("Invalid choice. Please select 1, 2, or 3.") else: # Normalize command line template input - if template == "starter": - template = "calculator" - elif template == "advanced": - template = "food-delivery" - elif template == "oauth": + template = template.lower() + if template in ("starter", "calculator"): + template = "starter" + elif template in ("advanced", "pizzaz", "food-delivery"): + template = "pizzaz" + elif template in ("oauth", "flight-booking"): template = "flight-booking" # 3. Description and Author @@ -456,52 +895,67 @@ def init_project(name: str, template: str = None): sys.stdout.flush() author = sys.stdin.readline().strip() or "developer" + # 4. Copy template directory + import nitrostack + import shutil + package_dir = os.path.dirname(nitrostack.__file__) + template_src_dir = os.path.join(package_dir, "templates", template) + + if not os.path.exists(template_src_dir): + print(f"Error: Template '{template}' not found at '{template_src_dir}'.") + sys.exit(1) + + shutil.copytree(template_src_dir, name) print("\n\033[32m✓\033[0m Project created") print("\033[32m✓\033[0m Dependencies installed") - print("\033[32m✓\033[0m Widget dependencies installed\n") - # Write template files - if template == "calculator": - os.makedirs(os.path.join(name, "modules", "calculator"), exist_ok=True) - with open(os.path.join(name, "app_module.py"), "w", encoding="utf-8") as f: - f.write(APP_MODULE_TEMPLATE) - with open(os.path.join(name, "modules", "calculator", "calculator_module.py"), "w", encoding="utf-8") as f: - f.write(CALC_MODULE_TEMPLATE) - with open(os.path.join(name, "modules", "calculator", "calculator_service.py"), "w", encoding="utf-8") as f: - f.write(CALC_SERVICE_TEMPLATE) - with open(os.path.join(name, "modules", "calculator", "calculator_tools.py"), "w", encoding="utf-8") as f: - f.write(CALC_TOOLS_TEMPLATE) - elif template == "food-delivery": - os.makedirs(os.path.join(name, "modules", "food_delivery"), exist_ok=True) - with open(os.path.join(name, "app_module.py"), "w", encoding="utf-8") as f: - f.write(FOOD_APP_MODULE_TEMPLATE) - with open(os.path.join(name, "modules", "food_delivery", "food_delivery_module.py"), "w", encoding="utf-8") as f: - f.write(FOOD_DELIVERY_MODULE_TEMPLATE) - with open(os.path.join(name, "modules", "food_delivery", "food_delivery_service.py"), "w", encoding="utf-8") as f: - f.write(FOOD_DELIVERY_SERVICE_TEMPLATE) - with open(os.path.join(name, "modules", "food_delivery", "food_delivery_tools.py"), "w", encoding="utf-8") as f: - f.write(FOOD_DELIVERY_TEMPLATE) if 'FOOD_DELIVERY_TEMPLATE' in globals() else f.write(FOOD_DELIVERY_TOOLS_TEMPLATE) - elif template == "flight-booking": - os.makedirs(os.path.join(name, "modules", "flight_booking"), exist_ok=True) - with open(os.path.join(name, "app_module.py"), "w", encoding="utf-8") as f: - f.write(FLIGHT_APP_MODULE_TEMPLATE) - with open(os.path.join(name, "modules", "flight_booking", "flight_booking_module.py"), "w", encoding="utf-8") as f: - f.write(FLIGHT_BOOKING_MODULE_TEMPLATE) - with open(os.path.join(name, "modules", "flight_booking", "flight_booking_service.py"), "w", encoding="utf-8") as f: - f.write(FLIGHT_BOOKING_SERVICE_TEMPLATE) - with open(os.path.join(name, "modules", "flight_booking", "flight_booking_tools.py"), "w", encoding="utf-8") as f: - f.write(FLIGHT_BOOKING_TOOLS_TEMPLATE) - with open(os.path.join(name, "OAUTH_SETUP.md"), "w", encoding="utf-8") as f: - f.write(OAUTH_SETUP_TEMPLATE) + # 5. Update .env file + env_path = os.path.join(name, ".env") + if os.path.exists(env_path): + with open(env_path, "r", encoding="utf-8") as f: + lines = f.readlines() + new_lines = [] + for line in lines: + if line.startswith("SERVER_DESC="): + new_lines.append(f'SERVER_DESC="{description}"\n') + elif line.startswith("SERVER_AUTHOR="): + new_lines.append(f'SERVER_AUTHOR="{author}"\n') + else: + new_lines.append(line) + has_desc = any(line.startswith("SERVER_DESC=") for line in new_lines) + has_author = any(line.startswith("SERVER_AUTHOR=") for line in new_lines) + if not has_desc: + new_lines.append(f'SERVER_DESC="{description}"\n') + if not has_author: + new_lines.append(f'SERVER_AUTHOR="{author}"\n') + with open(env_path, "w", encoding="utf-8") as f: + f.writelines(new_lines) + + # 6. Update widgets package.json + widgets_package_path = os.path.join(name, "src", "widgets", "package.json") + if os.path.exists(widgets_package_path): + import json + try: + with open(widgets_package_path, "r", encoding="utf-8") as f: + pkg = json.load(f) + pkg["name"] = f"{name}-widgets" + with open(widgets_package_path, "w", encoding="utf-8") as f: + json.dump(pkg, f, indent=2) + except Exception: + pass + + # 7. Run npm install inside widgets directory + widgets_dir = os.path.join(name, "src", "widgets") + if os.path.exists(widgets_dir): + print("Installing widget dependencies...") + try: + subprocess.run(["npm", "--version"], shell=True, capture_output=True, check=True) + subprocess.run(["npm", "install"], cwd=widgets_dir, shell=True, check=True) + print("\033[32m✓\033[0m Widget dependencies installed\n") + except Exception as e: + print(f"Warning: Failed to install widget dependencies: {e}") + print("Please run 'npm install' inside 'src/widgets' manually.\n") - with open(os.path.join(name, "main.py"), "w", encoding="utf-8") as f: - f.write(MAIN_TEMPLATE) - with open(os.path.join(name, ".env"), "w", encoding="utf-8") as f: - env_content = f"PORT=8000\nNODE_ENV=development\nSERVER_DESC=\"{description}\"\nSERVER_AUTHOR=\"{author}\"\n" - f.write(env_content) - with open(os.path.join(name, "requirements.txt"), "w", encoding="utf-8") as f: - f.write(REQUIREMENTS_TEMPLATE) - # Success Card abs_path = os.path.abspath(name) success_box = f"""\033[36m╔══════════════════════════════════════════════════════════╗ @@ -533,7 +987,22 @@ def run_dev(): print(f"Starting hot-reload development server for {target}...") process = None + widgets_process = None + # Check if Next.js widgets are present + widgets_dir = os.path.join(os.getcwd(), "src", "widgets") + if os.path.exists(widgets_dir) and os.path.exists(os.path.join(widgets_dir, "package.json")): + print("Starting widget development server on port 3001...") + try: + # Spawn npm run dev -- --port 3001 + widgets_process = subprocess.Popen( + ["npm", "run", "dev", "--", "--port", "3001"], + cwd=widgets_dir, + shell=True + ) + except Exception as e: + print(f"Warning: Could not start widget development server: {e}") + def start_process(): nonlocal process if process: @@ -549,82 +1018,88 @@ def start_process(): env["PYTHONPATH"] = os.path.abspath(".") process = subprocess.Popen([sys.executable, target], env=env) - start_process() - - watched_extensions = {".py", ".env"} - - def get_mtimes(): - mtimes = {} - for root, dirs, files in os.walk("."): - if any(part.startswith(".") or part in ("venv", "env", "__pycache__") for part in root.split(os.sep)): - continue - for file in files: - ext = os.path.splitext(file)[1] - if ext in watched_extensions: - path = os.path.join(root, file) - try: - mtimes[path] = os.path.getmtime(path) - except Exception: - pass - return mtimes - - # Try using watchfiles for high-performance, low-CPU file monitoring + def cleanup(): + nonlocal process, widgets_process + if process: + try: + process.terminate() + except Exception: + pass + if widgets_process: + try: + widgets_process.terminate() + except Exception: + pass + try: - from watchfiles import watch - print("Using watchfiles for high-performance file monitoring.") + start_process() - # Check if process is running and restart if needed - while True: - # We watch files blockingly. watch() yields when changes happen. - for changes in watch("."): - should_restart = False - for change_type, path in changes: - ext = os.path.splitext(path)[1] + watched_extensions = {".py", ".env"} + + def get_mtimes(): + mtimes = {} + for root, dirs, files in os.walk("."): + if any(part.startswith(".") or part in ("venv", "env", "__pycache__") for part in root.split(os.sep)): + continue + for file in files: + ext = os.path.splitext(file)[1] if ext in watched_extensions: - # Ignore standard hidden or package environment folders - parts = os.path.normpath(path).split(os.sep) - if not any(p in parts for p in ("venv", "env", "__pycache__", ".git", ".pytest_cache", "nitrostack.egg-info")): - should_restart = True - break - if should_restart: - print("File changes detected! Restarting server...") - start_process() - - # Fallback sleep if loop exits - time.sleep(0.5) - if process and process.poll() is not None: - print("Server process exited. Waiting for file changes to restart...") - - except ImportError: - print("watchfiles library not found. Falling back to standard polling...") - last_mtimes = get_mtimes() + path = os.path.join(root, file) + try: + mtimes[path] = os.path.getmtime(path) + except Exception: + pass + return mtimes + + # Try using watchfiles for high-performance, low-CPU file monitoring try: + from watchfiles import watch + print("Using watchfiles for high-performance file monitoring.") + while True: - time.sleep(1) + for changes in watch("."): + should_restart = False + for change_type, path in changes: + ext = os.path.splitext(path)[1] + if ext in watched_extensions: + parts = os.path.normpath(path).split(os.sep) + if not any(p in parts for p in ("venv", "env", "__pycache__", ".git", ".pytest_cache", "nitrostack.egg-info")): + should_restart = True + break + if should_restart: + print("File changes detected! Restarting server...") + start_process() + + time.sleep(0.5) if process and process.poll() is not None: print("Server process exited. Waiting for file changes to restart...") - current_mtimes = get_mtimes() - changed = False - if set(current_mtimes.keys()) != set(last_mtimes.keys()): - changed = True - else: - for path, mtime in current_mtimes.items(): - if last_mtimes.get(path) != mtime: - changed = True - break - if changed: - print("File changes detected! Restarting server...") - start_process() - last_mtimes = current_mtimes - except KeyboardInterrupt: - pass - except KeyboardInterrupt: - print("\nStopping development server...") - if process: + + except ImportError: + print("watchfiles library not found. Falling back to standard polling...") + last_mtimes = get_mtimes() try: - process.terminate() - except Exception: + while True: + time.sleep(1) + if process and process.poll() is not None: + print("Server process exited. Waiting for file changes to restart...") + current_mtimes = get_mtimes() + changed = False + if set(current_mtimes.keys()) != set(last_mtimes.keys()): + changed = True + else: + for path, mtime in current_mtimes.items(): + if last_mtimes.get(path) != mtime: + changed = True + break + if changed: + print("File changes detected! Restarting server...") + start_process() + last_mtimes = current_mtimes + except KeyboardInterrupt: pass + except KeyboardInterrupt: + print("\nStopping development server...") + cleanup() def run_start(): target = "main.py" @@ -764,7 +1239,7 @@ def main(): # init command init_parser = subparsers.add_parser("init", help="Initialize a new NitroStack MCP server project") init_parser.add_argument("name", help="Name of the project directory to create") - init_parser.add_argument("--template", choices=["calculator", "food-delivery", "flight-booking"], default=None, help="Template to use (default: interactive prompt)") + init_parser.add_argument("--template", choices=["calculator", "food-delivery", "flight-booking", "starter", "pizzaz", "oauth"], default=None, help="Template to use (default: interactive prompt)") # dev command subparsers.add_parser("dev", help="Start the hot-reloading development server") diff --git a/nitrostack/core/__pycache__/additional_decorators.cpython-312.pyc b/nitrostack/core/__pycache__/additional_decorators.cpython-312.pyc index fa581524b960cbcee4cb2697c0d3b3cc1d944434..7c978a446a67d45060fff53bba5f0c1d1e05958f 100644 GIT binary patch delta 50 zcmaEA`oVOAC6lVJvsFxJacWUuI|oQuO?9~mJ)Kh5L0BGW;I<%4jxhq z`ao7Wq=|#ZNz2|&=RD7GQ)G&tGELbaf9!zACXyMO$m?dDiSL4W+6G@78#P$pcr8du{gZ z1je8Axa_RUu}y8o-SEwE3p`|&4|~h@VA#d8?ya~7=7O%BqVs(d>!>d8;V$~2-RmNI z;M^`3F~e-wNA|*8xLmrcXt`OlY^ew)T!9PL8V|yKb=@ULJd;%8d09+J`iLVZAJ(Oe znih1a}#f{1Is;Rp5yF8%xg0mN;eo5`E0_S`~vpB9RC{DB4b5205BO`nQvNqUqYu!sspW z$K74i&Yv8+*PGj>d){tbb2hEl?_X+I^>wh&J#%XP;F0N`)w3?xB76|5ClY)a>m*5d6zk;)Z=C86nXIoebwL|kdc_9|iT4`%QEUf_jWbVU_rL`L z^Z*h9sA)GO&f7r~e54O%h5h7pHOP2DFXn0S7z{G#ML*zOvr5F99tS-n?ET`iP{wFd-7U82z z8@UW?86SBQ{>=D&BRIT{T|G@px-PZh4p~XjH=tR0zF`b!O(JZvtw9@YMRnN23W`dD zbq3uD_)s}X=8O&HuC4Z6#*}P%fhsuW8mtUGLuO%PsDms(W8O#RA(TH&zK6;DJ#rPg zhtGNzaK%ej^U>aN{#_tSL6Bsl?0KNN6tfkq2b9DmK<@PNvdI0P0L9^m$OP6{)E14yi@p$ zx0W@nKu;6si9#X0{1 DwQFfe delta 1102 zcmZXSOKcNo5XX1cUfX#&4RI1BLCzA8h(RD;B5jD$q)@d7NO{;27(21eZsJ7ouJw7T z?cmg*s_HtadKtAjP@~cT&;5s1&R(uH0;%kAdK06ap1i#zlS1@Zf~)% z9hSvE8a*{RNs;=ka-PA9gKbWSHMQaEeI7gex@b8V*y|Dhce{t$@Xfwv+&|=TE6zV{ zyn~BMaspO{JeX_t;GH39i~N4^Ra>-8cCXIfdpz2K>%7fP($$(PQZ))fR`AuDtzQM3 z&^%(m8tn0}$Af;k?l39ek(84|jzl8(%)cGHSQnU*D)mBr zn|VEO#RiRL=vb?LZIn0g)w3RIAycz0g!P0N{ub_o2tEu4#DBq*M|y2=6pu!4tqoFo zH*x-J?4#g#tvcJl;{Lnr5DcN4~g^MX)g4MEKK?w$0ytAg+uH&k5 zwrAK2V=1MlGpIybUR!yC#Nrq4r}Ra z*fA68IW9`IOR0)L_WHL~`=GCC9|uR?T8YZE-CUfR1$c^G`C;#ETFv?}y>ve7r`)6% z;R(*?x4}&;<@1m*`)6yQ%gTMMFT45^x^i+#9borSIlbJGtAWp73&fQ{j~#q(1P(_1 F{{#Q#CK~_% diff --git a/nitrostack/core/__pycache__/context.cpython-312.pyc b/nitrostack/core/__pycache__/context.cpython-312.pyc index dde59c8aa3363cb7fa9f17d32604439aee2fa237..6731ee253c87c7a718a2f90e4877eb4c90312cd3 100644 GIT binary patch delta 50 zcmaFq^u=j|HIu5nvsFxJacWUw+%6D0ru delta 39 ucmez3^wMdAHItNyvsFxJacWU8|%9C75hHkkzy!2(eh;gs}34Z_e&^#la+_Fq_V!LDoy#w?} zJv#b{ePUb+iD9@L+^FP`LJ=q?cGk2m(4zVv6H9b&S*M%ok@!+AKLIhD)JVUecitxU zKu+^!DBi#S5wnbAXUB$TrWA=NGYi7HD$J~9R3a;?kX}w?H`PUXEuo0Z`;C@Ow&O7r zxb50ODh9TbvZ>lbsc3yMTcqP97_GC*ANso6~FA;ZwHVuu2PsuFUJ zS{}2kD$6UWw7fQ-S%|yI+caRSnibY;X;)JO%1Qc?chKsFIIn-^eFcB4|Kc5;_@f1J z$9L7cm-ohh3=b99pIqT`VyG~2^P+Zf_d<#7EXVry&KKLx9J4| zp7JO4m~RTt=^yxboYwF75_!%Vxa0T>fiS`7M{KZY2@v|g6{k5)Dl4dq>DoI%fbNdo z10LW*w1Y&ff>u5AOaaYfk=bIe!7IRS>#n})E0kWXo1RpxCh?@+{Qadqc2!(2lyO4@B^kAAQv<};SV2Qvego6 zipZEusnc+V<}|%9$3k<=8u=H-O}6C_{6pI$q0^9Q3hz;vp>P=h2=Ok6ihWT~OhTB6 zlofHt`wiAf)d9uU~l?TpzGh941`M=OWiR~*-yz#uZ*mbtV zj+Gnz1((T#`>$a_mk1NnVa)-}iD!9p;)<<0EXhE01FjMVfawf}jneB+1j2CZ72iKT z`uAZc?|>i+6jJ){{fZ)W14nHk9^cd3LmQyq4YgWjNV&QHU1*M` z#JLmTKF_t_uk~fFpCOC-Jub-8)hU!Mv$M6?vP53Zszd?rp8lAd#K-hlWc1 z-sBPm+JY$h!^kn`6;LVVm5u#hBSD<+!NIb#!KefI)Eh9YAPpPxhYUN_9Ec5Uk7-Gx zo?1>6Xx<&Wl{8$`Vv#Sp+gtK>8V%HRBY(6QO_qG4MkBR7$jcSEu9BzQ@KW1{8pGSU qlDp0DQ!{|Pk=>S()ze`Fd-Qx$$Va`i*oA}J$^imlMD~(WD1@XQj0X%T#?)e|-7ToaEz>oMGrF!p4YQD<%mc64X+`|^r)^;CganN zqq%`KuBiBcKCCAUNl(%}NPBK0!g6fa-1-pB^sGqQvCcEc4XB(bjFo1a25OcTDz~gk zY09{rs+o1&K%G9`ImGoC)*VbI<(*DrJ zN|OymMchef+&uM`JKqxTAiRtp$&>VNpXC&MjDN~^D6c7LPN$L8&bo4yMSk@hNtLK^ zS8gD4sbS?$)y-<9-kiwmM%6?W%S3PSh1aQRGcB>U)h@gg+h;cKb;EtX6U?50xN-QyKM%RRHUIzs diff --git a/nitrostack/core/__pycache__/di.cpython-312.pyc b/nitrostack/core/__pycache__/di.cpython-312.pyc index 160f32dd0499f72b1a7db8e0f1ef9f22466807a1..0d33e9b01369bc870f273accf143527b76556411 100644 GIT binary patch delta 50 zcmeB{ohG|sH>0YRvsFxJacWUTwxb3Nk^ F769PK6L0_k delta 39 ucmZqT{m!!?l~Kyr*(xTqIJKxaCOxsVBr(P>wOk=BKPM%%Xmc0i4;BC!s}8FG diff --git a/nitrostack/core/__pycache__/module.cpython-312.pyc b/nitrostack/core/__pycache__/module.cpython-312.pyc index 7e9a1e41471de4f225fb1d06e85b8a40aa2770e9..63d733e961a568ec9fababae0d5267a08231e54f 100644 GIT binary patch delta 50 zcmdnZdyIEO36rXwvsFxJacWUNJ?D6^`AvsFxJacWU*?^62|}l delta 39 ucmdm?xmI&S5VMq_vsFxJacWUq54Zt7pbt3! diff --git a/nitrostack/templates/flight-booking/.env b/nitrostack/templates/flight-booking/.env new file mode 100644 index 0000000..b530c25 --- /dev/null +++ b/nitrostack/templates/flight-booking/.env @@ -0,0 +1,7 @@ +RESOURCE_URI=https://mcplocal +AUTH_SERVER_URL=https://dev-5dt0utuk315713tjm.us.auth0.com +TOKEN_AUDIENCE=https://mcplocal +TOKEN_ISSUER=https://dev-5dt0utuk315713tjm.us.auth0.com +DUFFEL_API_KEY=your-duffel-api-key +PORT=8000 +NODE_ENV=development diff --git a/nitrostack/templates/flight-booking/.env.example b/nitrostack/templates/flight-booking/.env.example new file mode 100644 index 0000000..b530c25 --- /dev/null +++ b/nitrostack/templates/flight-booking/.env.example @@ -0,0 +1,7 @@ +RESOURCE_URI=https://mcplocal +AUTH_SERVER_URL=https://dev-5dt0utuk315713tjm.us.auth0.com +TOKEN_AUDIENCE=https://mcplocal +TOKEN_ISSUER=https://dev-5dt0utuk315713tjm.us.auth0.com +DUFFEL_API_KEY=your-duffel-api-key +PORT=8000 +NODE_ENV=development diff --git a/nitrostack/templates/flight-booking/OAUTH_SETUP.md b/nitrostack/templates/flight-booking/OAUTH_SETUP.md new file mode 100644 index 0000000..e1f399c --- /dev/null +++ b/nitrostack/templates/flight-booking/OAUTH_SETUP.md @@ -0,0 +1,26 @@ +# OAuth 2.1 Server Setup Guide + +To run your flight booking MCP server with OAuth 2.1 protection, you need to configure an OAuth authorization server (like Keycloak, Auth0, Hydra, or a local mock OAuth server). + +## 1. Local Configuration + +Add the following environment variables to your `.env` file to configure resource protection: + +```env +# Introspection endpoint to validate access tokens +OAUTH_INTROSPECTION_ENDPOINT=http://localhost:3000/oauth/introspect + +# Or use JWKS (JSON Web Key Sets) to cryptographically verify signatures locally +# JWKS_URI=http://localhost:3000/oauth/jwks +# TOKEN_AUDIENCE=https://mcplocal +# TOKEN_ISSUER=https://dev-5dt0utuk315713tjm.us.auth0.com +``` + +## 2. Protected Routes + +The tools in this server use the `@use_guards(OAuthGuard, create_scope_guard([...]))` decorators to automatically protect endpoints: +* **Public**: No guards (or custom public filters). +* **Read-Protected**: Requires valid access token with `read` scope. +* **Write-Protected**: Requires valid access token with `write` scope. + +When calling protected tools, the client must pass a valid Bearer token in the `Authorization` header. diff --git a/nitrostack/templates/flight-booking/app_module.py b/nitrostack/templates/flight-booking/app_module.py new file mode 100644 index 0000000..2b35803 --- /dev/null +++ b/nitrostack/templates/flight-booking/app_module.py @@ -0,0 +1,30 @@ +from nitrostack import module, ConfigModule, OAuthModule +from modules.flights.flights_module import FlightsModule +from health.system_health import SystemHealthCheck +import os + +@module( + name="app", + imports=[ + ConfigModule.for_root( + env_file_path=".env", + defaults={"RESOURCE_URI": "https://mcplocal", "PORT": "8000"} + ), + OAuthModule.for_root( + resource_uri=os.environ.get("RESOURCE_URI", "https://mcplocal"), + authorization_servers=[os.environ.get("AUTH_SERVER_URL", "https://dev-5dt0utuk315713tjm.us.auth0.com")], + scopes_supported=["read", "write", "admin"], + token_introspection_endpoint=os.environ.get("INTROSPECTION_ENDPOINT"), + token_introspection_client_id=os.environ.get("INTROSPECTION_CLIENT_ID"), + token_introspection_client_secret=os.environ.get("INTROSPECTION_CLIENT_SECRET"), + audience=os.environ.get("TOKEN_AUDIENCE"), + issuer=os.environ.get("TOKEN_ISSUER") + ), + FlightsModule + ], + controllers=[], + providers=[SystemHealthCheck], + exports=[] +) +class AppModule: + pass diff --git a/nitrostack/templates/flight-booking/guards/oauth_guard.py b/nitrostack/templates/flight-booking/guards/oauth_guard.py new file mode 100644 index 0000000..03c3a07 --- /dev/null +++ b/nitrostack/templates/flight-booking/guards/oauth_guard.py @@ -0,0 +1,14 @@ +from nitrostack import ExecutionContext + +def create_scope_guard(required_scopes: list): + class ScopeGuard: + async def can_activate(self, context: ExecutionContext) -> bool: + user_scopes = getattr(context.auth, "scopes", []) + missing_scopes = [s for s in required_scopes if s not in user_scopes] + if missing_scopes: + raise ValueError( + f"Insufficient scope. Required: {', '.join(required_scopes)}. " + f"Missing: {', '.join(missing_scopes)}" + ) + return True + return ScopeGuard diff --git a/nitrostack/templates/flight-booking/health/system_health.py b/nitrostack/templates/flight-booking/health/system_health.py new file mode 100644 index 0000000..1e1a3bb --- /dev/null +++ b/nitrostack/templates/flight-booking/health/system_health.py @@ -0,0 +1,11 @@ +import time +from nitrostack import health_check + +class SystemHealthCheck: + def __init__(self): + self.start_time = time.time() + + @health_check("system") + def check_system(self) -> bool: + uptime = time.time() - self.start_time + return uptime >= 0 diff --git a/scratch/test-flight/main.py b/nitrostack/templates/flight-booking/main.py similarity index 100% rename from scratch/test-flight/main.py rename to nitrostack/templates/flight-booking/main.py diff --git a/nitrostack/templates/flight-booking/modules/flights/booking_tools.py b/nitrostack/templates/flight-booking/modules/flights/booking_tools.py new file mode 100644 index 0000000..48104e6 --- /dev/null +++ b/nitrostack/templates/flight-booking/modules/flights/booking_tools.py @@ -0,0 +1,96 @@ +from nitrostack import injectable, tool, widget, use_guards, OAuthGuard, ExecutionContext +from services.duffel_service import DuffelService +from guards.oauth_guard import create_scope_guard +from pydantic import BaseModel, Field +import json + +class CreateOrderInput(BaseModel): + offerId: str = Field(description="The offer ID to book") + passengers: str = Field(description="JSON string containing array of passenger objects. Each passenger must have: title (mr/ms/mrs/miss/dr), givenName (first name), familyName (last name), gender (M/F), bornOn (YYYY-MM-DD), email, phoneNumber.") + +class OrderDetailsInput(BaseModel): + orderId: str = Field(description="The order ID") + +class SeatMapInput(BaseModel): + offerId: str = Field(description="The offer ID to get seats for") + +class CancelOrderInput(BaseModel): + orderId: str = Field(description="The order ID to cancel") + +@injectable(deps=[DuffelService]) +class BookingTools: + def __init__(self, service: DuffelService): + self.service = service + + @tool( + name="create_order", + title="Create Order", + description="Create a flight order with hold (no payment required). Collect passenger details first.", + input_schema=CreateOrderInput + ) + @use_guards(OAuthGuard, create_scope_guard(["write"])) + @widget("order-summary") + async def create_order(self, input: CreateOrderInput, context: ExecutionContext) -> dict: + context.logger.info(f"Creating flight order for offer {input.offerId}") + try: + passengers_array = json.loads(input.passengers) + except Exception: + raise ValueError("Invalid passengers JSON format") + + passengers = [] + for p in passengers_array: + passengers.append({ + "title": p.get("title", "mr"), + "given_name": p.get("givenName"), + "family_name": p.get("familyName"), + "gender": p.get("gender", "M"), + "born_on": p.get("bornOn"), + "email": p.get("email"), + "phone_number": p.get("phoneNumber") + }) + + order_params = { + "selectedOffers": [input.offerId], + "passengers": passengers + } + res = await self.service.create_order(order_params) + return res + + @tool( + name="get_order_details", + title="Get Order Details", + description="Get detailed information about an order.", + input_schema=OrderDetailsInput + ) + @use_guards(OAuthGuard, create_scope_guard(["read"])) + @widget("order-summary") + async def get_order_details(self, input: OrderDetailsInput, context: ExecutionContext) -> dict: + context.logger.info(f"Fetching details for order {input.orderId}") + res = await self.service.get_order(input.orderId) + return res + + @tool( + name="get_seat_map", + title="Get Seat Map", + description="Get available seats for a flight offer to allow seat selection.", + input_schema=SeatMapInput + ) + @use_guards(OAuthGuard, create_scope_guard(["read"])) + @widget("seat-selection") + async def get_seat_map(self, input: SeatMapInput, context: ExecutionContext) -> dict: + context.logger.info(f"Fetching seat map for offer {input.offerId}") + res = await self.service.get_seats_for_offer(input.offerId) + return {"offerId": input.offerId, "cabins": res} + + @tool( + name="cancel_order", + title="Cancel Order", + description="Cancel a flight order and request refund if applicable.", + input_schema=CancelOrderInput + ) + @use_guards(OAuthGuard, create_scope_guard(["write"])) + @widget("order-cancellation") + async def cancel_order(self, input: CancelOrderInput, context: ExecutionContext) -> dict: + context.logger.info(f"Cancelling order {input.orderId}") + res = await self.service.cancel_order(input.orderId) + return res diff --git a/nitrostack/templates/flight-booking/modules/flights/flights_module.py b/nitrostack/templates/flight-booking/modules/flights/flights_module.py new file mode 100644 index 0000000..a7b3d9f --- /dev/null +++ b/nitrostack/templates/flight-booking/modules/flights/flights_module.py @@ -0,0 +1,15 @@ +from nitrostack import module +from modules.flights.flights_tools import FlightTools +from modules.flights.booking_tools import BookingTools +from modules.flights.flights_prompts import FlightPrompts +from modules.flights.flights_resources import FlightResources +from services.duffel_service import DuffelService + +@module( + name="flights", + controllers=[FlightTools, BookingTools, FlightPrompts, FlightResources], + providers=[DuffelService], + exports=[DuffelService] +) +class FlightsModule: + pass diff --git a/nitrostack/templates/flight-booking/modules/flights/flights_prompts.py b/nitrostack/templates/flight-booking/modules/flights/flights_prompts.py new file mode 100644 index 0000000..de02027 --- /dev/null +++ b/nitrostack/templates/flight-booking/modules/flights/flights_prompts.py @@ -0,0 +1,21 @@ +from nitrostack import injectable, prompt, ExecutionContext +from services.duffel_service import DuffelService + +@injectable(deps=[DuffelService]) +class FlightPrompts: + def __init__(self, service: DuffelService): + self.service = service + + @prompt( + name="flight_search_assistant", + description="An AI assistant specialized in helping users search for flights and make booking decisions." + ) + async def flight_search_assistant(self, args: dict, context: ExecutionContext) -> str: + return "You are a professional flight booking assistant. Help the user search for flights using search_flights, search_airports, and assist in booking holds." + + @prompt( + name="flight_comparison", + description="Compare multiple flight offers and provide recommendations." + ) + async def flight_comparison(self, args: dict, context: ExecutionContext) -> str: + return "Compare the provided flight offer details, check layover times, durations, and pricing, and give the user a summary of best options." diff --git a/nitrostack/templates/flight-booking/modules/flights/flights_resources.py b/nitrostack/templates/flight-booking/modules/flights/flights_resources.py new file mode 100644 index 0000000..f542dd0 --- /dev/null +++ b/nitrostack/templates/flight-booking/modules/flights/flights_resources.py @@ -0,0 +1,30 @@ +from nitrostack import injectable, resource, ExecutionContext +from services.duffel_service import DuffelService + +@injectable(deps=[DuffelService]) +class FlightResources: + def __init__(self, service: DuffelService): + self.service = service + + @resource( + uri="flight://popular-routes", + name="Popular Flight Routes", + description="Information about popular routes and pricing", + mime_type="application/json" + ) + async def popular_routes(self, context: ExecutionContext) -> dict: + return { + "routes": [ + {"route": "JFK -> LAX", "price": "$200-400"}, + {"route": "LHR -> JFK", "price": "$400-800"} + ] + } + + @resource( + uri="flight://booking-guide", + name="Flight Booking Guide", + description="Guide on searching and booking flights", + mime_type="text/markdown" + ) + async def booking_guide(self, context: ExecutionContext) -> str: + return "# Flight Booking Guide\n\n1. Search flights.\n2. Collect passenger info.\n3. Create order hold." diff --git a/nitrostack/templates/flight-booking/modules/flights/flights_tools.py b/nitrostack/templates/flight-booking/modules/flights/flights_tools.py new file mode 100644 index 0000000..9facf1d --- /dev/null +++ b/nitrostack/templates/flight-booking/modules/flights/flights_tools.py @@ -0,0 +1,77 @@ +from nitrostack import injectable, tool, widget, use_guards, OAuthGuard, ExecutionContext +from services.duffel_service import DuffelService +from guards.oauth_guard import create_scope_guard +from pydantic import BaseModel, Field +from typing import Optional + +class SearchFlightsInput(BaseModel): + origin: str = Field(description="Origin airport IATA code (e.g., 'JFK', 'LHR')") + destination: str = Field(description="Destination airport IATA code (e.g., 'LAX', 'CDG')") + departureDate: str = Field(description="Departure date in YYYY-MM-DD format") + returnDate: Optional[str] = Field(default=None, description="Return date in YYYY-MM-DD format for round trip") + adults: int = Field(default=1, description="Number of adult passengers (18+)") + cabinClass: str = Field(default="economy", description="Preferred cabin class (economy, premium_economy, business, first)") + +class FlightDetailsInput(BaseModel): + offerId: str = Field(description="The flight offer ID from search results") + +class AirportSearchInput(BaseModel): + query: str = Field(description="The search query for airports (e.g., 'London', 'New York')") + +@injectable(deps=[DuffelService]) +class FlightTools: + def __init__(self, service: DuffelService): + self.service = service + + @tool( + name="search_flights", + title="Search Flights", + description="Search for flight offers based on origin, destination, dates, and preferences.", + input_schema=SearchFlightsInput + ) + @use_guards(OAuthGuard, create_scope_guard(["read"])) + @widget("flight-search-results") + async def search_flights(self, input: SearchFlightsInput, context: ExecutionContext) -> dict: + context.logger.info(f"Searching flights from {input.origin} to {input.destination}") + res = await self.service.search_flights(input.model_dump()) + return res + + @tool( + name="get_flight_details", + title="Get Flight Details", + description="Get detailed information about a specific flight offer including baggage allowance, conditions.", + input_schema=FlightDetailsInput + ) + @use_guards(OAuthGuard, create_scope_guard(["read"])) + @widget("flight-details") + async def get_flight_details(self, input: FlightDetailsInput, context: ExecutionContext) -> dict: + context.logger.info(f"Fetching flight details for offer {input.offerId}") + res = await self.service.get_offer(input.offerId) + return res + + @tool( + name="search_airports", + title="Search Airports", + description="Search for airports by query string.", + input_schema=AirportSearchInput + ) + @use_guards(OAuthGuard, create_scope_guard(["read"])) + @widget("airport-search") + async def search_airports(self, input: AirportSearchInput, context: ExecutionContext) -> dict: + context.logger.info(f"Searching airports for query: {input.query}") + places = await self.service.search_airports(input.query) + return { + "query": input.query, + "results": places + } + + @tool( + name="get_airlines", + title="Get Airlines", + description="Get list of common airlines." + ) + @use_guards(OAuthGuard, create_scope_guard(["read"])) + async def get_airlines(self, context: ExecutionContext) -> dict: + context.logger.info("Fetching common airlines") + res = await self.service.get_airlines() + return {"airlines": res} diff --git a/scratch/test-flight/requirements.txt b/nitrostack/templates/flight-booking/requirements.txt similarity index 100% rename from scratch/test-flight/requirements.txt rename to nitrostack/templates/flight-booking/requirements.txt diff --git a/nitrostack/templates/flight-booking/services/duffel_service.py b/nitrostack/templates/flight-booking/services/duffel_service.py new file mode 100644 index 0000000..0fe6e85 --- /dev/null +++ b/nitrostack/templates/flight-booking/services/duffel_service.py @@ -0,0 +1,282 @@ +import os +import sys +import json +import urllib.request +import urllib.parse +from typing import List, Dict, Any, Optional +from nitrostack import injectable + +@injectable() +class DuffelService: + def __init__(self): + self.api_key = os.environ.get("DUFFEL_API_KEY") + self.is_mock = not self.api_key or self.api_key.startswith("your-") or len(self.api_key) < 5 + + def _request(self, method: str, path: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + if self.is_mock: + raise ValueError("Duffel API key is not configured. Running in mock mode.") + + headers = { + "Authorization": f"Bearer {self.api_key}", + "Duffel-Version": "v1", + "Content-Type": "application/json" + } + url = f"https://api.duffel.com{path}" + req_data = json.dumps({"data": data}).encode("utf-8") if data else None + + req = urllib.request.Request(url, data=req_data, headers=headers, method=method) + try: + with urllib.request.urlopen(req, timeout=10) as response: + res_payload = json.loads(response.read().decode("utf-8")) + return res_payload.get("data", {}) + except Exception as e: + sys.stderr.write(f"Duffel API Request failed: {e}\n") + sys.stderr.flush() + raise e + + async def search_flights(self, params: Dict[str, Any]) -> Dict[str, Any]: + if self.is_mock: + return { + "id": "orq_mock123456", + "offers": [ + { + "id": "off_mock123456", + "total_amount": "450.00", + "total_currency": "USD", + "expires_at": "2026-12-31T12:00:00Z", + "slices": [ + { + "origin": {"iata_code": params["origin"], "name": "Origin Airport", "city_name": "Origin City"}, + "destination": {"iata_code": params["destination"], "name": "Dest Airport", "city_name": "Dest City"}, + "duration": "PT6H30M", + "segments": [ + { + "id": "seg_outbound", + "origin": {"iata_code": params["origin"]}, + "destination": {"iata_code": params["destination"]}, + "departing_at": f"{params['departureDate']}T08:00:00Z", + "arriving_at": f"{params['departureDate']}T14:30:00Z", + "marketing_carrier": {"name": "Mock Airlines"}, + "marketing_carrier_flight_number": "MK123", + "aircraft": {"name": "Boeing 787"} + } + ] + } + ] + } + ] + } + + slices = [ + { + "origin": params["origin"], + "destination": params["destination"], + "departure_date": params["departureDate"] + } + ] + if params.get("returnDate"): + slices.append({ + "origin": params["destination"], + "destination": params["origin"], + "departure_date": params["returnDate"] + }) + + duffel_params = { + "slices": slices, + "passengers": [{"type": "adult"} for _ in range(params.get("adults", 1))], + "cabin_class": params.get("cabinClass", "economy"), + "return_offers": True + } + res = self._request("POST", "/offer_requests", duffel_params) + return { + "id": res.get("id"), + "offers": res.get("offers", []), + "passengers": res.get("passengers", []), + "slices": res.get("slices", []) + } + + async def get_offer(self, offer_id: str) -> Dict[str, Any]: + if self.is_mock: + return { + "id": offer_id, + "total_amount": "450.00", + "total_currency": "USD", + "expires_at": "2026-12-31T12:00:00Z", + "slices": [ + { + "origin": {"iata_code": "JFK", "name": "John F. Kennedy Airport", "city_name": "New York"}, + "destination": {"iata_code": "LAX", "name": "Los Angeles Airport", "city_name": "Los Angeles"}, + "duration": "PT6H30M", + "segments": [ + { + "id": "seg_mock", + "origin": {"iata_code": "JFK"}, + "destination": {"iata_code": "LAX"}, + "departing_at": "2026-07-15T08:00:00Z", + "arriving_at": "2026-07-15T14:30:00Z", + "marketing_carrier": {"name": "Mock Airlines"}, + "marketing_carrier_flight_number": "MK123", + "aircraft": {"name": "Boeing 787"} + } + ] + } + ] + } + return self._request("GET", f"/offers/{offer_id}") + + async def get_seats_for_offer(self, offer_id: str) -> List[Dict[str, Any]]: + if self.is_mock: + return [ + { + "cabin_class": "economy", + "rows": [ + { + "row_number": 10, + "sections": [ + { + "elements": [ + { + "type": "seat", + "id": "seat_10a", + "designator": "10A", + "available_services": [{"total_amount": "25.00", "total_currency": "USD"}], + "disclosures": ["window"] + }, + { + "type": "seat", + "id": "seat_10b", + "designator": "10B", + "available_services": [{"total_amount": "0.00", "total_currency": "USD"}], + "disclosures": ["middle"] + } + ] + } + ] + } + ] + } + ] + res = self._request("GET", f"/seat_maps?offer_id={offer_id}") + return res if isinstance(res, list) else [] + + async def create_order(self, params: Dict[str, Any]) -> Dict[str, Any]: + if self.is_mock: + return { + "id": "ord_mock123456", + "status": "held", + "total_amount": "450.00", + "total_currency": "USD", + "expires_at": "2026-12-31T12:00:00Z", + "booking_reference": "ABCXYZ", + "passengers": [ + { + "id": f"pax_{idx}", + "given_name": p["given_name"], + "family_name": p["family_name"], + "type": "adult" + } for idx, p in enumerate(params["passengers"]) + ], + "slices": [ + { + "origin": {"iata_code": "JFK"}, + "destination": {"iata_code": "LAX"}, + "duration": "PT6H30M", + "segments": [ + { + "departing_at": "2026-07-15T08:00:00Z", + "arriving_at": "2026-07-15T14:30:00Z" + } + ] + } + ] + } + + order_payload = { + "selected_offers": params["selectedOffers"], + "passengers": params["passengers"], + "type": "hold" + } + return self._request("POST", "/orders", order_payload) + + async def get_order(self, order_id: str) -> Dict[str, Any]: + if self.is_mock: + return { + "id": order_id, + "status": "held", + "total_amount": "450.00", + "total_currency": "USD", + "booking_reference": "ABCXYZ", + "created_at": "2026-06-25T12:00:00Z", + "expires_at": "2026-12-31T12:00:00Z", + "passengers": [ + { + "id": "pax_0", + "given_name": "John", + "family_name": "Doe", + "type": "adult", + "email": "john@example.com", + "phone_number": "+1234567890" + } + ], + "slices": [ + { + "id": "sli_mock", + "origin": {"iata_code": "JFK", "name": "John F. Kennedy Airport", "city_name": "New York"}, + "destination": {"iata_code": "LAX", "name": "Los Angeles Airport", "city_name": "Los Angeles"}, + "duration": "PT6H30M", + "segments": [ + { + "id": "seg_mock", + "origin": {"iata_code": "JFK"}, + "destination": {"iata_code": "LAX"}, + "departing_at": "2026-07-15T08:00:00Z", + "arriving_at": "2026-07-15T14:30:00Z", + "marketing_carrier": {"name": "Mock Airlines"}, + "marketing_carrier_flight_number": "MK123", + "aircraft": {"name": "Boeing 787"} + } + ] + } + ] + } + return self._request("GET", f"/orders/{order_id}") + + async def cancel_order(self, order_id: str) -> Dict[str, Any]: + if self.is_mock: + return { + "id": "ocr_mock123456", + "refund_amount": "450.00", + "refund_currency": "USD", + "confirmed_at": "2026-06-25T12:30:00Z" + } + cancel_payload = {"order_id": order_id} + return self._request("POST", "/order_cancellations", cancel_payload) + + async def get_airlines(self) -> List[Dict[str, Any]]: + if self.is_mock: + return [ + {"iata_code": "AA", "name": "American Airlines"}, + {"iata_code": "DL", "name": "Delta Air Lines"}, + {"iata_code": "UA", "name": "United Airlines"}, + {"iata_code": "BA", "name": "British Airways"} + ] + res = self._request("GET", "/airlines") + return res if isinstance(res, list) else [] + + async def search_airports(self, query: str) -> List[Dict[str, Any]]: + if self.is_mock: + return [ + { + "id": "arp_lhr_gb", + "name": "London Heathrow Airport", + "iata_code": "LHR", + "icao_code": "EGLL", + "city_name": "London", + "type": "airport", + "latitude": 51.4700, + "longitude": -0.4543, + "time_zone": "Europe/London" + } + ] + res = self._request("GET", f"/places?type=airport&query={urllib.parse.quote(query)}") + return res if isinstance(res, list) else [] diff --git a/nitrostack/templates/flight-booking/src/widgets/app/airport-search/page.tsx b/nitrostack/templates/flight-booking/src/widgets/app/airport-search/page.tsx new file mode 100644 index 0000000..8e72661 --- /dev/null +++ b/nitrostack/templates/flight-booking/src/widgets/app/airport-search/page.tsx @@ -0,0 +1,270 @@ +'use client'; + +import { useWidgetSDK, useTheme } from '@nitrostack/widgets'; + +/** + * Airport Search Widget + * + * Compact display of airport search results with IATA codes and locations. + */ + +interface AirportResult { + id: string; + name: string; + iataCode: string; + icaoCode?: string; + cityName?: string; + type: string; + latitude?: number; + longitude?: number; + timeZone?: string; +} + +interface AirportSearchData { + query: string; + results: AirportResult[]; +} + +export default function AirportSearch() { + const { getToolOutput } = useWidgetSDK(); + const theme = useTheme(); + const data = getToolOutput(); + + const isDark = theme === 'dark'; + + const getTypeIcon = (type: string) => { + const icons: Record = { + 'airport': '✈️', + 'city': '🏙️', + 'station': '🚉', + 'bus_station': '🚌', + 'heliport': '🚁' + }; + return icons[type] || '📍'; + }; + + if (!data) { + return ( +
+ Loading... +
+ ); + } + + return ( +
+ {/* Header */} +
+
+ 🔍 +

+ Airport Search +

+
+

+ Searching: "{data.query}" +

+
+ {data.results.length} result{data.results.length !== 1 ? 's' : ''} +
+
+ + {/* Results */} + {data.results.length > 0 ? ( +
+ {data.results.map((airport) => ( +
{ + e.currentTarget.style.transform = 'translateY(-2px)'; + e.currentTarget.style.boxShadow = isDark + ? '0 4px 12px rgba(59, 159, 255, 0.2)' + : '0 4px 12px rgba(59, 159, 255, 0.15)'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.transform = 'translateY(0)'; + e.currentTarget.style.boxShadow = isDark + ? '0 2px 8px rgba(0,0,0,0.3)' + : '0 4px 12px rgba(0,0,0,0.1)'; + }}> +
+ {/* Left side */} +
+
+ + {getTypeIcon(airport.type)} + +
+

+ {airport.name} +

+ {airport.cityName && ( +

+ 📍 {airport.cityName} +

+ )} +
+
+ + {/* Additional details */} +
+ {airport.timeZone && ( +
+ 🕐 + {airport.timeZone} +
+ )} + {airport.latitude && airport.longitude && ( +
+ 🌍 + {airport.latitude.toFixed(2)}, {airport.longitude.toFixed(2)} +
+ )} +
+
+ + {/* Right side - Codes */} +
+ {/* IATA Code */} +
+ {airport.iataCode || 'N/A'} +
+ + {/* ICAO Code */} + {airport.icaoCode && ( +
+ ICAO: {airport.icaoCode} +
+ )} + + {/* Type badge */} +
+ {airport.type.replace('_', ' ')} +
+
+
+
+ ))} +
+ ) : ( +
+
🔍
+
+ No airports found +
+
+ Try a different city or airport code +
+
+ )} + + {/* Help text */} +
+ 💡 Tip: Use the IATA code (3-letter) for flight searches +
+
+ ); +} diff --git a/nitrostack/templates/flight-booking/src/widgets/app/flight-details/page.tsx b/nitrostack/templates/flight-booking/src/widgets/app/flight-details/page.tsx new file mode 100644 index 0000000..d30f730 --- /dev/null +++ b/nitrostack/templates/flight-booking/src/widgets/app/flight-details/page.tsx @@ -0,0 +1,261 @@ +'use client'; + +import { useWidgetSDK, useTheme } from '@nitrostack/widgets'; + +/** + * Flight Details Widget - Compact view with segments, baggage, and fare conditions + */ + +interface Segment { + id: string; + origin: string; + destination: string; + departingAt: string; + arrivingAt: string; + duration: string; + airline: { name: string; code: string; flightNumber: string }; + aircraft?: string; +} + +interface Slice { + origin: { code: string; name: string; city: string }; + destination: { code: string; name: string; city: string }; + duration: string; + segments: Segment[]; +} + +interface FlightDetailsData { + id: string; + totalAmount: string; + totalCurrency: string; + slices: Slice[]; + passengers: Array<{ id: string; type: string; baggageAllowance?: Array<{ type: string; quantity: number }> }>; + conditions: { + refundBeforeDeparture: { allowed: boolean; penaltyAmount?: string; penaltyCurrency?: string }; + changeBeforeDeparture: { allowed: boolean; penaltyAmount?: string; penaltyCurrency?: string }; + }; +} + +export default function FlightDetails() { + const { getToolOutput } = useWidgetSDK(); + const theme = useTheme(); + const data = getToolOutput(); + + const isDark = theme === 'dark'; + + const formatDuration = (duration: string) => { + const match = duration.match(/PT(\d+H)?(\d+M)?/); + if (!match) return duration; + const hours = match[1] ? parseInt(match[1]) : 0; + const minutes = match[2] ? parseInt(match[2]) : 0; + return `${hours}h ${minutes}m`; + }; + + const formatTime = (isoString: string) => { + const date = new Date(isoString); + return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false }); + }; + + const formatDate = (isoString: string) => { + const date = new Date(isoString); + return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); + }; + + if (!data) { + return
Loading...
; + } + + return ( +
+ {/* Header */} +
+
+
+

Flight Details

+

+ Offer ID: {data.id} +

+
+
+
+ {data.totalCurrency} {parseFloat(data.totalAmount).toFixed(2)} +
+
Total Price
+
+
+
+ + {/* Flight Itinerary */} + {data.slices.map((slice, sliceIndex) => ( +
+

+ ✈️ + {slice.origin.city} → {slice.destination.city} +

+ +
+ Duration: {formatDuration(slice.duration)} +
+ + {/* Segments */} + {slice.segments.map((segment) => ( +
+
+
+ {segment.airline.name} {segment.airline.flightNumber} +
+
+ {formatDuration(segment.duration)} +
+
+ +
+
+
{formatTime(segment.departingAt)}
+
+ {segment.origin} +
+
+ {formatDate(segment.departingAt)} +
+
+ +
+
+
+ +
+
{formatTime(segment.arrivingAt)}
+
+ {segment.destination} +
+
+ {formatDate(segment.arrivingAt)} +
+
+
+ + {segment.aircraft && ( +
+ ✈️ {segment.aircraft} +
+ )} +
+ ))} +
+ ))} + + {/* Baggage */} +
+

+ 🧳 + Baggage Allowance +

+ {data.passengers.map((passenger, index) => ( +
+
+ Passenger {index + 1} ({passenger.type}) +
+ {passenger.baggageAllowance && passenger.baggageAllowance.length > 0 ? ( +
+ {passenger.baggageAllowance.map((bag, bagIndex) => ( + + {bag.quantity}x {bag.type.replace('_', ' ')} + + ))} +
+ ) : ( +
+ No baggage info +
+ )} +
+ ))} +
+ + {/* Fare Conditions */} + {data.conditions && ( +
+

+ 📋 + Fare Conditions +

+
+
+
+ {data.conditions.refundBeforeDeparture?.allowed ? '✓' : '✗'} Refund Before Departure +
+ {data.conditions.refundBeforeDeparture?.penaltyAmount && ( +
+ Penalty: {data.conditions.refundBeforeDeparture.penaltyCurrency} {data.conditions.refundBeforeDeparture.penaltyAmount} +
+ )} +
+ +
+
+ {data.conditions.changeBeforeDeparture?.allowed ? '✓' : '✗'} Changes Before Departure +
+ {data.conditions.changeBeforeDeparture?.penaltyAmount && ( +
+ Penalty: {data.conditions.changeBeforeDeparture.penaltyCurrency} {data.conditions.changeBeforeDeparture.penaltyAmount} +
+ )} +
+
+
+ )} +
+ ); +} diff --git a/nitrostack/templates/flight-booking/src/widgets/app/flight-search-results/page.tsx b/nitrostack/templates/flight-booking/src/widgets/app/flight-search-results/page.tsx new file mode 100644 index 0000000..60fd80a --- /dev/null +++ b/nitrostack/templates/flight-booking/src/widgets/app/flight-search-results/page.tsx @@ -0,0 +1,378 @@ +'use client'; + +import { useWidgetSDK, useTheme } from '@nitrostack/widgets'; + +/** + * Flight Search Results Widget + * + * Modern, compact display of flight search results with Nitrocloud branding. + */ + +interface FlightSegment { + origin: string; + destination: string; + departureTime: string; + arrivalTime: string; + duration: string; + stops: number; + airline: string; + flightNumber: string; +} + +interface FlightOffer { + id: string; + totalAmount: string; + totalCurrency: string; + outbound: FlightSegment; + return?: FlightSegment; + fareType: string; + refundable: boolean; + changeable: boolean; +} + +interface FlightSearchData { + searchParams: { + origin: string; + destination: string; + departureDate: string; + returnDate?: string; + passengers: { + adults: number; + children: number; + infants: number; + }; + cabinClass: string; + }; + totalOffers: number; + offers: FlightOffer[]; +} + +export default function FlightSearchResults() { + const { getToolOutput, callTool } = useWidgetSDK(); + const theme = useTheme(); + const data = getToolOutput(); + const isDark = theme === 'dark'; + + const handleFlightClick = async (offerId: string) => { + try { + await callTool('get_flight_details', { offerId }); + } catch (error) { + console.error('Failed to get flight details:', error); + } + }; + + const formatDuration = (duration: string) => { + const match = duration.match(/PT(\d+H)?(\d+M)?/); + if (!match) return duration; + const hours = match[1] ? parseInt(match[1]) : 0; + const minutes = match[2] ? parseInt(match[2]) : 0; + return `${hours}h ${minutes}m`; + }; + + const formatTime = (isoString: string) => { + return new Date(isoString).toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit', + hour12: false + }); + }; + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric' + }); + }; + + const getAirlineInitials = (name: string) => { + return name.split(' ').map(w => w[0]).join('').substring(0, 2).toUpperCase(); + }; + + if (!data?.searchParams) { + return ( +
+
⚠️
+
+ Invalid Data +
+
+ Flight search data is missing +
+
+ ); + } + + const totalPassengers = (data.searchParams.passengers?.adults || 0) + + (data.searchParams.passengers?.children || 0) + + (data.searchParams.passengers?.infants || 0); + + const FlightSegment = ({ segment, label }: { segment: FlightSegment; label: string }) => ( +
+
+ {label === 'Outbound' ? '✈️' : '🔄'} + {label} +
+ +
+
+
+ {formatTime(segment.departureTime)} +
+
+ {segment.origin} +
+
+ +
+
+ {formatDuration(segment.duration)} +
+
+ {segment.stops > 0 && ( +
+ )} +
+
+ {segment.stops === 0 ? 'Direct' : `${segment.stops} stop${segment.stops > 1 ? 's' : ''}`} +
+
+ +
+
+ {formatTime(segment.arrivalTime)} +
+
+ {segment.destination} +
+
+
+
+ ); + + return ( +
+ {/* Header */} +
+
+
+ {data.searchParams.origin} + ✈️ + {data.searchParams.destination} +
+ +
+ {data.totalOffers} flights +
+
+ +
+
+ 📅 + + {formatDate(data.searchParams.departureDate)} + {data.searchParams.returnDate && ` - ${formatDate(data.searchParams.returnDate)}`} + +
+
+ 👥 + {totalPassengers} pax +
+
+ 💺 + {(data.searchParams.cabinClass || 'economy').replace('_', ' ')} +
+
+
+ + {/* Flight Offers */} + {data.offers.length > 0 ? ( +
+ {data.offers.map((offer) => ( +
handleFlightClick(offer.id)} + onMouseEnter={(e) => { + e.currentTarget.style.transform = 'translateY(-2px)'; + e.currentTarget.style.boxShadow = isDark + ? '0 4px 12px rgba(59, 159, 255, 0.2)' + : '0 4px 12px rgba(59, 159, 255, 0.15)'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.transform = 'translateY(0)'; + e.currentTarget.style.boxShadow = isDark + ? '0 2px 8px rgba(0,0,0,0.3)' + : '0 2px 8px rgba(0,0,0,0.1)'; + }}> + {/* Offer Header */} +
+
+
+ {getAirlineInitials(offer.outbound.airline)} +
+
+
+ {offer.outbound.airline} +
+
+ {offer.outbound.flightNumber} +
+
+
+ +
+
+ {offer.totalCurrency} {parseFloat(offer.totalAmount).toFixed(0)} +
+
+ Total +
+
+
+ + {/* Flight Segments */} + + {offer.return && } + + {/* Badges */} +
+ + {offer.refundable ? '✓ Refundable' : '✗ Non-refundable'} + + {offer.changeable && ( + ✓ Changeable + )} + {offer.fareType} +
+
+ ))} +
+ ) : ( +
+
✈️
+
+ No flights found. Try adjusting your search. +
+
+ )} +
+ ); +} diff --git a/nitrostack/templates/flight-booking/src/widgets/app/globals.css b/nitrostack/templates/flight-booking/src/widgets/app/globals.css new file mode 100644 index 0000000..ca47c6b --- /dev/null +++ b/nitrostack/templates/flight-booking/src/widgets/app/globals.css @@ -0,0 +1,167 @@ +/* Nitrocloud Widget Styles - Professional Theme */ + +:root { + /* Professional Color Palette */ + --primary: #3B82F6; + --primary-hover: #2563EB; + --secondary: #6B7280; + --secondary-hover: #4B5563; + + --success: #10B981; + --warning: #F59E0B; + --error: #EF4444; + --info: #3B82F6; + + /* Light Theme */ + --bg-primary: #FFFFFF; + --bg-secondary: #F9FAFB; + --bg-tertiary: #F3F4F6; + --bg-elevated: #FFFFFF; + + --text-primary: #111827; + --text-secondary: #6B7280; + --text-muted: #9CA3AF; + + --border-color: #E5E7EB; + --border-hover: #D1D5DB; + + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1); +} + +.dark { + /* Dark Theme */ + --bg-primary: #111827; + --bg-secondary: #1F2937; + --bg-tertiary: #374151; + --bg-elevated: #1F2937; + + --text-primary: #F9FAFB; + --text-secondary: #D1D5DB; + --text-muted: #9CA3AF; + + --border-color: #374151; + --border-hover: #4B5563; + + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5); +} + +/* Reset */ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* Card Styles */ +.card { + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 16px; + transition: all 0.2s ease; + box-shadow: var(--shadow-sm); +} + +.card:hover { + border-color: var(--border-hover); + box-shadow: var(--shadow-md); +} + +/* Button Styles */ +.btn-primary { + background: var(--primary); + color: white; + border: none; + padding: 10px 20px; + border-radius: 6px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + font-size: 14px; +} + +.btn-primary:hover { + background: var(--primary-hover); + box-shadow: var(--shadow-md); +} + +.btn-secondary { + background: var(--bg-secondary); + color: var(--text-primary); + border: 1px solid var(--border-color); + padding: 10px 20px; + border-radius: 6px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + font-size: 14px; +} + +.btn-secondary:hover { + background: var(--bg-tertiary); + border-color: var(--border-hover); +} + +/* Badge Styles */ +.badge { + display: inline-flex; + align-items: center; + padding: 4px 12px; + border-radius: 6px; + font-size: 12px; + font-weight: 600; +} + +.badge-success { + background: rgba(16, 185, 129, 0.1); + color: var(--success); + border: 1px solid rgba(16, 185, 129, 0.2); +} + +.badge-warning { + background: rgba(245, 158, 11, 0.1); + color: var(--warning); + border: 1px solid rgba(245, 158, 11, 0.2); +} + +.badge-error { + background: rgba(239, 68, 68, 0.1); + color: var(--error); + border: 1px solid rgba(239, 68, 68, 0.2); +} + +.badge-info { + background: rgba(59, 130, 246, 0.1); + color: var(--info); + border: 1px solid rgba(59, 130, 246, 0.2); +} + +.dark .badge-success { + background: rgba(16, 185, 129, 0.2); + color: #6EE7B7; +} + +.dark .badge-warning { + background: rgba(245, 158, 11, 0.2); + color: #FCD34D; +} + +.dark .badge-error { + background: rgba(239, 68, 68, 0.2); + color: #FCA5A5; +} + +.dark .badge-info { + background: rgba(59, 130, 246, 0.2); + color: #93C5FD; +} \ No newline at end of file diff --git a/nitrostack/templates/flight-booking/src/widgets/app/layout.tsx b/nitrostack/templates/flight-booking/src/widgets/app/layout.tsx new file mode 100644 index 0000000..fe7980c --- /dev/null +++ b/nitrostack/templates/flight-booking/src/widgets/app/layout.tsx @@ -0,0 +1,18 @@ +'use client'; + +import { WidgetLayout } from '@nitrostack/widgets'; +import './globals.css'; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + {children} + + + ); +} diff --git a/nitrostack/templates/flight-booking/src/widgets/app/order-cancellation/page.tsx b/nitrostack/templates/flight-booking/src/widgets/app/order-cancellation/page.tsx new file mode 100644 index 0000000..d87d851 --- /dev/null +++ b/nitrostack/templates/flight-booking/src/widgets/app/order-cancellation/page.tsx @@ -0,0 +1,207 @@ +'use client'; + +import { useWidgetSDK, useTheme } from '@nitrostack/widgets'; + +/** + * Order Cancellation Widget - Cancellation confirmation with refund info + */ + +interface CancellationData { + orderId: string; + cancellationId: string; + status: string; + refundAmount?: string; + refundCurrency?: string; + confirmedAt: string; + message: string; +} + +export default function OrderCancellation() { + const { getToolOutput } = useWidgetSDK(); + const theme = useTheme(); + const data = getToolOutput(); + + const isDark = theme === 'dark'; + + const formatDateTime = (isoString: string) => { + return new Date(isoString).toLocaleString('en-US', { + month: 'long', + day: 'numeric', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + }; + + if (!data) { + return
Loading...
; + } + + const hasRefund = data.refundAmount && parseFloat(data.refundAmount) > 0; + + return ( +
+
+ {/* Icon */} +
+
+ {hasRefund ? '💰' : '✗'} +
+
+ + {/* Title */} +

+ Booking Cancelled +

+ +

+ {data.message} +

+ + {/* Cancellation Details */} +
+
+ {/* Order ID */} +
+
+ Order ID +
+
+ {data.orderId} +
+
+ + {/* Cancellation ID */} +
+
+ Cancellation Reference +
+
+ {data.cancellationId} +
+
+ + {/* Cancelled On */} +
+
+ Cancelled On +
+
+ {formatDateTime(data.confirmedAt)} +
+
+ + {/* Refund Information */} + {hasRefund ? ( +
+
+
💰
+
+
+ Refund Amount +
+
+ {data.refundCurrency} {parseFloat(data.refundAmount!).toFixed(2)} +
+
+
+
+ Refund will be processed to your original payment method within 5-10 business days. +
+
+ ) : ( +
+
+
ℹ️
+
+ No Refund Available: This booking was non-refundable or outside the cancellation window. +
+
+
+ )} +
+
+ + {/* Status Badge */} +
+
+ ✗ {data.status} +
+
+ + {/* Action Buttons */} +
+ + + +
+ + {/* Help Section */} +
+
+ 💬 + Need Help? +
+
+ Questions about this cancellation? Contact our support team 24/7. +
+ +
+
+
+ ); +} diff --git a/nitrostack/templates/flight-booking/src/widgets/app/order-summary/page.tsx b/nitrostack/templates/flight-booking/src/widgets/app/order-summary/page.tsx new file mode 100644 index 0000000..105361b --- /dev/null +++ b/nitrostack/templates/flight-booking/src/widgets/app/order-summary/page.tsx @@ -0,0 +1,245 @@ +'use client'; + +import { useWidgetSDK, useTheme } from '@nitrostack/widgets'; + +/** + * Order Summary Widget - Compact booking confirmation + */ + +interface OrderData { + orderId: string; + status: string; + bookingReference?: string; + totalAmount: string; + totalCurrency: string; + createdAt?: string; + expiresAt?: string; + passengers: Array<{ id: string; name: string; type: string; email?: string }>; + slices: Array<{ + origin: { code: string; city?: string }; + destination: { code: string; city?: string }; + segments?: Array<{ airline: string; flightNumber: string }>; + }>; + message?: string; +} + +export default function OrderSummary() { + const { getToolOutput } = useWidgetSDK(); + const theme = useTheme(); + const data = getToolOutput(); + + const isDark = theme === 'dark'; + + const formatDateTime = (isoString: string) => { + return new Date(isoString).toLocaleString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + }; + + const getStatusIcon = (status: string) => { + return { 'confirmed': '🎉', 'held': '⏱️', 'cancelled': '✗', 'pending': '⋯' }[status.toLowerCase()] || '📋'; + }; + + if (!data) { + return
Loading...
; + } + + return ( +
+ {/* Header */} +
+
+ {getStatusIcon(data.status)} +
+

+ {data.status === 'confirmed' ? 'Booking Confirmed!' : + data.status === 'held' ? 'Order On Hold' : 'Order Summary'} +

+ + {data.bookingReference && ( +
+ Reference: {data.bookingReference} +
+ )} + +
+ {data.status.toUpperCase()} +
+ + {data.message && ( +
+ {data.message} +
+ )} + + {data.expiresAt && data.status === 'held' && ( +
+
+ ⏰ Payment Required +
+
+ Complete before: {formatDateTime(data.expiresAt)} +
+
+ )} +
+ + {/* Order Info */} +
+

Order Information

+
+
+ Order ID: + {data.orderId} +
+
+ Total: + + {data.totalCurrency} {parseFloat(data.totalAmount).toFixed(2)} + +
+ {data.createdAt && ( +
+ Created: + {formatDateTime(data.createdAt)} +
+ )} +
+
+ + {/* Passengers */} +
+

+ 👥 + Passengers ({data.passengers.length}) +

+
+ {data.passengers.map((passenger, index) => ( +
+
+
+ {passenger.name} +
+
+ {passenger.type} +
+ {passenger.email && ( +
+ 📧 {passenger.email} +
+ )} +
+
+ {index + 1} +
+
+ ))} +
+
+ + {/* Flight Itinerary */} +
+

+ ✈️ + Flight Itinerary +

+
+ {data.slices.map((slice, index) => ( +
+
+ {index === 0 ? 'Outbound' : 'Return'} Flight +
+
+
+
+ {slice.origin.code} +
+ {slice.origin.city && ( +
+ {slice.origin.city} +
+ )} +
+ +
+ +
+
+ {slice.destination.code} +
+ {slice.destination.city && ( +
+ {slice.destination.city} +
+ )} +
+
+ + {slice.segments && slice.segments.length > 0 && ( +
+ {slice.segments.map((segment, segIndex) => ( +
+ {segment.airline} {segment.flightNumber} +
+ ))} +
+ )} +
+ ))} +
+
+
+ ); +} diff --git a/nitrostack/templates/flight-booking/src/widgets/app/payment-confirmation/page.tsx b/nitrostack/templates/flight-booking/src/widgets/app/payment-confirmation/page.tsx new file mode 100644 index 0000000..0898312 --- /dev/null +++ b/nitrostack/templates/flight-booking/src/widgets/app/payment-confirmation/page.tsx @@ -0,0 +1,152 @@ +'use client'; + +import { useWidgetSDK, useTheme } from '@nitrostack/widgets'; + +/** + * Payment Confirmation Widget - Success state with booking details + */ + +interface PaymentData { + orderId: string; + status: string; + totalAmount: string; + totalCurrency: string; + bookingReference?: string; + message?: string; +} + +export default function PaymentConfirmation() { + const { getToolOutput } = useWidgetSDK(); + const theme = useTheme(); + const data = getToolOutput(); + + const isDark = theme === 'dark'; + + if (!data) { + return
Loading...
; + } + + const isConfirmed = data.status === 'confirmed'; + + return ( +
+
+ {/* Success Icon */} +
+
+ {isConfirmed ? '✓' : '💳'} +
+
+ + {/* Title */} +

+ {isConfirmed ? 'Payment Successful!' : 'Complete Payment'} +

+ +

+ {data.message || (isConfirmed ? 'Your booking has been confirmed.' : 'Review and confirm your payment.')} +

+ + {/* Booking Details */} +
+ {data.bookingReference && ( +
+
+ Booking Reference +
+
+ {data.bookingReference} +
+
+ )} + +
+
+ Order ID: + {data.orderId} +
+ +
+ + {isConfirmed ? 'Amount Paid:' : 'Total Amount:'} + + + {data.totalCurrency} {parseFloat(data.totalAmount).toFixed(2)} + +
+
+
+ + {/* Action Buttons */} + {isConfirmed ? ( +
+ + +
+ ) : ( +
+ +
+ 🔒 Your payment is secure and encrypted +
+
+ )} + + {/* Footer Note */} + {isConfirmed && ( +
+ 📱 Important: A confirmation email has been sent to your registered email address. +
+ )} +
+
+ ); +} diff --git a/nitrostack/templates/flight-booking/src/widgets/app/seat-selection/page.tsx b/nitrostack/templates/flight-booking/src/widgets/app/seat-selection/page.tsx new file mode 100644 index 0000000..0ae6b06 --- /dev/null +++ b/nitrostack/templates/flight-booking/src/widgets/app/seat-selection/page.tsx @@ -0,0 +1,486 @@ +'use client'; + +import { useWidgetSDK, useTheme } from '@nitrostack/widgets'; +import { useState } from 'react'; + +/** + * Seat Selection Widget + * + * Interactive seat map with real-time selection for multiple passengers. + */ + +interface Seat { + id: string; + column: string; + available: boolean; + price?: string; + type: string; +} + +interface Row { + rowNumber: number; + seats: Seat[]; +} + +interface Cabin { + cabinClass: string; + rows: Row[]; +} + +interface SeatMapData { + offerId: string; + cabins: Cabin[]; + message?: string; +} + +export default function SeatSelection() { + const { getToolOutput } = useWidgetSDK(); + const theme = useTheme(); + const data = getToolOutput(); + + const isDark = theme === 'dark'; + const [selectedSeats, setSelectedSeats] = useState>({}); + const [activePassenger, setActivePassenger] = useState(0); + const [hoveredSeat, setHoveredSeat] = useState(null); + + const passengers = [ + { id: 'pax_1', name: 'Passenger 1' }, + { id: 'pax_2', name: 'Passenger 2' } + ]; + + const handleSeatClick = (seatId: string, seat: Seat) => { + if (!seat.available) return; + + const currentPassengerId = passengers[activePassenger].id; + const seatOwner = Object.entries(selectedSeats).find(([_, id]) => id === seatId)?.[0]; + + if (seatOwner && seatOwner !== currentPassengerId) return; + + setSelectedSeats(prev => { + const newSelections = { ...prev }; + if (newSelections[currentPassengerId] === seatId) { + delete newSelections[currentPassengerId]; + } else { + delete newSelections[currentPassengerId]; + newSelections[currentPassengerId] = seatId; + if (activePassenger < passengers.length - 1) { + setTimeout(() => setActivePassenger(activePassenger + 1), 200); + } + } + return newSelections; + }); + }; + + const getSeatStatus = (seatId: string, seat: Seat) => { + if (!seat.available) return 'unavailable'; + const owner = Object.entries(selectedSeats).find(([_, id]) => id === seatId)?.[0]; + if (owner) { + const passengerIndex = passengers.findIndex(p => p.id === owner); + return passengerIndex === activePassenger ? 'selected-active' : 'selected-other'; + } + return 'available'; + }; + + const getSeatColor = (status: string) => { + if (isDark) { + return { + 'available': '#334155', + 'selected-active': '#3B9FFF', + 'selected-other': '#22C55E', + 'unavailable': '#1E293B' + }[status] || '#334155'; + } + return { + 'available': '#E2E8F0', + 'selected-active': '#3B9FFF', + 'selected-other': '#22C55E', + 'unavailable': '#CBD5E1' + }[status] || '#E2E8F0'; + }; + + const calculateTotalPrice = () => { + let total = 0; + Object.values(selectedSeats).forEach(seatId => { + data?.cabins.forEach(cabin => { + cabin.rows.forEach(row => { + const seat = row.seats.find(s => s.id === seatId); + if (seat?.price) total += parseFloat(seat.price); + }); + }); + }); + return total; + }; + + const getSelectedSeatInfo = (passengerId: string) => { + const seatId = selectedSeats[passengerId]; + if (!seatId || !data) return null; + + for (const cabin of data.cabins) { + for (const row of cabin.rows) { + const seat = row.seats.find(s => s.id === seatId); + if (seat) return { seat, row: row.rowNumber }; + } + } + return null; + }; + + if (!data) { + return
Loading...
; + } + + return ( +
+ {/* Header */} +
+
+
+

+ 💺 + Select Seats +

+

+ {data.message || 'Choose seats for all passengers'} +

+
+ + {Object.keys(selectedSeats).length > 0 && ( +
+
Total
+
+ ${calculateTotalPrice().toFixed(2)} +
+
+ )} +
+
+ +
+ {/* Seat Map */} +
+ {/* Front Indicator */} +
+
✈️
+
+ FRONT +
+
+ + {data.cabins.map((cabin, cabinIndex) => ( +
+
+ {cabin.cabinClass.replace('_', ' ')} +
+ +
+ {cabin.rows.map((row) => ( +
+
+ {row.rowNumber} +
+ +
+ {row.seats.map((seat) => { + const status = getSeatStatus(seat.id, seat); + const isHovered = hoveredSeat === seat.id; + + return ( +
handleSeatClick(seat.id, seat)} + onMouseEnter={() => setHoveredSeat(seat.id)} + onMouseLeave={() => setHoveredSeat(null)} + style={{ + width: '40px', + height: '40px', + background: getSeatColor(status), + borderRadius: '6px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + cursor: seat.available ? 'pointer' : 'not-allowed', + transition: 'all 0.2s ease', + transform: isHovered && seat.available ? 'scale(1.1)' : 'scale(1)', + border: status === 'selected-active' ? '2px solid #fff' : 'none', + position: 'relative', + opacity: seat.available ? 1 : 0.4 + }} + > + 💺 +
+ {seat.column} +
+ {seat.price && parseFloat(seat.price) > 0 && isHovered && ( +
+ ${seat.price} +
+ )} +
+ ); + })} +
+ +
+ {row.rowNumber} +
+
+ ))} +
+
+ ))} + + {/* Legend */} +
+ {[ + { label: 'Available', color: isDark ? '#334155' : '#E2E8F0' }, + { label: 'Your Seat', color: '#3B9FFF' }, + { label: 'Other', color: '#22C55E' }, + { label: 'Taken', color: isDark ? '#1E293B' : '#CBD5E1' } + ].map(item => ( +
+
+ + {item.label} + +
+ ))} +
+
+ + {/* Sidebar */} +
+ {/* Passengers */} +
+

+ Passengers +

+ +
+ {passengers.map((passenger, index) => { + const seatInfo = getSelectedSeatInfo(passenger.id); + const isActive = activePassenger === index; + + return ( +
setActivePassenger(index)} + className={isActive ? 'nitro-gradient' : ''} + style={{ + padding: '12px', + background: isActive ? undefined : (isDark ? '#0F172A' : '#F8FAFC'), + borderRadius: '8px', + cursor: 'pointer', + transition: 'all 0.2s ease', + border: isActive ? '2px solid #fff' : '2px solid transparent' + }} + > +
+
+
+ {passenger.name} +
+ {seatInfo ? ( +
+ {seatInfo.row}{seatInfo.seat.column} + {seatInfo.seat.price && ` • $${seatInfo.seat.price}`} +
+ ) : ( +
+ No seat +
+ )} +
+ + {seatInfo && ( +
+ ✓ +
+ )} +
+
+ ); + })} +
+
+ + {/* Summary */} +
+

+ Summary +

+ +
+
+ Selected: + + {Object.keys(selectedSeats).length} / {passengers.length} + +
+ +
+ Total: + + ${calculateTotalPrice().toFixed(2)} + +
+
+ + +
+
+
+
+ ); +} diff --git a/nitrostack/templates/flight-booking/src/widgets/next-env.d.ts b/nitrostack/templates/flight-booking/src/widgets/next-env.d.ts new file mode 100644 index 0000000..40c3d68 --- /dev/null +++ b/nitrostack/templates/flight-booking/src/widgets/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/nitrostack/templates/flight-booking/src/widgets/next.config.js b/nitrostack/templates/flight-booking/src/widgets/next.config.js new file mode 100644 index 0000000..f35620c --- /dev/null +++ b/nitrostack/templates/flight-booking/src/widgets/next.config.js @@ -0,0 +1,45 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + transpilePackages: ['nitrostack'], + + // Static export for production builds + ...(process.env.NODE_ENV === 'production' && { + output: 'export', + distDir: 'out', + images: { + unoptimized: true, + }, + }), + + // Development optimizations to prevent cache corruption + ...(process.env.NODE_ENV === 'development' && { + // Use memory cache instead of filesystem cache in dev to avoid stale chunks + webpack: (config, { isServer }) => { + // Disable persistent caching in development to prevent chunk reference errors + if (config.cache && config.cache.type === 'filesystem') { + config.cache = { + type: 'memory', + }; + } + + // Improve cache busting for new files + if (!isServer) { + config.cache = false; // Disable cache completely on client in dev + } + + return config; + }, + + // Disable build activity indicator which can cause issues + devIndicators: { + buildActivity: false, + buildActivityPosition: 'bottom-right', + }, + + // Faster dev server + compress: false, + }), +}; + +export default nextConfig; diff --git a/nitrostack/templates/flight-booking/src/widgets/package.json b/nitrostack/templates/flight-booking/src/widgets/package.json new file mode 100644 index 0000000..6495572 --- /dev/null +++ b/nitrostack/templates/flight-booking/src/widgets/package.json @@ -0,0 +1,25 @@ +{ + "name": "flight-booking-widgets", + "version": "1.0.0", + "type": "module", + "private": true, + "scripts": { + "dev": "next dev -p 3001 --port 3001", + "build": "next build", + "start": "next start -p 3001" + }, + "dependencies": { + "next": "^14.2.5", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "@nitrostack/widgets": "^1", + "@modelcontextprotocol/ext-apps": ">=0.1.0" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "typescript": "^5" + } +} + diff --git a/nitrostack/templates/flight-booking/src/widgets/tsconfig.json b/nitrostack/templates/flight-booking/src/widgets/tsconfig.json new file mode 100644 index 0000000..2c0ad66 --- /dev/null +++ b/nitrostack/templates/flight-booking/src/widgets/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} + diff --git a/nitrostack/templates/flight-booking/src/widgets/widget-manifest.json b/nitrostack/templates/flight-booking/src/widgets/widget-manifest.json new file mode 100644 index 0000000..6dd7c34 --- /dev/null +++ b/nitrostack/templates/flight-booking/src/widgets/widget-manifest.json @@ -0,0 +1,395 @@ +{ + "version": "1.0.0", + "widgets": [ + { + "uri": "/flight-search-results", + "name": "Flight Search Results", + "description": "Displays flight search results with pricing, airlines, and flight details", + "examples": [ + { + "name": "Round Trip Search", + "description": "Shows results for a round trip flight search", + "data": { + "requestId": "orq_example123", + "searchParams": { + "origin": "JFK", + "destination": "LAX", + "departureDate": "2024-03-15", + "returnDate": "2024-03-22", + "passengers": { + "adults": 2, + "children": 0, + "infants": 0 + }, + "cabinClass": "economy" + }, + "totalOffers": 15, + "offers": [ + { + "id": "off_example123", + "totalAmount": "450.00", + "totalCurrency": "USD", + "expiresAt": "2024-03-01T12:00:00Z", + "outbound": { + "origin": "JFK", + "destination": "LAX", + "departureTime": "2024-03-15T08:00:00Z", + "arrivalTime": "2024-03-15T14:30:00Z", + "duration": "PT6H30M", + "stops": 0, + "airline": "American Airlines", + "flightNumber": "AA123", + "segments": [] + }, + "return": { + "origin": "LAX", + "destination": "JFK", + "departureTime": "2024-03-22T16:00:00Z", + "arrivalTime": "2024-03-23T00:30:00Z", + "duration": "PT5H30M", + "stops": 0, + "airline": "American Airlines", + "flightNumber": "AA456", + "segments": [] + }, + "fareType": "Domestic", + "refundable": false, + "changeable": true + } + ], + "message": "Found 15 flight options" + } + } + ], + "tags": [ + "flights", + "search", + "travel", + "booking" + ] + }, + { + "uri": "/flight-details", + "name": "Flight Details", + "description": "Displays comprehensive flight details including segments, baggage, and fare conditions", + "examples": [ + { + "name": "Flight Details Example", + "description": "Shows detailed information for a specific flight offer", + "data": { + "id": "off_example123", + "totalAmount": "450.00", + "totalCurrency": "USD", + "expiresAt": "2024-03-01T12:00:00Z", + "slices": [ + { + "origin": { + "code": "JFK", + "name": "John F. Kennedy International Airport", + "city": "New York" + }, + "destination": { + "code": "LAX", + "name": "Los Angeles International Airport", + "city": "Los Angeles" + }, + "duration": "PT6H30M", + "segments": [ + { + "id": "seg_123", + "origin": "JFK", + "destination": "LAX", + "departingAt": "2024-03-15T08:00:00Z", + "arrivingAt": "2024-03-15T14:30:00Z", + "duration": "PT6H30M", + "airline": { + "name": "American Airlines", + "code": "AA", + "flightNumber": "123" + }, + "aircraft": "Boeing 777-300ER" + } + ] + } + ], + "passengers": [ + { + "id": "pas_123", + "type": "adult", + "fareType": "economy", + "baggageAllowance": [ + { + "type": "checked", + "quantity": 1 + }, + { + "type": "carry_on", + "quantity": 1 + } + ] + } + ], + "conditions": { + "refundBeforeDeparture": { + "allowed": false + }, + "changeBeforeDeparture": { + "allowed": true, + "penaltyAmount": "75.00", + "penaltyCurrency": "USD" + } + }, + "paymentRequirements": { + "requiresInstantPayment": true, + "priceGuaranteeExpiresAt": "2024-03-01T12:00:00Z" + } + } + } + ], + "tags": [ + "flights", + "details", + "travel", + "booking" + ] + }, + { + "uri": "/airport-search", + "name": "Airport Search", + "description": "Displays airport search results with IATA codes and location details", + "examples": [ + { + "name": "London Airports", + "description": "Shows search results for London airports", + "data": { + "query": "London", + "results": [ + { + "id": "arp_lhr_gb", + "name": "Heathrow Airport", + "iataCode": "LHR", + "icaoCode": "EGLL", + "cityName": "London", + "type": "airport", + "latitude": 51.4700, + "longitude": -0.4543, + "timeZone": "Europe/London" + }, + { + "id": "arp_lgw_gb", + "name": "Gatwick Airport", + "iataCode": "LGW", + "icaoCode": "EGKK", + "cityName": "London", + "type": "airport", + "latitude": 51.1537, + "longitude": -0.1821, + "timeZone": "Europe/London" + }, + { + "id": "arp_stn_gb", + "name": "Stansted Airport", + "iataCode": "STN", + "icaoCode": "EGSS", + "cityName": "London", + "type": "airport", + "latitude": 51.8860, + "longitude": 0.2389, + "timeZone": "Europe/London" + } + ] + } + } + ], + "tags": [ + "airports", + "search", + "travel", + "iata" + ] + }, + { + "uri": "/order-summary", + "name": "Order Summary", + "description": "Displays booking confirmation with order details, passenger information, and flight itinerary", + "examples": [ + { + "name": "Confirmed Booking", + "description": "Shows a confirmed flight booking with all details", + "data": { + "orderId": "ord_example123", + "status": "confirmed", + "bookingReference": "ABC123", + "totalAmount": "450.00", + "totalCurrency": "USD", + "createdAt": "2024-03-01T10:00:00Z", + "passengers": [ + { + "id": "pax_1", + "name": "John Doe", + "type": "adult", + "email": "john@example.com" + } + ], + "slices": [ + { + "origin": { + "code": "JFK", + "name": "John F. Kennedy International", + "city": "New York" + }, + "destination": { + "code": "LAX", + "name": "Los Angeles International", + "city": "Los Angeles" + }, + "departureTime": "2024-03-15T08:00:00Z", + "arrivalTime": "2024-03-15T14:30:00Z", + "segments": [] + } + ], + "message": "Your booking has been confirmed!" + } + } + ], + "tags": [ + "booking", + "order", + "confirmation", + "travel" + ] + }, + { + "uri": "/seat-selection", + "name": "Seat Selection", + "description": "Interactive seat map for selecting seats with real-time pricing and availability", + "examples": [ + { + "name": "Economy Cabin Seats", + "description": "Shows available seats in economy cabin", + "data": { + "offerId": "off_example123", + "cabins": [ + { + "cabinClass": "economy", + "rows": [ + { + "rowNumber": 10, + "seats": [ + { + "id": "seat_10a", + "column": "A", + "available": true, + "price": "0", + "currency": "USD", + "type": "window" + }, + { + "id": "seat_10b", + "column": "B", + "available": true, + "price": "0", + "currency": "USD", + "type": "middle" + }, + { + "id": "seat_10c", + "column": "C", + "available": true, + "price": "0", + "currency": "USD", + "type": "aisle" + } + ] + } + ] + } + ], + "message": "Select your preferred seats" + } + } + ], + "tags": [ + "seats", + "selection", + "booking", + "travel" + ] + }, + { + "uri": "/payment-confirmation", + "name": "Payment Confirmation", + "description": "Payment processing and confirmation interface with order summary", + "examples": [ + { + "name": "Payment Success", + "description": "Shows successful payment confirmation", + "data": { + "orderId": "ord_example123", + "status": "confirmed", + "totalAmount": "450.00", + "totalCurrency": "USD", + "bookingReference": "ABC123", + "message": "Payment confirmed successfully!" + } + }, + { + "name": "Payment Pending", + "description": "Shows payment pending state", + "data": { + "orderId": "ord_example456", + "status": "pending", + "totalAmount": "650.00", + "totalCurrency": "USD" + } + } + ], + "tags": [ + "payment", + "confirmation", + "booking", + "checkout" + ] + }, + { + "uri": "/order-cancellation", + "name": "Order Cancellation", + "description": "Order cancellation confirmation with refund information and status", + "examples": [ + { + "name": "Cancellation with Refund", + "description": "Shows successful cancellation with refund", + "data": { + "orderId": "ord_example123", + "cancellationId": "ocr_example123", + "status": "cancelled", + "refundAmount": "450.00", + "refundCurrency": "USD", + "confirmedAt": "2024-03-01T12:00:00Z", + "message": "Order cancelled. Refund of USD 450.00 will be processed." + } + }, + { + "name": "Cancellation without Refund", + "description": "Shows cancellation with no refund", + "data": { + "orderId": "ord_example456", + "cancellationId": "ocr_example456", + "status": "cancelled", + "refundAmount": "0", + "refundCurrency": "USD", + "confirmedAt": "2024-03-01T12:00:00Z", + "message": "Order cancelled. No refund available for this booking." + } + } + ], + "tags": [ + "cancellation", + "refund", + "booking", + "order" + ] + } + ], + "generatedAt": "2025-01-01T00:00:00.000Z" +} \ No newline at end of file diff --git a/scratch/test-flight/.env b/nitrostack/templates/pizzaz/.env similarity index 100% rename from scratch/test-flight/.env rename to nitrostack/templates/pizzaz/.env diff --git a/nitrostack/templates/pizzaz/app_module.py b/nitrostack/templates/pizzaz/app_module.py new file mode 100644 index 0000000..2cfa7d7 --- /dev/null +++ b/nitrostack/templates/pizzaz/app_module.py @@ -0,0 +1,14 @@ +from nitrostack import module, ConfigModule +from modules.pizzaz.pizzaz_module import PizzazModule +from health.system_health import SystemHealthCheck + +@module( + name="app", + imports=[ + ConfigModule.for_root(env_file_path=".env", defaults={"PORT": "8000"}), + PizzazModule + ], + providers=[SystemHealthCheck] +) +class AppModule: + pass diff --git a/nitrostack/templates/pizzaz/health/system_health.py b/nitrostack/templates/pizzaz/health/system_health.py new file mode 100644 index 0000000..1e1a3bb --- /dev/null +++ b/nitrostack/templates/pizzaz/health/system_health.py @@ -0,0 +1,11 @@ +import time +from nitrostack import health_check + +class SystemHealthCheck: + def __init__(self): + self.start_time = time.time() + + @health_check("system") + def check_system(self) -> bool: + uptime = time.time() - self.start_time + return uptime >= 0 diff --git a/scratch/test-food/main.py b/nitrostack/templates/pizzaz/main.py similarity index 100% rename from scratch/test-food/main.py rename to nitrostack/templates/pizzaz/main.py diff --git a/nitrostack/templates/pizzaz/modules/pizzaz/pizzaz_data.py b/nitrostack/templates/pizzaz/modules/pizzaz/pizzaz_data.py new file mode 100644 index 0000000..f40e1af --- /dev/null +++ b/nitrostack/templates/pizzaz/modules/pizzaz/pizzaz_data.py @@ -0,0 +1,85 @@ +PIZZA_SHOPS = [ + { + "id": "tonys-pizza", + "name": "Tony's New York Pizza", + "description": "Authentic New York-style pizza with a crispy thin crust and fresh toppings", + "address": "123 Main St, San Francisco, CA 94102", + "coords": [-122.4194, 37.7749], + "rating": 4.5, + "reviews": 342, + "priceLevel": 2, + "cuisine": ["Italian", "Pizza", "New York Style"], + "hours": {"open": "11:00 AM", "close": "10:00 PM"}, + "phone": "(415) 555-0123", + "website": "https://tonyspizza.example.com", + "image": "https://images.unsplash.com/photo-1513104890138-7c749659a591", + "specialties": ["Margherita", "Pepperoni", "White Pizza"], + "openNow": True, + }, + { + "id": "bella-napoli", + "name": "Bella Napoli", + "description": "Traditional Neapolitan pizza baked in a wood-fired oven", + "address": "456 Market St, San Francisco, CA 94103", + "coords": [-122.4089, 37.7858], + "rating": 4.8, + "reviews": 521, + "priceLevel": 3, + "cuisine": ["Italian", "Pizza", "Neapolitan"], + "hours": {"open": "12:00 PM", "close": "11:00 PM"}, + "phone": "(415) 555-0456", + "website": "https://bellanapoli.example.com", + "image": "https://images.unsplash.com/photo-1574071318508-1cdbab80d002", + "specialties": ["Marinara", "Quattro Formaggi", "Diavola"], + "openNow": True, + }, + { + "id": "slice-house", + "name": "The Slice House", + "description": "Casual spot for creative pizza slices and craft beer", + "address": "789 Valencia St, San Francisco, CA 94110", + "coords": [-122.4216, 37.7599], + "rating": 4.3, + "reviews": 289, + "priceLevel": 1, + "cuisine": ["Pizza", "American", "Casual"], + "hours": {"open": "11:30 AM", "close": "9:00 PM"}, + "phone": "(415) 555-0789", + "image": "https://images.unsplash.com/photo-1565299624946-b28f40a0ae38", + "specialties": ["BBQ Chicken", "Hawaiian", "Veggie Supreme"], + "openNow": True, + }, + { + "id": "pizzeria-delfina", + "name": "Pizzeria Delfina", + "description": "Upscale pizzeria with seasonal ingredients and house-made mozzarella", + "address": "3611 18th St, San Francisco, CA 94110", + "coords": [-122.4252, 37.7615], + "rating": 4.6, + "reviews": 678, + "priceLevel": 3, + "cuisine": ["Italian", "Pizza", "Fine Dining"], + "hours": {"open": "5:00 PM", "close": "10:00 PM"}, + "phone": "(415) 555-3611", + "website": "https://pizzeriadelfina.example.com", + "image": "https://images.unsplash.com/photo-1571997478779-2adcbbe9ab2f", + "specialties": ["Salsiccia", "Funghi", "Prosciutto e Rucola"], + "openNow": False, + }, + { + "id": "golden-boy", + "name": "Golden Boy Pizza", + "description": "North Beach institution serving thick Sicilian-style squares", + "address": "542 Green St, San Francisco, CA 94133", + "coords": [-122.4102, 37.7999], + "rating": 4.4, + "reviews": 445, + "priceLevel": 1, + "cuisine": ["Pizza", "Sicilian", "Italian"], + "hours": {"open": "11:30 AM", "close": "11:30 PM"}, + "phone": "(415) 555-0542", + "image": "https://images.unsplash.com/photo-1628840042765-356cda07504e", + "specialties": ["Clam & Garlic", "Pesto", "Classic Cheese"], + "openNow": True, + }, +] diff --git a/nitrostack/templates/pizzaz/modules/pizzaz/pizzaz_module.py b/nitrostack/templates/pizzaz/modules/pizzaz/pizzaz_module.py new file mode 100644 index 0000000..8718534 --- /dev/null +++ b/nitrostack/templates/pizzaz/modules/pizzaz/pizzaz_module.py @@ -0,0 +1,12 @@ +from nitrostack import module +from modules.pizzaz.pizzaz_tools import PizzazTools +from modules.pizzaz.pizzaz_service import PizzazService + +@module( + name="pizzaz", + controllers=[PizzazTools], + providers=[PizzazService], + exports=[PizzazService] +) +class PizzazModule: + pass diff --git a/nitrostack/templates/pizzaz/modules/pizzaz/pizzaz_service.py b/nitrostack/templates/pizzaz/modules/pizzaz/pizzaz_service.py new file mode 100644 index 0000000..bb4323f --- /dev/null +++ b/nitrostack/templates/pizzaz/modules/pizzaz/pizzaz_service.py @@ -0,0 +1,33 @@ +from nitrostack import injectable +from modules.pizzaz.pizzaz_data import PIZZA_SHOPS + +@injectable() +class PizzazService: + def get_all_shops(self): + return PIZZA_SHOPS + + def get_shop_by_id(self, shop_id: str): + for shop in PIZZA_SHOPS: + if shop["id"] == shop_id: + return shop + return None + + def get_shops_filtered(self, filters: dict): + shops = PIZZA_SHOPS + if filters.get("openNow"): + shops = [shop for shop in shops if shop["openNow"]] + if filters.get("minRating") is not None: + shops = [shop for shop in shops if shop["rating"] >= filters["minRating"]] + if filters.get("maxPrice") is not None: + shops = [shop for shop in shops if shop["priceLevel"] <= filters["maxPrice"]] + if filters.get("cuisine"): + cuisine_lower = filters["cuisine"].lower() + shops = [ + shop for shop in shops + if any(cuisine_lower in c.lower() for c in shop["cuisine"]) + ] + return shops + + def get_top_rated_shops(self, limit: int = 5): + sorted_shops = sorted(PIZZA_SHOPS, key=lambda x: x["rating"], reverse=True) + return sorted_shops[:limit] diff --git a/nitrostack/templates/pizzaz/modules/pizzaz/pizzaz_tools.py b/nitrostack/templates/pizzaz/modules/pizzaz/pizzaz_tools.py new file mode 100644 index 0000000..d4f4520 --- /dev/null +++ b/nitrostack/templates/pizzaz/modules/pizzaz/pizzaz_tools.py @@ -0,0 +1,83 @@ +from nitrostack import injectable, tool, widget, ExecutionContext +from pydantic import BaseModel, Field +from typing import Literal, Optional +from modules.pizzaz.pizzaz_service import PizzazService + +class ShowMapInput(BaseModel): + filter: Literal["open_now", "top_rated", "all"] = Field(default="all", description="Filter to apply") + +class ShowListInput(BaseModel): + openNow: Optional[bool] = Field(default=None, description="Show only shops that are currently open") + minRating: Optional[float] = Field(default=None, description="Minimum rating (1-5)") + maxPrice: Optional[float] = Field(default=None, description="Maximum price level (1-3)") + +class ShowShopInput(BaseModel): + shopId: str = Field(description="ID of the pizza shop to display") + +@injectable(deps=[PizzazService]) +class PizzazTools: + def __init__(self, service: PizzazService): + self.service = service + + @tool( + name="show_pizza_map", + description="Display an interactive map of pizza shops in San Francisco", + input_schema=ShowMapInput + ) + @widget("pizza-map") + async def show_pizza_map(self, input: ShowMapInput, context: ExecutionContext) -> dict: + context.logger.info(f"Showing pizza map with filter: {input.filter}") + if input.filter == "open_now": + shops = self.service.get_shops_filtered({"openNow": True}) + elif input.filter == "top_rated": + shops = self.service.get_top_rated_shops() + else: + shops = self.service.get_all_shops() + + return { + "shops": shops, + "filter": input.filter, + "totalShops": len(shops) + } + + @tool( + name="show_pizza_list", + description="Display a list of pizza shops with details, ratings, and filters", + input_schema=ShowListInput + ) + @widget("pizza-list") + async def show_pizza_list(self, input: ShowListInput, context: ExecutionContext) -> dict: + context.logger.info("Showing pizza list") + filters = {} + if input.openNow is not None: + filters["openNow"] = input.openNow + if input.minRating is not None: + filters["minRating"] = input.minRating + if input.maxPrice is not None: + filters["maxPrice"] = input.maxPrice + + shops = self.service.get_shops_filtered(filters) + return { + "shops": shops, + "filters": { + "openNow": input.openNow, + "minRating": input.minRating, + "maxPrice": input.maxPrice + }, + "totalShops": len(shops) + } + + @tool( + name="show_pizza_shop", + description="Display detailed page for a single pizza shop, including menu, ratings, hours, and photos", + input_schema=ShowShopInput + ) + @widget("pizza-shop") + async def show_pizza_shop(self, input: ShowShopInput, context: ExecutionContext) -> dict: + context.logger.info(f"Showing pizza shop: {input.shopId}") + shop = self.service.get_shop_by_id(input.shopId) + if not shop: + raise ValueError(f"Pizza shop not found: {input.shopId}") + return { + "shop": shop + } diff --git a/scratch/test-food/requirements.txt b/nitrostack/templates/pizzaz/requirements.txt similarity index 100% rename from scratch/test-food/requirements.txt rename to nitrostack/templates/pizzaz/requirements.txt diff --git a/nitrostack/templates/pizzaz/src/widgets/app/layout.tsx b/nitrostack/templates/pizzaz/src/widgets/app/layout.tsx new file mode 100644 index 0000000..100a41a --- /dev/null +++ b/nitrostack/templates/pizzaz/src/widgets/app/layout.tsx @@ -0,0 +1,18 @@ +'use client'; + +import { WidgetLayout } from '@nitrostack/widgets'; +import 'mapbox-gl/dist/mapbox-gl.css'; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + {children} + + + ); +} diff --git a/nitrostack/templates/pizzaz/src/widgets/app/pizza-list/page.tsx b/nitrostack/templates/pizzaz/src/widgets/app/pizza-list/page.tsx new file mode 100644 index 0000000..fbabe86 --- /dev/null +++ b/nitrostack/templates/pizzaz/src/widgets/app/pizza-list/page.tsx @@ -0,0 +1,272 @@ +'use client'; + +import { useTheme, useWidgetState, useMaxHeight, useWidgetSDK } from '@nitrostack/widgets'; + +// Disable static generation - this is a dynamic widget +export const dynamic = 'force-dynamic'; +import { PizzaCard } from '../../components/PizzaCard'; +import { SlidersHorizontal } from 'lucide-react'; +import { useState } from 'react'; + +interface PizzaShop { + id: string; + name: string; + description: string; + address: string; + coords: [number, number]; + rating: number; + reviews: number; + priceLevel: 1 | 2 | 3; + cuisine: string[]; + hours: { open: string; close: string }; + phone: string; + website?: string; + image: string; + specialties: string[]; + openNow: boolean; +} + +interface WidgetData { + shops: PizzaShop[]; + filters: any; + totalShops: number; +} + +export default function PizzaListWidget() { + const theme = useTheme(); + const maxHeight = useMaxHeight(); + const isDark = theme === 'dark'; + + const { isReady, getToolOutput, callTool } = useWidgetSDK(); + + // Access tool output + const data = getToolOutput(); + + console.log('🍕 PizzaListWidget render:', { isReady, hasData: !!data, data }); + + // Persistent state for view mode and favorites + const [state, setState] = useWidgetState<{ + viewMode: 'grid' | 'list'; + favorites: string[]; + sortBy: 'rating' | 'name' | 'price'; + }>(() => ({ + viewMode: 'grid', + favorites: [], + sortBy: 'rating', + })); + + const [showFilters, setShowFilters] = useState(false); + + if (!data) { + return ( +
+ Loading pizza shops... {isReady ? '(SDK ready but no data)' : '(waiting for SDK)'} +
+ ); + } + + // Check if shops array exists + if (!data.shops || !Array.isArray(data.shops)) { + console.error('❌ Invalid data structure:', data); + return ( +
+ Error: Invalid data structure. Expected shops array. +
+                    {JSON.stringify(data, null, 2)}
+                
+
+ ); + } + + const toggleFavorite = (shopId: string) => { + const favorites = state?.favorites || []; + const newFavorites = favorites.includes(shopId) + ? favorites.filter(id => id !== shopId) + : [...favorites, shopId]; + + setState({ ...state, favorites: newFavorites }); + }; + + const handleShopClick = async (shopId: string) => { + // Call the show_pizza_shop tool to show shop details + await callTool('show_pizza_shop', { shopId }); + }; + + // Sort shops + let sortedShops = [...data.shops]; + switch (state?.sortBy) { + case 'rating': + sortedShops.sort((a, b) => b.rating - a.rating); + break; + case 'name': + sortedShops.sort((a, b) => a.name.localeCompare(b.name)); + break; + case 'price': + sortedShops.sort((a, b) => a.priceLevel - b.priceLevel); + break; + } + + return ( +
+ {/* Header */} +
+
+
+

+ 🍕 Pizza Shops +

+

+ {data.totalShops} shops found +

+
+ + {/* Sort and Filter Controls */} +
+ + + +
+
+ + {/* Filters */} + {showFilters && ( +
+

+ Filters coming soon... +

+
+ )} + + {/* Favorites Count */} + {state?.favorites && state.favorites.length > 0 && ( +
+ ❤️ {state.favorites.length} favorite{state.favorites.length !== 1 ? 's' : ''} +
+ )} +
+ + {/* Horizontal Scrolling Shop Cards */} +
+
+ {sortedShops.map(shop => ( +
+ handleShopClick(shop.id)} + /> +
+ ))} +
+
+ + {/* Footer */} +
+ Powered by NitroStack • Theme: {theme || 'light'} • Scroll horizontally → +
+
+ ); +} diff --git a/nitrostack/templates/pizzaz/src/widgets/app/pizza-map/page.tsx b/nitrostack/templates/pizzaz/src/widgets/app/pizza-map/page.tsx new file mode 100644 index 0000000..f1187d4 --- /dev/null +++ b/nitrostack/templates/pizzaz/src/widgets/app/pizza-map/page.tsx @@ -0,0 +1,216 @@ +'use client'; + +import { useTheme, useWidgetState, useMaxHeight, useDisplayMode, useWidgetSDK } from '@nitrostack/widgets'; + +// Disable static generation - this is a dynamic widget +export const dynamic = 'force-dynamic'; +import { useEffect, useRef, useState } from 'react'; +import mapboxgl from 'mapbox-gl'; +import 'mapbox-gl/dist/mapbox-gl.css'; +import { CompactShopCard } from '../../components/CompactShopCard'; +import { Maximize2 } from 'lucide-react'; + +interface PizzaShop { + id: string; + name: string; + description: string; + address: string; + coords: [number, number]; + rating: number; + reviews: number; + priceLevel: 1 | 2 | 3; + cuisine: string[]; + hours: { open: string; close: string }; + phone: string; + website?: string; + image: string; + specialties: string[]; + openNow: boolean; +} + +interface WidgetData { + shops: PizzaShop[]; + filter: string; + totalShops: number; +} + +export default function PizzaMapWidget() { + const theme = useTheme(); + const maxHeight = useMaxHeight(); + const displayMode = useDisplayMode(); + const isDark = theme === 'dark'; + const mapContainer = useRef(null); + const map = useRef(null); + const [selectedShop, setSelectedShop] = useState(null); + + const { isReady, getToolOutput, callTool, requestFullscreen } = useWidgetSDK(); + + // Access tool output + const data = getToolOutput(); + + // Persistent state + const [state, setState] = useWidgetState<{ + favorites: string[]; + }>(() => ({ + favorites: [], + })); + + useEffect(() => { + if (!mapContainer.current || !data || map.current) return; + + // Initialize Mapbox + const initMap = async () => { + try { + // Set your Mapbox token here + mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN || 'YOUR_MAPBOX_TOKEN'; + + const mapInstance = new mapboxgl.Map({ + container: mapContainer.current!, + style: isDark ? 'mapbox://styles/mapbox/dark-v11' : 'mapbox://styles/mapbox/streets-v12', + center: data.shops[0]?.coords || [-122.4194, 37.7749], + zoom: 12, + }); + + // Add markers + data.shops.forEach(shop => { + const el = document.createElement('div'); + el.className = 'marker'; + el.style.backgroundImage = 'url(https://docs.mapbox.com/mapbox-gl-js/assets/custom_marker.png)'; + el.style.width = '30px'; + el.style.height = '40px'; + el.style.backgroundSize = '100%'; + el.style.cursor = 'pointer'; + + el.addEventListener('click', () => { + setSelectedShop(shop); + }); + + new mapboxgl.Marker(el) + .setLngLat(shop.coords) + .addTo(mapInstance); + }); + + // Fit bounds to show all markers + if (data.shops.length > 1) { + const bounds = new mapboxgl.LngLatBounds(); + data.shops.forEach(shop => bounds.extend(shop.coords)); + mapInstance.fitBounds(bounds, { padding: 50 }); + } + + map.current = mapInstance; + } catch (error) { + console.error('Failed to load Mapbox:', error); + } + }; + + initMap(); + + return () => { + if (map.current) { + map.current.remove(); + } + }; + }, [data, isDark]); + + if (!data) { + return ( +
+ Loading map... {isReady ? '(SDK ready but no data)' : '(waiting for SDK)'} +
+ ); + } + + const handleShopClick = async (shopId: string) => { + // Call the show_pizza_shop tool to show shop details + await callTool('show_pizza_shop', { shopId }); + }; + + const requestFullscreenMode = async () => { + await requestFullscreen(); + }; + + return ( +
+ {/* Map Container - Full Screen */} +
+ + {/* Enlarge Button */} + + + {/* Overlay Shop Cards - Bottom */} +
+
+ {data.shops.map(shop => ( +
+ { + setSelectedShop(shop); + handleShopClick(shop.id); + }} + isDark={isDark} + /> +
+ ))} +
+
+
+ ); +} diff --git a/nitrostack/templates/pizzaz/src/widgets/app/pizza-shop/page.tsx b/nitrostack/templates/pizzaz/src/widgets/app/pizza-shop/page.tsx new file mode 100644 index 0000000..4f2a731 --- /dev/null +++ b/nitrostack/templates/pizzaz/src/widgets/app/pizza-shop/page.tsx @@ -0,0 +1,374 @@ +'use client'; + +import { useTheme, useMaxHeight, useWidgetSDK } from '@nitrostack/widgets'; + +// Disable static generation - this is a dynamic widget +export const dynamic = 'force-dynamic'; +import { Star, MapPin, Phone, Globe, Clock, Heart, Share2, Navigation } from 'lucide-react'; + +interface PizzaShop { + id: string; + name: string; + description: string; + address: string; + coords: [number, number]; + rating: number; + reviews: number; + priceLevel: 1 | 2 | 3; + cuisine: string[]; + hours: { open: string; close: string }; + phone: string; + website?: string; + image: string; + specialties: string[]; + openNow: boolean; +} + +interface WidgetData { + shop: PizzaShop; + relatedShops: PizzaShop[]; +} + +export default function PizzaShopWidget() { + const theme = useTheme(); + const maxHeight = useMaxHeight(); + const isDark = theme === 'dark'; + const { isReady, getToolOutput, openExternal } = useWidgetSDK(); + + // Access tool output + const data = getToolOutput(); + + if (!data) { + return ( +
+ Loading shop details... {isReady ? '(SDK ready but no data)' : '(waiting for SDK)'} +
+ ); + } + + const { shop, relatedShops } = data; + const priceSymbol = '$'.repeat(shop.priceLevel); + + const openMaps = () => { + const url = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(shop.address)}`; + openExternal(url); + }; + + const callPhone = () => { + openExternal(`tel:${shop.phone}`); + }; + + const visitWebsite = () => { + if (shop.website) { + openExternal(shop.website); + } + }; + + return ( +
+ {/* Hero Image */} +
+ {shop.name} +
+ + {/* Shop Name Overlay */} +
+

+ {shop.name} +

+
+
+ + + {shop.rating} + + + ({shop.reviews} reviews) + +
+ {priceSymbol} + {shop.openNow && ( + + Open Now + + )} +
+
+
+ + {/* Content */} +
+ {/* Description */} +

+ {shop.description} +

+ + {/* Cuisine Tags */} +
+

+ Cuisine +

+
+ {shop.cuisine.map(c => ( + + {c} + + ))} +
+
+ + {/* Specialties */} +
+

+ Specialties +

+
+ {shop.specialties.map(s => ( + + 🍕 {s} + + ))} +
+
+ + {/* Contact Info */} +
+

+ Contact & Hours +

+ + {/* Address */} +
+ +
+

+ {shop.address} +

+ +
+
+ + {/* Phone */} +
+ + +
+ + {/* Website */} + {shop.website && ( +
+ + +
+ )} + + {/* Hours */} +
+ + + {shop.hours.open} - {shop.hours.close} + +
+
+ + {/* Related Shops */} + {relatedShops.length > 0 && ( +
+

+ You Might Also Like +

+
+ {relatedShops.map(related => ( +
+ {related.name} +
+

+ {related.name} +

+
+ + + {related.rating} + +
+

+ {related.description} +

+
+
+ ))} +
+
+ )} +
+
+ ); +} diff --git a/nitrostack/templates/pizzaz/src/widgets/components/CompactShopCard.tsx b/nitrostack/templates/pizzaz/src/widgets/components/CompactShopCard.tsx new file mode 100644 index 0000000..7799ba9 --- /dev/null +++ b/nitrostack/templates/pizzaz/src/widgets/components/CompactShopCard.tsx @@ -0,0 +1,144 @@ +'use client'; + +interface PizzaShop { + id: string; + name: string; + description: string; + address: string; + coords: [number, number]; + rating: number; + reviews: number; + priceLevel: 1 | 2 | 3; + cuisine: string[]; + hours: { open: string; close: string }; + phone: string; + website?: string; + image: string; + specialties: string[]; + openNow: boolean; +} + +interface CompactShopCardProps { + shop: PizzaShop; + isSelected: boolean; + onClick: () => void; + isDark?: boolean; +} + +export function CompactShopCard({ shop, isSelected, onClick, isDark = true }: CompactShopCardProps) { + const priceSymbol = '$'.repeat(shop.priceLevel); + + return ( +
{ + if (!isSelected) { + e.currentTarget.style.transform = 'translateY(-2px)'; + e.currentTarget.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.15)'; + } + }} + onMouseLeave={(e) => { + if (!isSelected) { + e.currentTarget.style.transform = 'translateY(0)'; + e.currentTarget.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.1)'; + } + }} + > + {/* Image */} + {shop.name} + + {/* Info */} +
+ {/* Name */} +

+ {shop.name} +

+ + {/* Description */} +

+ {shop.description} +

+ + {/* Rating & Price */} +
+
+ + {shop.rating} +
+ + {priceSymbol} + {shop.openNow && ( + <> + + + Open Now + + + )} +
+
+
+ ); +} diff --git a/nitrostack/templates/pizzaz/src/widgets/components/PizzaCard.tsx b/nitrostack/templates/pizzaz/src/widgets/components/PizzaCard.tsx new file mode 100644 index 0000000..7821977 --- /dev/null +++ b/nitrostack/templates/pizzaz/src/widgets/components/PizzaCard.tsx @@ -0,0 +1,191 @@ +'use client'; + +import { useTheme, useWidgetState, useMaxHeight, useDisplayMode } from '@nitrostack/widgets'; +import { Star, MapPin, Phone, Globe, Clock } from 'lucide-react'; + +interface PizzaShop { + id: string; + name: string; + description: string; + address: string; + coords: [number, number]; + rating: number; + reviews: number; + priceLevel: 1 | 2 | 3; + cuisine: string[]; + hours: { open: string; close: string }; + phone: string; + website?: string; + image: string; + specialties: string[]; + openNow: boolean; +} + +interface PizzaCardProps { + shop: PizzaShop; + onSelect?: (shop: PizzaShop) => void; + isFavorite?: boolean; + onToggleFavorite?: (shopId: string) => void; +} + +export function PizzaCard({ shop, onSelect, isFavorite, onToggleFavorite }: PizzaCardProps) { + const theme = useTheme(); + const isDark = theme === 'dark'; + + const priceSymbol = '$'.repeat(shop.priceLevel); + + return ( +
onSelect?.(shop)} + style={{ + background: isDark ? '#1a1a1a' : '#ffffff', + border: `1px solid ${isDark ? '#333' : '#e5e7eb'}`, + borderRadius: '12px', + overflow: 'hidden', + cursor: onSelect ? 'pointer' : 'default', + transition: 'all 0.2s', + boxShadow: isDark ? '0 2px 8px rgba(0,0,0,0.3)' : '0 2px 8px rgba(0,0,0,0.1)', + }} + onMouseEnter={(e) => { + if (onSelect) { + e.currentTarget.style.transform = 'translateY(-2px)'; + e.currentTarget.style.boxShadow = isDark + ? '0 4px 12px rgba(0,0,0,0.4)' + : '0 4px 12px rgba(0,0,0,0.15)'; + } + }} + onMouseLeave={(e) => { + if (onSelect) { + e.currentTarget.style.transform = 'translateY(0)'; + e.currentTarget.style.boxShadow = isDark + ? '0 2px 8px rgba(0,0,0,0.3)' + : '0 2px 8px rgba(0,0,0,0.1)'; + } + }} + > + {/* Image */} +
+ {shop.name} + {shop.openNow && ( +
+ Open Now +
+ )} + {onToggleFavorite && ( + + )} +
+ + {/* Content */} +
+

+ {shop.name} +

+ + {/* Rating & Price */} +
+
+ + + {shop.rating} + + + ({shop.reviews}) + +
+ + {priceSymbol} + +
+ + {/* Description */} +

+ {shop.description} +

+ + {/* Cuisine Tags */} +
+ {shop.cuisine.slice(0, 3).map(c => ( + + {c} + + ))} +
+ + {/* Address */} +
+ + + {shop.address} + +
+ + {/* Hours */} +
+ + + {shop.hours.open} - {shop.hours.close} + +
+
+
+ ); +} diff --git a/nitrostack/templates/pizzaz/src/widgets/next.config.js b/nitrostack/templates/pizzaz/src/widgets/next.config.js new file mode 100644 index 0000000..f35620c --- /dev/null +++ b/nitrostack/templates/pizzaz/src/widgets/next.config.js @@ -0,0 +1,45 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + transpilePackages: ['nitrostack'], + + // Static export for production builds + ...(process.env.NODE_ENV === 'production' && { + output: 'export', + distDir: 'out', + images: { + unoptimized: true, + }, + }), + + // Development optimizations to prevent cache corruption + ...(process.env.NODE_ENV === 'development' && { + // Use memory cache instead of filesystem cache in dev to avoid stale chunks + webpack: (config, { isServer }) => { + // Disable persistent caching in development to prevent chunk reference errors + if (config.cache && config.cache.type === 'filesystem') { + config.cache = { + type: 'memory', + }; + } + + // Improve cache busting for new files + if (!isServer) { + config.cache = false; // Disable cache completely on client in dev + } + + return config; + }, + + // Disable build activity indicator which can cause issues + devIndicators: { + buildActivity: false, + buildActivityPosition: 'bottom-right', + }, + + // Faster dev server + compress: false, + }), +}; + +export default nextConfig; diff --git a/nitrostack/templates/pizzaz/src/widgets/package.json b/nitrostack/templates/pizzaz/src/widgets/package.json new file mode 100644 index 0000000..1e97ecf --- /dev/null +++ b/nitrostack/templates/pizzaz/src/widgets/package.json @@ -0,0 +1,31 @@ +{ + "name": "pizzaz-widgets", + "version": "1.0.0", + "type": "module", + "private": true, + "scripts": { + "dev": "next dev -p 3001 --port 3001", + "build": "next build", + "start": "next start -p 3001" + }, + "dependencies": { + "next": "^14.2.5", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "@nitrostack/widgets": "^1", + "@modelcontextprotocol/ext-apps": ">=0.1.0", + "mapbox-gl": "^3.0.1", + "framer-motion": "^10.16.16", + "lucide-react": "^0.294.0" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "@types/mapbox-gl": "^3.0.0", + "typescript": "^5", + "tailwindcss": "^3.4.0", + "postcss": "^8.4.32", + "autoprefixer": "^10.4.16" + } +} \ No newline at end of file diff --git a/nitrostack/templates/pizzaz/src/widgets/tsconfig.json b/nitrostack/templates/pizzaz/src/widgets/tsconfig.json new file mode 100644 index 0000000..2c0ad66 --- /dev/null +++ b/nitrostack/templates/pizzaz/src/widgets/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} + diff --git a/nitrostack/templates/pizzaz/src/widgets/widget-manifest.json b/nitrostack/templates/pizzaz/src/widgets/widget-manifest.json new file mode 100644 index 0000000..b7f81be --- /dev/null +++ b/nitrostack/templates/pizzaz/src/widgets/widget-manifest.json @@ -0,0 +1,253 @@ +{ + "version": "1.0.0", + "widgets": [ + { + "uri": "/pizza-map", + "name": "Pizza Map", + "description": "Interactive map showing pizza shop locations", + "examples": [ + { + "name": "All Shops", + "description": "Display all pizza shops on the map", + "data": { + "shops": [ + { + "id": "tonys-pizza", + "name": "Tony's New York Pizza", + "description": "Authentic New York-style pizza with a crispy thin crust", + "address": "1570 Stockton St, San Francisco, CA 94133", + "coords": [ + -122.4106, + 37.8006 + ], + "rating": 4.5, + "reviews": 1250, + "priceLevel": 2, + "cuisine": [ + "Italian", + "Pizza", + "New York Style" + ], + "hours": { + "open": "11:00 AM", + "close": "10:00 PM" + }, + "phone": "(415) 835-9888", + "website": "https://tonyspizzasf.com", + "image": "https://images.unsplash.com/photo-1513104890138-7c749659a591", + "specialties": [ + "Margherita", + "Pepperoni", + "White Pizza" + ], + "openNow": true + }, + { + "id": "bella-napoli", + "name": "Bella Napoli", + "description": "Traditional Neapolitan pizza baked in a wood-fired oven", + "address": "3854 Geary Blvd, San Francisco, CA 94118", + "coords": [ + -122.4603, + 37.7808 + ], + "rating": 4.7, + "reviews": 890, + "priceLevel": 3, + "cuisine": [ + "Italian", + "Pizza", + "Neapolitan" + ], + "hours": { + "open": "12:00 PM", + "close": "9:00 PM" + }, + "phone": "(415) 221-0305", + "image": "https://images.unsplash.com/photo-1574071318508-1cdbab80d002", + "specialties": [ + "Marinara", + "Quattro Formaggi", + "Prosciutto e Funghi" + ], + "openNow": true + } + ], + "filter": "all", + "totalShops": 2 + } + } + ], + "tags": [ + "map", + "location", + "interactive" + ] + }, + { + "uri": "/pizza-list", + "name": "Pizza List", + "description": "List view of pizza shops with filtering and sorting", + "examples": [ + { + "name": "All Shops List", + "description": "Display all shops in a list", + "data": { + "shops": [ + { + "id": "tonys-pizza", + "name": "Tony's New York Pizza", + "description": "Authentic New York-style pizza with a crispy thin crust", + "address": "1570 Stockton St, San Francisco, CA 94133", + "coords": [ + -122.4106, + 37.8006 + ], + "rating": 4.5, + "reviews": 1250, + "priceLevel": 2, + "cuisine": [ + "Italian", + "Pizza", + "New York Style" + ], + "hours": { + "open": "11:00 AM", + "close": "10:00 PM" + }, + "phone": "(415) 835-9888", + "website": "https://tonyspizzasf.com", + "image": "https://images.unsplash.com/photo-1513104890138-7c749659a591", + "specialties": [ + "Margherita", + "Pepperoni", + "White Pizza" + ], + "openNow": true + }, + { + "id": "bella-napoli", + "name": "Bella Napoli", + "description": "Traditional Neapolitan pizza baked in a wood-fired oven", + "address": "3854 Geary Blvd, San Francisco, CA 94118", + "coords": [ + -122.4603, + 37.7808 + ], + "rating": 4.7, + "reviews": 890, + "priceLevel": 3, + "cuisine": [ + "Italian", + "Pizza", + "Neapolitan" + ], + "hours": { + "open": "12:00 PM", + "close": "9:00 PM" + }, + "phone": "(415) 221-0305", + "image": "https://images.unsplash.com/photo-1574071318508-1cdbab80d002", + "specialties": [ + "Marinara", + "Quattro Formaggi", + "Prosciutto e Funghi" + ], + "openNow": true + } + ], + "filters": {}, + "totalShops": 2 + } + } + ], + "tags": [ + "list", + "filter", + "sort" + ] + }, + { + "uri": "/pizza-shop", + "name": "Pizza Shop Details", + "description": "Detailed information about a specific pizza shop", + "examples": [ + { + "name": "Shop Detail", + "description": "Show details for a specific shop", + "data": { + "shop": { + "id": "tonys-pizza", + "name": "Tony's New York Pizza", + "description": "Authentic New York-style pizza with a crispy thin crust", + "address": "1570 Stockton St, San Francisco, CA 94133", + "coords": [ + -122.4106, + 37.8006 + ], + "rating": 4.5, + "reviews": 1250, + "priceLevel": 2, + "cuisine": [ + "Italian", + "Pizza", + "New York Style" + ], + "hours": { + "open": "11:00 AM", + "close": "10:00 PM" + }, + "phone": "(415) 835-9888", + "website": "https://tonyspizzasf.com", + "image": "https://images.unsplash.com/photo-1513104890138-7c749659a591", + "specialties": [ + "Margherita", + "Pepperoni", + "White Pizza" + ], + "openNow": true + }, + "relatedShops": [ + { + "id": "bella-napoli", + "name": "Bella Napoli", + "description": "Traditional Neapolitan pizza baked in a wood-fired oven", + "address": "3854 Geary Blvd, San Francisco, CA 94118", + "coords": [ + -122.4603, + 37.7808 + ], + "rating": 4.7, + "reviews": 890, + "priceLevel": 3, + "cuisine": [ + "Italian", + "Pizza", + "Neapolitan" + ], + "hours": { + "open": "12:00 PM", + "close": "9:00 PM" + }, + "phone": "(415) 221-0305", + "image": "https://images.unsplash.com/photo-1574071318508-1cdbab80d002", + "specialties": [ + "Marinara", + "Quattro Formaggi", + "Prosciutto e Funghi" + ], + "openNow": true + } + ] + } + } + ], + "tags": [ + "detail", + "info", + "contact" + ] + } + ], + "generatedAt": "2025-12-03T00:00:00.000Z" +} \ No newline at end of file diff --git a/scratch/test-food/.env b/nitrostack/templates/starter/.env similarity index 100% rename from scratch/test-food/.env rename to nitrostack/templates/starter/.env diff --git a/nitrostack/templates/starter/app_module.py b/nitrostack/templates/starter/app_module.py new file mode 100644 index 0000000..029a7a7 --- /dev/null +++ b/nitrostack/templates/starter/app_module.py @@ -0,0 +1,14 @@ +from nitrostack import module, ConfigModule +from modules.calculator.calculator_module import CalculatorModule +from health.system_health import SystemHealthCheck + +@module( + name="app", + imports=[ + ConfigModule.for_root(env_file_path=".env", defaults={"PORT": "8000"}), + CalculatorModule + ], + providers=[SystemHealthCheck] +) +class AppModule: + pass diff --git a/nitrostack/templates/starter/health/system_health.py b/nitrostack/templates/starter/health/system_health.py new file mode 100644 index 0000000..1e1a3bb --- /dev/null +++ b/nitrostack/templates/starter/health/system_health.py @@ -0,0 +1,11 @@ +import time +from nitrostack import health_check + +class SystemHealthCheck: + def __init__(self): + self.start_time = time.time() + + @health_check("system") + def check_system(self) -> bool: + uptime = time.time() - self.start_time + return uptime >= 0 diff --git a/nitrostack/templates/starter/main.py b/nitrostack/templates/starter/main.py new file mode 100644 index 0000000..6de69ef --- /dev/null +++ b/nitrostack/templates/starter/main.py @@ -0,0 +1,10 @@ +import asyncio +from nitrostack import McpApplicationFactory +from app_module import AppModule + +async def main(): + app = await McpApplicationFactory.create(AppModule) + await app.start() + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/nitrostack/templates/starter/modules/calculator/calculator_module.py b/nitrostack/templates/starter/modules/calculator/calculator_module.py new file mode 100644 index 0000000..b908f45 --- /dev/null +++ b/nitrostack/templates/starter/modules/calculator/calculator_module.py @@ -0,0 +1,11 @@ +from nitrostack import module +from modules.calculator.calculator_tools import CalculatorTools + +@module( + name="calculator", + controllers=[CalculatorTools], + providers=[], + exports=[] +) +class CalculatorModule: + pass diff --git a/nitrostack/templates/starter/modules/calculator/calculator_tools.py b/nitrostack/templates/starter/modules/calculator/calculator_tools.py new file mode 100644 index 0000000..abd1143 --- /dev/null +++ b/nitrostack/templates/starter/modules/calculator/calculator_tools.py @@ -0,0 +1,117 @@ +import os +import base64 +from nitrostack import injectable, tool, widget, ExecutionContext +from pydantic import BaseModel, Field +from typing import Literal, Optional + +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 ConvertTemperatureInput(BaseModel): + file_name: str = Field(description="Name of the uploaded file") + file_type: str = Field(description="MIME type of the uploaded file") + file_content: str = Field(description="Base64 encoded file content. Will be injected by system.") + value: Optional[float] = Field(default=None, description="Temperature value to convert") + from_unit: Optional[Literal["C", "F"]] = Field(default=None, description="Unit to convert from (C or F)") + to_unit: Optional[Literal["C", "F"]] = Field(default=None, description="Unit to convert to (C or F)") + +@injectable() +class CalculatorTools: + @tool( + name="calculate", + description="Perform basic arithmetic calculations", + input_schema=CalculateInput + ) + @widget("calculator-result") + async def calculate(self, input: CalculateInput, context: ExecutionContext) -> dict: + context.logger.info(f"Performing calculation: {input.operation} on {input.a} and {input.b}") + + result = 0.0 + symbol = "" + + if input.operation == "add": + result = input.a + input.b + symbol = "+" + elif input.operation == "subtract": + result = input.a - input.b + symbol = "-" + elif input.operation == "multiply": + result = input.a * input.b + symbol = "×" + elif input.operation == "divide": + if input.b == 0: + raise ValueError("Cannot divide by zero") + result = input.a / input.b + symbol = "÷" + + return { + "operation": input.operation, + "a": input.a, + "b": input.b, + "result": result, + "expression": f"{input.a} {symbol} {input.b} = {result}" + } + + @tool( + name="convert_temperature", + description="Convert temperature units based on file content or direct input. Supports Celsius (C) and Fahrenheit (F).", + input_schema=ConvertTemperatureInput + ) + async def convert_temperature(self, input: ConvertTemperatureInput, context: ExecutionContext) -> dict: + context.logger.info(f"Processing temperature file: {input.file_name}") + + uploads_dir = os.path.join(os.getcwd(), "uploads") + os.makedirs(uploads_dir, exist_ok=True) + file_path = os.path.join(uploads_dir, input.file_name) + + if input.file_content: + try: + content_str = input.file_content + if "," in content_str: + content_str = content_str.split(",", 1)[1] + data = base64.b64decode(content_str) + with open(file_path, "wb") as f: + f.write(data) + context.logger.info(f"Saved file to {file_path}") + except Exception as e: + context.logger.error(f"Failed to save file: {e}") + + file_stats = { + "name": input.file_name, + "type": input.file_type, + "saved_path": file_path, + "status": "saved" + } + + result = None + message = f"Successfully processed and saved file {input.file_name}" + + if input.value is not None and input.from_unit and input.to_unit: + try: + message += f". Converting {input.value}°{input.from_unit} to {input.to_unit}" + if input.from_unit == input.to_unit: + result = input.value + elif input.from_unit == "C" and input.to_unit == "F": + result = (input.value * 9 / 5) + 32 + elif input.from_unit == "F" and input.to_unit == "C": + result = (input.value - 32) * 5 / 9 + else: + raise ValueError("Unsupported unit conversion") + + if result is not None: + result = round(result, 2) + message += f". Result: {result}°{input.to_unit}" + except Exception as e: + message += f". Conversion failed: {e}" + else: + message += ". No valid conversion parameters detected from manual input or file extraction." + + return { + "status": "success", + "message": message, + "file_info": file_stats, + "conversion_result": {"value": result, "unit": input.to_unit} if result is not None else None, + "original_value": {"value": input.value, "unit": input.from_unit} if input.value is not None else None + } diff --git a/nitrostack.egg-info/top_level.txt b/nitrostack/templates/starter/requirements.txt similarity index 100% rename from nitrostack.egg-info/top_level.txt rename to nitrostack/templates/starter/requirements.txt diff --git a/nitrostack/templates/starter/src/widgets/app/calculator-result/page.tsx b/nitrostack/templates/starter/src/widgets/app/calculator-result/page.tsx new file mode 100644 index 0000000..6d9859e --- /dev/null +++ b/nitrostack/templates/starter/src/widgets/app/calculator-result/page.tsx @@ -0,0 +1,180 @@ +'use client'; + +import { useTheme, useWidgetState, useWidgetSDK } from '@nitrostack/widgets'; + +/** + * Example widget demonstrating NitroStack Widget SDK + * This widget is fully compatible with OpenAI ChatGPT + */ + +interface CalculatorData { + operation: string; + a: number; + b: number; + result: number; + expression: string; +} + +export default function CalculatorResult() { + // Use Widget SDK hooks + const theme = useTheme(); + const { getToolOutput } = useWidgetSDK(); + const [state, setState] = useWidgetState<{ viewMode: 'compact' | 'detailed' }>(() => ({ + viewMode: 'detailed' + })); + + // Access tool output from Widget SDK + const data = getToolOutput(); + + if (!data) { + return ( +
+ Loading... +
+ ); + } + + const getOperationColor = (op: string) => { + const colors: Record = { + add: '#10b981', + subtract: '#f59e0b', + multiply: '#3b82f6', + divide: '#8b5cf6' + }; + return colors[op] || '#6b7280'; + }; + + const getOperationIcon = (op: string) => { + const icons: Record = { + add: '➕', + subtract: '➖', + multiply: '✖️', + divide: '➗' + }; + return icons[op] || '🔢'; + }; + + const isDark = theme === 'dark'; + const bgColor = isDark ? '#1a1a1a' : '#ffffff'; + const textColor = isDark ? '#ffffff' : '#000000'; + const mutedColor = isDark ? 'rgba(255,255,255,0.6)' : 'rgba(0,0,0,0.6)'; + + return ( +
+
+
+ + {getOperationIcon(data.operation)} + +
+

+ Calculator Result +

+

+ {data.operation.charAt(0).toUpperCase() + data.operation.slice(1)} +

+
+
+ + {/* View mode toggle */} + +
+ +
+
+ {data.expression} +
+ + {state?.viewMode === 'detailed' && ( +
+
+
First
+
{data.a}
+
+
+
Second
+
{data.b}
+
+
+
Result
+
+ {data.result} +
+
+
+ )} +
+ +
+ ✨ NitroStack Calculator + + Theme: {theme || 'light'} | Mode: {state?.viewMode || 'detailed'} + +
+
+ ); +} diff --git a/nitrostack/templates/starter/src/widgets/app/layout.tsx b/nitrostack/templates/starter/src/widgets/app/layout.tsx new file mode 100644 index 0000000..40b03ef --- /dev/null +++ b/nitrostack/templates/starter/src/widgets/app/layout.tsx @@ -0,0 +1,18 @@ +'use client'; + +import { WidgetLayout } from '@nitrostack/widgets'; + + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + {children} + + + ); +} diff --git a/nitrostack/templates/starter/src/widgets/next.config.js b/nitrostack/templates/starter/src/widgets/next.config.js new file mode 100644 index 0000000..f35620c --- /dev/null +++ b/nitrostack/templates/starter/src/widgets/next.config.js @@ -0,0 +1,45 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + transpilePackages: ['nitrostack'], + + // Static export for production builds + ...(process.env.NODE_ENV === 'production' && { + output: 'export', + distDir: 'out', + images: { + unoptimized: true, + }, + }), + + // Development optimizations to prevent cache corruption + ...(process.env.NODE_ENV === 'development' && { + // Use memory cache instead of filesystem cache in dev to avoid stale chunks + webpack: (config, { isServer }) => { + // Disable persistent caching in development to prevent chunk reference errors + if (config.cache && config.cache.type === 'filesystem') { + config.cache = { + type: 'memory', + }; + } + + // Improve cache busting for new files + if (!isServer) { + config.cache = false; // Disable cache completely on client in dev + } + + return config; + }, + + // Disable build activity indicator which can cause issues + devIndicators: { + buildActivity: false, + buildActivityPosition: 'bottom-right', + }, + + // Faster dev server + compress: false, + }), +}; + +export default nextConfig; diff --git a/nitrostack/templates/starter/src/widgets/package.json b/nitrostack/templates/starter/src/widgets/package.json new file mode 100644 index 0000000..3f40ae3 --- /dev/null +++ b/nitrostack/templates/starter/src/widgets/package.json @@ -0,0 +1,25 @@ +{ + "name": "calculator-widgets", + "version": "1.0.0", + "type": "module", + "private": true, + "scripts": { + "dev": "next dev -p 3001 --port 3001", + "build": "next build", + "start": "next start -p 3001" + }, + "dependencies": { + "next": "^14.2.5", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "@nitrostack/widgets": "^1", + "@modelcontextprotocol/ext-apps": ">=0.1.0" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "typescript": "^5" + } +} + diff --git a/nitrostack/templates/starter/src/widgets/tsconfig.json b/nitrostack/templates/starter/src/widgets/tsconfig.json new file mode 100644 index 0000000..2c0ad66 --- /dev/null +++ b/nitrostack/templates/starter/src/widgets/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} + diff --git a/nitrostack/templates/starter/src/widgets/widget-manifest.json b/nitrostack/templates/starter/src/widgets/widget-manifest.json new file mode 100644 index 0000000..1fc23e5 --- /dev/null +++ b/nitrostack/templates/starter/src/widgets/widget-manifest.json @@ -0,0 +1,48 @@ +{ + "version": "1.0.0", + "widgets": [ + { + "uri": "/calculator-result", + "name": "Calculator Result", + "description": "Displays the result of a calculation with operation details", + "examples": [ + { + "name": "Addition Example", + "description": "Shows the result of adding 5 + 3", + "data": { + "operation": "add", + "a": 5, + "b": 3, + "result": 8, + "expression": "5 + 3 = 8" + } + }, + { + "name": "Multiplication Example", + "description": "Shows the result of multiplying 6 × 7", + "data": { + "operation": "multiply", + "a": 6, + "b": 7, + "result": 42, + "expression": "6 × 7 = 42" + } + }, + { + "name": "Division Example", + "description": "Shows the result of dividing 20 ÷ 4", + "data": { + "operation": "divide", + "a": 20, + "b": 4, + "result": 5, + "expression": "20 ÷ 4 = 5" + } + } + ], + "tags": ["calculator", "math", "result"] + } + ], + "generatedAt": "2025-01-01T00:00:00.000Z" +} + diff --git a/nitrostack/testing/__pycache__/__init__.cpython-312.pyc b/nitrostack/testing/__pycache__/__init__.cpython-312.pyc index ee92482b69ba68f377379f21bc537173e5693466..b3e6d2d16a29ff9328fa652f1927de2445da94f0 100644 GIT binary patch delta 1531 zcmaJ>TWlLe6rFv{uH)VHBkMfvq)wbRar0=&qd_z!O4b!p2`y@P2q_R;+e+gYrtp$7gQ6$$iVPUorSY2lM1b+iGxj5HA4$bgJ6+N6TlQ&%W zFWB?~zseMM;@nIG0MuvlrEF+;ixH9yOPOIjnhS;tb|z);@EX-)WVM}X~T$;jTr*Q!oUtA;{;tG!IN+_%zHpve}el4eZy z;W?J8Dih*NKEtK!56q80Fh3c;?5u;F$)T61KIWgU=mW;tjBua|#$n}5g-qR7{?fk9 z2><#Hj0=?tnN-y7XO>w;Fom~jZLP>uD?Aa4|36}3ZLzEqu>{J66|wx`oI8Z58ZAdj z0JOkF;^$-Y`6xK&Kj!`&Hz!HhfCTA}s zEu}0qTV$x0X6)-LSX_t-8x0b8BY1WicgW46o#SNiQ}XHIz>yXG#_3$3_e*`f@79&u zEjP`@&|>uCBj2eDBGuc}vXPV8^8VI$$L=Y<#pjojIVDWo9e?MxzZ0*?X|WHuPwwsZ z5Y06XI3xAUhRj4DzoCq_5ofd46A{4{%M-pON|73FtD2?U$539w5sr_+TJ|V?C~o3C zs21M{rSe7)eR22^p#jP!pY-5!F2AUK!M(g@1AdBJK2V zcTlifZg=}6al`PTYg_E>qa>t50(RGmO&h|E?pEPR8kwi}biQ}n*PHEMZ@BBzc3j#5 zcmIF8w<`cyT^w9Glat!>YU7R?+Ezn9%b~oxdcj$619yb^Q&-99Z;wc$D!r7%Q6rS1H_1fNDuVXuD{u*0OqTrB!+e#Y(mF=`rC0bEjeh#Uk;9aV?#!+V% ziqKjWaA`%6Qs&SgAzEl9imFNmE)lmRQaOMGh#(>h32sO|StOTo;>|j1S}~Hp_q~}n zZ|v{A=O2b2Btze8S`fjCbf?Nk)vrP?linkRxy6?(lUj@Cm*2FP7f+j)60fgT43mCy zm3)V>&CW<)uo*p$bFAOjcQWk|gY-a-nrQ1h7hjHOi1&mRr%6XFhg@M%#5TX)Bj`WfIDOTA{c{V6kRxGHf}M>9xvg=|aPAn{V4oZ!K5o(~!WD0oW;jM)(r3F>P{u`sdhmeP|=_ODz9e zD1U|Q`;gkTR{A8fqa^MW??!LGTF0L+?#2$Syy4CXuwG3>)YKYj=Te!#N9X9tNR)JV14*1VtR z2nK1(raD47#Qq5OT<&O}6HgvPys&@}eF4N5TjCo+O?oVoXh`i$8WbSn<@nFmq$l1M z4PX0)3O8b2n-Fm&7UftM_%EfrYUs|o&X4SSR zZ;^(4;L<_xQX#garY?^HrD+Z+4jB&o*sUZT<7^xtB~$+G=ro53fQEXIY{~5l&GX!b zpi?}o-3y7n-UO66DIu9CSw{PI;^S^7cwc`5d zjy4SL&H0b#SzW8jlSqH&(f%kjNAIa%A&oaxTu6wUTJ9J)+giAgMB9f&&XY=EifoSy zoKIrTrwH&8ffv0c0(l=kdw(K!E%f!w6P-cI?$~;QabIuQ2#U=t%KkJ%}(mwLD*~kj~4Sg2r Q*H2;`{Tp$lhv1EW0j!4$EC2ui diff --git a/nitrostack/transports/__pycache__/stdio.cpython-312.pyc b/nitrostack/transports/__pycache__/stdio.cpython-312.pyc deleted file mode 100644 index 0c6fa0471d78b439896409b7215c05a9323d33f4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2181 zcmbVNO=ufO6rR~1X(j*FiQ^x+4NF`@w%DSO9NbWv1|I?`D5a*93c<40j+|B3yXx$y zwu~A~(IKS?bxNsy^s&M1y|>bHFE&YVS-TMErM<~fprQ28H=|uiu3Z8h*f;a$y?Ha= zH*enlJTQ<((0-gS$|DLwzi}mQpo`YBq1!|@vayTu?H=bb=#ncf%Xt|i30+6F+(5Rn zBefUh6@tqmc3Y37trFQzOn_RF>|{gPi46Hf4IA1YT+10aOkF1)p%(LLF>(-H=SWvT z-9!ZOW8@_p=Ve=JpuA$sM70%?C@HquQ1VGT%{R$wwnkLgFa_gOEn)P9!wXi4EHK-z zvJa?LsSvuhmDZs}nqKm$9@K(tAbfOo%VIhuwnIsg1-j+BIwPx0U!e{ofzJHcnNT_- z0xedVUZEr)^b>IChRQBhOC>_Z28)ho(VBi^;bw03y?5tzMlCO>_>}1-YAur>DZUi#jg%`bP8Mfn%2%dVq6=6sAm{I!my<8z$NZf=MMHZYdm! zEoK{gz(eZ#Uvu+U3hx7)ps-|BnN`S<6#(Jdgcdx9Q9oc-@uNZqLQsI^j-Rd6Ld`TC za52rb%n91Fv5hB7FbZNF{W5X!j&gVK$>{X{=*-^e%pK)kUyCCM729G~n4pAJsdtPQ z&WYgVD;R7d#@$6bjghh%Tf2Q7??f!&7p+cqSH+_^k6~>pp6f*2ot~z z(V^D&W%~2DL& z0}F@&-&SvfW zJxO75i7<;XN;L=(-^w_Qdv7J|GtI~ka{HB&x6@-D<7+ty0Apx$gKgfve|wuf9GcnH zW&{q{8XUxRoD;TPu6r-<=zqSaCS0=uubTe(v3|M^DR#2D3k z-e=cd!&CoZSwXAvhiC7thP)Mv6vidE+LUN&d6GhDoC_?-xbA<6pyQ%{nIBYX#5QXSvy6iGS_UK#Fr3B>Q{ z`8s-*Kx3KB^8NDq&BNgf+gBeBUuw)ZZZzSdrVq4%C+A+hE8j~$-!OUicffz8pG{xW z@VCkw`mP@aKMY(`<)2g;hDK66zA#;ELw23TFu`gSKt%Zi5oZktBWuKmI1|0V&AdQ} z+zGWe+j;mFHDHcEsNgDyri?K@K_A7}P(rYR-y^p>-G Vac=MY8&44^Eg4CZM|@gr@DCM**2Dk+ diff --git a/pyproject.toml b/pyproject.toml index aa67535..55d134e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "nitrostack" -version = "0.3.1" +version = "0.3.2" description = "NitroStack Python SDK — A Python-idiomatic port of the NitroStack MCP framework" readme = "README.md" requires-python = ">=3.10" @@ -24,7 +24,7 @@ nitrostack-studio = "nitrostack.studio:start_server_cli" include = ["nitrostack*"] [tool.setuptools.package-data] -nitrostack = ["static/*"] +nitrostack = ["static/*", "templates/**/*"] [tool.ruff] line-length = 100 diff --git a/scratch/test-flight/__pycache__/app_module.cpython-312.pyc b/scratch/test-flight/__pycache__/app_module.cpython-312.pyc deleted file mode 100644 index c4a1ca63b1baf887fee3aac05bb22d24ec2c8e92..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 956 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 A8vp dict: - return self.flights - - def book_flight(self, flight_id: str, passenger_name: str) -> dict: - if flight_id not in self.flights: - return {"status": "error", "message": f"Flight {flight_id} not found."} - - self.booking_counter += 1 - booking_id = f"BKG-{self.booking_counter}" - - self.bookings[booking_id] = { - "booking_id": booking_id, - "flight_id": flight_id, - "passenger_name": passenger_name, - "price": self.flights[flight_id]["price"], - "status": "Confirmed" - } - return self.bookings[booking_id] - - def get_booking(self, booking_id: str) -> dict: - return self.bookings.get(booking_id, {"status": "error", "message": f"Booking {booking_id} not found."}) diff --git a/scratch/test-flight/modules/flight_booking/flight_booking_tools.py b/scratch/test-flight/modules/flight_booking/flight_booking_tools.py deleted file mode 100644 index 2068259..0000000 --- a/scratch/test-flight/modules/flight_booking/flight_booking_tools.py +++ /dev/null @@ -1,49 +0,0 @@ -from nitrostack import injectable, tool, use_guards, OAuthGuard, ExecutionContext -from modules.flight_booking.flight_booking_service import FlightBookingService -from pydantic import BaseModel, Field - -class SearchFlightsInput(BaseModel): - pass - -class BookFlightInput(BaseModel): - flight_id: str = Field(description="The flight identifier (e.g., FL101, FL202)") - passenger_name: str = Field(description="Passenger's full name") - -class BookingInfoInput(BaseModel): - booking_id: str = Field(description="The flight booking ID (e.g., BKG-5001)") - -@injectable(deps=[FlightBookingService]) -class FlightBookingController: - def __init__(self, service: FlightBookingService): - self.service = service - - @tool( - name="search_flights", - description="Search for available flights and schedules", - input_schema=SearchFlightsInput - ) - async def search_flights(self, input: SearchFlightsInput, context: ExecutionContext) -> dict: - context.logger.info("Searching available flights...") - return {"flights": self.service.list_flights()} - - @tool( - name="book_flight", - description="Book a flight ticket. Requires OAuth authentication.", - input_schema=BookFlightInput - ) - @use_guards(OAuthGuard) - async def book_flight(self, input: BookFlightInput, context: ExecutionContext) -> dict: - # OAuthGuard validates access token and populates context.auth - user_id = getattr(context.auth, "subject", "unknown-user") - context.logger.info(f"Booking flight {input.flight_id} for user {user_id}") - return self.service.book_flight(input.flight_id, input.passenger_name) - - @tool( - name="get_booking_details", - description="Get booking receipt and details. Requires OAuth authentication.", - input_schema=BookingInfoInput - ) - @use_guards(OAuthGuard) - async def get_booking_details(self, input: BookingInfoInput, context: ExecutionContext) -> dict: - context.logger.info(f"Retrieving details for booking {input.booking_id}") - return self.service.get_booking(input.booking_id) diff --git a/scratch/test-food/__pycache__/app_module.cpython-312.pyc b/scratch/test-food/__pycache__/app_module.cpython-312.pyc deleted file mode 100644 index ae7ed59ce0f26f1733a8ce25be2ec96974f654e6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 585 zcmY*VK}+L66n-;lo2ItiTG->FN3*crU1dQO#e+QvUP539nX%j5q=_?WwRrVE_#34D z6E7aafCt%=w@McTPrgaw!54V*ee)*Y_ujkjbXtJ1akc%25P*M%e1u(rQ2JtsqU^x~`3bSZI3`r-{szUI~+NCX*-QXne2pdcYpv1|L#rDG+}zULXeH#7S+2)ba-;m5mgJ|TebmYg520qa)= zoB;)jIOHfo$YRWK?nF+79p1{_$VGsGA=(-ObB2hy_4mb{yR{OA$3WYsK)pGx|I8bU zs6Fe1{twGSq}#cm6V42}^hgN$knwEF_b(MHtkfGSx0r&J#(D_g9uTG@C=1m?_G3% zR&_fF9o-?s8WN(rgw#VA+#}@OB;j@DGpw!ZpO?pX;up%8$HQb&B=LZ~KR6NGv=ggT zCPn%>E|@BA+lk^@O2r%FZL-GhXgrgF(XWDCyRxr%3acG(eYtm@8HAqhjxw`ORV3+K z>6nBL8=IpIn@vUvA-Iw)TYu|+>dlDuX{7C??rhhhs*`=I(z#d5+?SUPwM$nIOjtPx zp(R{h!qX)@`G&mGzf$)4gKOdfU*SmR25r E0UX`Zc>n+a diff --git a/scratch/test-food/modules/food_delivery/__pycache__/food_delivery_service.cpython-312.pyc b/scratch/test-food/modules/food_delivery/__pycache__/food_delivery_service.cpython-312.pyc deleted file mode 100644 index 319e8135a712098d3f9f65019c81e61fbe80a5fd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2293 zcmaJ?OKcNI7@mE4Z6}RI?2so}(}Z9u0zxP)EuxW{^0=fFTB)%ttrpM3+0?Pq*)?Ft z5gc+zq)GwxkW>VfV-TpykwZ^CRqDm=Ls+X;0`=0|4CzDS)c@bLlYm0!@IPk$`S*W) zvp=@Bh6t>~bMH-mrx5Z7D$NDDx3*3LTOpKCAxmb2q#zKHFd;)8iVpl_Y zQ;8{5W-3)!U{a;(f}GT70QOLqsRnwm7)*rR_Otms9b;MRD&xh=j9<0VEZvBA`(qMF zTNi;qegYM#v?fxSDjrv9z~dSXdOWlymx@8G3hBYkWqnp@;%s+=y%isUqH@btoWuBiD7ekClQzbz zzes-YZt7DT%u*S%;Fzg#c1@qmXW?SXPIJ>qPo*4YJ4Yv>im91AEo7OULdBqsn!c@%!?1FeV;H60J(@7wJjFEaGeB;Vx=QrHjT6<_NKGFA-Z4_^e{&;Vjh(FZ zqaGW17F1%PMfuBMy@l-W0wG-Mi!aK*YTaAlfxOgN%+Tv>HwY`xr)e~KWnG4W98U;* zAMEoO5Wg^dKZvfDVUB^t+&hEjX#}se0J%wOZSBj&rD8?vYOVkXUYuhPF~liMZ_iWHgpekZ=B2U(nQYJ4j>T1h2XT7K!W}^2U5UzT z;$_JCx-c#8ST!Z8lv<%);?A^E75wgDd57E;=V9je`es?eENK8~Su8;-=av7p)X;KX zEz6LF=wRmFDl0VDd^2dMtkUorMx!;)cX(OGcj$Fz=p8!RGVUsj^E@x97oh(7P^oiB z&*dE$?Lv;~R!(=On2wDdzAw6}?U+u%b^|law#^LN#PXNak1maUc1s5w{#G?QvKR!=cg1Vb1GP^5w;ugoY+bnXeyt;3J23RDH4q1|FNc@Hx6W6! zzIud2qRVqjbF1%E+XgDyfQK=THA(LV1XjfZ)OiqI2mI;X5Cl71W#Lr-HHQ+0N*U)m z*uY@myQ-JMI}Z8Z%{cr3;T6uAFn@*{G7SF( z2f4*C;7!WFF@1iuWPhuWk+ z=jh)5*xM)}x*HGkX+M)Q{1ogVUTv&&T@nQ0cM^Xpgm3NpMTq_@4hoh~CrGy>BF3J1 I8=j;80KxPt761SM diff --git a/scratch/test-food/modules/food_delivery/__pycache__/food_delivery_tools.cpython-312.pyc b/scratch/test-food/modules/food_delivery/__pycache__/food_delivery_tools.cpython-312.pyc deleted file mode 100644 index 2faf15708b4698e68f2bde7a9da2837ec3154156..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3571 zcmb_eO>7&-6`olx$t6XKv@NBw<0u<9md!?{rMf?kA{9|AyH;(}t{poLt$MNM4&`N* zq&mB_MOHwKLf$I!S-3}R4=6lsc2nJ+C>oD6)3qG=_)$TZE1YFe>uR4i=AH0`Gq z-STJFNE0{y*X3hV3m0w1?S)0X;^+$*c2mulEram|JI8e=cYVQOwsR;CAHCpjWiMby zgD({!boVrj+fFZ4IZ`YPGHRn)B@J2OiS~F`fHI6?X`TeL9pFVElm!g>P->Hrm06I5 zjKDPbi82`Tkt+~F&vrZ$qP6tBT9 z<-FL|-r5)V2^%c0S1gAQfv2Yc2PMaJs+m-T55NdVN_iZOf@TD?ODUeKJYiHZw4))7nenGxS4!irV)|1+;x!d1-ENfp3?Y{StJ9(-; zbo$oG+qt@Y+TQ@zc2UtfHKh41nE7w1sLH9JmlQZ(fDf+NKH7&7?fE&x#etR&HDK;= zJ@-x<^RwsYj-Nj?IWaMr;=ABKJw^Cwrjbd7`(iW-UVJy2J!pE+cmc=IAhI^b2WSlK zcE{eT4_)?SzwF13PH-e;gDu%z7=S8?(!3AVHU=BLuR3e!M4&E zAxu6NHWa4hLq=p(-ckzCAn=bG11#F*AMNmub@-zW1&z2d$YNdou@3)4hkv}9OzrTV z>l7E%9A368#=S5caTJ`#LRW|;CeTy?m`sZDeb9y^mThL`MaRZv zs>(6(P-)DP>1bN*#s2@CZaeG%cRK>+7HNEA^tSxVn4rLm+yM2`iXdOL@1c;1Ji)H2 zmkiZ1ZAaB_=%$6Lx_xeKF}uVeBpU=zMp5(6SIk5e)a=}KR@A+q=y~CsFTY-hGpE8! zxpt1j_Xr5ygSxp4<`kg_g;cJ&s)tWPCTbST3l2%BeEo2>Q3+-&`7;EQ+NiUG! z25!<+AX6JX!9Z`#(xM+{TVPd0Bb`orGB#4O7qZHWpiZDarF>a%;wg*F5g@JwI39nV zLW2j`#*bE8eDN{p)YNBCwZ#Mqm3;)v-^f32k&PIMBtIB@fAER&{JQeIyLY;-%ruhE z{Yw5#?ANg;BZt;U4m}<@>?S5$W#Vfa0eJW56lmoVO0Oo)a_tqN!>ZmBnlA_t1GYWj7qV|*m%63obzgz!*Z(mm1 z%`sE|CK!in`<_8?2O+3@_Bk+pDIT5p zbolep&qm#e*B?obzWb-CKTbUwclVyHE9V-CT_3Exzw#t;U_Ei*e(;m%TJ+Q7pJzVH zJWd>Qm1AGq2}s zabnt4rrWFz&L-hf80@{15i`CY4RYka{P4jC7ps|bgy{HB6p^&zdd3KP@9mdx3=KYP z>_=c$$%agWFS_cHCV~2Ch=ihz_()^ubYpnmW>^k|Hlk8+^4?E22vqLO8NWV%>F?NY z4zwMfDq`WD#?WkI_~osIbM)TNam5^UXMfykT*~6wIl8rYj&84>Zw&9>T0P?)_&zS0 zao;%a*Kb|HwKHPrQ~0nM|CNCuWm0i33O6M$1F5-pcmT$Hv; dict: - item_lower = item.lower() - if item_lower not in self.menu: - return {"status": "error", "message": f"Item '{item}' not found in the menu."} - - self.order_counter += 1 - order_id = f"ORDER-{self.order_counter}" - - price = self.menu[item_lower]["price"] * quantity - prep_time = self.menu[item_lower]["prep_time"] - - self.orders[order_id] = { - "order_id": order_id, - "item": item_lower, - "quantity": quantity, - "total_price": round(price, 2), - "status": "Preparing", - "time_remaining": prep_time - } - return self.orders[order_id] - - def get_order_status(self, order_id: str) -> dict: - return self.orders.get(order_id, {"status": "error", "message": f"Order {order_id} not found."}) diff --git a/scratch/test-food/modules/food_delivery/food_delivery_tools.py b/scratch/test-food/modules/food_delivery/food_delivery_tools.py deleted file mode 100644 index 46efa3f..0000000 --- a/scratch/test-food/modules/food_delivery/food_delivery_tools.py +++ /dev/null @@ -1,45 +0,0 @@ -from nitrostack import injectable, tool, ExecutionContext -from modules.food_delivery.food_delivery_service import FoodDeliveryService -from pydantic import BaseModel, Field - -class ViewMenuInput(BaseModel): - pass - -class PlaceOrderInput(BaseModel): - item: str = Field(description="The food item to order (e.g., pizza, burger, salad, sushi)") - quantity: int = Field(default=1, description="Number of items to order") - -class OrderStatusInput(BaseModel): - order_id: str = Field(description="The ID of the order to track (e.g., ORDER-1001)") - -@injectable(deps=[FoodDeliveryService]) -class FoodDeliveryController: - def __init__(self, service: FoodDeliveryService): - self.service = service - - @tool( - name="view_menu", - description="View the menu and list available food items and prices", - input_schema=ViewMenuInput - ) - async def view_menu(self, input: ViewMenuInput, context: ExecutionContext) -> dict: - context.logger.info("Fetching food delivery menu...") - return {"menu": self.service.get_menu()} - - @tool( - name="place_order", - description="Place a food delivery order for a menu item", - input_schema=PlaceOrderInput - ) - async def place_order(self, input: PlaceOrderInput, context: ExecutionContext) -> dict: - context.logger.info(f"Placing order for {input.quantity}x {input.item}") - return self.service.place_order(input.item, input.quantity) - - @tool( - name="track_order", - description="Track the status of an existing food delivery order", - input_schema=OrderStatusInput - ) - async def track_order(self, input: OrderStatusInput, context: ExecutionContext) -> dict: - context.logger.info(f"Tracking order status for {input.order_id}") - return self.service.get_order_status(input.order_id) diff --git a/scratch/test_mcp_init_direct.py b/scratch/test_mcp_init_direct.py deleted file mode 100644 index 8a919ef..0000000 --- a/scratch/test_mcp_init_direct.py +++ /dev/null @@ -1,13 +0,0 @@ -from mcp.server.lowlevel.server import Server -import mcp.types as types - -server = Server("test") - -async def my_handler(notification): - print("Handshake completed!") - -server.notification_handlers[types.InitializedNotification] = my_handler - -print("Registered notification handlers:") -for k, v in server.notification_handlers.items(): - print(k, v) diff --git a/test_custom_file.log b/test_custom_file.log deleted file mode 100644 index e69de29..0000000 diff --git a/tests/__pycache__/test_basic.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_basic.cpython-312-pytest-9.0.2.pyc index 0e0f14c1d4defac61764977eaaadab1fc5ffc5c7..51d1b21acd612f09e8a23d9f8ce1d0451bc20548 100644 GIT binary patch delta 50 zcmZpTnGmz#in6MOvsFxJacWUe6YKy0 delta 39 ucmbOb(;Bnkin5fhvsFxJacWUeV84o7_ diff --git a/tests/__pycache__/test_basic.cpython-312.pyc b/tests/__pycache__/test_basic.cpython-312.pyc deleted file mode 100644 index bf324f285d750fa24d23df907c04ecf35f25576a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4072 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 literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_oauth.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_oauth.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a575ff8b4c8ada9347e1ba07142fea77acebeca3 GIT binary patch literal 9410 zcmeG?TWlNGm3PP?IebVYMN)6eloi>bYg2F0wq(bS?X-qhY14SKvDVoMJu{L>ksNhq zXj?QHI8CupQ7`KL)Gm+@@Kh$+Qy4)g8kTY z?{FS+q$nx8i(pY=c<(vqo;i+7Wt)5#vKf0QvY;3Zc7* zM?957^AzbFc_-(fP@bN5QTm8`-c3e4^B&SO^9=NkoHy^A_vQWbehLXpw%ZVH_1QiP zHXt4eo*jAj85~x>3+H}|!+>_-95lwQUI^a@pYexx-goGbK{0URH=XOqN9H5>=zKIE zn~xEiE7zItn(xYY&vygv{sEfr;XTmy3Vl4Y{4_!m<3K|1eO)Y~&?>b`CA|NJ@#hl` z%{!M~%y4rex2(BV(h9d!awL-pr{+I*;jCCt(wTxFX`%Bs1g@xL!~&)XHx$kH?K4GX z>6^v0#Dfq&!RAE1m=m-BL0%N3YZ*@9T6#&xK>G%KKW^_p=pxv17bz{z!FS5=nL>)w zxGhT?kkwnhC?12F#j+p!@DAR&Nbxl9T6FO4UwH1fWPyCY!D`5=Af?Tq@eJ=(f(B-A zWY8~shJ*@LM%l2zVfAIxhc_TkZ-N&;b=-CSt;>Zf4x6X`vcneFK*d>hl$}LXq06-V zGbLik%SH{z>iM=Zy+HBp_>KjKVGkcvx(uG3GTHFq8j@o+`Mt(HRu7i7A|ly2`z;Ow zvN#JA=(nuiiPvdJe6- zaoQpn*(rj2l#gX7TNFLJMA2i&{#+CpTNFFX%wthxzH$`d4qFtvo>&wa!-GAeXmUan zyKHm+b%Y~+7DcbES{$%4#ej_5&kh?Dt5*&hurXg1pOP?W*~128^<^L5%lGjC zu%Q2bt4d9wihq|(@)~l@OmepBLBN(t`^tgGGAZztGYRgnWzxVC%cOwe!Je69azZ8z zd>t~W?9U#xB4@)maEdfclMOq zzf|V58CHHSbAqJG>6 zL|Hj;Y;<&VB+sps`aU-_frrF2oLrUs3%ShVl5#>4(tIh41KA;t0jLqC(_#MwanJZZjyp1g;8FoLdsOCNi!DH^jD#o1lqe=}=4iqy(Gu?b?s)+K-de zEr9lKVeMm}G*ck1fEe9p09uAXlIkP?S|mvD)-)vbN7GkH(AeQIsS{KZuZeQdaPd#_ z&y3?X4@+lXEOMM6%Y(aDI6W;%U`Z8`|##5nr@XJFDs`P1S~;oKSPQGRI~Gv zBuZL@#SeR0VIi4w=>l;`3oHsy!3wgHE^vb80sF-q)JZS479Y@SZjmI1)}@ykVxhpZ zBCxa9Gs==i;~dsJtt8UidJrXi_Lu0=T#NWe3C)i)x>dm>{7R?!v17W-aXT`mBf6=R zc4S&>BT|e?B=rzEnAWNhS{qqSNUeuoJG5XPen_zub_xH4B;iv=EoRjyT<%xWt2r^v zOHVy!TFU_4&I!9U$LvN=+mR_fXu1buqxPXu89&4Eoj;!)JA2~FUqX$NuPnlxrmqw- zcyUwG-11S#ktMNkROXkj;AD|W%Mx#sE2~;ROHxSpp3JyjK1htEOQ16VU-{o5N&gl7 z*DdrR?Nxox)alcY+{hJb(6K5VtJ7T{(!mDZU8TF5vC-!KS#{uKv-i-(fZB7cIXw5D zUYE=Ls2#O+{pc@lorfDEL(RzkX2-q{gWW&6*$hV>d0i3rt&3X$)HBfNK3wfSyfIVj zPBl7Gw=NQeBk%UrX%M+zrT4GR+#6Q?`x`XAHg#M_%(jZDI##p5I=%niu*qWrzzinE zhV3SG46dJ2!{cCA%zbMP?COg*dXH3lk8HeB>z!#tXKu|AyT;x>&)p-yvTQ%f}98sZl8B`D5Zvvfq!B5tqWTr6zOV)ztIf#HDiO# zV9$T}-TT}s-2;yMf~J4!bAP?*y`sJYD80OUgFw;^$T?-+3b(2Onhzi z-Z?cGZ!q}U)Nvg#+bX8&Sj__KO#I$Clg9*r*>bI|&YaqEqfpO}U;E)}Dw9~h1kKuO z&;=6>249;xt|Mk!1uAI73^fa^V_u#50LwDjO#qmi!ERmI6z~RztHEKFncC=wMhy;c z^jCvZ4F+GEErO1jZIzkAnlVSs0_y;RW=wrR?la+yejT*8Ai5dE8rQw=w7=bsf%Zmx zsv4gHaIK<3Z?l3B^xLt{FTP^`3VI-X6ff zKx1&aIyepB+G{HG!RZErugw-g$7&eUL9-=d%;EJO9jp_XM8pGT8YG(hCIF(%h@@~t z4poDPR3^1C3ym5)v@u%^rWy>sHd_Q8GutYY!V$q7H4Cf*2u5P+19G1UZ_MhTy#>*q zbRpj>)T3VH@~OUWZ_?k{x=EqPu}`-;QEc|ptvKqM{qz$jehRDJmp|J|qS!Hlze1_r zEB`H5pz44B%oy*Y-kYGfIQ`z_*#m(5wv*z9=-+mo3j*@~!5EitzCS~82i@<_o^=EA zd&e0HMm})HxZ}7A7YE3f@qWkMWbC{dG4%JrDni@TBF{0+$gr zr&KJEW$x$bV|-eBz! zsdr}Io_*KxYtOGdzYhK?_#37+df^`@YeV0uMP8_*@BRxqT}7uqLNkA)ze`asP=8!< bP}C@hqyuZE2hN^0OQzycr%z9s6p6iJDYC`#1Z)XS16$+9F$mgFdA?Z6OrNr3_gdUr{R z2q4R`8qtk1mXo&954S^4)kLbDi8D`m$?^61$W!bS$tMNCx|YJg10*i&+Eq zb8x90W+9nVd}V2uGYb3p7|NA41q(ffvYF-EPo?Hvp2lSNyD$UjSPF}iu?vaXWL;lf zvc9iA+0fUJZ0u`HF6vuE@;r&f$)>)hRXj;>1#>0_O&M4`r4AK z`&K8{^sPy@_qEe@SUseL5VTPJNDE=;Ppe#`grP^ZESJ#N0ewA7VKw!wl^dWJ>*Pl8 zJLE;+uU9(d#TVK+PTdT3tzeX-2P@~qq&cy5b7CEHV(aI`HpoqEu6S;oALqkMeu*>G z8|DrF{-~xLPsvIm?dt3ddyHVukprnAJsKZUR3i`{8dPF>^jtzQDj>N(J^-{M5UZrI z(~3G8k10m=!HY_4M31M2u&{Db54((}9%Wb=l9i#@_$ft8B}n!`RZXczEiFBwMzIJ< zgT4d~#S_Z0)W86=Tys3GY4M=}yAB`_WMx>3nTtyv=0@Wl)qK z*lkoFijE}oaqO0wN+c9DW^PGR9>Z?P68=#6t>9_PrHqFxvFFFvm+s`lbQbVQH7I24C=G(8%-uzq-4Kc5;}ugMo))D=zFh#whNVFqJ3 z?#|(HLySb?P$UvbHfgYgak54x4^%_>1@)0 zui5)bM+)Kw1s1ZOY(HkX3^#}^mN*^TAkMDQkF-ISZa>S>%C0P*;k919lF<#~;7T0u zBgUD8G~~%12R+VmiOX2yi}PtO$7NkQP9SlPk~qO7F5_Y?vFDe7|7ZDeu7%SI`Ba^hWqM*$LJxwDTFcQ2Y+I}2lD|1k0({ttyaJexx*W-_|P3bz0s^siVu;hw}rL97cmV2(r%KpIjpr~ z%9gMe4rC3trmF_8+!54eP!aZHGkROl+xBUdqnkTuH+O_<49~C{2Wfz>f>AM`=)i%2 zVGn^A6IEp(uIY+Oc%1`?@r(F9@rVbSo%EP)-w!n9)>4HBW9-J~{SbB^q-&d4Sw?RID6DGfQZ zWI7^0j3zP_bYDf}c*NdJ>MkHg7OBB04#d@c!Ji9kD+t@~S1&DAx96+dr>fUpJ~AUz z7KO&V&{$ZsrXaM>dOe=?Gh$6qY|4vGg(YhX;<}koeNOCoW27kdd?fbd8hao>!xNb1 z`~lr>?gv{RaxTyM`(kCT>c9`4D~N|@LaTCO$F11y=D+HPKrXauvOgE1Yam*xDNZpq(4OX^xJa zlGT#LgZR#tIN8g1lk*iD(7(3yP668coDy!rR zwBjxdZ9;D|dRxFl5o%6EN>#UE@^tS_G zvAC-s?EZxiDhdsGp`jow`jwZfS#|lytRQ-J-WThO;_|$>d}f(6)3D;#9=AU*D{|Eh zIdM12#N8i>yK{}ZAwa_@6U`V603FE05r5NW0*y_P7!6xKDQx9`_kfi_1NX%4yS$anf(5br{X->`Gz!%bnYfe6 z#9RB;?XTuOs16+3>;7O#&(cHP?tkp|K%~4(ybQ;v$7!RoOSjbga{8$3#*>OI31va{ zyys<7k7%i7WFtz#L5A)@dFDVbv}9sAy)*01xH4|UctE}g*Lf|VTbydJjEs|gpsVqq zxj`vgXOsOp>hQ!3S~?_40gTNIg2ZTW)L%w&Wh^`n__N)UGTNg}3H;eV53Qvw&qHVF z#-Lp;bVN;#I?Ig6o_~i7eB#I+Ede8Ru1mSuurzQv__l}Wb{@GxuFQB$-Og+4rjGuW zlaSVViS^f^+qpo$6Xu`-Rg8+~bmU6J0XkACD;1p#^b=Zq=%B5})IeCe*DdAKaa@#ImzcW`29PH+?bJ02gz;1QIOk(^2pcm+{&AY!D?; ziDMi`w+`hfajcJU^0a$Y<_*#xDFJWYB6;zGd*tL-W%z(STk~=@YhIxTi=W)< zOn{AV9y)6;%T4A9wL)GZFU?e#{K2ze{@`IPvZu)(0>B%BSwXJOR%ZP2GCVkDT^ZLP zo?}WJ?3GG_5(Y8>?Xsii<#x*QOaOMu3M^so6i1mdYniEJeVd2Q;`j5=*|WFuZ8zbr zpY`lj!tCJ;y0U@;bCuXkmYYTPhR)%<3+4=Lh8E0u%h&wB!+EP&@6^V5IIl$s_eby# zZ!lx7tZ$49H}|GHEPcqnzcEqAc$(@%DGO^fd}sHa>OInX*r@M4a^}>D(`WV_I2t*2 z;&9~H!OtB$maZfR>9MFf)R{;DvSvj=eGwsr>Ma6ZZQS8Mb?x3A=F|NqAV{%7Kn6{$ za1(_%H(a zDqNWX*0>S9O=>H=f+~tuqOhbx4*anTnl=Q$NjIitiE#)~jSE22 zvABL-B3+csE_526o`d_JKb)?2Y8$A?k-4fXnb1Q=jvX{75NX1^dJKB@OVB_-*ZkDA z2d9o8u7Ii1fkVKaCjzqqxC=0=w1>_ROo!o4C__diRv0-S9g^WfO#}4xIUF;h7-|c8 zWC|?BO~s=)QShL}E%Rf%u!5cE;93&56E1Wh8+Oymt@%ywT+ke*M)ctkUD8tw1;>7O zhnEsX(r~3T!w0%>TulwB2vZ>1N^T!LWZu=2*zaf1BZ_4ghERans$+?sX$T2`j}4K{ zjNyqTQX1Trs!B37syss{47Nd3KW})Ii$DwzFU^F@R=Dsqf#@KP-6U6EfF_L!=Zu7_ z)DxKKz;nBtkeKQ!1>xYwUaqS4 z`nqCmYreL%Si3%7yZ)of&Ix`-^uOtQ!*|U;Bi0qgC3$g4QEbnP?YC>D#dWhDu4)66 z6a!bsUK_i5>9tGOQw87Z`(oft{~P}68w%nooT@J8RL!*SVbRvS(1tU&u29=~`6y5} zcNC>fd1+Ho+IdgfIaPlyFYUb9^$+O}oB!o(@%YR6<1bI|jehO9oV4@u@m&46`@yP- z>=aJ)s>vGgV74b~^1)R_0e{nG0*zUIURZT)GmTBzK`Z;7uxcV}WwT&`)J2nlVzS1f zw*pYvOt3j8?4Nw;{jPl5RtV+Vwies==iByU81f(x++P&%H_cLLbc#f2_#Q1t$Vk{M zLUXAP#mUMY{&0Ej?C0}GUML>v&mZY82m_zdI_4TX3O=}(APbqRZJ!pyvu;l8{OyA} zt|7*22ps&-yYkdd;fJdQ@BxgfK&uKxrHC0=AH;?u(cxjE;dpF#-|%oE9wS2aP&B5e z)N#X0ul)@#7#1Xlb))hmc^-$Gxafdl1W)W6(a)P!8x+tsw<-}2rFM)@g!#K@-%MBN$La!l=z*Xj_mCq=NM9S<(Gg^9pwTNOiH>88` z9D_PR4&mWhy@cMY;Az-Aa6-xk1NIoxQrT(fv?-9QYY2b*2lt!H+{Xc~dimt)_cs(m z`*Y&{-xBclOI6iifFi00_NaJUp`yy8j)DiaF!7kKqMA;6rhXYis17&`J-&!h^v=Sc zhAVr8`?)7LQz^~VcYf?~d%d$F?>WFvo|)zFncLDs10ODbKoTDMf#z3)hE|J}86Fu| z*jgl95o%iV7~34&;`Ww8$Q!JTunM{>dtp{fMp!bAuRsMVdN0GD<^k^t_pgFTyM*a6 z__G}D0$i4d8St+1kB10lcY;s4{)Feb2_BRm;fk1bK!W9ze-}U_Y`e3=Gbjdlq#{nX z)HXaQhrMtOZyk=!El&47+v(Dn3a>W28E_3xj!AF}P}_?pL}%#rJ)NRC<3j-Q>+tum z8dm?$c)HKus2;R>LHmNf4S%rSz7EC$UxeBDbJq>`mpvZ15CpIXXSBr0e4Jyd&m(1c z2XN}j@Uk1%i&4XTFlGYaj*fTT#4);Z04nQU**Y9XI!~%Y1~bZcaoxc=>OpHJ;xfE$ znQO3oRGk5i4az=Qw9PUGt!qS9)&|GObsWBm#FaC%)U69QHYxx`>=I=Z=|@WdBq0La zT4%NMMsY1SimfRB2VmzeJu+;@BlEPSf&S#nKyUoymMl&m01FM6qu*? z7SI6jEH%AxI{+7Z&F^^^5l#B%t%|mDLVY%Ae2|l?- zuFd#NAWmHHzOc-U+02?ioB%MA7j}Wk{j&ayzZ_@Fx@4dNL#=>qHU`%^9)J{0UX=~X zb=itcFjGNxQ)Q+SIFiRPzViF2682L)mU8T;hR)@AW)%)~e4#XT!PnTU)N(ARGNW^;IxBx$$iu0WPZ$)Xr3yyDhZf-4QBuRoF<=>$I|S1Z0A~(e5Jl zRsw9584C2glV}%1dLT;&1I^F?K!S*anXPvLP-ghRaxkjvYS>57FMC)P0H~gZ?hw## z^IR-&tBD0}4c8jpn5sm<UAW%03HQ-C>XSrnmFvhykYdZ(c6jMAbJV#oS@KC6iK3c zqifTgSl1ksY0I40HubAelU4&BXian6WvWBD)sdm}T7oChd5D-wp%$^u%9gnc(Bgz> zCjepm4bh`!7OyQX?#eIj`tG`^#e0gidnSA{^-GKOtMm1%b8EWq?w_jPTddkU;U-oQ z&6AraUwLO+L0n6sEw|3xZn+)3v-;becRHuqo&w7SGyUxC)9=^Z`P{d^aOVqmKlg(( zQ*F-{#H05^O*dQK)~7KwUG7mfY-mYcwYwzCL>6(p#4xFp06-I(XN{u3wrQy$ut# z1!8wY;N?PF?uPQA!$lE)ADIa>X8Cz6N@G)Y5KKI;kJ8k85WHjsC+*bRFo{?(5I{>- z_-=>>OMIwtHpJEK{q0QE-rs2wY^>LtPI~#b`IDUR7Z~~;f6^^{|3mkUme<-}jdbGHhq}m%V)&xk_)sjK zQvVWq0~$9z=qrSny7V-CXQ=hj<6al_9UztAPeY?9z#0hPwY?zh_^GhrBLY zX`U$XBLbLn2X-!g(X>?QrAqLYZzQhB@0 zDSUTmUZ|1^L;6Cu5%~wWB+P9D&Lm)k8L=I?oWMfWPgV=s!KDeeR;BVSYpfStO3u`6-+eeBiJ6IsZH2IzY)mijb z0H_J`I5sUDe**LPFMymL$2=Bhy9dw`vOwa>TKIW(L7wcLhsuXkj#mf))e^^;1^`<@ zmQ~gU9w3_m04d;0&>&OpoM2cvHpA*3#1m18(`#7*dvyu>Ec22B`*c0RK5f;e3?L&{ z5cXMF!an~e7T|XM<=_;HDb9hMBsL=ZIfs2#ITWpOv^M*+TQ5gvv(J#lKG9O#W}hvr z2aXn7S(^Qvk9`7oipoccgY^)8ul1<_U=uL02ju(zAO6l3NM^8)eAspC!fKqH3#E>gbIAWw1E;=W3N~E8+j!(Cnq&k$ML3q6DxBI~2!rTv3r+23J( z1ktdLW$NEx8Vce^j$u@u7lowVwKLELpAA>B;3yWtOnrBvS$*tHrvqZYN_}4zcy2GGVQX$F*h*}j*agpgaBES(-?W)PW0s$X zWH{d;#grW^ATdq7N2;^vtpHHNiIO(hesgzDXrI`1b9X-2UKH>*P2)6T`FTi%){tPz z4#Jn8*cztZ6QC{{boxMyjdj1#ofBGbE(LF*`{vSou(c@QZ<@ww#Paiy3XLJblpQP} zEls_Lby?(A0Eo%SH99SvBPUn<&lQsJ|6suX&9Eh~FOfjq-u#~%;8W_T4F5xhrb);y zG+{V^pmLmkwjv=IlR#j*>}7|Uoe^?^fTg4iksno0ih~mYFt+gzmzlDFwk!FY3gS zj{YALc(zs#q4x@UM2|v!Mb*(8K@V*ir~w0c^YL{e77GLpCCCNQa>%J%jv;CX9-r4oO>-)a%_`V$dIUmf`w&iM87x*=wxLo|oSq{91ZqC&{i_r&SkYE2`Nj1OoL94{C z{iKKI_)C0|%T94w_}LB!mp}1mpE}Y*RmtG>Zth_E`S06 literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_tasks.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_tasks.cpython-312-pytest-9.0.2.pyc index 124c284def763dcfbc4bd887123ea3f06f71b17d..505d9429ea375559d2e9d2a2f5a10e46c7c0c54a 100644 GIT binary patch delta 522 zcmaD+*Ve$udzzP*ivb8k1e~%~P2`ni`oOqR;{u~fia-lPlwztIlb%>wk{FYhSyGf=T#}faty@r8l98XM zTbz=;IhMIajdA7VN1EnLO7WAWv<@)(O}?#_%ow`aUYnbR(SNeHzAt0Q=9&6EjEoVJ z)eNl|qbDaC$}z@GZZNc9jGw&IP(dUSX!k9iqSWHjoRavW)WZ1WqNK@h4doewCi5Cu z2=T|4q!yRNC+6hDmn0Tv7ZE z2`h{Z^ZIJ2dSDPR6!`*WHJOXlfYdGKqSCygl|WXJFo+NV5uzXozR7Mz{MKuL%=JKA zEC?hT81Arew|h5wPvO5Tpm>!<39S4UM|^y4VrE`^yr%Lkj-=AeoRZAE;-aF-LAJ(> zC6n81j~ao^2N_kw2O`jH;;_lhPbtkwwJX{Wlm$7o`0nIdyQ2!9+*lYHl|QgCu<&$L VU1FBJ$s%->MdSlB1B(>cW&rnse?tHO delta 345 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 Y?>v9u`D^rsm%U4NR;-9@*FIYQFA$3r(EtDd diff --git a/tests/__pycache__/test_widget_metadata.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_widget_metadata.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e1f17304add777cc48d5d15596172592d3af5e5f GIT binary patch literal 6602 zcmeGgTWlN0aqoC{d>kd}K|Sq6bYiDwWigWckP_Q+9`Xy0g~V~tf^|Tgc_-;)zPfje zO@;)N*l1)VKx7oC{gDsnD@mQA%~yV#zXJUzWlE8HNzfK8(DaXq45V>C+L=8bd1jqD z1yZ0bvN+Dp%+AiuV|S0Uziw*_5NJC(4=&vH67okRoW@3g*!+|uec#hcx=t_0iI|bH*CU|@PUNid6P2|9|XB>HJ}a7aqr5`DAO1O z`8J2skl&)VUn23X+I?#1+)hGhKg^dF4%A=d>VUu1ndSJ&kpfrXq8)!hxtSs#i$z5% zpl24eq^X=uX_jDSvnfkDud8#KX-Vm027?LPtwdTg6;&~nSgt^2P{aUQW{3bTkqyB|J01QVe)xckZSxd>rXLV$ zkIJdMCcNu$769->Rd52YikhHGngmP4J1eQaOF|{%T-K((GB2J^4CRD zYbTup*S1$eLlxi9mxciRmqjl(6ZO+Jz-c>z5CR;svx>>2aNrS3Q1ztA-pP!!lgsP~ zqYMI^Ya9n~iTpwIZ?p|=gulDdGjz)N8tq^0mHO(yU1x8(8H($+)vF!I zLx9mYFa(!KEkH!SZ4-{%@k(6QEWu~tdtfKnbj*NMJByejCMJ=!1&nh`GbM-N%(w{O zjTd3gPxz~zABiFBJJ#;@tx`QDah6lrj9LChJqD_1 zHokWYnB11;Sp5PqdR+SZTf~4Y@lNEZTN2aQ(wdSP4&L9i`fjq#V@P^~FG7~el557+ z7S%wiUdImAS47%hl5~#E^qNDCyh}&K;WhhGLx-d5WysP6h;vzRt*!ugQvVvm`g+ir zU*c6+CC*`5_LY2#ZFh_Yb=N4%qVM0%BFu1QQU1oV=yN>2XBHchkVV-Q&u=D+ZH^uP z@hrBNe9bJDd<)y1JU08XsD{dZo|MFr-?6J4P&>+UDNvGeXO@Gdpt0tP0v^#VP3gj{}A?t8z@{XKH}|C{^1yX3xK?a;Yz_kQzuYo3btSSSCrd*Ag{A8<|y zs5r?4Sr%UVJA)sxf?;r`xT{_{)=QX(swJ~jF|#z{RA-U2lFLDD8H>f(U-u|?Thfh4 zCJWVPv3JMLTwye7L~Kq3$=y-Il1+s|MU=oI32I9Z2qD;Ayx&z?M9wQrPlRf7x?yTm zQ;QveDaC+V(qyXkf-0D{w>WgFAz~L^RLfD#&@xc&K#}#RC7`*M$ZE=B|7onv8V-PM zY27fuhhqC3Baas4y#BDMrE@6-UXRxr;2nSPAb55hUD+2k0x#B)$U$b(xWfXZ&I12q z;B2706Git#jcA}8IcSSN0+dLjpcx$*N3aLMJ_L^xLoR2>#=u)4R~Q>B_GEKfM$sRJ z(hzFL({%$aKCfFcxSh?@r1l)ui{F1XpG<0ou`ObkaCi6&iKK9!kSeeYYHCEusF4>m zs?X{`=$VK)uSHHi`E-QE#K>t$7)i2t#-iPpn4>yWMpz|SQj%&IA*@&|53C0(kWS_j zkSEY*1yYbbmTJSUsREBd-4C@f%0v@$sq9%L#jYSMf5K2^VRhtC+iaGm70}EUN!pmR+8<{qtb)UoW~tTf zh-eoetr_Nz<%Rqxrb#UiAkmQJomXfE0%wW1LJbQ4dWglDmW-y^y2stbm`&I(-{U6s z(Oo##HF39_*y|=H-Nb|)2V3RX=pEF!fpsuc>F&lVc39|&o}tge@HY4vPeE13D)Wy8 ze3~pIE@!Mc$%}d(5p>Q2~up&$^?Z2j0 zqp-{RSk_U3BT{#QV}MW_AHwfdCR&m0neIE+e8DdAlIg|Y5 zST-9nmHTkM`}t6qU_l+cHoYorgBS%nUZ1=?SrMj|PXf3+xqK301X$&vDTIJOG_@j6 ztqb_9IUJj8z!hN%Z9$Q06I}&#(&pYo{SA~w8ij!k>TP}t+>k@IVaF=r;fin!Yzz-q zrQZDxhE+ za&HRoh6cI>k*ae+xsA6#w${`&L96X2p~tu{A;E3?{$}7w@bb>n4|6}|PDh0s!+?Fz z%bngOe9$+D`1q+w=vH@e0P&I(w__p^9kP7RiAE2C3Tq%?pMf#^G=bwOMz!-)hr2$1 zgJpT^Jh~s}con0NN$Od+zJu?cQNYiEV$NFWmV+adbJ!5%QowEBzFk@Cv zvAf#l-7CdABKsOnL<}#5>}$BLrcexzY4|YO$Gk6BP?e0SC#fHW*_|xAnZt9RojL55 z&+^cGrsb~7@*S`*iKpl+2;;4l@f!d&565x8!;7zr?H>{UMr~2U2)$V6j$*Dh+ z<7?#jCuHmsa{n!X str: class TestInitialModule: pass -async def test_initial_tool_hook(): +async def _test_initial_tool_hook(): print("Testing @initial_tool auto-call hook...") # Create testing harness @@ -65,5 +65,8 @@ async def test_initial_tool_hook(): assert ExecutionState.called is True print("Test passed! @initial_tool was automatically called when InitializedNotification arrived.") +def test_initial_tool_hook(): + asyncio.run(_test_initial_tool_hook()) + if __name__ == "__main__": - asyncio.run(test_initial_tool_hook()) + test_initial_tool_hook() diff --git a/tests/test_oauth.py b/tests/test_oauth.py index 36292ea..548da51 100644 --- a/tests/test_oauth.py +++ b/tests/test_oauth.py @@ -9,7 +9,7 @@ from nitrostack import DIContainer, ExecutionContext, OAuthGuard from nitrostack.auth.oauth import OAuthModule, OAuthService -async def test_oauth_guard_validation(): +async def _test_oauth_guard_validation(): print("Testing OAuthGuard and OAuth 2.1 validation flow...") # 1. Setup DIContainer and OAuthModule @@ -93,5 +93,8 @@ async def test_oauth_guard_validation(): print("Success! OAuthGuard and OAuth 2.1 token validations work perfectly.") +def test_oauth_guard_validation(): + asyncio.run(_test_oauth_guard_validation()) + if __name__ == "__main__": - asyncio.run(test_oauth_guard_validation()) + test_oauth_guard_validation() diff --git a/tests/test_tasks.py b/tests/test_tasks.py index 520e7b7..25ade6e 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -41,7 +41,7 @@ async def delayed_tool(self, input: AsyncTaskInput, context: ExecutionContext) - class TestTasksModule: pass -async def test_all_tasks(): +async def _test_all_tasks(): print("Initializing NitroTestingModule with TestTasksModule...") harness = await NitroTestingModule.create(TestTasksModule) @@ -172,5 +172,8 @@ async def test_all_tasks(): print("\nAll task registry and protocol tests passed successfully!") +def test_all_tasks(): + asyncio.run(_test_all_tasks()) + if __name__ == "__main__": - asyncio.run(test_all_tasks()) + test_all_tasks() diff --git a/tests/test_widget_metadata.py b/tests/test_widget_metadata.py new file mode 100644 index 0000000..1860c60 --- /dev/null +++ b/tests/test_widget_metadata.py @@ -0,0 +1,69 @@ +import asyncio +import os +import sys + +# Ensure parent directory is in sys.path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) + +from nitrostack import injectable, tool, widget, module, ExecutionContext +from nitrostack.testing import NitroTestingModule +from pydantic import BaseModel + +class DummyInput(BaseModel): + pass + +@injectable() +class WidgetController: + @tool( + name="widget_tool", + description="A tool with a widget", + input_schema=DummyInput + ) + @widget("my-custom-widget-route") + async def widget_tool(self, input: DummyInput, context: ExecutionContext) -> dict: + return {"status": "ok"} + +@module( + name="widget_test", + controllers=[WidgetController] +) +class WidgetTestModule: + pass + +async def main(): + print("Testing widget decorator metadata mapping...") + + # 1. Initialize test harness + harness = await NitroTestingModule.create(WidgetTestModule) + + # 2. Extract tools from FastMCP server + tools = await harness.app.mcp_server.list_tools() + + # Find our tool + target_tool = None + for t in tools: + if t.name == "widget_tool": + target_tool = t + break + + assert target_tool is not None, "widget_tool was not registered" + + print("Registered tool representation:", target_tool) + + # Verify metadata fields are present + meta = getattr(target_tool, "meta", None) + if meta is None: + meta = getattr(target_tool, "_meta", {}) + + assert meta is not None, "Tool metadata is missing" + print("Tool metadata:", meta) + + # Check that widget fields are populated in metadata + assert meta.get("ui/template") == "my-custom-widget-route" + assert meta.get("openai/outputTemplate") == "my-custom-widget-route" + assert meta.get("ui") == {"resourceUri": "my-custom-widget-route"} + + print("Success! Widget metadata is correctly mapped and verified in the MCP Tool specification.") + +if __name__ == "__main__": + asyncio.run(main())