From 134b5409bd1465e8401fb4588a10b3caac25cb63 Mon Sep 17 00:00:00 2001 From: Rahman Date: Fri, 5 Dec 2025 18:22:13 +0100 Subject: [PATCH 01/10] feat(pluggable-widgets-mcp): introduce pluggable-widgets-mcp --- packages/pluggable-widgets-mcp/.gitignore | 3 + .../pluggable-widgets-mcp/.prettierrc.cjs | 1 + packages/pluggable-widgets-mcp/.prettierrc.js | 1 + packages/pluggable-widgets-mcp/AGENTS.md | 205 ++ packages/pluggable-widgets-mcp/README.md | 148 ++ .../pluggable-widgets-mcp/eslint.config.mjs | 3 + .../pluggable-widgets-mcp/package-lock.json | 2139 +++++++++++++++++ packages/pluggable-widgets-mcp/package.json | 42 + .../pluggable-widgets-mcp/src/api/handlers.ts | 0 packages/pluggable-widgets-mcp/src/config.ts | 25 + packages/pluggable-widgets-mcp/src/index.ts | 23 + .../pluggable-widgets-mcp/src/server/http.ts | 35 + .../src/server/routes.ts | 77 + .../src/server/server.ts | 49 + .../src/server/session.ts | 77 + .../pluggable-widgets-mcp/src/server/stdio.ts | 31 + .../pluggable-widgets-mcp/src/tools/index.ts | 13 + .../src/tools/scaffolding.tools.ts | 157 ++ .../pluggable-widgets-mcp/src/tools/types.ts | 69 + .../src/tools/utils/generator.ts | 250 ++ .../src/tools/utils/notifications.ts | 36 + .../src/tools/utils/progress-tracker.ts | 237 ++ .../src/tools/utils/response.ts | 19 + packages/pluggable-widgets-mcp/tsconfig.json | 34 + packages/pluggable-widgets-mcp/tscpaths.json | 4 + pnpm-lock.yaml | 105 +- 26 files changed, 3682 insertions(+), 101 deletions(-) create mode 100644 packages/pluggable-widgets-mcp/.gitignore create mode 100644 packages/pluggable-widgets-mcp/.prettierrc.cjs create mode 100644 packages/pluggable-widgets-mcp/.prettierrc.js create mode 100644 packages/pluggable-widgets-mcp/AGENTS.md create mode 100644 packages/pluggable-widgets-mcp/README.md create mode 100644 packages/pluggable-widgets-mcp/eslint.config.mjs create mode 100644 packages/pluggable-widgets-mcp/package-lock.json create mode 100644 packages/pluggable-widgets-mcp/package.json create mode 100644 packages/pluggable-widgets-mcp/src/api/handlers.ts create mode 100644 packages/pluggable-widgets-mcp/src/config.ts create mode 100644 packages/pluggable-widgets-mcp/src/index.ts create mode 100644 packages/pluggable-widgets-mcp/src/server/http.ts create mode 100644 packages/pluggable-widgets-mcp/src/server/routes.ts create mode 100644 packages/pluggable-widgets-mcp/src/server/server.ts create mode 100644 packages/pluggable-widgets-mcp/src/server/session.ts create mode 100644 packages/pluggable-widgets-mcp/src/server/stdio.ts create mode 100644 packages/pluggable-widgets-mcp/src/tools/index.ts create mode 100644 packages/pluggable-widgets-mcp/src/tools/scaffolding.tools.ts create mode 100644 packages/pluggable-widgets-mcp/src/tools/types.ts create mode 100644 packages/pluggable-widgets-mcp/src/tools/utils/generator.ts create mode 100644 packages/pluggable-widgets-mcp/src/tools/utils/notifications.ts create mode 100644 packages/pluggable-widgets-mcp/src/tools/utils/progress-tracker.ts create mode 100644 packages/pluggable-widgets-mcp/src/tools/utils/response.ts create mode 100644 packages/pluggable-widgets-mcp/tsconfig.json create mode 100644 packages/pluggable-widgets-mcp/tscpaths.json diff --git a/packages/pluggable-widgets-mcp/.gitignore b/packages/pluggable-widgets-mcp/.gitignore new file mode 100644 index 0000000000..e6cd8c7e6d --- /dev/null +++ b/packages/pluggable-widgets-mcp/.gitignore @@ -0,0 +1,3 @@ +dist/ +generations/ +node_modules/ \ No newline at end of file diff --git a/packages/pluggable-widgets-mcp/.prettierrc.cjs b/packages/pluggable-widgets-mcp/.prettierrc.cjs new file mode 100644 index 0000000000..0892704ab0 --- /dev/null +++ b/packages/pluggable-widgets-mcp/.prettierrc.cjs @@ -0,0 +1 @@ +module.exports = require("@mendix/prettier-config-web-widgets"); diff --git a/packages/pluggable-widgets-mcp/.prettierrc.js b/packages/pluggable-widgets-mcp/.prettierrc.js new file mode 100644 index 0000000000..0892704ab0 --- /dev/null +++ b/packages/pluggable-widgets-mcp/.prettierrc.js @@ -0,0 +1 @@ +module.exports = require("@mendix/prettier-config-web-widgets"); diff --git a/packages/pluggable-widgets-mcp/AGENTS.md b/packages/pluggable-widgets-mcp/AGENTS.md new file mode 100644 index 0000000000..7a08310679 --- /dev/null +++ b/packages/pluggable-widgets-mcp/AGENTS.md @@ -0,0 +1,205 @@ +# Pluggable Widgets MCP Server - AI Agent Guide + +This document provides context for AI development assistants working on the MCP (Model Context Protocol) server for Mendix pluggable widgets. + +## Overview + +This package implements an MCP server that enables AI assistants to scaffold and manage Mendix pluggable widgets programmatically. It supports both HTTP and STDIO transports for flexible integration with various MCP clients. + +### Key Characteristics + +- **MCP SDK**: Built on `@modelcontextprotocol/sdk` for standardized AI tool integration +- **Dual Transport**: HTTP (Express) for web clients, STDIO for CLI clients (Claude Desktop, etc.) +- **TypeScript**: Fully typed with Zod schemas for runtime validation +- **Widget Generator**: Wraps `@mendix/generator-widget` via PTY for interactive scaffolding + +## Project Structure + +``` +src/ +├── index.ts # Entry point - transport mode selection +├── config.ts # Server configuration and constants +├── server/ +│ ├── server.ts # MCP server factory and tool registration +│ ├── http.ts # HTTP transport setup (Express) +│ ├── stdio.ts # STDIO transport setup +│ ├── routes.ts # Express route handlers +│ └── session.ts # HTTP session management +└── tools/ + ├── index.ts # Tool aggregation + ├── types.ts # MCP tool type definitions + ├── scaffolding.tools.ts # Widget creation tool + └── utils/ + ├── generator.ts # Widget generator PTY wrapper + ├── progress-tracker.ts # Progress/logging helper + ├── notifications.ts # MCP notification utilities + └── response.ts # Tool response helpers +``` + +## Architecture + +### Transport Layer + +The server supports two transport modes selected via CLI argument: + +- **HTTP** (default): Multi-session Express server on port 3100 +- **STDIO**: Single-session stdin/stdout for CLI integration + +### Tool Registration + +Tools are defined using the `ToolDefinition` interface: + +```typescript +interface ToolDefinition { + name: string; // Tool identifier + title: string; // Human-readable name + description: string; // LLM-facing description + inputSchema: ZodType; // Zod schema for validation + handler: ToolHandler; // Async handler function +} +``` + +New tools should be: + +1. Created in `src/tools/` with a `*.tools.ts` suffix +2. Export a `get*Tools()` function returning `ToolDefinition[]` +3. Registered in `src/tools/index.ts` + +### Widget Generator Integration + +The `create-widget` tool uses `node-pty` to interact with the Mendix widget generator CLI. Key implementation details: + +- **PTY Simulation**: Required because the generator uses interactive prompts +- **Prompt Detection**: Matches expected prompts in terminal output +- **Answer Automation**: Sends pre-configured answers based on user input +- **Progress Tracking**: Reports progress via MCP notifications + +## Development Commands + +```bash +pnpm dev # Development mode with hot reload (tsx watch) +pnpm build # TypeScript compilation + path alias resolution +pnpm start # Build and run (HTTP mode) +pnpm start:stdio # Build and run (STDIO mode) +pnpm lint # ESLint + Prettier check +``` + +## Adding New Tools + +1. **Create tool file**: `src/tools/my-feature.tools.ts` + +```typescript +import { z } from "zod"; +import type { ToolDefinition, ToolResponse } from "@/tools/types"; +import { createToolResponse, createErrorResponse } from "@/tools/utils/response"; + +const mySchema = z.object({ + param: z.string().describe("Parameter description for LLM") +}); + +type MyInput = z.infer; + +export function getMyTools(): ToolDefinition[] { + return [ + { + name: "my-tool", + title: "My Tool", + description: "What this tool does (shown to LLM)", + inputSchema: mySchema, + handler: async (args, context) => { + // Implementation + return createToolResponse("Success message"); + } + } + ]; +} +``` + +2. **Register in index**: Update `src/tools/index.ts` + +```typescript +import { getMyTools } from "./my-feature.tools"; + +export function getAllTools(): AnyToolDefinition[] { + return [ + ...getScaffoldingTools(), + ...getMyTools() // Add here + ]; +} +``` + +## Code Conventions + +### Imports + +- Use `@/` path alias for absolute imports from `src/` +- Prefer specific file imports over barrel exports when dealing with circular dependencies +- Group imports: node builtins → external packages → internal modules + +### Error Handling + +- Use `createErrorResponse()` for user-facing errors +- Log to `console.error` (not stdout) in STDIO mode +- Use `ProgressTracker` for long-running operations + +### Type Safety + +- All tool inputs must have Zod schemas +- Use `ToolContext` for MCP-provided context (notifications, progress) +- Avoid `any` except in `AnyToolDefinition` (required for heterogeneous tool arrays) + +## Testing + +Use MCP Inspector for interactive testing: + +```bash +# STDIO mode +npx @modelcontextprotocol/inspector node dist/index.js stdio + +# HTTP mode +pnpm start +npx @modelcontextprotocol/inspector +# Connect to http://localhost:3100/mcp +``` + +## Key Files Reference + +| File | Purpose | +| -------------------------- | -------------------------------------- | +| `config.ts` | All constants (ports, timeouts, paths) | +| `tools/types.ts` | MCP tool type definitions | +| `tools/utils/generator.ts` | Widget generator prompts and defaults | +| `server/session.ts` | HTTP session lifecycle management | + +## Common Patterns + +### Progress Notifications + +```typescript +const tracker = new ProgressTracker({ + context, + logger: "my-tool", + totalSteps: 5 +}); + +tracker.start("initializing"); +await tracker.progress(25, "Step 1 complete"); +await tracker.info("Detailed log message", { key: "value" }); +tracker.stop(); +``` + +### Long-Running Operations + +- Use `ProgressTracker` for heartbeat and stuck detection +- Set appropriate timeouts (see `SCAFFOLD_TIMEOUT_MS`) +- Call `tracker.markComplete()` before expected long waits (e.g., npm install) + +## Roadmap Context + +Current focus is widget scaffolding. Planned additions: + +- Widget property editing +- XML configuration management +- Build and deployment automation + +When adding features, maintain the existing patterns for tool registration, progress tracking, and transport-agnostic design. diff --git a/packages/pluggable-widgets-mcp/README.md b/packages/pluggable-widgets-mcp/README.md new file mode 100644 index 0000000000..d3c8baeded --- /dev/null +++ b/packages/pluggable-widgets-mcp/README.md @@ -0,0 +1,148 @@ +# Mendix Pluggable Widgets MCP Server + +> **Work in Progress** - This is an MVP focused on widget scaffolding. Widget editing capabilities coming soon. + +A Model Context Protocol (MCP) server that enables AI assistants to scaffold Mendix pluggable widgets programmatically. + +## Quick Start + +```bash +pnpm install +pnpm start # HTTP mode (default) +pnpm start:stdio # STDIO mode +``` + +## Transport Modes + +### HTTP Mode (default) + +Runs an HTTP server for web-based MCP clients. + +```bash +pnpm start +pnpm start:http +``` + +- Server runs on `http://localhost:3100` (override with `PORT` env var) +- Health check: `GET /health` +- MCP endpoint: `POST /mcp` + +### STDIO Mode + +Runs via stdin/stdout for CLI-based MCP clients (Claude Desktop, etc.). + +```bash +pnpm start:stdio +``` + +## MCP Client Configuration + +### HTTP + +```json +{ + "mcpServers": { + "pluggable-widgets-mcp": { + "url": "http://localhost:3100/mcp" + } + } +} +``` + +### STDIO + +**_Some client setups like Claude Desktop support STDIO only (for now)_** + +```json +{ + "mcpServers": { + "pluggable-widgets-mcp": { + "command": "node", + "args": ["/path/to/pluggable-widgets-mcp/dist/index.js", "stdio"] + } + } +} +``` + +## Available Tools + +### create-widget + +Scaffolds a new Mendix pluggable widget using `@mendix/generator-widget`. + +| Parameter | Required | Default | Description | +| --------------------- | -------- | ------------ | ------------------------------------ | +| `name` | Yes | - | Widget name (PascalCase recommended) | +| `description` | Yes | - | Brief description of the widget | +| `version` | No | `1.0.0` | Initial version (semver) | +| `author` | No | `Mendix` | Author name | +| `license` | No | `Apache-2.0` | License type | +| `organization` | No | `Mendix` | Organization namespace | +| `template` | No | `empty` | `full` (sample code) or `empty` | +| `programmingLanguage` | No | `typescript` | `typescript` or `javascript` | +| `unitTests` | No | `true` | Include unit test setup (Jest/TS) | +| `e2eTests` | No | `false` | Include E2E test setup (Playwright) | + +Generated widgets are placed in `generations/` directory within this package. + +## Development + +```bash +pnpm dev # Development mode with hot reload +pnpm build # Build for production +pnpm start # Build and run +``` + +## Testing with MCP Inspector + +The [MCP Inspector](https://github.com/modelcontextprotocol/inspector) is an interactive debugging tool for testing MCP servers. It provides a web UI to connect to your server, explore available tools, and execute them with custom inputs. + +### Quick Start + +```bash +# Run Inspector against this server (STDIO mode) +npx @modelcontextprotocol/inspector node dist/index.js stdio + +# Or for HTTP mode, start the server first then connect via Inspector +pnpm start +npx @modelcontextprotocol/inspector +# Then enter http://localhost:3100/mcp as the server URL +``` + +### Using the Inspector + +1. **Connect** - The Inspector will automatically connect to your MCP server +2. **Explore Tools** - View all registered tools (`create-widget`, etc.) with their schemas +3. **Execute Tools** - Fill in parameters and run tools to test behavior +4. **View Responses** - See JSON responses, progress notifications, and logs in real-time + +### Example: Testing `create-widget` + +1. Start the Inspector: `npx @modelcontextprotocol/inspector node dist/index.js stdio` +2. Select the `create-widget` tool from the tools list +3. Fill in required parameters: + ```json + { + "name": "TestWidget", + "description": "A test widget", + ... // Defaults for other optional values if not entered + } + ``` +4. Click "Execute" and watch progress notifications as the widget is scaffolded +5. Check `generations/testwidget/` for the created widget + +This is useful for verifying tool behavior without needing a full AI client integration. + +## Roadmap + +- [x] Widget scaffolding +- [x] HTTP transport +- [x] STDIO transport +- [x] Progress notifications +- [ ] Widget editing and modification +- [ ] Property management +- [ ] Build and deployment tools + +## License + +Apache-2.0 - Mendix Technology BV 2025 diff --git a/packages/pluggable-widgets-mcp/eslint.config.mjs b/packages/pluggable-widgets-mcp/eslint.config.mjs new file mode 100644 index 0000000000..ed68ae9e78 --- /dev/null +++ b/packages/pluggable-widgets-mcp/eslint.config.mjs @@ -0,0 +1,3 @@ +import config from "@mendix/eslint-config-web-widgets/widget-ts.mjs"; + +export default config; diff --git a/packages/pluggable-widgets-mcp/package-lock.json b/packages/pluggable-widgets-mcp/package-lock.json new file mode 100644 index 0000000000..4de60372f3 --- /dev/null +++ b/packages/pluggable-widgets-mcp/package-lock.json @@ -0,0 +1,2139 @@ +{ + "name": "pluggable-widgets-mcp", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pluggable-widgets-mcp", + "version": "0.1.0", + "license": "Apache-2.0", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.24.2", + "node-pty": "^1.0.0", + "tsx": "^4.21.0", + "zod": "^4.1.13" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/node": "^24.10.1", + "tsc-alias": "^1.8.16", + "typescript": "^5.9.3" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz", + "integrity": "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.1.tgz", + "integrity": "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.1.tgz", + "integrity": "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.1.tgz", + "integrity": "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.1.tgz", + "integrity": "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.1.tgz", + "integrity": "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.1.tgz", + "integrity": "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.1.tgz", + "integrity": "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.1.tgz", + "integrity": "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.1.tgz", + "integrity": "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.1.tgz", + "integrity": "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.1.tgz", + "integrity": "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.1.tgz", + "integrity": "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.1.tgz", + "integrity": "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.1.tgz", + "integrity": "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.1.tgz", + "integrity": "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz", + "integrity": "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.1.tgz", + "integrity": "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.1.tgz", + "integrity": "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.1.tgz", + "integrity": "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.1.tgz", + "integrity": "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.1.tgz", + "integrity": "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.1.tgz", + "integrity": "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.1.tgz", + "integrity": "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.1.tgz", + "integrity": "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.1.tgz", + "integrity": "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.24.2.tgz", + "integrity": "sha512-hS/kzSfchqzvUeJUsdiDHi84/kNhLIZaZ6coGQVwbYIelOBbcAwUohUfaQTLa1MvFOK/jbTnGFzraHSFwB7pjQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "jose": "^6.1.1", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.1.tgz", + "integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.1", + "@esbuild/android-arm": "0.27.1", + "@esbuild/android-arm64": "0.27.1", + "@esbuild/android-x64": "0.27.1", + "@esbuild/darwin-arm64": "0.27.1", + "@esbuild/darwin-x64": "0.27.1", + "@esbuild/freebsd-arm64": "0.27.1", + "@esbuild/freebsd-x64": "0.27.1", + "@esbuild/linux-arm": "0.27.1", + "@esbuild/linux-arm64": "0.27.1", + "@esbuild/linux-ia32": "0.27.1", + "@esbuild/linux-loong64": "0.27.1", + "@esbuild/linux-mips64el": "0.27.1", + "@esbuild/linux-ppc64": "0.27.1", + "@esbuild/linux-riscv64": "0.27.1", + "@esbuild/linux-s390x": "0.27.1", + "@esbuild/linux-x64": "0.27.1", + "@esbuild/netbsd-arm64": "0.27.1", + "@esbuild/netbsd-x64": "0.27.1", + "@esbuild/openbsd-arm64": "0.27.1", + "@esbuild/openbsd-x64": "0.27.1", + "@esbuild/openharmony-arm64": "0.27.1", + "@esbuild/sunos-x64": "0.27.1", + "@esbuild/win32-arm64": "0.27.1", + "@esbuild/win32-ia32": "0.27.1", + "@esbuild/win32-x64": "0.27.1" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mylas": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/mylas/-/mylas-2.1.14.tgz", + "integrity": "sha512-BzQguy9W9NJgoVn2mRWzbFrFWWztGCcng2QI9+41frfk+Athwgx3qhqhvStz7ExeUUu7Kzw427sNzHpEZNINog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/raouldeheer" + } + }, + "node_modules/nan": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.24.0.tgz", + "integrity": "sha512-Vpf9qnVW1RaDkoNKFUvfxqAbtI8ncb8OJlqZ9wwpXzWPEsvsB1nvdUi6oYrHIkQ1Y/tMDnr1h4nczS0VB9Xykg==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-pty": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz", + "integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "nan": "^2.17.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/plimit-lit": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/plimit-lit/-/plimit-lit-1.6.1.tgz", + "integrity": "sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "queue-lit": "^1.5.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-lit": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/queue-lit/-/queue-lit-1.5.2.tgz", + "integrity": "sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsc-alias": { + "version": "1.8.16", + "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.16.tgz", + "integrity": "sha512-QjCyu55NFyRSBAl6+MTFwplpFcnm2Pq01rR/uxfqJoLMm6X3O14KEGtaSDZpJYaE1bJBGDjD0eSuiIWPe2T58g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.3", + "commander": "^9.0.0", + "get-tsconfig": "^4.10.0", + "globby": "^11.0.4", + "mylas": "^2.1.9", + "normalize-path": "^3.0.0", + "plimit-lit": "^1.2.6" + }, + "bin": { + "tsc-alias": "dist/bin/index.js" + }, + "engines": { + "node": ">=16.20.2" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", + "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.0", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz", + "integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + } + } +} diff --git a/packages/pluggable-widgets-mcp/package.json b/packages/pluggable-widgets-mcp/package.json new file mode 100644 index 0000000000..b23cdf64bc --- /dev/null +++ b/packages/pluggable-widgets-mcp/package.json @@ -0,0 +1,42 @@ +{ + "name": "pluggable-widgets-mcp", + "version": "0.1.0", + "description": "MCP server for Mendix Pluggable Widgets", + "copyright": "© Mendix Technology BV 2025. All rights reserved.", + "author": "Mendix", + "license": "Apache-2.0", + "type": "module", + "main": "dist/index.js", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc && tsc-alias -p tsconfig.json --resolve-full-paths", + "dev": "tsx watch src/index.ts", + "generate-source-map": "tsc --sourceMap --declaration --declarationMap", + "lint": "eslint src/ package.json", + "start": "npm run build && node dist/index.js", + "start:http": "npm run build && node dist/index.js http", + "start:stdio": "npm run build && node dist/index.js stdio" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.24.2", + "cors": "^2.8.5", + "express": "^5.1.0", + "node-pty": "^1.0.0", + "tsx": "^4.21.0", + "zod": "^4.1.13" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.2", + "@types/node": "^24.10.1", + "tsc-alias": "^1.8.16", + "typescript": "^5.9.3" + }, + "keywords": [], + "packageManager": "pnpm@10.17.0", + "engines": { + "node": ">=22" + } +} diff --git a/packages/pluggable-widgets-mcp/src/api/handlers.ts b/packages/pluggable-widgets-mcp/src/api/handlers.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/pluggable-widgets-mcp/src/config.ts b/packages/pluggable-widgets-mcp/src/config.ts new file mode 100644 index 0000000000..6dceeb505d --- /dev/null +++ b/packages/pluggable-widgets-mcp/src/config.ts @@ -0,0 +1,25 @@ +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +// Server configuration +export const SERVER_NAME = "pluggable-widgets-mcp"; +export const SERVER_VERSION = "0.1.0"; +export const PORT = parseInt(process.env.PORT || "3100", 10); + +// Server metadata +export const SERVER_ICON = { + src: "https://avatars.githubusercontent.com/u/133443?s=200&v=4", + sizes: ["128x128"], + mimeType: "image/png" +}; +export const SERVER_WEBSITE_URL = "https://github.com/mendix/web-widgets"; +export const SERVER_INSTRUCTIONS = + "This is a MCP server for Mendix Pluggable Widgets. It allows you to create and edit widgets."; + +// Paths - use fileURLToPath for Node.js 18 compatibility (import.meta.dirname requires Node 20.11+) +const __dirname = import.meta.dirname ?? dirname(fileURLToPath(import.meta.url)); +export const PACKAGE_ROOT = join(__dirname, "../"); +export const GENERATIONS_DIR = join(PACKAGE_ROOT, "generations"); + +// Timeouts +export const SCAFFOLD_TIMEOUT_MS = 300000; // 5 minutes diff --git a/packages/pluggable-widgets-mcp/src/index.ts b/packages/pluggable-widgets-mcp/src/index.ts new file mode 100644 index 0000000000..553f709399 --- /dev/null +++ b/packages/pluggable-widgets-mcp/src/index.ts @@ -0,0 +1,23 @@ +import { startHttpServer } from "@/server/http"; +import { startStdioServer } from "@/server/stdio"; + +type TransportMode = "http" | "stdio"; + +const mode = (process.argv[2] as TransportMode) || "http"; + +async function main(): Promise { + switch (mode) { + case "stdio": + await startStdioServer(); + break; + case "http": + default: + await startHttpServer(); + break; + } +} + +main().catch(err => { + console.error("Fatal error:", err); + process.exit(1); +}); diff --git a/packages/pluggable-widgets-mcp/src/server/http.ts b/packages/pluggable-widgets-mcp/src/server/http.ts new file mode 100644 index 0000000000..c1b5fed9c6 --- /dev/null +++ b/packages/pluggable-widgets-mcp/src/server/http.ts @@ -0,0 +1,35 @@ +import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; +import cors from "cors"; +import { PORT } from "@/config"; +import { setupRoutes } from "./routes"; +import { sessionManager } from "./session"; + +/** + * Starts the MCP server with HTTP/Streamable transport. + * Supports multiple concurrent sessions via Express. + */ +export async function startHttpServer(): Promise { + const app = createMcpExpressApp(); + app.use(cors()); + + setupRoutes(app); + + app.listen(PORT, () => { + console.log(`[HTTP] MCP Server started on port ${PORT}`); + console.log(`[HTTP] Health check: http://localhost:${PORT}/health`); + console.log(`[HTTP] MCP endpoint: http://localhost:${PORT}/mcp`); + }); + + setupGracefulShutdown(); +} + +function setupGracefulShutdown(): void { + const shutdown = async (): Promise => { + console.log("\n[HTTP] Shutting down server..."); + await sessionManager.closeAll(); + process.exit(0); + }; + + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); +} diff --git a/packages/pluggable-widgets-mcp/src/server/routes.ts b/packages/pluggable-widgets-mcp/src/server/routes.ts new file mode 100644 index 0000000000..9d888eb491 --- /dev/null +++ b/packages/pluggable-widgets-mcp/src/server/routes.ts @@ -0,0 +1,77 @@ +import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; +import type { Express, Request, Response } from "express"; +import { SERVER_NAME, SERVER_VERSION } from "@/config"; +import { createMcpServer } from "./server"; +import { sessionManager } from "./session"; + +/** + * Sets up all routes for the Express application. + */ +export function setupRoutes(app: Express): void { + setupHealthRoute(app); + setupMcpRoute(app); +} + +/** + * Health check endpoint for monitoring. + */ +function setupHealthRoute(app: Express): void { + app.get("/health", (_req: Request, res: Response) => { + res.json({ + status: "ok", + server: SERVER_NAME, + version: SERVER_VERSION, + sessions: sessionManager.sessionCount + }); + }); +} + +/** + * Main MCP endpoint handling session management and request routing. + */ +function setupMcpRoute(app: Express): void { + app.all("/mcp", async (req: Request, res: Response) => { + const sessionId = req.headers["mcp-session-id"] as string | undefined; + + try { + // Case 1: Existing session - reuse transport + if (sessionId && sessionManager.hasSession(sessionId)) { + const transport = sessionManager.getTransport(sessionId)!; + await transport.handleRequest(req, res, req.body); + return; + } + + // Case 2: New session - create transport and server + if (!sessionId && isInitializeRequest(req.body)) { + const transport = sessionManager.createTransport(); + const server = createMcpServer(); + await server.connect(transport); + await transport.handleRequest(req, res, req.body); + return; + } + + // Case 3: Invalid request + sendJsonRpcError(res, 400, "Bad Request: No valid session ID provided"); + } catch { + sendJsonRpcError( + res, + 400, + "Invalid session. Send an initialize request without session ID to start a new session." + ); + } + }); +} + +/** + * Sends a JSON-RPC error response. + */ +function sendJsonRpcError(res: Response, statusCode: number, message: string): void { + res.status(statusCode).json({ + jsonrpc: "2.0", + error: { + code: -32000, + message + }, + id: null + }); +} diff --git a/packages/pluggable-widgets-mcp/src/server/server.ts b/packages/pluggable-widgets-mcp/src/server/server.ts new file mode 100644 index 0000000000..42029e0d8a --- /dev/null +++ b/packages/pluggable-widgets-mcp/src/server/server.ts @@ -0,0 +1,49 @@ +import { SERVER_ICON, SERVER_INSTRUCTIONS, SERVER_NAME, SERVER_VERSION, SERVER_WEBSITE_URL } from "@/config"; +import { getAllTools } from "@/tools"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; + +/** + * Creates and configures a new MCP server instance with all registered tools. + */ +export function createMcpServer(): McpServer { + const server = new McpServer( + { + name: SERVER_NAME, + version: SERVER_VERSION, + icons: [SERVER_ICON], + websiteUrl: SERVER_WEBSITE_URL + }, + { + capabilities: { + logging: {}, + prompts: {}, + resources: {}, + tools: {} + }, + instructions: SERVER_INSTRUCTIONS + } + ); + + registerTools(server); + + return server; +} + +/** + * Registers all available tools with the MCP server. + */ +function registerTools(server: McpServer): void { + const tools = getAllTools(); + + for (const tool of tools) { + server.registerTool( + tool.name, + { + title: tool.title, + description: tool.description, + inputSchema: tool.inputSchema + }, + tool.handler + ); + } +} diff --git a/packages/pluggable-widgets-mcp/src/server/session.ts b/packages/pluggable-widgets-mcp/src/server/session.ts new file mode 100644 index 0000000000..4015739e11 --- /dev/null +++ b/packages/pluggable-widgets-mcp/src/server/session.ts @@ -0,0 +1,77 @@ +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { randomUUID } from "node:crypto"; + +export interface Session { + transport: StreamableHTTPServerTransport; + createdAt: Date; +} + +/** + * Manages MCP sessions and their associated transports. + */ +export class SessionManager { + private sessions = new Map(); + + /** + * Creates a new transport with session lifecycle callbacks. + * The transport is added to sessions when initialized via the callback. + */ + createTransport(): StreamableHTTPServerTransport { + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + onsessioninitialized: sessionId => { + this.sessions.set(sessionId, { + transport, + createdAt: new Date() + }); + console.log(`[MCP] Session initialized: ${sessionId}`); + }, + onsessionclosed: sessionId => { + this.sessions.delete(sessionId); + console.log(`[MCP] Session closed: ${sessionId}`); + } + }); + + return transport; + } + + /** + * Gets an existing session's transport by session ID. + */ + getTransport(sessionId: string): StreamableHTTPServerTransport | undefined { + return this.sessions.get(sessionId)?.transport; + } + + /** + * Checks if a session exists. + */ + hasSession(sessionId: string): boolean { + return this.sessions.has(sessionId); + } + + /** + * Gets the count of active sessions. + */ + get sessionCount(): number { + return this.sessions.size; + } + + /** + * Closes all sessions gracefully. + */ + async closeAll(): Promise { + const closePromises = Array.from(this.sessions.entries()).map(async ([sessionId, session]) => { + try { + console.log(`[MCP] Closing session: ${sessionId}`); + await session.transport.close(); + } catch (error) { + console.error(`[MCP] Error closing session ${sessionId}:`, error); + } + }); + + await Promise.all(closePromises); + this.sessions.clear(); + } +} + +export const sessionManager = new SessionManager(); diff --git a/packages/pluggable-widgets-mcp/src/server/stdio.ts b/packages/pluggable-widgets-mcp/src/server/stdio.ts new file mode 100644 index 0000000000..96039e6cd6 --- /dev/null +++ b/packages/pluggable-widgets-mcp/src/server/stdio.ts @@ -0,0 +1,31 @@ +import { createMcpServer } from "./server"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; + +/** + * Starts the MCP server with STDIO transport. + * Communicates via stdin/stdout for CLI-based MCP clients. + */ +export async function startStdioServer(): Promise { + const server = createMcpServer(); + const transport = new StdioServerTransport(); + + // Log to stderr since stdout is used for MCP communication + console.error("[STDIO] Starting MCP server..."); + + await server.connect(transport); + + console.error("[STDIO] MCP server connected and ready"); + + setupGracefulShutdown(transport); +} + +function setupGracefulShutdown(transport: StdioServerTransport): void { + const shutdown = async (): Promise => { + console.error("\n[STDIO] Shutting down server..."); + await transport.close(); + process.exit(0); + }; + + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); +} diff --git a/packages/pluggable-widgets-mcp/src/tools/index.ts b/packages/pluggable-widgets-mcp/src/tools/index.ts new file mode 100644 index 0000000000..e80ee7c511 --- /dev/null +++ b/packages/pluggable-widgets-mcp/src/tools/index.ts @@ -0,0 +1,13 @@ +import type { AnyToolDefinition } from "@/tools/types"; +import { getScaffoldingTools } from "./scaffolding.tools"; + +/** + * Gets all tool definitions for registration with the MCP server. + */ +export function getAllTools(): AnyToolDefinition[] { + const tools: AnyToolDefinition[] = []; + + tools.push(...getScaffoldingTools()); + + return tools; +} diff --git a/packages/pluggable-widgets-mcp/src/tools/scaffolding.tools.ts b/packages/pluggable-widgets-mcp/src/tools/scaffolding.tools.ts new file mode 100644 index 0000000000..962a8a7282 --- /dev/null +++ b/packages/pluggable-widgets-mcp/src/tools/scaffolding.tools.ts @@ -0,0 +1,157 @@ +import { mkdir } from "node:fs/promises"; +import { z } from "zod"; +import { GENERATIONS_DIR } from "@/config"; +import type { ToolContext, ToolDefinition, ToolResponse } from "@/tools/types"; +import { + buildWidgetOptions, + DEFAULT_WIDGET_OPTIONS, + GENERATOR_PROMPTS, + runWidgetGenerator, + SCAFFOLD_PROGRESS +} from "@/tools/utils/generator"; +import { ProgressTracker } from "@/tools/utils/progress-tracker"; +import { createErrorResponse, createToolResponse } from "@/tools/utils/response"; + +const createWidgetSchema = z.object({ + name: z + .string() + .min(1) + .max(100) + .describe("[REQUIRED] The name of the widget in PascalCase (e.g., 'MyAwesomeWidget', 'DataChart')"), + description: z.string().min(1).max(200).describe("[REQUIRED] A brief description of what the widget does"), + version: z + .string() + .regex(/^\d+\.\d+\.\d+$/, "Version must be in semver format: x.y.z") + .optional() + .describe(`[OPTIONAL] Initial version in semver format. Default: "${DEFAULT_WIDGET_OPTIONS.version}"`), + author: z + .string() + .min(1) + .max(100) + .optional() + .describe(`[OPTIONAL] Author name. Default: "${DEFAULT_WIDGET_OPTIONS.author}"`), + license: z + .string() + .min(1) + .max(50) + .optional() + .describe(`[OPTIONAL] License type. Default: "${DEFAULT_WIDGET_OPTIONS.license}"`), + organization: z + .string() + .min(1) + .max(100) + .optional() + .describe( + `[OPTIONAL] Organization name for the widget namespace. Default: "${DEFAULT_WIDGET_OPTIONS.organization}"` + ), + template: z + .enum(["full", "empty"]) + .optional() + .describe( + `[OPTIONAL] Widget template: "full" includes sample code and examples, "empty" is minimal/blank. Default: "${DEFAULT_WIDGET_OPTIONS.template}"` + ), + programmingLanguage: z + .enum(["typescript", "javascript"]) + .optional() + .describe( + `[OPTIONAL] Programming language for the widget source code. Default: "${DEFAULT_WIDGET_OPTIONS.programmingLanguage}"` + ), + unitTests: z + .boolean() + .optional() + .describe(`[OPTIONAL] Include unit test setup with Jest. Default: ${DEFAULT_WIDGET_OPTIONS.unitTests}`), + e2eTests: z + .boolean() + .optional() + .describe( + `[OPTIONAL] Include end-to-end test setup with Playwright. Default: ${DEFAULT_WIDGET_OPTIONS.e2eTests}` + ) +}); + +type CreateWidgetInput = z.infer; + +const CREATE_WIDGET_DESCRIPTION = `Scaffolds a new Mendix pluggable widget using the official @mendix/generator-widget. + +BEFORE RUNNING: Please confirm all options with the user. Show them the full list of configurable parameters: + +REQUIRED: + • name: Widget name in PascalCase (e.g., "MyAwesomeWidget") + • description: Brief description of what the widget does + +OPTIONAL (with defaults): + • version: Initial version (default: "${DEFAULT_WIDGET_OPTIONS.version}") + • author: Author name (default: "${DEFAULT_WIDGET_OPTIONS.author}") + • license: License type (default: "${DEFAULT_WIDGET_OPTIONS.license}") + • organization: Namespace organization (default: "${DEFAULT_WIDGET_OPTIONS.organization}") + • template: "full" (with examples) or "empty" (minimal) (default: "${DEFAULT_WIDGET_OPTIONS.template}") + • programmingLanguage: "typescript" or "javascript" (default: "${DEFAULT_WIDGET_OPTIONS.programmingLanguage}") + • unitTests: Include Jest test setup (default: ${DEFAULT_WIDGET_OPTIONS.unitTests}) + • e2eTests: Include Playwright E2E tests (default: ${DEFAULT_WIDGET_OPTIONS.e2eTests}) + +Ask the user if they want to customize any options before proceeding.`; + +export function getScaffoldingTools(): Array> { + return [ + { + name: "create-widget", + title: "Create Widget", + description: CREATE_WIDGET_DESCRIPTION, + inputSchema: createWidgetSchema, + handler: handleCreateWidget + } + ]; +} + +async function handleCreateWidget(args: CreateWidgetInput, context: ToolContext): Promise { + const options = buildWidgetOptions(args); + const tracker = new ProgressTracker({ + context, + logger: "scaffolding", + totalSteps: GENERATOR_PROMPTS.length + }); + + try { + console.error(`[create-widget] Starting widget scaffolding for "${options.name}"...`); + await tracker.progress(SCAFFOLD_PROGRESS.START, `Starting widget scaffolding for "${options.name}"...`); + await tracker.info(`Starting widget scaffolding for "${options.name}"...`, { + widgetName: options.name, + template: options.template, + organization: options.organization + }); + + // Ensure generations directory exists + await mkdir(GENERATIONS_DIR, { recursive: true }); + + const widgetFolder = await runWidgetGenerator(options, tracker); + const widgetPath = `${GENERATIONS_DIR}/${widgetFolder}`; + + console.error(`[create-widget] Widget created successfully at ${widgetPath}`); + await tracker.progress(SCAFFOLD_PROGRESS.COMPLETE, "Widget created successfully!"); + await tracker.info("Widget created successfully!", { + widgetName: options.name, + path: widgetPath + }); + + return createToolResponse( + [ + `Widget "${options.name}" created successfully!`, + "", + `Location: ${widgetPath}`, + "", + "Next steps:", + `1. cd ${widgetPath}`, + "2. pnpm install", + "3. pnpm start (to build and watch for changes)", + "", + "The widget will be available in Mendix Studio Pro after syncing the app directory." + ].join("\n") + ); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + await tracker.error(`Failed to create widget: ${message}`, { + widgetName: options.name, + error: message + }); + return createErrorResponse(`Failed to create widget: ${message}`); + } +} diff --git a/packages/pluggable-widgets-mcp/src/tools/types.ts b/packages/pluggable-widgets-mcp/src/tools/types.ts new file mode 100644 index 0000000000..65b1e252b8 --- /dev/null +++ b/packages/pluggable-widgets-mcp/src/tools/types.ts @@ -0,0 +1,69 @@ +import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js"; +import type { ServerNotification, ServerRequest } from "@modelcontextprotocol/sdk/types.js"; +import type { ZodType } from "zod"; + +// ============================================================================= +// MCP Core Types +// ============================================================================= + +/** + * Standard response format for MCP tool handlers. + * Index signature required for MCP SDK compatibility. + */ +export interface ToolResponse { + [key: string]: unknown; + content: Array<{ type: "text"; text: string }>; +} + +/** + * Extra context provided to tool handlers by the MCP server. + */ +export type ToolContext = RequestHandlerExtra; + +/** + * Type for tool handler functions. + */ +export type ToolHandler = (args: T, context: ToolContext) => Promise; + +/** + * Definition for an MCP tool. + */ +export interface ToolDefinition { + name: string; + title: string; + description: string; + inputSchema: ZodType; + handler: ToolHandler; +} + +/** + * Type for collections of tools with heterogeneous input types. + * Uses 'any' because TypeScript's variance rules prevent using 'unknown' + * for handlers that only accept specific input types. + */ +export type AnyToolDefinition = ToolDefinition; + +/** + * Log levels supported by MCP logging notifications. + */ +export type LogLevel = "debug" | "info" | "notice" | "warning" | "error"; + +// ============================================================================= +// Widget Generator Types +// ============================================================================= + +/** + * Options for creating a new Mendix pluggable widget. + */ +export interface WidgetOptions { + name: string; + description: string; + version: string; + author: string; + license: string; + organization?: string; + template?: "full" | "empty"; + programmingLanguage?: "typescript" | "javascript"; + unitTests?: boolean; + e2eTests?: boolean; +} diff --git a/packages/pluggable-widgets-mcp/src/tools/utils/generator.ts b/packages/pluggable-widgets-mcp/src/tools/utils/generator.ts new file mode 100644 index 0000000000..6fc590b63c --- /dev/null +++ b/packages/pluggable-widgets-mcp/src/tools/utils/generator.ts @@ -0,0 +1,250 @@ +import * as pty from "node-pty"; +import { GENERATIONS_DIR, SCAFFOLD_TIMEOUT_MS } from "@/config"; +import type { WidgetOptions } from "@/tools/types"; +import { ProgressTracker } from "./progress-tracker"; + +/** + * Generator prompt patterns in order - must match answers array. + */ +export const GENERATOR_PROMPTS = [ + "What is the name", + "Enter a description", + "organization", + "copyright", + "license", + "version", + "author", + "path", + "programming language", + "type of components", + "type of widget", + "template", + "unit tests", + "end-to-end" +] as const; + +/** + * Progress milestones for widget scaffolding. + */ +export const SCAFFOLD_PROGRESS = { + START: 0, + PROMPTS_START: 5, + PROMPTS_END: 70, + INSTALLING: 75, + COMPLETE: 100 +} as const; + +/** + * Default values for widget options. + */ +export const DEFAULT_WIDGET_OPTIONS = { + version: "1.0.0", + author: "Mendix", + license: "Apache-2.0", + organization: "Mendix", + template: "empty" as const, + programmingLanguage: "typescript" as const, + unitTests: true, + e2eTests: false +} as const; + +/** + * Local state for tracking generator process progress. + */ +interface GeneratorLocalState { + output: string; + answerIndex: number; + promptMatchedIndex: number; + allPromptsAnswered: boolean; +} + +/** + * Builds widget options from input arguments with defaults applied. + */ +export function buildWidgetOptions( + args: Partial & Pick +): WidgetOptions { + return { + name: args.name, + description: args.description, + version: args.version ?? DEFAULT_WIDGET_OPTIONS.version, + author: args.author ?? DEFAULT_WIDGET_OPTIONS.author, + license: args.license ?? DEFAULT_WIDGET_OPTIONS.license, + organization: args.organization ?? DEFAULT_WIDGET_OPTIONS.organization, + template: args.template ?? DEFAULT_WIDGET_OPTIONS.template, + programmingLanguage: DEFAULT_WIDGET_OPTIONS.programmingLanguage, + unitTests: args.unitTests ?? DEFAULT_WIDGET_OPTIONS.unitTests, + e2eTests: args.e2eTests ?? DEFAULT_WIDGET_OPTIONS.e2eTests + }; +} + +/** + * Builds the answers array for the generator prompts. + */ +export function buildGeneratorAnswers(options: WidgetOptions): string[] { + return [ + "", // Widget name - already passed as CLI arg + options.description, + options.organization ?? DEFAULT_WIDGET_OPTIONS.organization, + "© Mendix Technology BV 2025", // Copyright + options.license, + options.version, + options.author, + "../", // Project path (relative to widget folder inside generations/) + "", // Programming language - Enter for TypeScript (default) + "", // Component type - Enter for Function Components (default) + "", // Platform - Enter for web (default) + options.template ?? DEFAULT_WIDGET_OPTIONS.template, + options.unitTests !== false ? "yes" : "no", + options.e2eTests === true ? "yes" : "no" + ]; +} + +/** + * Calculates progress percentage for a given prompt index. + */ +export function calculatePromptProgress(promptIndex: number): number { + const progressRange = SCAFFOLD_PROGRESS.PROMPTS_END - SCAFFOLD_PROGRESS.PROMPTS_START; + const promptProgress = (promptIndex / GENERATOR_PROMPTS.length) * progressRange; + return Math.round(SCAFFOLD_PROGRESS.PROMPTS_START + promptProgress); +} + +/** + * Removes ANSI escape codes and spinner characters from terminal output. + */ +export function cleanTerminalOutput(data: string): string { + return ( + data + // eslint-disable-next-line no-control-regex -- Intentionally matching ANSI escape sequences + .replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "") + .replace(/[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]/g, "") + .replace(/[\r\n]+/g, " ") + .replace(/\[[\dD\dC\dK\dG]+/g, "") + .trim() + ); +} + +/** + * Handles generator output and sends answers when prompts are detected. + */ +function handleGeneratorOutput( + state: GeneratorLocalState, + tracker: ProgressTracker, + sendNextAnswer: () => void, + onAllPromptsAnswered: () => void +): void { + if (state.answerIndex < GENERATOR_PROMPTS.length) { + // Skip if we've already matched this prompt + if (state.promptMatchedIndex >= state.answerIndex) { + return; + } + + const expectedPattern = GENERATOR_PROMPTS[state.answerIndex]; + const recentOutput = state.output.slice(-500).toLowerCase(); + + if (recentOutput.includes(expectedPattern.toLowerCase())) { + state.promptMatchedIndex = state.answerIndex; + tracker.updateStep(expectedPattern, state.answerIndex + 1); + + const progress = calculatePromptProgress(state.answerIndex + 1); + const message = `Configuring: ${expectedPattern}`; + + tracker.progress(progress, message).catch(() => undefined); + tracker + .info(message, { + step: expectedPattern, + promptIndex: state.answerIndex + 1, + totalPrompts: GENERATOR_PROMPTS.length + }) + .catch(() => undefined); + + setTimeout(sendNextAnswer, 150); + } + } else { + onAllPromptsAnswered(); + } +} + +/** + * Runs the Mendix widget generator using node-pty for terminal interaction. + */ +export function runWidgetGenerator(options: WidgetOptions, tracker: ProgressTracker): Promise { + const answers = buildGeneratorAnswers(options); + + return new Promise((resolve, reject) => { + const state: GeneratorLocalState = { + output: "", + answerIndex: 0, + promptMatchedIndex: -1, + allPromptsAnswered: false + }; + + tracker.start("initializing"); + + const ptyProcess = pty.spawn("npx", ["@mendix/generator-widget", options.name], { + name: "xterm-color", + cols: 120, + rows: 30, + cwd: GENERATIONS_DIR, + env: { ...process.env, FORCE_COLOR: "0" } + }); + + const sendNextAnswer = (): void => { + if (state.answerIndex < answers.length) { + const answer = answers[state.answerIndex]; + const displayAnswer = answer === "" ? "(Enter)" : `"${answer}"`; + const idx = state.answerIndex + 1; + console.error(`[create-widget] [${idx}/${answers.length}] Sending: ${displayAnswer}`); + state.answerIndex++; + ptyProcess.write(answer + "\r"); + } + }; + + ptyProcess.onData(data => { + state.output += data; + handleGeneratorOutput(state, tracker, sendNextAnswer, () => { + if (!state.allPromptsAnswered) { + state.allPromptsAnswered = true; + tracker.updateStep("installing", GENERATOR_PROMPTS.length); + tracker.markComplete(); + console.error("[create-widget] Installing dependencies..."); + tracker.progress(SCAFFOLD_PROGRESS.INSTALLING, "Installing dependencies...").catch(() => undefined); + tracker.info("Installing dependencies...").catch(() => undefined); + } + }); + }); + + ptyProcess.onExit(({ exitCode }) => { + tracker.stop(); + if (exitCode === 0) { + const widgetFolder = `${options.name.toLowerCase()}-web`; + console.error(`[create-widget] Widget scaffolded successfully: ${widgetFolder}`); + resolve(widgetFolder); + } else { + console.error(`[create-widget] Widget scaffold failed with exit code ${exitCode}`); + const cleanOutput = cleanTerminalOutput(state.output); + tracker + .error(`Scaffold failed with exit code ${exitCode}`, { + lastOutput: cleanOutput.slice(-500) + }) + .catch(() => undefined); + reject(new Error(`Generator exited with code ${exitCode}\nOutput: ${cleanOutput.slice(-2000)}`)); + } + }); + + const timeout = setTimeout(() => { + tracker.stop(); + console.error("[create-widget] Widget scaffold timed out after 5 minutes"); + tracker + .error("Widget scaffold timed out after 5 minutes", { + step: tracker.state.step, + stepIndex: tracker.state.stepIndex + }) + .catch(() => undefined); + ptyProcess.kill(); + reject(new Error("Widget scaffold timed out after 5 minutes")); + }, SCAFFOLD_TIMEOUT_MS); + + ptyProcess.onExit(() => clearTimeout(timeout)); + }); +} diff --git a/packages/pluggable-widgets-mcp/src/tools/utils/notifications.ts b/packages/pluggable-widgets-mcp/src/tools/utils/notifications.ts new file mode 100644 index 0000000000..f214154620 --- /dev/null +++ b/packages/pluggable-widgets-mcp/src/tools/utils/notifications.ts @@ -0,0 +1,36 @@ +import type { LogLevel, ToolContext } from "@/tools/types"; + +/** + * Sends a progress notification to the MCP client. + * Only sends if the client provided a progressToken in the request. + */ +export async function sendProgress(context: ToolContext, progress: number, message?: string): Promise { + const progressToken = context._meta?.progressToken; + if (progressToken) { + await context.sendNotification({ + method: "notifications/progress", + params: { progressToken, progress, total: 100, message } + }); + } +} + +/** + * Sends a logging message notification to the MCP client. + * Works independently of progressToken and provides detailed context. + */ +export async function sendLogMessage( + context: ToolContext, + level: LogLevel, + message: string, + data?: Record, + logger = "mcp-tools" +): Promise { + await context.sendNotification({ + method: "notifications/message", + params: { + level, + logger, + data: { message, ...data } + } + }); +} diff --git a/packages/pluggable-widgets-mcp/src/tools/utils/progress-tracker.ts b/packages/pluggable-widgets-mcp/src/tools/utils/progress-tracker.ts new file mode 100644 index 0000000000..87d74637db --- /dev/null +++ b/packages/pluggable-widgets-mcp/src/tools/utils/progress-tracker.ts @@ -0,0 +1,237 @@ +import type { LogLevel, ToolContext } from "@/tools/types"; +import { sendLogMessage, sendProgress } from "./notifications"; + +/** + * Default timing constants for progress tracking. + */ +const DEFAULT_HEARTBEAT_INTERVAL_MS = 3000; +const DEFAULT_STUCK_WARNING_MS = 10000; + +/** + * Configuration options for the ProgressTracker. + */ +export interface ProgressTrackerOptions { + /** MCP tool context for sending notifications */ + context: ToolContext; + /** Logger name used in notifications/message (e.g., "scaffolding", "build") */ + logger: string; + /** Interval in ms between heartbeat logs (default: 3000) */ + heartbeatIntervalMs?: number; + /** Time in ms before sending a stuck warning (default: 10000) */ + stuckWarningMs?: number; + /** Total number of steps for progress calculation */ + totalSteps?: number; +} + +/** + * Current state snapshot from the tracker. + */ +export interface ProgressTrackerState { + step: string; + stepIndex: number; + elapsedSeconds: number; + isComplete: boolean; +} + +/** + * A reusable progress tracker for MCP tools. + * + * Provides: + * - Heartbeat logging at regular intervals + * - Stuck detection with warnings + * - Convenient logging methods (info, warning, error, debug) + * - Progress notification helpers + * - Step tracking with timing + * + * @example + * ```typescript + * const tracker = new ProgressTracker({ + * context, + * logger: "scaffolding", + * totalSteps: 10 + * }); + * + * tracker.start("initializing"); + * + * // Update step when progressing + * tracker.updateStep("configuring", 1); + * await tracker.info("Configuration started"); + * + * // On completion + * tracker.stop(); + * await tracker.info("Complete!"); + * ``` + */ +export class ProgressTracker { + private readonly context: ToolContext; + private readonly logger: string; + private readonly heartbeatIntervalMs: number; + private readonly stuckWarningMs: number; + private readonly totalSteps: number; + + private startTime: number = 0; + private lastStepTime: number = 0; + private currentStep: string = "idle"; + private currentStepIndex: number = 0; + private stuckWarningShown: boolean = false; + private isComplete: boolean = false; + private heartbeatInterval?: ReturnType; + + constructor(options: ProgressTrackerOptions) { + this.context = options.context; + this.logger = options.logger; + this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS; + this.stuckWarningMs = options.stuckWarningMs ?? DEFAULT_STUCK_WARNING_MS; + this.totalSteps = options.totalSteps ?? 0; + } + + /** + * Starts the progress tracker with optional initial step. + * Begins heartbeat interval for periodic status updates. + */ + start(initialStep = "starting"): void { + this.startTime = Date.now(); + this.lastStepTime = Date.now(); + this.currentStep = initialStep; + this.currentStepIndex = 0; + this.stuckWarningShown = false; + this.isComplete = false; + + this.heartbeatInterval = setInterval(() => { + this.sendHeartbeat(); + this.checkStuckWarning(); + }, this.heartbeatIntervalMs); + } + + /** + * Stops the progress tracker and cleans up intervals. + * Should be called when the operation completes or fails. + */ + stop(): void { + if (this.heartbeatInterval) { + clearInterval(this.heartbeatInterval); + this.heartbeatInterval = undefined; + } + } + + /** + * Updates the current step being tracked. + * Resets the stuck warning timer. + */ + updateStep(step: string, index?: number): void { + this.currentStep = step; + if (index !== undefined) { + this.currentStepIndex = index; + } + this.lastStepTime = Date.now(); + this.stuckWarningShown = false; + } + + /** + * Marks the tracker as complete. + * Subsequent stuck warnings will not be sent. + */ + markComplete(): void { + this.isComplete = true; + } + + /** + * Gets the elapsed time in seconds since start. + */ + get elapsedSeconds(): number { + if (this.startTime === 0) { + return 0; + } + return Math.round((Date.now() - this.startTime) / 1000); + } + + /** + * Gets the current state snapshot. + */ + get state(): ProgressTrackerState { + return { + step: this.currentStep, + stepIndex: this.currentStepIndex, + elapsedSeconds: this.elapsedSeconds, + isComplete: this.isComplete + }; + } + + /** + * Sends a log message with the specified level. + */ + async log(level: LogLevel, message: string, data?: Record): Promise { + await sendLogMessage(this.context, level, message, data, this.logger); + } + + /** + * Sends an info-level log message. + */ + async info(message: string, data?: Record): Promise { + await this.log("info", message, data); + } + + /** + * Sends a warning-level log message. + */ + async warning(message: string, data?: Record): Promise { + await this.log("warning", message, data); + } + + /** + * Sends an error-level log message. + */ + async error(message: string, data?: Record): Promise { + await this.log("error", message, data); + } + + /** + * Sends a debug-level log message. + */ + async debug(message: string, data?: Record): Promise { + await this.log("debug", message, data); + } + + /** + * Sends a progress notification to the client. + */ + async progress(value: number, message?: string): Promise { + await sendProgress(this.context, value, message); + } + + /** + * Sends a heartbeat debug log with current status. + */ + private sendHeartbeat(): void { + const elapsed = this.elapsedSeconds; + const stepInfo = this.totalSteps > 0 ? ` [${this.currentStepIndex}/${this.totalSteps}]` : ""; + + this.debug(`In progress...${stepInfo} (${elapsed}s elapsed)`, { + step: this.currentStep, + stepIndex: this.currentStepIndex, + totalSteps: this.totalSteps, + elapsedSeconds: elapsed + }).catch(() => undefined); + } + + /** + * Checks if the current step has exceeded the stuck warning threshold. + */ + private checkStuckWarning(): void { + if (this.isComplete || this.stuckWarningShown) { + return; + } + + const timeSinceLastStep = Date.now() - this.lastStepTime; + if (timeSinceLastStep > this.stuckWarningMs) { + this.stuckWarningShown = true; + const waitingSec = Math.round(timeSinceLastStep / 1000); + + this.warning(`Waiting for response (step: ${this.currentStep}, ${waitingSec}s elapsed)`, { + step: this.currentStep, + stepIndex: this.currentStepIndex, + waitingSeconds: waitingSec + }).catch(() => undefined); + } + } +} diff --git a/packages/pluggable-widgets-mcp/src/tools/utils/response.ts b/packages/pluggable-widgets-mcp/src/tools/utils/response.ts new file mode 100644 index 0000000000..7ac521fc25 --- /dev/null +++ b/packages/pluggable-widgets-mcp/src/tools/utils/response.ts @@ -0,0 +1,19 @@ +import type { ToolResponse } from "@/tools/types"; + +/** + * Creates a successful tool response with text content. + */ +export function createToolResponse(text: string): ToolResponse { + return { + content: [{ type: "text", text }] + }; +} + +/** + * Creates an error tool response with a message. + */ +export function createErrorResponse(message: string): ToolResponse { + return { + content: [{ type: "text", text: message }] + }; +} diff --git a/packages/pluggable-widgets-mcp/tsconfig.json b/packages/pluggable-widgets-mcp/tsconfig.json new file mode 100644 index 0000000000..91fd5af0b7 --- /dev/null +++ b/packages/pluggable-widgets-mcp/tsconfig.json @@ -0,0 +1,34 @@ +{ + "include": ["./src", "config.ts"], + "compilerOptions": { + "lib": ["ES2022"], + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, + "module": "esnext", + "moduleResolution": "node", + "target": "ES2022", + "outDir": "./dist", + "rootDir": "./src", + "types": ["node"], + "strict": true, + "noEmitOnError": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedParameters": true, + "noUnusedLocals": true, + "forceConsistentCasingInFileNames": true, + "esModuleInterop": true, + "declaration": true, + "sourceMap": true, + "declarationMap": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "useUnknownInCatchVariables": false, + "exactOptionalPropertyTypes": false + }, + "exclude": ["node_modules", "dist"] +} diff --git a/packages/pluggable-widgets-mcp/tscpaths.json b/packages/pluggable-widgets-mcp/tscpaths.json new file mode 100644 index 0000000000..b8f0327956 --- /dev/null +++ b/packages/pluggable-widgets-mcp/tscpaths.json @@ -0,0 +1,4 @@ +{ + "resolveFullPaths": true, + "resolveFullExtension": ".js" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b4e4374c2d..8c4c053a3f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4043,10 +4043,6 @@ packages: resolution: {integrity: sha512-44F4l4Enf+MirJN8X/NhdGkl71k5rBYiwdVlo4HxOwbu0sHV8QKrGEedb1VUU4K3W7fBKE0HGfbn7eZm0Ti3zg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/diff-sequences@30.0.1': - resolution: {integrity: sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/environment@29.7.0': resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4055,10 +4051,6 @@ packages: resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - '@jest/expect-utils@30.2.0': - resolution: {integrity: sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/expect@29.7.0': resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4067,10 +4059,6 @@ packages: resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - '@jest/get-type@30.1.0': - resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/globals@29.7.0': resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4878,9 +4866,6 @@ packages: '@types/jest@29.4.0': resolution: {integrity: sha512-VaywcGQ9tPorCX/Jkkni7RWGFfI11whqzs8dvxF41P17Z+z872thvEvlIbznjPJ02kl1HMX3LmLOonsj2n7HeQ==} - '@types/jest@30.0.0': - resolution: {integrity: sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==} - '@types/js-beautify@1.14.3': resolution: {integrity: sha512-FMbQHz+qd9DoGvgLHxeqqVPaNRffpIu5ZjozwV8hf9JAGpIOzuAf4wGbRSo8LNITHqGjmmVjaMggTT5P4v4IHg==} @@ -6877,10 +6862,6 @@ packages: resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - expect@30.2.0: - resolution: {integrity: sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - exponential-backoff@3.1.3: resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} @@ -7801,10 +7782,6 @@ packages: resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - jest-diff@30.2.0: - resolution: {integrity: sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-docblock@29.7.0: resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -7850,26 +7827,14 @@ packages: resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - jest-matcher-utils@30.2.0: - resolution: {integrity: sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-message-util@29.7.0: resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - jest-message-util@30.2.0: - resolution: {integrity: sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-mock@29.7.0: resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - jest-mock@30.2.0: - resolution: {integrity: sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-pnp-resolver@1.2.3: resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} engines: {node: '>=6'} @@ -9196,10 +9161,6 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - pretty-format@30.2.0: - resolution: {integrity: sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - pretty-quick@4.2.2: resolution: {integrity: sha512-uAh96tBW1SsD34VhhDmWuEmqbpfYc/B3j++5MC/6b3Cb8Ow7NJsvKFhg0eoGu2xXX+o9RkahkTK6sUdd8E7g5w==} engines: {node: '>=14'} @@ -12183,8 +12144,6 @@ snapshots: dependencies: '@jest/types': 30.2.0 - '@jest/diff-sequences@30.0.1': {} - '@jest/environment@29.7.0': dependencies: '@jest/fake-timers': 29.7.0 @@ -12196,10 +12155,6 @@ snapshots: dependencies: jest-get-type: 29.6.3 - '@jest/expect-utils@30.2.0': - dependencies: - '@jest/get-type': 30.1.0 - '@jest/expect@29.7.0': dependencies: expect: 29.7.0 @@ -12216,8 +12171,6 @@ snapshots: jest-mock: 29.7.0 jest-util: 29.7.0 - '@jest/get-type@30.1.0': {} - '@jest/globals@29.7.0': dependencies: '@jest/environment': 29.7.0 @@ -13214,11 +13167,6 @@ snapshots: expect: 29.7.0 pretty-format: 29.7.0 - '@types/jest@30.0.0': - dependencies: - expect: 30.2.0 - pretty-format: 30.2.0 - '@types/js-beautify@1.14.3': {} '@types/jsdom@20.0.1': @@ -13349,7 +13297,7 @@ snapshots: '@types/testing-library__jest-dom@5.14.9': dependencies: - '@types/jest': 30.0.0 + '@types/jest': 29.4.0 '@types/tough-cookie@4.0.5': {} @@ -14308,7 +14256,8 @@ snapshots: ci-info@3.9.0: {} - ci-info@4.3.1: {} + ci-info@4.3.1: + optional: true cjs-module-lexer@1.4.3: {} @@ -15638,15 +15587,6 @@ snapshots: jest-message-util: 29.7.0 jest-util: 29.7.0 - expect@30.2.0: - dependencies: - '@jest/expect-utils': 30.2.0 - '@jest/get-type': 30.1.0 - jest-matcher-utils: 30.2.0 - jest-message-util: 30.2.0 - jest-mock: 30.2.0 - jest-util: 30.2.0 - exponential-backoff@3.1.3: {} ext@1.7.0: @@ -16700,13 +16640,6 @@ snapshots: jest-get-type: 29.6.3 pretty-format: 29.7.0 - jest-diff@30.2.0: - dependencies: - '@jest/diff-sequences': 30.0.1 - '@jest/get-type': 30.1.0 - chalk: 4.1.2 - pretty-format: 30.2.0 - jest-docblock@29.7.0: dependencies: detect-newline: 3.1.0 @@ -16802,13 +16735,6 @@ snapshots: jest-get-type: 29.6.3 pretty-format: 29.7.0 - jest-matcher-utils@30.2.0: - dependencies: - '@jest/get-type': 30.1.0 - chalk: 4.1.2 - jest-diff: 30.2.0 - pretty-format: 30.2.0 - jest-message-util@29.7.0: dependencies: '@babel/code-frame': 7.27.1 @@ -16821,30 +16747,12 @@ snapshots: slash: 3.0.0 stack-utils: 2.0.6 - jest-message-util@30.2.0: - dependencies: - '@babel/code-frame': 7.27.1 - '@jest/types': 30.2.0 - '@types/stack-utils': 2.0.3 - chalk: 4.1.2 - graceful-fs: 4.2.11 - micromatch: 4.0.8 - pretty-format: 30.2.0 - slash: 3.0.0 - stack-utils: 2.0.6 - jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 '@types/node': 22.14.1 jest-util: 29.7.0 - jest-mock@30.2.0: - dependencies: - '@jest/types': 30.2.0 - '@types/node': 22.14.1 - jest-util: 30.2.0 - jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): optionalDependencies: jest-resolve: 29.7.0 @@ -16971,6 +16879,7 @@ snapshots: ci-info: 4.3.1 graceful-fs: 4.2.11 picomatch: 4.0.3 + optional: true jest-validate@29.7.0: dependencies: @@ -18487,12 +18396,6 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 - pretty-format@30.2.0: - dependencies: - '@jest/schemas': 30.0.5 - ansi-styles: 5.2.0 - react-is: 18.3.1 - pretty-quick@4.2.2(prettier@3.5.3): dependencies: '@pkgr/core': 0.2.9 From da170cc9f3befed8b390b867479167e25a74da09 Mon Sep 17 00:00:00 2001 From: Rahman Date: Wed, 17 Dec 2025 23:00:46 +0100 Subject: [PATCH 02/10] refactor(pluggable-widgets-mcp): fix prompt timeout, refactor schema, add custom dir --- packages/pluggable-widgets-mcp/package.json | 2 +- .../src/tools/scaffolding.tools.ts | 94 +++++--------- .../pluggable-widgets-mcp/src/tools/types.ts | 96 +++++++++++++-- .../src/tools/utils/generator.ts | 115 +++++++++++++----- 4 files changed, 208 insertions(+), 99 deletions(-) diff --git a/packages/pluggable-widgets-mcp/package.json b/packages/pluggable-widgets-mcp/package.json index b23cdf64bc..ce8f39a6fd 100644 --- a/packages/pluggable-widgets-mcp/package.json +++ b/packages/pluggable-widgets-mcp/package.json @@ -37,6 +37,6 @@ "keywords": [], "packageManager": "pnpm@10.17.0", "engines": { - "node": ">=22" + "node": ">=20" } } diff --git a/packages/pluggable-widgets-mcp/src/tools/scaffolding.tools.ts b/packages/pluggable-widgets-mcp/src/tools/scaffolding.tools.ts index 962a8a7282..59f35930c9 100644 --- a/packages/pluggable-widgets-mcp/src/tools/scaffolding.tools.ts +++ b/packages/pluggable-widgets-mcp/src/tools/scaffolding.tools.ts @@ -1,70 +1,27 @@ import { mkdir } from "node:fs/promises"; import { z } from "zod"; import { GENERATIONS_DIR } from "@/config"; -import type { ToolContext, ToolDefinition, ToolResponse } from "@/tools/types"; import { - buildWidgetOptions, DEFAULT_WIDGET_OPTIONS, - GENERATOR_PROMPTS, - runWidgetGenerator, - SCAFFOLD_PROGRESS -} from "@/tools/utils/generator"; + widgetOptionsSchema, + type ToolContext, + type ToolDefinition, + type ToolResponse +} from "@/tools/types"; +import { buildWidgetOptions, GENERATOR_PROMPTS, runWidgetGenerator, SCAFFOLD_PROGRESS } from "@/tools/utils/generator"; import { ProgressTracker } from "@/tools/utils/progress-tracker"; import { createErrorResponse, createToolResponse } from "@/tools/utils/response"; -const createWidgetSchema = z.object({ - name: z +/** + * Schema for create-widget tool input. + * Extends the base widgetOptionsSchema with tool-specific options like outputPath. + */ +const createWidgetSchema = widgetOptionsSchema.extend({ + outputPath: z .string() - .min(1) - .max(100) - .describe("[REQUIRED] The name of the widget in PascalCase (e.g., 'MyAwesomeWidget', 'DataChart')"), - description: z.string().min(1).max(200).describe("[REQUIRED] A brief description of what the widget does"), - version: z - .string() - .regex(/^\d+\.\d+\.\d+$/, "Version must be in semver format: x.y.z") - .optional() - .describe(`[OPTIONAL] Initial version in semver format. Default: "${DEFAULT_WIDGET_OPTIONS.version}"`), - author: z - .string() - .min(1) - .max(100) - .optional() - .describe(`[OPTIONAL] Author name. Default: "${DEFAULT_WIDGET_OPTIONS.author}"`), - license: z - .string() - .min(1) - .max(50) - .optional() - .describe(`[OPTIONAL] License type. Default: "${DEFAULT_WIDGET_OPTIONS.license}"`), - organization: z - .string() - .min(1) - .max(100) - .optional() - .describe( - `[OPTIONAL] Organization name for the widget namespace. Default: "${DEFAULT_WIDGET_OPTIONS.organization}"` - ), - template: z - .enum(["full", "empty"]) - .optional() - .describe( - `[OPTIONAL] Widget template: "full" includes sample code and examples, "empty" is minimal/blank. Default: "${DEFAULT_WIDGET_OPTIONS.template}"` - ), - programmingLanguage: z - .enum(["typescript", "javascript"]) - .optional() - .describe( - `[OPTIONAL] Programming language for the widget source code. Default: "${DEFAULT_WIDGET_OPTIONS.programmingLanguage}"` - ), - unitTests: z - .boolean() - .optional() - .describe(`[OPTIONAL] Include unit test setup with Jest. Default: ${DEFAULT_WIDGET_OPTIONS.unitTests}`), - e2eTests: z - .boolean() .optional() .describe( - `[OPTIONAL] Include end-to-end test setup with Playwright. Default: ${DEFAULT_WIDGET_OPTIONS.e2eTests}` + "[OPTIONAL] Directory where widget will be created. Defaults to ./generations/ within the MCP server package." ) }); @@ -87,9 +44,22 @@ OPTIONAL (with defaults): • programmingLanguage: "typescript" or "javascript" (default: "${DEFAULT_WIDGET_OPTIONS.programmingLanguage}") • unitTests: Include Jest test setup (default: ${DEFAULT_WIDGET_OPTIONS.unitTests}) • e2eTests: Include Playwright E2E tests (default: ${DEFAULT_WIDGET_OPTIONS.e2eTests}) + • outputPath: Directory where widget will be created (default: ./generations/) Ask the user if they want to customize any options before proceeding.`; +/** + * Returns scaffolding-related tools for widget creation and management. + * + * Currently contains only the create-widget tool, but structured as an array + * for extensibility. This modular pattern allows easy addition of related tools + * such as: + * - Widget property editing + * - XML configuration management + * - Build and deployment automation + * + * @see AGENTS.md Roadmap Context section for planned additions + */ export function getScaffoldingTools(): Array> { return [ { @@ -104,6 +74,7 @@ export function getScaffoldingTools(): Array> async function handleCreateWidget(args: CreateWidgetInput, context: ToolContext): Promise { const options = buildWidgetOptions(args); + const outputDir = args.outputPath ?? GENERATIONS_DIR; const tracker = new ProgressTracker({ context, logger: "scaffolding", @@ -116,14 +87,15 @@ async function handleCreateWidget(args: CreateWidgetInput, context: ToolContext) await tracker.info(`Starting widget scaffolding for "${options.name}"...`, { widgetName: options.name, template: options.template, - organization: options.organization + organization: options.organization, + outputDir }); - // Ensure generations directory exists - await mkdir(GENERATIONS_DIR, { recursive: true }); + // Ensure output directory exists + await mkdir(outputDir, { recursive: true }); - const widgetFolder = await runWidgetGenerator(options, tracker); - const widgetPath = `${GENERATIONS_DIR}/${widgetFolder}`; + const widgetFolder = await runWidgetGenerator(options, tracker, outputDir); + const widgetPath = `${outputDir}/${widgetFolder}`; console.error(`[create-widget] Widget created successfully at ${widgetPath}`); await tracker.progress(SCAFFOLD_PROGRESS.COMPLETE, "Widget created successfully!"); diff --git a/packages/pluggable-widgets-mcp/src/tools/types.ts b/packages/pluggable-widgets-mcp/src/tools/types.ts index 65b1e252b8..87e80083c7 100644 --- a/packages/pluggable-widgets-mcp/src/tools/types.ts +++ b/packages/pluggable-widgets-mcp/src/tools/types.ts @@ -1,6 +1,6 @@ import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js"; import type { ServerNotification, ServerRequest } from "@modelcontextprotocol/sdk/types.js"; -import type { ZodType } from "zod"; +import { z, type ZodType } from "zod"; // ============================================================================= // MCP Core Types @@ -53,7 +53,89 @@ export type LogLevel = "debug" | "info" | "notice" | "warning" | "error"; // ============================================================================= /** - * Options for creating a new Mendix pluggable widget. + * Default values for widget options. + * Centralized here to be used by both schema descriptions and buildWidgetOptions(). + */ +export const DEFAULT_WIDGET_OPTIONS = { + version: "1.0.0", + author: "Mendix", + license: "Apache-2.0", + organization: "Mendix", + template: "empty" as const, + programmingLanguage: "typescript" as const, + unitTests: true, + e2eTests: false +} as const; + +/** + * Zod schema for widget creation options. + * Single source of truth for widget options - type is derived via z.infer. + */ +export const widgetOptionsSchema = z.object({ + name: z + .string() + .min(1) + .max(100) + .describe("[REQUIRED] The name of the widget in PascalCase (e.g., 'MyAwesomeWidget', 'DataChart')"), + description: z.string().min(1).max(200).describe("[REQUIRED] A brief description of what the widget does"), + version: z + .string() + .regex(/^\d+\.\d+\.\d+$/, "Version must be in semver format: x.y.z") + .optional() + .describe(`[OPTIONAL] Initial version in semver format. Default: "${DEFAULT_WIDGET_OPTIONS.version}"`), + author: z + .string() + .min(1) + .max(100) + .optional() + .describe(`[OPTIONAL] Author name. Default: "${DEFAULT_WIDGET_OPTIONS.author}"`), + license: z + .string() + .min(1) + .max(50) + .optional() + .describe(`[OPTIONAL] License type. Default: "${DEFAULT_WIDGET_OPTIONS.license}"`), + organization: z + .string() + .min(1) + .max(100) + .optional() + .describe( + `[OPTIONAL] Organization name for the widget namespace. Default: "${DEFAULT_WIDGET_OPTIONS.organization}"` + ), + template: z + .enum(["full", "empty"]) + .optional() + .describe( + `[OPTIONAL] Widget template: "full" includes sample code and examples, "empty" is minimal/blank. Default: "${DEFAULT_WIDGET_OPTIONS.template}"` + ), + programmingLanguage: z + .enum(["typescript", "javascript"]) + .optional() + .describe( + `[OPTIONAL] Programming language for the widget source code. Default: "${DEFAULT_WIDGET_OPTIONS.programmingLanguage}"` + ), + unitTests: z + .boolean() + .optional() + .describe(`[OPTIONAL] Include unit test setup with Jest. Default: ${DEFAULT_WIDGET_OPTIONS.unitTests}`), + e2eTests: z + .boolean() + .optional() + .describe( + `[OPTIONAL] Include end-to-end test setup with Playwright. Default: ${DEFAULT_WIDGET_OPTIONS.e2eTests}` + ) +}); + +/** + * Input options for creating a new Mendix pluggable widget (with optional fields). + * Derived from widgetOptionsSchema to ensure type-schema consistency. + */ +export type WidgetOptionsInput = z.infer; + +/** + * Resolved widget options with all defaults applied (all fields required). + * This is the type returned by buildWidgetOptions() after applying defaults. */ export interface WidgetOptions { name: string; @@ -61,9 +143,9 @@ export interface WidgetOptions { version: string; author: string; license: string; - organization?: string; - template?: "full" | "empty"; - programmingLanguage?: "typescript" | "javascript"; - unitTests?: boolean; - e2eTests?: boolean; + organization: string; + template: "full" | "empty"; + programmingLanguage: "typescript" | "javascript"; + unitTests: boolean; + e2eTests: boolean; } diff --git a/packages/pluggable-widgets-mcp/src/tools/utils/generator.ts b/packages/pluggable-widgets-mcp/src/tools/utils/generator.ts index 6fc590b63c..d6152428a4 100644 --- a/packages/pluggable-widgets-mcp/src/tools/utils/generator.ts +++ b/packages/pluggable-widgets-mcp/src/tools/utils/generator.ts @@ -1,8 +1,11 @@ import * as pty from "node-pty"; import { GENERATIONS_DIR, SCAFFOLD_TIMEOUT_MS } from "@/config"; -import type { WidgetOptions } from "@/tools/types"; +import { DEFAULT_WIDGET_OPTIONS, type WidgetOptions, type WidgetOptionsInput } from "@/tools/types"; import { ProgressTracker } from "./progress-tracker"; +// Re-export for backward compatibility with existing imports +export { DEFAULT_WIDGET_OPTIONS }; + /** * Generator prompt patterns in order - must match answers array. */ @@ -35,18 +38,15 @@ export const SCAFFOLD_PROGRESS = { } as const; /** - * Default values for widget options. + * Buffer size for prompt detection in terminal output. + * Increased from 500 to improve reliability with terminal buffering. */ -export const DEFAULT_WIDGET_OPTIONS = { - version: "1.0.0", - author: "Mendix", - license: "Apache-2.0", - organization: "Mendix", - template: "empty" as const, - programmingLanguage: "typescript" as const, - unitTests: true, - e2eTests: false -} as const; +const PROMPT_DETECTION_BUFFER_SIZE = 1000; + +/** + * Delay between sending answers to allow terminal to process. + */ +const ANSWER_SEND_DELAY_MS = 200; /** * Local state for tracking generator process progress. @@ -56,14 +56,15 @@ interface GeneratorLocalState { answerIndex: number; promptMatchedIndex: number; allPromptsAnswered: boolean; + lastActivityTime: number; } /** * Builds widget options from input arguments with defaults applied. + * Takes the schema-validated input (with optional fields) and returns + * fully resolved options (all fields required). */ -export function buildWidgetOptions( - args: Partial & Pick -): WidgetOptions { +export function buildWidgetOptions(args: WidgetOptionsInput): WidgetOptions { return { name: args.name, description: args.description, @@ -72,31 +73,49 @@ export function buildWidgetOptions( license: args.license ?? DEFAULT_WIDGET_OPTIONS.license, organization: args.organization ?? DEFAULT_WIDGET_OPTIONS.organization, template: args.template ?? DEFAULT_WIDGET_OPTIONS.template, - programmingLanguage: DEFAULT_WIDGET_OPTIONS.programmingLanguage, + programmingLanguage: args.programmingLanguage ?? DEFAULT_WIDGET_OPTIONS.programmingLanguage, unitTests: args.unitTests ?? DEFAULT_WIDGET_OPTIONS.unitTests, e2eTests: args.e2eTests ?? DEFAULT_WIDGET_OPTIONS.e2eTests }; } +/** + * Arrow key escape sequence for navigating interactive prompts. + */ +const ARROW_DOWN = "\x1b[B"; + +/** + * Maps programming language option to the key sequence needed. + * TypeScript is the first option (just Enter), JavaScript needs arrow down first. + */ +function getLanguageKeySequence(language: "typescript" | "javascript"): string { + return language === "javascript" ? ARROW_DOWN : ""; +} + /** * Builds the answers array for the generator prompts. + * @param options - Fully resolved widget options (all fields required) + * @param outputDir - Output directory (used for project path calculation) */ -export function buildGeneratorAnswers(options: WidgetOptions): string[] { +export function buildGeneratorAnswers(options: WidgetOptions, outputDir?: string): string[] { + // Calculate relative project path from widget folder to parent directory + const projectPath = outputDir ? "../" : "../"; + return [ "", // Widget name - already passed as CLI arg options.description, - options.organization ?? DEFAULT_WIDGET_OPTIONS.organization, + options.organization, "© Mendix Technology BV 2025", // Copyright options.license, options.version, options.author, - "../", // Project path (relative to widget folder inside generations/) - "", // Programming language - Enter for TypeScript (default) + projectPath, // Project path (relative to widget folder) + getLanguageKeySequence(options.programmingLanguage), // Programming language selection "", // Component type - Enter for Function Components (default) "", // Platform - Enter for web (default) - options.template ?? DEFAULT_WIDGET_OPTIONS.template, - options.unitTests !== false ? "yes" : "no", - options.e2eTests === true ? "yes" : "no" + options.template, + options.unitTests ? "yes" : "no", + options.e2eTests ? "yes" : "no" ]; } @@ -126,6 +145,7 @@ export function cleanTerminalOutput(data: string): string { /** * Handles generator output and sends answers when prompts are detected. + * Uses a larger buffer and improved logging for reliability. */ function handleGeneratorOutput( state: GeneratorLocalState, @@ -133,6 +153,9 @@ function handleGeneratorOutput( sendNextAnswer: () => void, onAllPromptsAnswered: () => void ): void { + // Update activity timestamp for stuck detection + state.lastActivityTime = Date.now(); + if (state.answerIndex < GENERATOR_PROMPTS.length) { // Skip if we've already matched this prompt if (state.promptMatchedIndex >= state.answerIndex) { @@ -140,7 +163,7 @@ function handleGeneratorOutput( } const expectedPattern = GENERATOR_PROMPTS[state.answerIndex]; - const recentOutput = state.output.slice(-500).toLowerCase(); + const recentOutput = state.output.slice(-PROMPT_DETECTION_BUFFER_SIZE).toLowerCase(); if (recentOutput.includes(expectedPattern.toLowerCase())) { state.promptMatchedIndex = state.answerIndex; @@ -158,7 +181,15 @@ function handleGeneratorOutput( }) .catch(() => undefined); - setTimeout(sendNextAnswer, 150); + setTimeout(sendNextAnswer, ANSWER_SEND_DELAY_MS); + } else { + // Debug logging for unmatched prompts (only log occasionally to avoid spam) + const cleanedRecent = cleanTerminalOutput(recentOutput.slice(-200)); + if (cleanedRecent.length > 0 && state.output.length % 500 < 50) { + console.error( + `[create-widget] Waiting for prompt "${expectedPattern}" (index ${state.answerIndex}), recent: "${cleanedRecent.slice(-100)}"` + ); + } } } else { onAllPromptsAnswered(); @@ -167,16 +198,24 @@ function handleGeneratorOutput( /** * Runs the Mendix widget generator using node-pty for terminal interaction. + * @param options - Widget configuration options + * @param tracker - Progress tracker for notifications + * @param outputDir - Directory where the widget will be created (defaults to GENERATIONS_DIR) */ -export function runWidgetGenerator(options: WidgetOptions, tracker: ProgressTracker): Promise { - const answers = buildGeneratorAnswers(options); +export function runWidgetGenerator( + options: WidgetOptions, + tracker: ProgressTracker, + outputDir: string = GENERATIONS_DIR +): Promise { + const answers = buildGeneratorAnswers(options, outputDir); return new Promise((resolve, reject) => { const state: GeneratorLocalState = { output: "", answerIndex: 0, promptMatchedIndex: -1, - allPromptsAnswered: false + allPromptsAnswered: false, + lastActivityTime: Date.now() }; tracker.start("initializing"); @@ -185,14 +224,15 @@ export function runWidgetGenerator(options: WidgetOptions, tracker: ProgressTrac name: "xterm-color", cols: 120, rows: 30, - cwd: GENERATIONS_DIR, + cwd: outputDir, env: { ...process.env, FORCE_COLOR: "0" } }); const sendNextAnswer = (): void => { if (state.answerIndex < answers.length) { const answer = answers[state.answerIndex]; - const displayAnswer = answer === "" ? "(Enter)" : `"${answer}"`; + const displayAnswer = + answer === "" ? "(Enter)" : answer.startsWith("\x1b") ? "(Arrow+Enter)" : `"${answer}"`; const idx = state.answerIndex + 1; console.error(`[create-widget] [${idx}/${answers.length}] Sending: ${displayAnswer}`); state.answerIndex++; @@ -200,6 +240,19 @@ export function runWidgetGenerator(options: WidgetOptions, tracker: ProgressTrac } }; + // Stuck detection: if no progress for 30 seconds, try resending current answer + const stuckCheckInterval = setInterval(() => { + const timeSinceActivity = Date.now() - state.lastActivityTime; + if (timeSinceActivity > 30000 && !state.allPromptsAnswered && state.answerIndex > 0) { + console.error( + `[create-widget] No progress for ${Math.round(timeSinceActivity / 1000)}s at step ${state.answerIndex}, retrying...` + ); + // Resend Enter to potentially unstick the process + ptyProcess.write("\r"); + state.lastActivityTime = Date.now(); + } + }, 10000); + ptyProcess.onData(data => { state.output += data; handleGeneratorOutput(state, tracker, sendNextAnswer, () => { @@ -215,6 +268,7 @@ export function runWidgetGenerator(options: WidgetOptions, tracker: ProgressTrac }); ptyProcess.onExit(({ exitCode }) => { + clearInterval(stuckCheckInterval); tracker.stop(); if (exitCode === 0) { const widgetFolder = `${options.name.toLowerCase()}-web`; @@ -233,6 +287,7 @@ export function runWidgetGenerator(options: WidgetOptions, tracker: ProgressTrac }); const timeout = setTimeout(() => { + clearInterval(stuckCheckInterval); tracker.stop(); console.error("[create-widget] Widget scaffold timed out after 5 minutes"); tracker From a413b625df64822c979701c5e7651a0f8e36f7c4 Mon Sep 17 00:00:00 2001 From: Rahman Date: Tue, 30 Dec 2025 11:59:54 +0100 Subject: [PATCH 03/10] feat(pluggable-widgets-mcp): wip --- packages/pluggable-widgets-mcp/package.json | 2 +- packages/pluggable-widgets-mcp/src/config.ts | 6 + packages/pluggable-widgets-mcp/src/index.ts | 26 +- .../src/resources/guidelines.ts | 98 ++++++ .../src/resources/index.ts | 44 +++ .../pluggable-widgets-mcp/src/server/http.ts | 10 +- .../src/server/server.ts | 4 +- .../src/tools/file-operations.tools.ts | 281 ++++++++++++++++++ .../pluggable-widgets-mcp/src/tools/index.ts | 6 + .../src/tools/scaffolding.tools.ts | 36 ++- .../src/tools/utils/generator.ts | 37 ++- 11 files changed, 518 insertions(+), 32 deletions(-) create mode 100644 packages/pluggable-widgets-mcp/src/resources/guidelines.ts create mode 100644 packages/pluggable-widgets-mcp/src/resources/index.ts create mode 100644 packages/pluggable-widgets-mcp/src/tools/file-operations.tools.ts diff --git a/packages/pluggable-widgets-mcp/package.json b/packages/pluggable-widgets-mcp/package.json index ce8f39a6fd..4e0dffa324 100644 --- a/packages/pluggable-widgets-mcp/package.json +++ b/packages/pluggable-widgets-mcp/package.json @@ -29,7 +29,7 @@ }, "devDependencies": { "@types/cors": "^2.8.19", - "@types/express": "^5.0.2", + "@types/express": "^5.0.6", "@types/node": "^24.10.1", "tsc-alias": "^1.8.16", "typescript": "^5.9.3" diff --git a/packages/pluggable-widgets-mcp/src/config.ts b/packages/pluggable-widgets-mcp/src/config.ts index 6dceeb505d..8d6e557332 100644 --- a/packages/pluggable-widgets-mcp/src/config.ts +++ b/packages/pluggable-widgets-mcp/src/config.ts @@ -21,5 +21,11 @@ const __dirname = import.meta.dirname ?? dirname(fileURLToPath(import.meta.url)) export const PACKAGE_ROOT = join(__dirname, "../"); export const GENERATIONS_DIR = join(PACKAGE_ROOT, "generations"); +// Path to docs/requirements (relative to monorepo root) +export const DOCS_DIR = join(PACKAGE_ROOT, "../../docs/requirements"); + +// Allowed file extensions for widget file operations +export const ALLOWED_EXTENSIONS = [".tsx", ".ts", ".xml", ".scss", ".css", ".json", ".md", ".editorConfig.ts"]; + // Timeouts export const SCAFFOLD_TIMEOUT_MS = 300000; // 5 minutes diff --git a/packages/pluggable-widgets-mcp/src/index.ts b/packages/pluggable-widgets-mcp/src/index.ts index 553f709399..7ffcf600e7 100644 --- a/packages/pluggable-widgets-mcp/src/index.ts +++ b/packages/pluggable-widgets-mcp/src/index.ts @@ -5,19 +5,15 @@ type TransportMode = "http" | "stdio"; const mode = (process.argv[2] as TransportMode) || "http"; -async function main(): Promise { - switch (mode) { - case "stdio": - await startStdioServer(); - break; - case "http": - default: - await startHttpServer(); - break; - } +switch (mode) { + case "stdio": + startStdioServer().catch(err => { + console.error("Fatal error:", err); + process.exit(1); + }); + break; + case "http": + default: + startHttpServer(); + break; } - -main().catch(err => { - console.error("Fatal error:", err); - process.exit(1); -}); diff --git a/packages/pluggable-widgets-mcp/src/resources/guidelines.ts b/packages/pluggable-widgets-mcp/src/resources/guidelines.ts new file mode 100644 index 0000000000..e88918d001 --- /dev/null +++ b/packages/pluggable-widgets-mcp/src/resources/guidelines.ts @@ -0,0 +1,98 @@ +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; +import { DOCS_DIR } from "@/config"; + +/** + * Definition for a guideline resource. + */ +export interface GuidelineResource { + /** Unique resource name */ + name: string; + /** Resource URI (e.g., mendix://guidelines/frontend) */ + uri: string; + /** Human-readable title */ + title: string; + /** Description of what this guideline covers */ + description: string; + /** Source markdown file name */ + filename: string; +} + +/** + * All available guideline resources. + */ +export const GUIDELINE_RESOURCES: GuidelineResource[] = [ + { + name: "frontend-guidelines", + uri: "mendix://guidelines/frontend", + title: "Frontend Guidelines", + description: "CSS/SCSS styling, naming conventions, component best practices, and Atlas UI integration", + filename: "frontend-guidelines.md" + }, + { + name: "implementation-plan", + uri: "mendix://guidelines/implementation", + title: "Implementation Plan", + description: "Step-by-step guide for creating new widgets, including PR templates and testing requirements", + filename: "implementation-plan.md" + }, + { + name: "app-flow", + uri: "mendix://guidelines/app-flow", + title: "Application Flow", + description: "Complete widget development lifecycle from scaffolding to Studio Pro integration", + filename: "app-flow.md" + }, + { + name: "backend-structure", + uri: "mendix://guidelines/backend-structure", + title: "Backend Structure", + description: + "Widget-to-Mendix runtime integration, data handling with EditableValue/ActionValue, and event management", + filename: "backend-structure.md" + }, + { + name: "tech-stack", + uri: "mendix://guidelines/tech-stack", + title: "Technology Stack", + description: "Core technologies (TypeScript, React, SCSS), monorepo structure, and development tools", + filename: "tech-stack.md" + } +]; + +/** + * Cache for loaded guideline content to avoid repeated file reads. + */ +const guidelineCache = new Map(); + +/** + * Loads the content of a guideline file. + * Caches content after first load for performance. + * + * @param filename - The markdown filename to load + * @returns The file content as a string + */ +export async function loadGuidelineContent(filename: string): Promise { + // Check cache first + if (guidelineCache.has(filename)) { + return guidelineCache.get(filename)!; + } + + const filePath = join(DOCS_DIR, filename); + + try { + const content = await readFile(filePath, "utf-8"); + guidelineCache.set(filename, content); + return content; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to load guideline ${filename}: ${message}`); + } +} + +/** + * Clears the guideline cache. Useful for testing or hot-reloading. + */ +export function clearGuidelineCache(): void { + guidelineCache.clear(); +} diff --git a/packages/pluggable-widgets-mcp/src/resources/index.ts b/packages/pluggable-widgets-mcp/src/resources/index.ts new file mode 100644 index 0000000000..2ef570bfb3 --- /dev/null +++ b/packages/pluggable-widgets-mcp/src/resources/index.ts @@ -0,0 +1,44 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { GUIDELINE_RESOURCES, loadGuidelineContent } from "./guidelines"; + +/** + * Registers all MCP resources with the server. + * + * Resources are read-only data sources that clients can fetch on-demand. + * We expose the Mendix widget development guidelines as resources so LLMs + * can access them when implementing widget functionality. + */ +export function registerResources(server: McpServer): void { + registerGuidelineResources(server); +} + +/** + * Registers guideline documentation as MCP resources. + */ +function registerGuidelineResources(server: McpServer): void { + for (const resource of GUIDELINE_RESOURCES) { + server.registerResource( + resource.name, + resource.uri, + { + title: resource.title, + description: resource.description, + mimeType: "text/markdown" + }, + async uri => { + const content = await loadGuidelineContent(resource.filename); + return { + contents: [ + { + uri: uri.href, + mimeType: "text/markdown", + text: content + } + ] + }; + } + ); + } + + console.error(`[resources] Registered ${GUIDELINE_RESOURCES.length} guideline resources`); +} diff --git a/packages/pluggable-widgets-mcp/src/server/http.ts b/packages/pluggable-widgets-mcp/src/server/http.ts index c1b5fed9c6..0486078512 100644 --- a/packages/pluggable-widgets-mcp/src/server/http.ts +++ b/packages/pluggable-widgets-mcp/src/server/http.ts @@ -8,26 +8,22 @@ import { sessionManager } from "./session"; * Starts the MCP server with HTTP/Streamable transport. * Supports multiple concurrent sessions via Express. */ -export async function startHttpServer(): Promise { +export function startHttpServer(): void { const app = createMcpExpressApp(); app.use(cors()); setupRoutes(app); - app.listen(PORT, () => { + const server = app.listen(PORT, () => { console.log(`[HTTP] MCP Server started on port ${PORT}`); console.log(`[HTTP] Health check: http://localhost:${PORT}/health`); console.log(`[HTTP] MCP endpoint: http://localhost:${PORT}/mcp`); }); - setupGracefulShutdown(); -} - -function setupGracefulShutdown(): void { const shutdown = async (): Promise => { console.log("\n[HTTP] Shutting down server..."); await sessionManager.closeAll(); - process.exit(0); + server.close(() => process.exit(0)); }; process.on("SIGINT", shutdown); diff --git a/packages/pluggable-widgets-mcp/src/server/server.ts b/packages/pluggable-widgets-mcp/src/server/server.ts index 42029e0d8a..44c0ec1f05 100644 --- a/packages/pluggable-widgets-mcp/src/server/server.ts +++ b/packages/pluggable-widgets-mcp/src/server/server.ts @@ -1,9 +1,10 @@ import { SERVER_ICON, SERVER_INSTRUCTIONS, SERVER_NAME, SERVER_VERSION, SERVER_WEBSITE_URL } from "@/config"; +import { registerResources } from "@/resources"; import { getAllTools } from "@/tools"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; /** - * Creates and configures a new MCP server instance with all registered tools. + * Creates and configures a new MCP server instance with all registered tools and resources. */ export function createMcpServer(): McpServer { const server = new McpServer( @@ -25,6 +26,7 @@ export function createMcpServer(): McpServer { ); registerTools(server); + registerResources(server); return server; } diff --git a/packages/pluggable-widgets-mcp/src/tools/file-operations.tools.ts b/packages/pluggable-widgets-mcp/src/tools/file-operations.tools.ts new file mode 100644 index 0000000000..8e865b591c --- /dev/null +++ b/packages/pluggable-widgets-mcp/src/tools/file-operations.tools.ts @@ -0,0 +1,281 @@ +import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises"; +import { dirname, extname, join, resolve } from "node:path"; +import { z } from "zod"; +import { ALLOWED_EXTENSIONS } from "@/config"; +import type { AnyToolDefinition, ToolResponse } from "@/tools/types"; +import { createErrorResponse, createToolResponse } from "@/tools/utils/response"; + +// ============================================================================= +// Path Validation Utilities +// ============================================================================= + +/** + * Validates that a file path is within the allowed widget directory. + * Prevents directory traversal attacks. + * + * @param basePath - The base widget directory path + * @param relativePath - The relative file path to validate + * @returns true if the path is safe, false otherwise + */ +function isPathWithinDirectory(basePath: string, relativePath: string): boolean { + // Resolve both paths to absolute paths + const resolvedBase = resolve(basePath); + const resolvedFull = resolve(basePath, relativePath); + + // Check that the resolved path starts with the base path + // This prevents ../ traversal attacks + return resolvedFull.startsWith(resolvedBase + "/") || resolvedFull === resolvedBase; +} + +/** + * Validates that a file extension is allowed for write operations. + * + * @param filePath - The file path to check + * @returns true if the extension is allowed, false otherwise + */ +function isExtensionAllowed(filePath: string): boolean { + const ext = extname(filePath).toLowerCase(); + // Also allow files without extension (like .gitignore patterns) + // and special config files + if (ext === "") { + const filename = filePath.split("/").pop() || ""; + // Allow common config files without extensions + return ["package", "tsconfig", "eslintrc", ".gitignore", ".prettierrc"].some( + name => filename.includes(name) || filename.startsWith(".") + ); + } + return ALLOWED_EXTENSIONS.includes(ext); +} + +/** + * Validates widget path and file path for security. + * Throws an error if validation fails. + */ +function validatePaths(widgetPath: string, filePath: string, checkExtension = false): void { + // Check for obvious path traversal attempts + if (filePath.includes("..")) { + throw new Error("Path traversal not allowed: '..' detected in file path"); + } + + // Validate path is within widget directory + if (!isPathWithinDirectory(widgetPath, filePath)) { + throw new Error("File path must be within the widget directory"); + } + + // For write operations, check extension + if (checkExtension && !isExtensionAllowed(filePath)) { + throw new Error(`File extension not allowed. Allowed extensions: ${ALLOWED_EXTENSIONS.join(", ")}`); + } +} + +// ============================================================================= +// Schemas +// ============================================================================= + +const listWidgetFilesSchema = z.object({ + widgetPath: z.string().min(1).describe("Absolute path to the widget directory (returned by create-widget tool)") +}); + +const readWidgetFileSchema = z.object({ + widgetPath: z.string().min(1).describe("Absolute path to the widget directory"), + filePath: z + .string() + .min(1) + .describe("Relative path to the file within the widget directory (e.g., 'src/MyWidget.tsx')") +}); + +const writeWidgetFileSchema = z.object({ + widgetPath: z.string().min(1).describe("Absolute path to the widget directory"), + filePath: z + .string() + .min(1) + .describe("Relative path to the file within the widget directory (e.g., 'src/components/MyComponent.tsx')"), + content: z.string().describe("The content to write to the file") +}); + +type ListWidgetFilesInput = z.infer; +type ReadWidgetFileInput = z.infer; +type WriteWidgetFileInput = z.infer; + +// ============================================================================= +// Tool Handlers +// ============================================================================= + +/** + * Recursively lists all files in a directory. + */ +async function listFilesRecursive( + dir: string, + basePath: string, + files: Array<{ path: string; type: string }> = [] +): Promise> { + const entries = await readdir(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = join(dir, entry.name); + const relativePath = fullPath.replace(basePath + "/", ""); + + if (entry.isDirectory()) { + // Skip node_modules and other common non-source directories + if (["node_modules", ".git", "dist", "build"].includes(entry.name)) { + continue; + } + await listFilesRecursive(fullPath, basePath, files); + } else { + const ext = extname(entry.name).toLowerCase(); + files.push({ + path: relativePath, + type: ext || "file" + }); + } + } + + return files; +} + +async function handleListWidgetFiles(args: ListWidgetFilesInput): Promise { + try { + // Verify the directory exists + const stats = await stat(args.widgetPath); + if (!stats.isDirectory()) { + return createErrorResponse(`Path is not a directory: ${args.widgetPath}`); + } + + const files = await listFilesRecursive(args.widgetPath, args.widgetPath); + + // Group files by type for better readability + const byType = files.reduce>((acc, file) => { + const type = file.type || "other"; + if (!acc[type]) acc[type] = []; + acc[type].push(file.path); + return acc; + }, {}); + + const output = [`Widget files in ${args.widgetPath}:`, "", `Total: ${files.length} files`, ""]; + + // Sort types for consistent output + const sortedTypes = Object.keys(byType).sort(); + for (const type of sortedTypes) { + output.push(`${type} files (${byType[type].length}):`); + for (const path of byType[type].sort()) { + output.push(` - ${path}`); + } + output.push(""); + } + + return createToolResponse(output.join("\n")); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return createErrorResponse(`Failed to list widget files: ${message}`); + } +} + +async function handleReadWidgetFile(args: ReadWidgetFileInput): Promise { + try { + validatePaths(args.widgetPath, args.filePath); + + const fullPath = join(args.widgetPath, args.filePath); + const content = await readFile(fullPath, "utf-8"); + + return createToolResponse( + [`File: ${args.filePath}`, `Path: ${fullPath}`, "", "Content:", "```", content, "```"].join("\n") + ); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return createErrorResponse(`Failed to read file: ${message}`); + } +} + +async function handleWriteWidgetFile(args: WriteWidgetFileInput): Promise { + try { + validatePaths(args.widgetPath, args.filePath, true); + + const fullPath = join(args.widgetPath, args.filePath); + + // Ensure parent directory exists + const parentDir = dirname(fullPath); + await mkdir(parentDir, { recursive: true }); + + // Write the file + await writeFile(fullPath, args.content, "utf-8"); + + console.error(`[file-operations] Wrote file: ${fullPath}`); + + return createToolResponse( + [ + `Successfully wrote file: ${args.filePath}`, + `Full path: ${fullPath}`, + `Size: ${args.content.length} characters` + ].join("\n") + ); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return createErrorResponse(`Failed to write file: ${message}`); + } +} + +// ============================================================================= +// Tool Definitions +// ============================================================================= + +const LIST_WIDGET_FILES_DESCRIPTION = `Lists all files in a widget directory. + +Use this tool after scaffolding a widget to understand its structure. +Returns files grouped by type (.tsx, .xml, .scss, etc.). + +Excludes: node_modules, .git, dist, build directories.`; + +const READ_WIDGET_FILE_DESCRIPTION = `Reads the contents of a file from a widget directory. + +Use this to inspect existing code before making modifications. +The file path should be relative to the widget directory. + +Examples: + - src/MyWidget.tsx (main component) + - src/MyWidget.xml (properties definition) + - src/components/Header.tsx (sub-component)`; + +const WRITE_WIDGET_FILE_DESCRIPTION = `Writes content to a file in a widget directory. + +Use this to implement widget functionality after scaffolding. +Creates parent directories if they don't exist. + +IMPORTANT: Follow Mendix widget development guidelines: + - Use TypeScript and React + - Follow Atlas UI styling conventions + - Use proper Mendix API types (EditableValue, ActionValue, etc.) + - Fetch mendix://guidelines/* resources for detailed instructions + +Allowed file types: ${ALLOWED_EXTENSIONS.join(", ")}`; + +/** + * Returns file operation tools for reading and writing widget files. + * + * These tools enable LLMs to implement widget functionality after scaffolding + * by reading existing code and writing new/updated files. + */ +export function getFileOperationTools(): AnyToolDefinition[] { + return [ + { + name: "list-widget-files", + title: "List Widget Files", + description: LIST_WIDGET_FILES_DESCRIPTION, + inputSchema: listWidgetFilesSchema, + handler: handleListWidgetFiles + }, + { + name: "read-widget-file", + title: "Read Widget File", + description: READ_WIDGET_FILE_DESCRIPTION, + inputSchema: readWidgetFileSchema, + handler: handleReadWidgetFile + }, + { + name: "write-widget-file", + title: "Write Widget File", + description: WRITE_WIDGET_FILE_DESCRIPTION, + inputSchema: writeWidgetFileSchema, + handler: handleWriteWidgetFile + } + ]; +} diff --git a/packages/pluggable-widgets-mcp/src/tools/index.ts b/packages/pluggable-widgets-mcp/src/tools/index.ts index e80ee7c511..651692c514 100644 --- a/packages/pluggable-widgets-mcp/src/tools/index.ts +++ b/packages/pluggable-widgets-mcp/src/tools/index.ts @@ -1,13 +1,19 @@ import type { AnyToolDefinition } from "@/tools/types"; +import { getFileOperationTools } from "./file-operations.tools"; import { getScaffoldingTools } from "./scaffolding.tools"; /** * Gets all tool definitions for registration with the MCP server. + * + * Tools are organized by category: + * - Scaffolding: Widget creation (create-widget) + * - File Operations: Read/write widget files (list-widget-files, read-widget-file, write-widget-file) */ export function getAllTools(): AnyToolDefinition[] { const tools: AnyToolDefinition[] = []; tools.push(...getScaffoldingTools()); + tools.push(...getFileOperationTools()); return tools; } diff --git a/packages/pluggable-widgets-mcp/src/tools/scaffolding.tools.ts b/packages/pluggable-widgets-mcp/src/tools/scaffolding.tools.ts index 59f35930c9..71f0f8fe83 100644 --- a/packages/pluggable-widgets-mcp/src/tools/scaffolding.tools.ts +++ b/packages/pluggable-widgets-mcp/src/tools/scaffolding.tools.ts @@ -1,16 +1,16 @@ -import { mkdir } from "node:fs/promises"; -import { z } from "zod"; import { GENERATIONS_DIR } from "@/config"; import { DEFAULT_WIDGET_OPTIONS, - widgetOptionsSchema, type ToolContext, type ToolDefinition, - type ToolResponse + type ToolResponse, + widgetOptionsSchema } from "@/tools/types"; import { buildWidgetOptions, GENERATOR_PROMPTS, runWidgetGenerator, SCAFFOLD_PROGRESS } from "@/tools/utils/generator"; import { ProgressTracker } from "@/tools/utils/progress-tracker"; import { createErrorResponse, createToolResponse } from "@/tools/utils/response"; +import { mkdir } from "node:fs/promises"; +import { z } from "zod"; /** * Schema for create-widget tool input. @@ -110,10 +110,34 @@ async function handleCreateWidget(args: CreateWidgetInput, context: ToolContext) "", `Location: ${widgetPath}`, "", - "Next steps:", + "=== TO IMPLEMENT WIDGET FUNCTIONALITY ===", + "", + "1. FETCH GUIDELINES (MCP Resources):", + " - mendix://guidelines/frontend (CSS/SCSS, Atlas UI, naming conventions)", + " - mendix://guidelines/implementation (step-by-step widget development)", + " - mendix://guidelines/backend-structure (Mendix data API: EditableValue, ActionValue)", + "", + "2. EXPLORE WIDGET STRUCTURE:", + ` Use list-widget-files tool with widgetPath: "${widgetPath}"`, + "", + "3. READ EXISTING CODE:", + ` Use read-widget-file tool to inspect:`, + ` - src/${options.name}.tsx (main component entry point)`, + ` - src/${options.name}.xml (widget properties definition)`, + ` - src/components/ (UI components - create if needed)`, + "", + "4. IMPLEMENT CHANGES:", + ` Use write-widget-file tool to create/update files`, + "", + "=== KEY FILES ===", + `- ${widgetPath}/src/${options.name}.tsx - Main widget component`, + `- ${widgetPath}/src/${options.name}.xml - Properties configuration`, + `- ${widgetPath}/src/${options.name}.editorPreview.tsx - Studio Pro preview`, + "", + "=== BUILD & TEST ===", `1. cd ${widgetPath}`, "2. pnpm install", - "3. pnpm start (to build and watch for changes)", + "3. pnpm start (builds and watches for changes)", "", "The widget will be available in Mendix Studio Pro after syncing the app directory." ].join("\n") diff --git a/packages/pluggable-widgets-mcp/src/tools/utils/generator.ts b/packages/pluggable-widgets-mcp/src/tools/utils/generator.ts index d6152428a4..c791191739 100644 --- a/packages/pluggable-widgets-mcp/src/tools/utils/generator.ts +++ b/packages/pluggable-widgets-mcp/src/tools/utils/generator.ts @@ -1,4 +1,4 @@ -import * as pty from "node-pty"; +import type * as NodePty from "node-pty"; import { GENERATIONS_DIR, SCAFFOLD_TIMEOUT_MS } from "@/config"; import { DEFAULT_WIDGET_OPTIONS, type WidgetOptions, type WidgetOptionsInput } from "@/tools/types"; import { ProgressTracker } from "./progress-tracker"; @@ -143,6 +143,38 @@ export function cleanTerminalOutput(data: string): string { ); } +type NodePtyModule = typeof NodePty; + +async function loadNodePty(): Promise { + try { + // NOTE: node-pty is a native addon. Import it lazily so the MCP server can still start + // in environments where the addon is not available/built (e.g. missing toolchain). + const mod: any = await import("node-pty"); + const pty = (mod?.default ?? mod) as NodePtyModule; + + if (typeof pty?.spawn !== "function") { + throw new Error("node-pty loaded but does not expose spawn()"); + } + + return pty; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + + throw new Error( + [ + "Failed to load `node-pty` (native addon). This is required for the `create-widget` tool.", + "", + "Fix (macOS):", + "- Install Xcode Command Line Tools: `xcode-select --install`", + "- Rebuild the addon: `pnpm -w rebuild node-pty` (or run it from the repo root)", + "- If you're on Node.js 22+, consider upgrading `node-pty` to a version that supports your Node version", + "", + `Original error: ${message}` + ].join("\n") + ); + } +} + /** * Handles generator output and sends answers when prompts are detected. * Uses a larger buffer and improved logging for reliability. @@ -202,11 +234,12 @@ function handleGeneratorOutput( * @param tracker - Progress tracker for notifications * @param outputDir - Directory where the widget will be created (defaults to GENERATIONS_DIR) */ -export function runWidgetGenerator( +export async function runWidgetGenerator( options: WidgetOptions, tracker: ProgressTracker, outputDir: string = GENERATIONS_DIR ): Promise { + const pty = await loadNodePty(); const answers = buildGeneratorAnswers(options, outputDir); return new Promise((resolve, reject) => { From 1278c34b26b4474eca6efc3a05f0dbba6da57316 Mon Sep 17 00:00:00 2001 From: Rahman Date: Wed, 14 Jan 2026 09:45:12 +0100 Subject: [PATCH 04/10] feat(pluggable-widgets-mcp): addition of resources and build tools, default to stdio --- package.json | 1 + packages/pluggable-widgets-mcp/.prettierrc.js | 1 - packages/pluggable-widgets-mcp/README.md | 71 +- .../docs/property-types.md | 588 ++++++++++++ .../docs/widget-patterns.md | 556 +++++++++++ packages/pluggable-widgets-mcp/package.json | 7 +- .../pluggable-widgets-mcp/src/api/handlers.ts | 0 packages/pluggable-widgets-mcp/src/config.ts | 6 +- .../src/generators/types.ts | 161 ++++ .../src/generators/xml-generator.ts | 256 +++++ packages/pluggable-widgets-mcp/src/index.ts | 21 +- .../src/resources/guidelines.ts | 42 +- .../pluggable-widgets-mcp/src/server/http.ts | 10 +- .../src/server/routes.ts | 24 +- .../src/server/server.ts | 23 +- .../src/tools/build.tools.ts | 442 +++++++++ .../src/tools/file-operations.tools.ts | 130 ++- .../pluggable-widgets-mcp/src/tools/index.ts | 26 +- .../src/tools/scaffolding.tools.ts | 65 +- .../pluggable-widgets-mcp/src/tools/types.ts | 21 +- .../src/tools/utils/generator.ts | 14 +- .../src/tools/utils/response.ts | 103 ++ pnpm-lock.yaml | 876 +++++++++++++++++- pnpm-workspace.yaml | 7 +- 24 files changed, 3285 insertions(+), 166 deletions(-) delete mode 100644 packages/pluggable-widgets-mcp/.prettierrc.js create mode 100644 packages/pluggable-widgets-mcp/docs/property-types.md create mode 100644 packages/pluggable-widgets-mcp/docs/widget-patterns.md delete mode 100644 packages/pluggable-widgets-mcp/src/api/handlers.ts create mode 100644 packages/pluggable-widgets-mcp/src/generators/types.ts create mode 100644 packages/pluggable-widgets-mcp/src/generators/xml-generator.ts create mode 100644 packages/pluggable-widgets-mcp/src/tools/build.tools.ts diff --git a/package.json b/package.json index cc027c26c4..15bfe029cf 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "create-gh-release": "turbo run create-gh-release --concurrency 1", "create-translation": "turbo run create-translation", "postinstall": "turbo run agent-rules", + "start:mcp": "pnpm --filter pluggable-widgets-mcp run start", "lint": "turbo run lint --continue --concurrency 1", "prepare": "husky install", "prepare-release": "pnpm --filter @mendix/automation-utils run prepare-release", diff --git a/packages/pluggable-widgets-mcp/.prettierrc.js b/packages/pluggable-widgets-mcp/.prettierrc.js deleted file mode 100644 index 0892704ab0..0000000000 --- a/packages/pluggable-widgets-mcp/.prettierrc.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("@mendix/prettier-config-web-widgets"); diff --git a/packages/pluggable-widgets-mcp/README.md b/packages/pluggable-widgets-mcp/README.md index d3c8baeded..20d3555f3f 100644 --- a/packages/pluggable-widgets-mcp/README.md +++ b/packages/pluggable-widgets-mcp/README.md @@ -8,10 +8,26 @@ A Model Context Protocol (MCP) server that enables AI assistants to scaffold Men ```bash pnpm install +pnpm build # Build the server pnpm start # HTTP mode (default) pnpm start:stdio # STDIO mode ``` +## Global Installation + +For use with MCP clients (Cursor, Claude Desktop, LMStudio), install globally: + +```bash +# Build first +pnpm build + +# Link globally using npm (NOT pnpm - better MCP client compatibility) +npm link + +# Verify installation +which pluggable-widgets-mcp +``` + ## Transport Modes ### HTTP Mode (default) @@ -53,6 +69,21 @@ pnpm start:stdio **_Some client setups like Claude Desktop support STDIO only (for now)_** +**Option 1: Global command (after `npm link`)** + +```json +{ + "mcpServers": { + "pluggable-widgets-mcp": { + "command": "pluggable-widgets-mcp", + "args": ["stdio"] + } + } +} +``` + +**Option 2: Absolute path (more reliable during development)** + ```json { "mcpServers": { @@ -64,6 +95,8 @@ pnpm start:stdio } ``` +> **Note:** After rebuilding the server, you may need to restart/reconnect your MCP client to pick up changes. + ## Available Tools ### create-widget @@ -85,6 +118,34 @@ Scaffolds a new Mendix pluggable widget using `@mendix/generator-widget`. Generated widgets are placed in `generations/` directory within this package. +### File Operation Tools + +| Tool | Description | +| -------------------------- | ------------------------------------------------------------ | +| `list-widget-files` | Lists all files in a widget directory, grouped by type | +| `read-widget-file` | Reads the contents of a file from a widget directory | +| `write-widget-file` | Writes content to a file (creates parent dirs automatically) | +| `batch-write-widget-files` | Writes multiple files atomically | + +**Security:** Path traversal is blocked; only allowed extensions: `.tsx`, `.ts`, `.xml`, `.scss`, `.css`, `.json`, `.md` + +### build-widget + +Builds a widget using `pluggable-widgets-tools`, producing an `.mpk` file. + +| Parameter | Required | Description | +| ------------ | -------- | ------------------------------------- | +| `widgetPath` | Yes | Absolute path to the widget directory | + +Returns structured errors for TypeScript, XML, or dependency issues. + +## Available Resources + +| URI | Description | +| ------------------------------------- | -------------------------------------------------------------------------- | +| `mendix://guidelines/property-types` | Complete reference for all Mendix widget property types | +| `mendix://guidelines/widget-patterns` | Reusable patterns for common widget types (Button, Input, Container, etc.) | + ## Development ```bash @@ -135,13 +196,15 @@ This is useful for verifying tool behavior without needing a full AI client inte ## Roadmap -- [x] Widget scaffolding +- [x] Widget scaffolding (`create-widget`) - [x] HTTP transport - [x] STDIO transport - [x] Progress notifications -- [ ] Widget editing and modification -- [ ] Property management -- [ ] Build and deployment tools +- [x] File operations (list, read, write, batch-write) +- [x] Build tool (`build-widget`) +- [x] Guideline resources (property-types, widget-patterns) +- [ ] Widget property editing (XML manipulation) +- [ ] TypeScript error recovery suggestions ## License diff --git a/packages/pluggable-widgets-mcp/docs/property-types.md b/packages/pluggable-widgets-mcp/docs/property-types.md new file mode 100644 index 0000000000..c2936c7890 --- /dev/null +++ b/packages/pluggable-widgets-mcp/docs/property-types.md @@ -0,0 +1,588 @@ +# Mendix Widget Property Types Reference + +This document defines all available property types for Mendix pluggable widgets. Use this reference when defining properties in the JSON schema for XML generation. + +## Property Definition Schema + +When defining properties for the XML generator, use this JSON structure: + +```json +{ + "key": "propertyName", + "type": "string", + "caption": "Display Caption", + "description": "Optional description shown in Studio Pro", + "required": false, + "defaultValue": "optional default" +} +``` + +--- + +## Basic Types + +### string + +Simple text input. + +```json +{ + "key": "label", + "type": "string", + "caption": "Label", + "description": "Text label for the widget", + "defaultValue": "Click me" +} +``` + +**XML Output:** + +```xml + + Label + Text label for the widget + +``` + +--- + +### boolean + +True/false toggle. + +```json +{ + "key": "showIcon", + "type": "boolean", + "caption": "Show icon", + "description": "Display an icon next to the text", + "defaultValue": true +} +``` + +**XML Output:** + +```xml + + Show icon + Display an icon next to the text + +``` + +--- + +### integer + +Whole number input. + +```json +{ + "key": "maxItems", + "type": "integer", + "caption": "Maximum items", + "description": "Maximum number of items to display", + "defaultValue": 10 +} +``` + +**XML Output:** + +```xml + + Maximum items + Maximum number of items to display + +``` + +--- + +### decimal + +Decimal number input. + +```json +{ + "key": "opacity", + "type": "decimal", + "caption": "Opacity", + "description": "Opacity level (0-1)", + "defaultValue": 0.8 +} +``` + +--- + +## Text Types + +### textTemplate + +Text with parameter substitution. Allows dynamic text with placeholders. + +```json +{ + "key": "legend", + "type": "textTemplate", + "caption": "Legend", + "description": "Text template with parameters", + "required": false +} +``` + +**XML Output:** + +```xml + + Legend + Text template with parameters + +``` + +--- + +### expression + +Dynamic expression that can reference attributes and return computed values. + +```json +{ + "key": "visibleExpression", + "type": "expression", + "caption": "Visible", + "description": "Expression to determine visibility", + "defaultValue": "true" +} +``` + +**With return type:** + +```json +{ + "key": "valueExpression", + "type": "expression", + "caption": "Value", + "returnType": "String" +} +``` + +**XML Output (with returnType):** + +```xml + + Value + + +``` + +--- + +## Action Types + +### action + +Event handler that triggers actions (microflows, nanoflows, etc.). + +```json +{ + "key": "onClick", + "type": "action", + "caption": "On click", + "description": "Action to execute when clicked", + "required": false +} +``` + +**XML Output:** + +```xml + + On click + Action to execute when clicked + +``` + +--- + +## Data Types + +### attribute + +Links to an entity attribute. Must specify allowed attribute types. + +```json +{ + "key": "value", + "type": "attribute", + "caption": "Value", + "description": "Attribute to store the value", + "required": true, + "attributeTypes": ["String"] +} +``` + +**Multiple attribute types:** + +```json +{ + "key": "numberValue", + "type": "attribute", + "attributeTypes": ["Integer", "Decimal", "Long"] +} +``` + +**XML Output:** + +```xml + + Value + Attribute to store the value + + + + +``` + +**Valid attributeTypes:** + +- `String` +- `Integer` +- `Long` +- `Decimal` +- `Boolean` +- `DateTime` +- `Enum` +- `HashString` +- `Binary` +- `AutoNumber` + +--- + +### datasource + +Data source for list-based widgets. + +```json +{ + "key": "dataSource", + "type": "datasource", + "caption": "Data source", + "description": "Source of items to display", + "isList": true, + "required": false +} +``` + +**XML Output:** + +```xml + + Data source + Source of items to display + +``` + +--- + +### association + +Links to an entity association. + +```json +{ + "key": "parent", + "type": "association", + "caption": "Parent association", + "required": false +} +``` + +--- + +### entity + +Entity selector. + +```json +{ + "key": "targetEntity", + "type": "entity", + "caption": "Target entity" +} +``` + +--- + +## Selection Types + +### enumeration + +Dropdown with predefined options. Must include `enumValues` array. + +```json +{ + "key": "alignment", + "type": "enumeration", + "caption": "Alignment", + "defaultValue": "left", + "enumValues": [ + { "key": "left", "caption": "Left" }, + { "key": "center", "caption": "Center" }, + { "key": "right", "caption": "Right" } + ] +} +``` + +**XML Output:** + +```xml + + Alignment + + Left + Center + Right + + +``` + +--- + +### icon + +Icon picker. + +```json +{ + "key": "icon", + "type": "icon", + "caption": "Icon", + "required": false +} +``` + +--- + +### image + +Image picker. + +```json +{ + "key": "image", + "type": "image", + "caption": "Image", + "required": false +} +``` + +--- + +### file + +File selector. + +```json +{ + "key": "document", + "type": "file", + "caption": "Document" +} +``` + +--- + +## Container Types + +### widgets + +Container for child widgets. Used to create widget slots. + +```json +{ + "key": "content", + "type": "widgets", + "caption": "Content", + "description": "Widgets to display inside" +} +``` + +**With datasource reference:** + +```json +{ + "key": "content", + "type": "widgets", + "caption": "Content", + "dataSource": "dataSource" +} +``` + +**XML Output:** + +```xml + + Content + Widgets to display inside + +``` + +--- + +### object + +Complex nested property with sub-properties. Used for repeating structures. + +```json +{ + "key": "columns", + "type": "object", + "caption": "Columns", + "isList": true, + "properties": [ + { + "key": "header", + "type": "textTemplate", + "caption": "Header" + }, + { + "key": "width", + "type": "integer", + "caption": "Width", + "defaultValue": 100 + } + ] +} +``` + +**XML Output:** + +```xml + + Columns + + + + Header + + + Width + + + + +``` + +--- + +## System Properties + +System properties are predefined by Mendix. Reference them by key only. + +```json +{ + "systemProperties": ["Name", "TabIndex", "Visibility"] +} +``` + +**Available system properties:** + +- `Name` - Widget name in Studio Pro +- `TabIndex` - Tab order for accessibility +- `Visibility` - Conditional visibility settings + +**XML Output:** + +```xml + + + + + + + +``` + +--- + +## Property Groups + +Properties can be organized into groups for better Studio Pro UI. + +```json +{ + "propertyGroups": [ + { + "caption": "General", + "properties": ["label", "showIcon"] + }, + { + "caption": "Events", + "properties": ["onClick", "onHover"] + } + ] +} +``` + +--- + +## Full Widget Definition Example + +```json +{ + "name": "TooltipButton", + "description": "A button with tooltip on hover", + "properties": [ + { + "key": "buttonText", + "type": "textTemplate", + "caption": "Button text", + "description": "Text to display on the button" + }, + { + "key": "tooltipText", + "type": "textTemplate", + "caption": "Tooltip text", + "description": "Text to show on hover" + }, + { + "key": "buttonStyle", + "type": "enumeration", + "caption": "Style", + "defaultValue": "primary", + "enumValues": [ + { "key": "primary", "caption": "Primary" }, + { "key": "secondary", "caption": "Secondary" }, + { "key": "danger", "caption": "Danger" } + ] + }, + { + "key": "onClick", + "type": "action", + "caption": "On click", + "required": false + } + ], + "systemProperties": ["Name", "TabIndex", "Visibility"] +} +``` + +--- + +## Type Quick Reference + +| Type | Use Case | Requires | +| -------------- | ---------------- | ----------------------- | +| `string` | Simple text | - | +| `boolean` | Toggle | `defaultValue` | +| `integer` | Whole numbers | - | +| `decimal` | Decimal numbers | - | +| `textTemplate` | Dynamic text | - | +| `expression` | Computed values | `returnType` (optional) | +| `action` | Event handlers | - | +| `attribute` | Entity binding | `attributeTypes` | +| `datasource` | List data | `isList: true` | +| `enumeration` | Dropdown | `enumValues` | +| `widgets` | Child widgets | - | +| `object` | Nested structure | `properties`, `isList` | +| `icon` | Icon picker | - | +| `image` | Image picker | - | +| `association` | Entity relation | - | diff --git a/packages/pluggable-widgets-mcp/docs/widget-patterns.md b/packages/pluggable-widgets-mcp/docs/widget-patterns.md new file mode 100644 index 0000000000..6cf76aa6c4 --- /dev/null +++ b/packages/pluggable-widgets-mcp/docs/widget-patterns.md @@ -0,0 +1,556 @@ +# Mendix Widget Patterns + +This document provides reusable patterns for common widget types. Use these as templates when implementing widget components. + +--- + +## Pattern: Display Widget + +Display widgets show read-only data. Examples: Badge, Progress Bar, Label. + +### Typical Properties + +```json +{ + "properties": [ + { "key": "value", "type": "textTemplate", "caption": "Value" }, + { "key": "type", "type": "enumeration", "caption": "Style", "enumValues": [...] }, + { "key": "onClick", "type": "action", "caption": "On click", "required": false } + ], + "systemProperties": ["Name", "TabIndex", "Visibility"] +} +``` + +### TSX Structure + +```tsx +import { ReactNode, useCallback } from "react"; +import { executeAction } from "@mendix/widget-plugin-platform/framework/execute-action"; +import { MyWidgetContainerProps } from "../typings/MyWidgetProps"; +import "./ui/MyWidget.scss"; + +export default function MyWidget(props: MyWidgetContainerProps): ReactNode { + const { value, type, onClick, tabIndex, class: className, style } = props; + + const handleClick = useCallback(() => { + executeAction(onClick); + }, [onClick]); + + const isClickable = onClick?.canExecute; + + return ( +
+ {value?.value ?? ""} +
+ ); +} +``` + +### SCSS Structure + +```scss +.widget-mywidget { + display: inline-block; + padding: 4px 8px; + border-radius: 4px; + + &-primary { + background-color: var(--brand-primary); + color: white; + } + + &-secondary { + background-color: var(--brand-secondary); + color: white; + } +} +``` + +--- + +## Pattern: Button Widget + +Button widgets trigger actions on click. May include icons, loading states. + +### Typical Properties + +```json +{ + "properties": [ + { "key": "caption", "type": "textTemplate", "caption": "Caption" }, + { "key": "icon", "type": "icon", "caption": "Icon", "required": false }, + { + "key": "buttonStyle", + "type": "enumeration", + "caption": "Style", + "defaultValue": "primary", + "enumValues": [ + { "key": "primary", "caption": "Primary" }, + { "key": "secondary", "caption": "Secondary" }, + { "key": "danger", "caption": "Danger" } + ] + }, + { "key": "onClick", "type": "action", "caption": "On click" } + ], + "systemProperties": ["Name", "TabIndex", "Visibility"] +} +``` + +### TSX Structure + +```tsx +import { ReactNode, useCallback, KeyboardEvent } from "react"; +import { executeAction } from "@mendix/widget-plugin-platform/framework/execute-action"; +import { MyButtonContainerProps } from "../typings/MyButtonProps"; +import "./ui/MyButton.scss"; + +export default function MyButton(props: MyButtonContainerProps): ReactNode { + const { caption, icon, buttonStyle, onClick, tabIndex, class: className, style } = props; + + const handleClick = useCallback(() => { + executeAction(onClick); + }, [onClick]); + + const handleKeyDown = useCallback( + (event: KeyboardEvent) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + handleClick(); + } + }, + [handleClick] + ); + + const isDisabled = !onClick?.canExecute; + + return ( + + ); +} +``` + +### SCSS Structure + +```scss +.widget-mybutton { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + &-primary { + background-color: var(--brand-primary); + color: white; + } + + &-secondary { + background-color: transparent; + border: 1px solid var(--brand-primary); + color: var(--brand-primary); + } + + &-danger { + background-color: var(--brand-danger); + color: white; + } +} +``` + +--- + +## Pattern: Input Widget + +Input widgets bind to entity attributes for data entry. + +### Typical Properties + +```json +{ + "properties": [ + { + "key": "value", + "type": "attribute", + "caption": "Value", + "attributeTypes": ["String"], + "required": true + }, + { "key": "placeholder", "type": "textTemplate", "caption": "Placeholder", "required": false }, + { "key": "readOnly", "type": "boolean", "caption": "Read-only", "defaultValue": false }, + { "key": "onChange", "type": "action", "caption": "On change", "required": false }, + { "key": "onEnter", "type": "action", "caption": "On enter", "required": false } + ], + "systemProperties": ["Name", "TabIndex", "Visibility"] +} +``` + +### TSX Structure + +```tsx +import { ReactNode, useCallback, ChangeEvent, KeyboardEvent } from "react"; +import { executeAction } from "@mendix/widget-plugin-platform/framework/execute-action"; +import { MyInputContainerProps } from "../typings/MyInputProps"; +import "./ui/MyInput.scss"; + +export default function MyInput(props: MyInputContainerProps): ReactNode { + const { value, placeholder, readOnly, onChange, onEnter, tabIndex, class: className, style } = props; + + const handleChange = useCallback( + (event: ChangeEvent) => { + if (value?.status === "available" && !value.readOnly) { + value.setValue(event.target.value); + executeAction(onChange); + } + }, + [value, onChange] + ); + + const handleKeyDown = useCallback( + (event: KeyboardEvent) => { + if (event.key === "Enter") { + executeAction(onEnter); + } + }, + [onEnter] + ); + + const isReadOnly = readOnly || value?.readOnly; + + return ( + + ); +} +``` + +### SCSS Structure + +```scss +.widget-myinput { + width: 100%; + padding: 8px 12px; + border: 1px solid var(--border-color); + border-radius: 4px; + font-size: 14px; + + &:focus { + outline: none; + border-color: var(--brand-primary); + box-shadow: 0 0 0 2px rgba(var(--brand-primary-rgb), 0.2); + } + + &:read-only { + background-color: var(--bg-color-secondary); + } +} +``` + +--- + +## Pattern: Container Widget + +Container widgets hold child widgets. Examples: Fieldset, Card, Accordion. + +### Typical Properties + +```json +{ + "properties": [ + { "key": "content", "type": "widgets", "caption": "Content" }, + { "key": "header", "type": "textTemplate", "caption": "Header", "required": false }, + { "key": "collapsible", "type": "boolean", "caption": "Collapsible", "defaultValue": false } + ], + "systemProperties": ["Name", "TabIndex", "Visibility"] +} +``` + +### TSX Structure + +```tsx +import { ReactNode, useState, useCallback } from "react"; +import { MyContainerContainerProps } from "../typings/MyContainerProps"; +import "./ui/MyContainer.scss"; + +export default function MyContainer(props: MyContainerContainerProps): ReactNode { + const { content, header, collapsible, tabIndex, class: className, style } = props; + const [isOpen, setIsOpen] = useState(true); + + const handleToggle = useCallback(() => { + if (collapsible) { + setIsOpen(prev => !prev); + } + }, [collapsible]); + + return ( +
+ {header?.value && ( +
+ {header.value} + {collapsible && } +
+ )} + {isOpen &&
{content}
} +
+ ); +} +``` + +### SCSS Structure + +```scss +.widget-mycontainer { + border: 1px solid var(--border-color); + border-radius: 4px; + overflow: hidden; + + &-header { + padding: 12px 16px; + background-color: var(--bg-color-secondary); + font-weight: 600; + display: flex; + justify-content: space-between; + align-items: center; + + &[role="button"] { + cursor: pointer; + } + } + + &-toggle { + transition: transform 0.2s; + + &.open { + transform: rotate(180deg); + } + } + + &-content { + padding: 16px; + } +} +``` + +--- + +## Pattern: Data List Widget + +Data list widgets display items from a datasource. + +### Typical Properties + +```json +{ + "properties": [ + { "key": "dataSource", "type": "datasource", "caption": "Data source", "isList": true }, + { "key": "content", "type": "widgets", "caption": "Content", "dataSource": "dataSource" }, + { "key": "emptyMessage", "type": "textTemplate", "caption": "Empty message", "required": false }, + { "key": "onItemClick", "type": "action", "caption": "On item click", "required": false } + ], + "systemProperties": ["Name", "Visibility"] +} +``` + +### TSX Structure + +```tsx +import { ReactNode, useCallback } from "react"; +import { ValueStatus, ObjectItem } from "mendix"; +import { executeAction } from "@mendix/widget-plugin-platform/framework/execute-action"; +import { MyListContainerProps } from "../typings/MyListProps"; +import "./ui/MyList.scss"; + +export default function MyList(props: MyListContainerProps): ReactNode { + const { dataSource, content, emptyMessage, onItemClick, class: className, style } = props; + + // Loading state + if (dataSource?.status !== ValueStatus.Available) { + return ( +
+ Loading... +
+ ); + } + + const items = dataSource?.items ?? []; + + // Empty state + if (items.length === 0) { + return ( +
+ {emptyMessage?.value ?? "No items"} +
+ ); + } + + return ( +
+ {items.map((item: ObjectItem) => ( +
executeAction(onItemClick)}> + {content?.get(item)} +
+ ))} +
+ ); +} +``` + +### SCSS Structure + +```scss +.widget-mylist { + display: flex; + flex-direction: column; + gap: 8px; + + &-loading, + &-empty { + padding: 16px; + text-align: center; + color: var(--text-color-secondary); + } + + &-item { + padding: 12px; + border: 1px solid var(--border-color); + border-radius: 4px; + + &:hover { + background-color: var(--bg-color-hover); + } + } +} +``` + +--- + +## Common Imports + +Every widget typically needs these imports: + +```tsx +// React +import { ReactNode, useCallback, useState } from "react"; + +// Mendix helpers +import { executeAction } from "@mendix/widget-plugin-platform/framework/execute-action"; +import { ValueStatus } from "mendix"; + +// Generated types (from XML via pwt build) +import { MyWidgetContainerProps } from "../typings/MyWidgetProps"; + +// Styles +import "./ui/MyWidget.scss"; +``` + +--- + +## Key Patterns + +### Action Execution + +Always use `executeAction` from the platform helpers: + +```tsx +import { executeAction } from "@mendix/widget-plugin-platform/framework/execute-action"; + +const handleClick = useCallback(() => { + executeAction(props.onClick); +}, [props.onClick]); + +// Check if action can execute +const isClickable = props.onClick?.canExecute; +``` + +### Attribute Value Handling + +Check status before reading/writing: + +```tsx +// Reading +const displayValue = props.value?.value ?? ""; + +// Writing +if (props.value?.status === "available" && !props.value.readOnly) { + props.value.setValue(newValue); +} +``` + +### Loading States + +Handle datasource loading: + +```tsx +if (props.dataSource?.status !== ValueStatus.Available) { + return
Loading...
; +} +``` + +### Accessibility + +Always include proper accessibility attributes: + +```tsx + + ); +} +`; +} + +/** + * Generates the Input pattern component. + */ +function generateInputPattern(widgetName: string, properties: PropertyDefinition[]): string { + const imports = generateImports(widgetName, properties, "input"); + + // Find relevant properties + const attributeProps = properties.filter(p => p.type === "attribute"); + const mainAttribute = attributeProps[0]; + const actionProps = properties.filter(p => p.type === "action"); + const textProps = properties.filter(p => p.type === "textTemplate" || p.type === "string"); + + // Generate destructuring + const allProps = [...attributeProps, ...actionProps, ...textProps]; + const propsToDestructure = ["class: className", "style", "tabIndex", ...allProps.map(p => p.key)]; + + // Determine input type based on attribute type + const attrType = mainAttribute?.attributeTypes?.[0] ?? "String"; + let inputType = "text"; + if (attrType === "Integer" || attrType === "Long" || attrType === "Decimal") { + inputType = "number"; + } else if (attrType === "Boolean") { + inputType = "checkbox"; + } + + const mainKey = mainAttribute?.key ?? "value"; + + // Find change action + const changeAction = actionProps.find(p => p.key.toLowerCase().includes("change")); + const changeHandler = changeAction ? true : false; + + // Determine if we need Big conversion for numeric attributes + const usesBig = inputType === "number"; + const valueExtraction = usesBig ? `${mainKey}?.value?.toNumber() ?? 0` : `${mainKey}?.value ?? ""`; + const valueConversion = usesBig ? `new Big(Number(event.target.value))` : `event.target.value`; + + return `${imports} + +export default function ${widgetName}(props: ${widgetName}ContainerProps): ReactElement { + const { ${propsToDestructure.join(", ")} } = props; + + const currentValue = ${valueExtraction}; + const isReadOnly = ${mainKey}?.readOnly ?? false; + + const handleInputChange = useCallback((event: React.ChangeEvent) => { + if (${mainKey}?.status === "available" && !${mainKey}.readOnly) { + ${mainKey}.setValue(${valueConversion}); + }${changeHandler ? `\n executeAction(${changeAction?.key});` : ""} + }, [${mainKey}${changeHandler ? `, ${changeAction?.key}` : ""}]); + + return ( + + ); +} +`; +} + +/** + * Generates the Container pattern component. + */ +function generateContainerPattern(widgetName: string, properties: PropertyDefinition[]): string { + const imports = generateImports(widgetName, properties, "container"); + + // Find relevant properties + const widgetProps = properties.filter(p => p.type === "widgets"); + const mainContent = widgetProps[0]; + const textProps = properties.filter(p => p.type === "textTemplate" || p.type === "string"); + const headerProp = textProps.find(p => p.key === "header" || p.key === "title") || textProps[0]; + const boolProps = properties.filter(p => p.type === "boolean"); + const collapsibleProp = boolProps.find(p => p.key === "collapsible"); + + // Generate destructuring + const allProps = [...widgetProps, ...textProps, ...boolProps]; + const propsToDestructure = ["class: className", "style", "tabIndex", ...allProps.map(p => p.key)]; + + const contentKey = mainContent?.key ?? "content"; + const headerValue = headerProp ? `${headerProp.key}?.value` : "undefined"; + const isCollapsible = collapsibleProp?.key ?? "false"; + + return `${imports} + +export default function ${widgetName}(props: ${widgetName}ContainerProps): ReactElement { + const { ${propsToDestructure.join(", ")} } = props; + + const [isOpen, setIsOpen] = useState(true); + + const handleToggle = useCallback(() => { + if (${isCollapsible}) { + setIsOpen(prev => !prev); + } + }, [${isCollapsible}]); + + const headerValue = ${headerValue}; + + return ( +
+ {headerValue && ( +
+ {headerValue} + {${isCollapsible} && } +
+ )} + {isOpen &&
{${contentKey}}
} +
+ ); +} +`; +} + +/** + * Generates the Data List pattern component. + */ +function generateDataListPattern(widgetName: string, properties: PropertyDefinition[]): string { + const imports = generateImports(widgetName, properties, "dataList"); + + // Find relevant properties + const datasourceProp = properties.find(p => p.type === "datasource"); + const widgetProps = properties.filter(p => p.type === "widgets"); + const contentProp = widgetProps.find(p => p.dataSource) || widgetProps[0]; + const textProps = properties.filter(p => p.type === "textTemplate" || p.type === "string"); + const emptyMessageProp = textProps.find(p => p.key.toLowerCase().includes("empty")); + const actionProps = properties.filter(p => p.type === "action"); + const itemClickAction = actionProps.find(p => p.key.toLowerCase().includes("item")); + + // Generate destructuring + const allProps = [datasourceProp, contentProp, emptyMessageProp, itemClickAction].filter( + Boolean + ) as PropertyDefinition[]; + const propsToDestructure = ["class: className", "style", ...allProps.map(p => p.key)]; + + // Generate action handlers + const actionHandlers = itemClickAction ? [generateActionHandler(itemClickAction)] : []; + + const dsKey = datasourceProp?.key ?? "dataSource"; + const contentKey = contentProp?.key ?? "content"; + const emptyMessage = emptyMessageProp ? `${emptyMessageProp.key}?.value ?? "No items"` : '"No items"'; + const itemHandler = itemClickAction + ? `handle${itemClickAction.key.charAt(0).toUpperCase() + itemClickAction.key.slice(1)}` + : "undefined"; + + return `${imports} +import { ObjectItem } from "mendix"; + +export default function ${widgetName}(props: ${widgetName}ContainerProps): ReactElement { + const { ${propsToDestructure.join(", ")} } = props; + + ${actionHandlers.join("\n\n ")} + + // Loading state + if (${dsKey}?.status !== ValueStatus.Available) { + return ( +
+ Loading... +
+ ); + } + + const items = ${dsKey}?.items ?? []; + + // Empty state + if (items.length === 0) { + return ( +
+ {${emptyMessage}} +
+ ); + } + + return ( +
+ {items.map((item: ObjectItem) => ( +
+ {${contentKey}?.get(item)} +
+ ))} +
+ ); +} +`; +} + +/** + * Generates the complete widget TSX from a widget definition. + */ +export function generateWidgetTsx( + widgetName: string, + properties: PropertyDefinition[], + pattern?: WidgetPattern +): TsxGeneratorResult { + try { + // Detect pattern if not specified + const detectedPattern = pattern ?? detectWidgetPattern(properties); + + let mainComponent: string; + + switch (detectedPattern) { + case "display": + mainComponent = generateDisplayPattern(widgetName, properties); + break; + case "button": + mainComponent = generateButtonPattern(widgetName, properties); + break; + case "input": + mainComponent = generateInputPattern(widgetName, properties); + break; + case "container": + mainComponent = generateContainerPattern(widgetName, properties); + break; + case "dataList": + mainComponent = generateDataListPattern(widgetName, properties); + break; + default: + mainComponent = generateDisplayPattern(widgetName, properties); + } + + return { + success: true, + mainComponent, + pattern: detectedPattern + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error) + }; + } +} diff --git a/packages/pluggable-widgets-mcp/src/tools/code-generation.tools.ts b/packages/pluggable-widgets-mcp/src/tools/code-generation.tools.ts new file mode 100644 index 0000000000..a458c7dd96 --- /dev/null +++ b/packages/pluggable-widgets-mcp/src/tools/code-generation.tools.ts @@ -0,0 +1,459 @@ +/** + * Code Generation Tools for Mendix Pluggable Widgets. + * + * Provides the `generate-widget-code` tool that transforms widget descriptions + * and property definitions into working XML and TSX code. + */ + +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { mkdir, stat, writeFile } from "node:fs/promises"; +import { basename, dirname, join } from "node:path"; +import { z } from "zod"; +import { generateWidgetXml, validateWidgetDefinition } from "@/generators/xml-generator"; +import { detectWidgetPattern, generateWidgetTsx, type WidgetPattern } from "@/generators/tsx-generator"; +import type { PropertyDefinition, WidgetDefinition } from "@/generators/types"; +import { validateFilePath } from "@/security"; +import type { ToolResponse } from "@/tools/types"; +import { createErrorResponse, createToolResponse } from "@/tools/utils/response"; + +// ============================================================================= +// Schemas +// ============================================================================= + +/** + * Schema for enumeration values. + */ +const enumValueSchema = z.object({ + key: z.string().min(1).describe("Unique identifier for this enum value"), + caption: z.string().min(1).describe("Display caption shown in Studio Pro") +}); + +/** + * Schema for property definitions. + * Matches the PropertyDefinition type from generators/types.ts + */ +const propertyDefinitionSchema = z.object({ + key: z + .string() + .min(1) + .regex(/^[a-z][a-zA-Z0-9]*$/, "Must be camelCase (e.g., 'myProperty')") + .describe("Property key in camelCase"), + type: z + .enum([ + "string", + "boolean", + "integer", + "decimal", + "textTemplate", + "expression", + "action", + "attribute", + "datasource", + "association", + "entity", + "enumeration", + "icon", + "image", + "file", + "widgets", + "object" + ]) + .describe("Mendix property type"), + caption: z.string().min(1).describe("Display caption shown in Studio Pro"), + description: z.string().optional().describe("Help text shown in Studio Pro"), + required: z.boolean().optional().describe("Whether this property is required"), + defaultValue: z.union([z.string(), z.number(), z.boolean()]).optional().describe("Default value for this property"), + enumValues: z.array(enumValueSchema).optional().describe("Allowed values for enumeration type"), + attributeTypes: z + .array( + z.enum([ + "String", + "Integer", + "Long", + "Decimal", + "Boolean", + "DateTime", + "Enum", + "HashString", + "Binary", + "AutoNumber" + ]) + ) + .optional() + .describe("Allowed attribute types for attribute property"), + isList: z.boolean().optional().describe("Whether datasource returns a list"), + dataSource: z.string().optional().describe("Reference to datasource property key (for widgets type)"), + returnType: z + .enum(["String", "Integer", "Decimal", "Boolean", "DateTime"]) + .optional() + .describe("Return type for expression property") +}); + +/** + * Schema for the generate-widget-code tool input. + */ +const generateWidgetCodeSchema = z.object({ + widgetPath: z.string().min(1).describe("Absolute path to the scaffolded widget directory"), + description: z.string().min(1).describe("Description of what the widget should do"), + properties: z + .array(propertyDefinitionSchema) + .optional() + .describe("Array of property definitions. If not provided, returns suggestions."), + widgetPattern: z + .enum(["display", "button", "input", "container", "dataList"]) + .optional() + .describe("Optional hint for TSX generation pattern") +}); + +type GenerateWidgetCodeInput = z.infer; + +// ============================================================================= +// Helper Functions +// ============================================================================= + +/** + * Extracts widget name from path (e.g., /path/to/MyWidget -> MyWidget) + */ +function extractWidgetName(widgetPath: string): string { + const base = basename(widgetPath); + // Convert to PascalCase if needed + return base.charAt(0).toUpperCase() + base.slice(1); +} + +/** + * Generates property suggestions based on widget description. + */ +function generatePropertySuggestions(description: string): string { + const descLower = description.toLowerCase(); + + // Common patterns to suggest + const suggestions: Array<{ + key: string; + type: string; + caption: string; + purpose: string; + }> = []; + + // Counter-like widgets + if (descLower.includes("counter") || descLower.includes("count") || descLower.includes("increment")) { + suggestions.push( + { + key: "value", + type: "attribute", + caption: "Value", + purpose: "Current counter value (bind to Integer attribute)" + }, + { key: "step", type: "integer", caption: "Step", purpose: "Amount to increment/decrement (default: 1)" }, + { key: "minValue", type: "integer", caption: "Minimum", purpose: "Lower bound (optional)" }, + { key: "maxValue", type: "integer", caption: "Maximum", purpose: "Upper bound (optional)" }, + { key: "onIncrement", type: "action", caption: "On Increment", purpose: "Action when value increases" }, + { key: "onDecrement", type: "action", caption: "On Decrement", purpose: "Action when value decreases" } + ); + } + + // Display/badge-like widgets + if ( + descLower.includes("display") || + descLower.includes("show") || + descLower.includes("badge") || + descLower.includes("label") + ) { + suggestions.push( + { key: "value", type: "textTemplate", caption: "Value", purpose: "Text to display" }, + { key: "type", type: "enumeration", caption: "Style", purpose: "Visual style variant" }, + { key: "onClick", type: "action", caption: "On Click", purpose: "Action when clicked" } + ); + } + + // Button-like widgets + if (descLower.includes("button") || descLower.includes("click") || descLower.includes("trigger")) { + suggestions.push( + { key: "caption", type: "textTemplate", caption: "Caption", purpose: "Button text" }, + { key: "icon", type: "icon", caption: "Icon", purpose: "Button icon (optional)" }, + { key: "buttonStyle", type: "enumeration", caption: "Style", purpose: "Button appearance variant" }, + { key: "onClick", type: "action", caption: "On Click", purpose: "Action when clicked" } + ); + } + + // Input-like widgets + if ( + descLower.includes("input") || + descLower.includes("edit") || + descLower.includes("enter") || + descLower.includes("form") + ) { + suggestions.push( + { key: "value", type: "attribute", caption: "Value", purpose: "Bound attribute for data entry" }, + { key: "placeholder", type: "textTemplate", caption: "Placeholder", purpose: "Hint text when empty" }, + { key: "onChange", type: "action", caption: "On Change", purpose: "Action when value changes" }, + { key: "onEnter", type: "action", caption: "On Enter", purpose: "Action when Enter key pressed" } + ); + } + + // List-like widgets + if ( + descLower.includes("list") || + descLower.includes("items") || + descLower.includes("collection") || + descLower.includes("data") + ) { + suggestions.push( + { key: "dataSource", type: "datasource", caption: "Data Source", purpose: "Source of items to display" }, + { key: "content", type: "widgets", caption: "Content", purpose: "Template for each item" }, + { key: "emptyMessage", type: "textTemplate", caption: "Empty Message", purpose: "Text when no items" }, + { key: "onItemClick", type: "action", caption: "On Item Click", purpose: "Action when item clicked" } + ); + } + + // Container-like widgets + if ( + descLower.includes("container") || + descLower.includes("card") || + descLower.includes("panel") || + descLower.includes("section") + ) { + suggestions.push( + { key: "content", type: "widgets", caption: "Content", purpose: "Child widgets" }, + { key: "header", type: "textTemplate", caption: "Header", purpose: "Container title" }, + { key: "collapsible", type: "boolean", caption: "Collapsible", purpose: "Allow expand/collapse" } + ); + } + + // Default suggestions if nothing matched + if (suggestions.length === 0) { + suggestions.push( + { key: "value", type: "textTemplate", caption: "Value", purpose: "Main display value" }, + { key: "onClick", type: "action", caption: "On Click", purpose: "Action when clicked" } + ); + } + + // Detect pattern from suggestions + let suggestedPattern: WidgetPattern = "display"; + const types = suggestions.map(s => s.type); + if (types.includes("datasource") && types.includes("widgets")) { + suggestedPattern = "dataList"; + } else if (types.includes("widgets")) { + suggestedPattern = "container"; + } else if (types.includes("attribute")) { + suggestedPattern = "input"; + } else if (suggestions.length <= 4 && types.includes("action")) { + suggestedPattern = "button"; + } + + // Build markdown table + const table = [ + "| Property | Type | Caption | Purpose |", + "|----------|------|---------|---------|", + ...suggestions.map(s => `| ${s.key} | ${s.type} | ${s.caption} | ${s.purpose} |`) + ].join("\n"); + + return `📋 Widget requirements analysis needed + +Based on your description "${description}", suggested properties: + +${table} + +Suggested pattern: **${suggestedPattern}** (${getPatternDescription(suggestedPattern)}) + +Please call generate-widget-code again with the properties array to generate the widget code. + +Example: +\`\`\`json +{ + "widgetPath": "", + "description": "${description}", + "properties": [ + { "key": "value", "type": "textTemplate", "caption": "Value" }, + { "key": "onClick", "type": "action", "caption": "On Click" } + ] +} +\`\`\``; +} + +/** + * Returns a human-readable description of a widget pattern. + */ +function getPatternDescription(pattern: WidgetPattern): string { + switch (pattern) { + case "display": + return "read-only data display"; + case "button": + return "action trigger with click handler"; + case "input": + return "data entry with attribute binding"; + case "container": + return "holds child widgets"; + case "dataList": + return "renders items from datasource"; + default: + return "general purpose"; + } +} + +// ============================================================================= +// Tool Handler +// ============================================================================= + +async function handleGenerateWidgetCode(args: GenerateWidgetCodeInput): Promise { + const { widgetPath, description, properties, widgetPattern } = args; + + try { + // Verify widget directory exists + const pathStats = await stat(widgetPath); + if (!pathStats.isDirectory()) { + return createErrorResponse(`Widget path is not a directory: ${widgetPath}`); + } + + // If no properties provided, return suggestions + if (!properties || properties.length === 0) { + console.error(`[code-generation] No properties provided, returning suggestions`); + return createToolResponse(generatePropertySuggestions(description)); + } + + // Extract widget name from path + const widgetName = extractWidgetName(widgetPath); + + console.error(`[code-generation] Generating code for ${widgetName} with ${properties.length} properties`); + + // Build widget definition for XML generator + const widgetDefinition: WidgetDefinition = { + name: widgetName, + description, + properties: properties as PropertyDefinition[], + systemProperties: ["Name", "TabIndex", "Visibility"] + }; + + // Validate widget definition + const validationErrors = validateWidgetDefinition(widgetDefinition); + if (validationErrors.length > 0) { + return createErrorResponse( + [ + "❌ Widget definition validation failed:", + "", + ...validationErrors.map(e => ` • ${e}`), + "", + "Please fix the above issues and try again." + ].join("\n") + ); + } + + // Generate XML + console.error(`[code-generation] Generating XML...`); + const xmlResult = generateWidgetXml(widgetDefinition); + if (!xmlResult.success || !xmlResult.xml) { + return createErrorResponse(`XML generation failed: ${xmlResult.error}`); + } + + // Detect or use provided pattern + const pattern = widgetPattern ?? detectWidgetPattern(properties as PropertyDefinition[]); + console.error(`[code-generation] Using pattern: ${pattern}`); + + // Generate TSX + console.error(`[code-generation] Generating TSX...`); + const tsxResult = generateWidgetTsx(widgetName, properties as PropertyDefinition[], pattern); + if (!tsxResult.success || !tsxResult.mainComponent) { + return createErrorResponse(`TSX generation failed: ${tsxResult.error}`); + } + + // Prepare files to write + const filesToWrite = [ + { path: `src/${widgetName}.xml`, content: xmlResult.xml }, + { path: `src/${widgetName}.tsx`, content: tsxResult.mainComponent } + ]; + + // Validate and write files + const writtenFiles: string[] = []; + for (const file of filesToWrite) { + try { + validateFilePath(widgetPath, file.path, true); + const fullPath = join(widgetPath, file.path); + + // Ensure parent directory exists + const parentDir = dirname(fullPath); + await mkdir(parentDir, { recursive: true }); + + // Write file + await writeFile(fullPath, file.content, "utf-8"); + writtenFiles.push(file.path); + console.error(`[code-generation] Wrote: ${fullPath}`); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return createErrorResponse(`Failed to write ${file.path}: ${message}`); + } + } + + // Build success response + const propSummary = properties.map(p => p.key).join(", "); + + return createToolResponse( + [ + `✅ Widget code generated successfully!`, + "", + `📁 Files modified:`, + ` • src/${widgetName}.xml - Added ${properties.length} properties (${propSummary})`, + ` • src/${widgetName}.tsx - Implemented using ${pattern} pattern`, + "", + `🔨 Next steps:`, + ` 1. Run build-widget to compile and validate`, + ` 2. Review generated code for customization`, + ` 3. Test in Mendix Studio Pro` + ].join("\n") + ); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error(`[code-generation] Error: ${message}`); + return createErrorResponse(`Widget code generation failed: ${message}`); + } +} + +// ============================================================================= +// Tool Registration +// ============================================================================= + +const GENERATE_WIDGET_CODE_DESCRIPTION = `Generates XML properties and TSX component code for a Mendix pluggable widget. + +**Usage:** + +1. **With properties (generates code):** + Provide widgetPath, description, and properties array to generate XML + TSX files. + +2. **Without properties (gets suggestions):** + Provide only widgetPath and description to receive suggested properties based on your description. + +**Supported property types:** +- Basic: string, boolean, integer, decimal +- Dynamic: textTemplate, expression +- Interactive: action, attribute (for data binding) +- Complex: datasource, widgets (for containers/lists), enumeration + +**Pattern detection:** +The tool automatically detects the appropriate widget pattern (display, button, input, container, dataList) based on property types, or you can specify it explicitly. + +**Example - Counter widget:** +\`\`\`json +{ + "widgetPath": "/path/to/CounterWidget", + "description": "A counter that increments and decrements", + "properties": [ + { "key": "value", "type": "attribute", "caption": "Value", "attributeTypes": ["Integer"] }, + { "key": "onIncrement", "type": "action", "caption": "On Increment" } + ] +} +\`\`\``; + +/** + * Registers code generation tools for creating widget XML and TSX. + */ +export function registerCodeGenerationTools(server: McpServer): void { + server.registerTool( + "generate-widget-code", + { + title: "Generate Widget Code", + description: GENERATE_WIDGET_CODE_DESCRIPTION, + inputSchema: generateWidgetCodeSchema + }, + handleGenerateWidgetCode + ); + + console.error("[code-generation] Registered 1 tool"); +} diff --git a/packages/pluggable-widgets-mcp/src/tools/index.ts b/packages/pluggable-widgets-mcp/src/tools/index.ts index 584e00d6c2..31e508e651 100644 --- a/packages/pluggable-widgets-mcp/src/tools/index.ts +++ b/packages/pluggable-widgets-mcp/src/tools/index.ts @@ -1,5 +1,6 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { registerBuildTools } from "./build.tools"; +import { registerCodeGenerationTools } from "./code-generation.tools"; import { registerFileOperationTools } from "./file-operations.tools"; import { registerScaffoldingTools } from "./scaffolding.tools"; @@ -10,6 +11,7 @@ import { registerScaffoldingTools } from "./scaffolding.tools"; * - Scaffolding: Widget creation (create-widget) * - File Operations: Read/write widget files (list-widget-files, read-widget-file, write-widget-file, batch-write-widget-files) * - Build: Widget building and validation (build-widget) + * - Code Generation: Generate widget XML and TSX (generate-widget-code) * * Each category registers its tools directly with the server, preserving * full type safety through the SDK's generic inference. @@ -18,4 +20,5 @@ export function registerAllTools(server: McpServer): void { registerScaffoldingTools(server); registerFileOperationTools(server); registerBuildTools(server); + registerCodeGenerationTools(server); } diff --git a/packages/pluggable-widgets-mcp/src/tools/scaffolding.tools.ts b/packages/pluggable-widgets-mcp/src/tools/scaffolding.tools.ts index 9434792ae6..640e0a8e25 100644 --- a/packages/pluggable-widgets-mcp/src/tools/scaffolding.tools.ts +++ b/packages/pluggable-widgets-mcp/src/tools/scaffolding.tools.ts @@ -9,7 +9,8 @@ import { createToolResponse, type ErrorCode } from "@/tools/utils/response"; -import { mkdir } from "node:fs/promises"; +import { access, mkdir } from "node:fs/promises"; +import { dirname } from "node:path"; import { z } from "zod"; /** @@ -81,6 +82,23 @@ async function handleCreateWidget(args: CreateWidgetInput, context: ToolContext) }); try { + // Pre-validate ONLY for default path (catches Claude Desktop's non-existent cwd) + // For user-provided paths, let mkdir try and give a specific error if it fails + if (!args.outputPath) { + const parentDir = dirname(outputDir); + try { + await access(parentDir); + } catch { + return createStructuredErrorResponse( + createStructuredError("ERR_OUTPUT_PATH_REQUIRED", "Cannot create widget in default location", { + suggestion: + "The default output directory is not accessible (common in Claude Desktop). Please provide an explicit 'outputPath' parameter with a valid directory path on your system (e.g., '/Users/yourname/Projects/widgets', '~/widgets', or '/tmp/widgets').", + rawOutput: `Default path "${outputDir}" is not accessible. The working directory may not exist in this environment.` + }) + ); + } + } + console.error(`[create-widget] Starting widget scaffolding for "${options.name}"...`); await tracker.progress(SCAFFOLD_PROGRESS.START, `Starting widget scaffolding for "${options.name}"...`); await tracker.info(`Starting widget scaffolding for "${options.name}"...`, { @@ -167,8 +185,13 @@ async function handleCreateWidget(args: CreateWidgetInput, context: ToolContext) "The generator prompts may have changed. This could be a version mismatch. Please report this issue."; } else if (message.includes("ENOENT") || message.includes("not found")) { code = "ERR_NOT_FOUND"; - suggestion = - "A required file or command was not found. Ensure node, npm, and npx are installed and in PATH."; + // Check if this is a path issue vs a command issue + if (message.includes("mkdir") || message.includes(outputDir)) { + suggestion = `Cannot create directory "${outputDir}". Try a different 'outputPath' that you have write access to.`; + } else { + suggestion = + "Node.js, npm, or npx was not found. This tool requires a local development environment with npm installed. It cannot run in sandboxed environments like Claude Desktop's artifact sandbox."; + } } return createStructuredErrorResponse( diff --git a/packages/pluggable-widgets-mcp/src/tools/utils/response.ts b/packages/pluggable-widgets-mcp/src/tools/utils/response.ts index d61a43361d..0a968776f6 100644 --- a/packages/pluggable-widgets-mcp/src/tools/utils/response.ts +++ b/packages/pluggable-widgets-mcp/src/tools/utils/response.ts @@ -14,7 +14,9 @@ export type ErrorCode = | "ERR_SCAFFOLD_FAILED" // Generic scaffold failure | "ERR_FILE_PATH" // Invalid file path | "ERR_FILE_WRITE" // File write failure - | "ERR_NOT_FOUND"; // Resource not found + | "ERR_NOT_FOUND" // Resource not found + | "ERR_OUTPUT_PATH_REQUIRED" // Output path required (e.g., in Claude Desktop) + | "ERR_OUTPUT_PATH_INVALID"; // Output path is not accessible /** * Structured error with code, message, and optional details. From 6754fae0a15ae85d482b4a564771a498788db242 Mon Sep 17 00:00:00 2001 From: Rahman Date: Tue, 20 Jan 2026 02:13:26 +0100 Subject: [PATCH 08/10] feat(pluggable-widgets-mcp): update readme and agents.md, add security.md, clarify notifications --- packages/pluggable-widgets-mcp/AGENTS.md | 268 ++++-------------- packages/pluggable-widgets-mcp/README.md | 53 ++++ .../docs/agent/security.md | 20 ++ .../pluggable-widgets-mcp/package-lock.json | 126 +++++++- packages/pluggable-widgets-mcp/package.json | 2 +- .../src/tools/utils/notifications.ts | 29 ++ pnpm-lock.yaml | 10 +- 7 files changed, 281 insertions(+), 227 deletions(-) create mode 100644 packages/pluggable-widgets-mcp/docs/agent/security.md diff --git a/packages/pluggable-widgets-mcp/AGENTS.md b/packages/pluggable-widgets-mcp/AGENTS.md index 26b90fd4ab..9b34567841 100644 --- a/packages/pluggable-widgets-mcp/AGENTS.md +++ b/packages/pluggable-widgets-mcp/AGENTS.md @@ -1,257 +1,113 @@ -# Pluggable Widgets MCP Server - AI Agent Guide +# Pluggable Widgets MCP Server -This document provides context for AI development assistants working on the MCP (Model Context Protocol) server for Mendix pluggable widgets. +MCP server enabling AI assistants to scaffold and manage Mendix pluggable widgets via STDIO (default) or HTTP transport. -## Overview +## Quick Reference -This package implements an MCP server that enables AI assistants to scaffold and manage Mendix pluggable widgets programmatically. It supports both HTTP and STDIO transports for flexible integration with various MCP clients. - -### Key Characteristics - -- **MCP SDK**: Built on `@modelcontextprotocol/sdk` for standardized AI tool integration -- **Dual Transport**: HTTP (Express) for web clients, STDIO for CLI clients (Claude Desktop, etc.) -- **TypeScript**: Fully typed with Zod schemas for runtime validation -- **Widget Generator**: Wraps `@mendix/generator-widget` via PTY for interactive scaffolding +```bash +pnpm dev # Development with hot reload +pnpm build # TypeScript compilation + path alias resolution +pnpm start # Build and run (STDIO mode, default) +pnpm start:http # Build and run (HTTP mode, port 3100) +pnpm lint # ESLint check +``` ## Project Structure ``` src/ -├── index.ts # Entry point - transport mode selection -├── config.ts # Server configuration and constants -├── security/ -│ ├── guardrails.ts # Security validation (path traversal, extension whitelist) -│ └── index.ts # Security module exports -├── server/ -│ ├── server.ts # MCP server factory and tool/resource registration -│ ├── http.ts # HTTP transport setup (Express) -│ ├── stdio.ts # STDIO transport setup -│ ├── routes.ts # Express route handlers -│ └── session.ts # HTTP session management -├── resources/ -│ ├── index.ts # Resource registration -│ └── guidelines.ts # Widget development guidelines -└── tools/ - ├── index.ts # Tool registration aggregation - ├── types.ts # MCP tool type definitions - ├── scaffolding.tools.ts # Widget creation (create-widget) - ├── file-operations.tools.ts # File read/write/list operations - ├── build.tools.ts # Widget building and validation - └── utils/ - ├── generator.ts # Widget generator PTY wrapper - ├── progress-tracker.ts # Progress/logging helper - ├── notifications.ts # MCP notification utilities - └── response.ts # Tool response helpers -``` - -## Architecture - -### Transport Layer - -The server supports two transport modes selected via CLI argument: - -- **STDIO** (default): Single-session stdin/stdout for CLI integration (Claude Code, Claude Desktop) -- **HTTP**: Multi-session Express server on port 3100 for web clients and testing - -### Tool Registration - -Tools are registered directly with the MCP server using the SDK's `server.tool()` method. The current architecture uses category-based registration functions: - -```typescript -// src/tools/index.ts -export function registerAllTools(server: McpServer): void { - registerScaffoldingTools(server); // Widget creation - registerFileOperationTools(server); // File operations - registerBuildTools(server); // Building & validation -} -``` - -**Available Tools**: - -- **Scaffolding**: `create-widget` - Scaffolds new widgets via PTY interaction -- **File Operations**: - - `list-widget-files` - Lists files in widget directory - - `read-widget-file` - Reads widget file contents - - `write-widget-file` - Writes single file - - `batch-write-widget-files` - Writes multiple files atomically -- **Build**: `build-widget` - Compiles widget and parses errors (TypeScript, XML, dependencies) - -### Resources - -MCP resources provide read-only documentation that clients can fetch on-demand: - -```typescript -// src/resources/index.ts -export function registerResources(server: McpServer): void { - registerGuidelineResources(server); // Widget development guidelines -} -``` - -Resources are loaded from `docs/` directory and exposed via URIs like `resource://guidelines/property-types`. - -### Widget Generator Integration - -The `create-widget` tool uses `node-pty` to interact with the Mendix widget generator CLI. Key implementation details: - -- **PTY Simulation**: Required because the generator uses interactive prompts -- **Prompt Detection**: Matches expected prompts in terminal output -- **Answer Automation**: Sends pre-configured answers based on user input -- **Progress Tracking**: Reports progress via MCP notifications - -## Development Commands - -```bash -pnpm dev # Development mode with hot reload (tsx watch) -pnpm build # TypeScript compilation + path alias resolution (preserves shebang) -pnpm start # Build and run (HTTP mode on port 3100) -pnpm start:stdio # Build and run (STDIO mode) -pnpm lint # ESLint check +├── index.ts # Entry point - transport mode selection +├── config.ts # Server configuration and constants +├── security/ # Path traversal & extension validation +├── server/ # HTTP and STDIO transport setup +├── resources/ # MCP resources (guidelines) +├── generators/ # XML and TSX code generators +└── tools/ # MCP tool implementations ``` -## Adding New Tools +## Adding Tools -1. **Create tool file**: `src/tools/my-feature.tools.ts` +1. Create `src/tools/my-feature.tools.ts`: ```typescript import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; -const myToolSchema = z.object({ - param: z.string().describe("Parameter description for LLM") -}); - export function registerMyTools(server: McpServer): void { server.tool( - "my-tool", // Tool name - "Description shown to LLM", // Tool description - myToolSchema, // Input validation schema - async ({ param }) => { - // Handler with typed args - // Implementation - return { - content: [ - { - type: "text", - text: "Success message" - } - ] - }; - } + "my-tool", + "Description shown to LLM", + z.object({ param: z.string().describe("Parameter description") }), + async ({ param }) => ({ + content: [{ type: "text", text: "Success" }] + }) ); - - console.error("[my-feature] Registered 1 tool"); } ``` -2. **Register in index**: Update `src/tools/index.ts` +2. Register in `src/tools/index.ts`: ```typescript import { registerMyTools } from "./my-feature.tools"; export function registerAllTools(server: McpServer): void { - registerScaffoldingTools(server); - registerFileOperationTools(server); - registerBuildTools(server); - registerMyTools(server); // Add here + // ... existing registrations + registerMyTools(server); } ``` -## Code Conventions +## Code Patterns -### Imports +- **Imports**: Use `@/` path alias for absolute imports from `src/` +- **Schemas**: All tool inputs require Zod schemas +- **Errors**: Use `createErrorResponse()` from `@/tools/utils/response` +- **Long operations**: Use `ProgressTracker` from `@/tools/utils/progress-tracker` -- Use `@/` path alias for absolute imports from `src/` -- Prefer specific file imports over barrel exports when dealing with circular dependencies -- Group imports: node builtins → external packages → internal modules +## Notification Behavior (Important for AI Agents) -### Error Handling +When using this MCP server, understand where different types of output appear: -- Use `createErrorResponse()` for user-facing errors -- Log to `console.error` (not stdout) in STDIO mode -- Use `ProgressTracker` for long-running operations +| Output Type | Visibility | Purpose | +| -------------------------- | -------------------------- | ------------------------------------------------------- | +| **Tool Results** | ✅ Visible in conversation | Final outcomes, structured data, success/error messages | +| **Progress Notifications** | ❌ Not in conversation | Client UI indicators only (spinners, progress bars) | +| **Log Messages** | ❌ Not in conversation | Debug console/MCP Inspector only | -### Type Safety +**Key Implications for AI Agents:** -- All tool inputs must have Zod schemas -- Tool handlers receive fully typed arguments via Zod inference -- Use `McpServer` methods directly for type-safe tool registration +1. **Don't expect intermediate progress in chat**: Long operations (scaffolding, building) will show results only when complete. The conversation won't contain step-by-step progress updates. -## Testing +2. **Tool results are authoritative**: Only tool result content appears in the conversation history. Use this for: -Use MCP Inspector for interactive testing: + - Success confirmations with file paths + - Structured error messages with suggestions + - Any information the AI needs to continue the workflow -```bash -# STDIO mode -npx @modelcontextprotocol/inspector node dist/index.js stdio +3. **Progress tracking is for humans**: `sendProgress()` and `sendLogMessage()` are for human observers using MCP Inspector or UI indicators, not for AI decision-making. -# HTTP mode -pnpm start -npx @modelcontextprotocol/inspector -# Connect to http://localhost:3100/mcp -``` +4. **When debugging**: + - If operations seem to "hang", check MCP Inspector's Notifications/Logs panels + - Progress notifications confirm the server is working, even if the chat is quiet + - This is per MCP specification, not a bug -## Security - -All security validation is centralized in `src/security/guardrails.ts` for easy auditing: +**Example Workflow:** ```typescript -import { validateFilePath, ALLOWED_EXTENSIONS } from "@/security"; +// ❌ This progress won't appear in AI's context +await sendProgress(context, 50, "Scaffolding widget..."); -// Validates path traversal and extension whitelist -validateFilePath(widgetPath, filePath, true); // true = check extension +// ✅ This result WILL appear in AI's context +return createToolResponse(`Widget created at ${widgetPath}`); ``` -### Security Measures - -| Protection | Function | Description | -| ------------------- | ------------------------- | ------------------------------------------------------------------- | -| Path Traversal | `validateFilePath()` | Blocks `..` sequences and resolved path escapes | -| Extension Whitelist | `isExtensionAllowed()` | Only allows: `.tsx`, `.ts`, `.xml`, `.scss`, `.css`, `.json`, `.md` | -| Directory Boundary | `isPathWithinDirectory()` | Ensures files stay within widget directory | - -When adding file operation tools, always use `validateFilePath()` from the security module. - -## Key Files Reference - -| File | Purpose | -| -------------------------- | ------------------------------------------------ | -| `config.ts` | Server constants (ports, timeouts, paths) | -| `security/guardrails.ts` | Security validation (path traversal, extensions) | -| `tools/index.ts` | Tool registration aggregation | -| `tools/utils/generator.ts` | Widget generator PTY prompts and defaults | -| `resources/guidelines.ts` | Widget development guideline resources | -| `server/session.ts` | HTTP session lifecycle management | -| `server/server.ts` | MCP server factory and registration entry point | - -## Common Patterns - -### Progress Notifications - -```typescript -const tracker = new ProgressTracker({ - context, - logger: "my-tool", - totalSteps: 5 -}); +## Testing -tracker.start("initializing"); -await tracker.progress(25, "Step 1 complete"); -await tracker.info("Detailed log message", { key: "value" }); -tracker.stop(); +```bash +npx @modelcontextprotocol/inspector node dist/index.js ``` -### Long-Running Operations - -- Use `ProgressTracker` for heartbeat and stuck detection -- Set appropriate timeouts (see `SCAFFOLD_TIMEOUT_MS`) -- Call `tracker.markComplete()` before expected long waits (e.g., npm install) - -## Roadmap Context - -Current focus is widget scaffolding. Planned additions: +## Security -- Widget property editing -- XML configuration management -- Build and deployment automation +**Read before implementing file operations**: [docs/agent/security.md](docs/agent/security.md) -When adding features, maintain the existing patterns for tool registration, progress tracking, and transport-agnostic design. +All file operation tools must use `validateFilePath()` from `@/security` to prevent path traversal attacks. diff --git a/packages/pluggable-widgets-mcp/README.md b/packages/pluggable-widgets-mcp/README.md index f00a08b245..39ac713ce2 100644 --- a/packages/pluggable-widgets-mcp/README.md +++ b/packages/pluggable-widgets-mcp/README.md @@ -197,6 +197,59 @@ npx @modelcontextprotocol/inspector This is useful for verifying tool behavior without needing a full AI client integration. +## Understanding Feedback and Notifications + +This server uses MCP's notification system to provide progress updates and logging. However, **different types of feedback appear in different places**—not all feedback shows up in your chat conversation. + +### Where Different Types of Feedback Appear + +| Feedback Type | Where It Appears | Example | +| -------------------------- | -------------------------------------- | ----------------------------------------------------------------- | +| **Tool Results** | ✅ Chat conversation | Widget created at `/path/to/widget`, Build completed successfully | +| **Progress Notifications** | ⚙️ Client UI (spinners, progress bars) | "Scaffolding widget...", "Building widget..." | +| **Log Messages** | 🔍 Debug console (MCP Inspector) | Detailed operation logs, debug info | + +### Why Progress Doesn't Show in Chat + +**This is by design per the MCP specification**, not a bug. The MCP architecture separates concerns: + +- **`notifications/progress`** → Routed to client UI indicators (loading spinners, status bars) +- **`notifications/message`** → Routed to debug/inspector consoles for developers +- **Tool results** → Returned to the conversation when operations complete + +This means: + +- Long operations (scaffolding, building) will show **results** when complete +- You won't see intermediate progress steps in the chat history +- MCP Inspector shows all notifications in real-time (bottom-right panel) + +### Viewing Debug Output + +**With MCP Inspector:** + +1. Run: `npx @modelcontextprotocol/inspector node dist/index.js stdio` +2. Execute a tool (e.g., `create-widget`) +3. Watch the **Notifications panel** (bottom-right) for progress updates +4. Check the **Logs panel** for detailed debug output + +**With Claude Desktop:** + +- Progress notifications may appear as UI indicators (client-dependent) +- Check Claude Desktop's developer console for log messages (if available) +- Tool results will always appear in the conversation + +### Expected Behavior Examples + +**During widget scaffolding:** + +- Chat shows: "Starting scaffolding..." → (wait) → "Widget created at `/path`" +- Inspector shows: Step-by-step progress notifications for all 14 prompts + +**During widget building:** + +- Chat shows: "Building..." → (wait) → "Build successful" or structured error +- Inspector shows: TypeScript compilation progress, dependency resolution + ## Roadmap - [x] Widget scaffolding (`create-widget`) diff --git a/packages/pluggable-widgets-mcp/docs/agent/security.md b/packages/pluggable-widgets-mcp/docs/agent/security.md new file mode 100644 index 0000000000..816e26ff25 --- /dev/null +++ b/packages/pluggable-widgets-mcp/docs/agent/security.md @@ -0,0 +1,20 @@ +# Security + +All security validation is centralized in `src/security/guardrails.ts`: + +```typescript +import { validateFilePath, ALLOWED_EXTENSIONS } from "@/security"; + +// Validates path traversal and extension whitelist +validateFilePath(widgetPath, filePath, true); // true = check extension +``` + +## Security Measures + +| Protection | Function | Description | +| ------------------- | ------------------------- | ------------------------------------------------------------------- | +| Path Traversal | `validateFilePath()` | Blocks `..` sequences and resolved path escapes | +| Extension Whitelist | `isExtensionAllowed()` | Only allows: `.tsx`, `.ts`, `.xml`, `.scss`, `.css`, `.json`, `.md` | +| Directory Boundary | `isPathWithinDirectory()` | Ensures files stay within widget directory | + +**Rule**: When adding file operation tools, always use `validateFilePath()` from the security module. diff --git a/packages/pluggable-widgets-mcp/package-lock.json b/packages/pluggable-widgets-mcp/package-lock.json index 4de60372f3..8f138be2ec 100644 --- a/packages/pluggable-widgets-mcp/package-lock.json +++ b/packages/pluggable-widgets-mcp/package-lock.json @@ -1,27 +1,33 @@ { - "name": "pluggable-widgets-mcp", + "name": "@mendix/pluggable-widgets-mcp", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "pluggable-widgets-mcp", + "name": "@mendix/pluggable-widgets-mcp", "version": "0.1.0", "license": "Apache-2.0", "dependencies": { "@modelcontextprotocol/sdk": "^1.24.2", - "node-pty": "^1.0.0", + "cors": "^2.8.5", + "express": "^5.1.0", + "node-pty": "1.2.0-beta.7", "tsx": "^4.21.0", "zod": "^4.1.13" }, + "bin": { + "pluggable-widgets-mcp": "dist/index.js" + }, "devDependencies": { "@types/cors": "^2.8.19", - "@types/node": "^24.10.1", + "@types/express": "^5.0.6", + "@types/node": "*", "tsc-alias": "^1.8.16", "typescript": "^5.9.3" }, "engines": { - "node": ">=22" + "node": ">=20" } }, "node_modules/@esbuild/aix-ppc64": { @@ -515,6 +521,27 @@ "node": ">= 8" } }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/cors": { "version": "2.8.19", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", @@ -525,6 +552,38 @@ "@types/node": "*" } }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.10.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", @@ -535,6 +594,41 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -975,6 +1069,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -1504,12 +1599,6 @@ "url": "https://github.com/sponsors/raouldeheer" } }, - "node_modules/nan": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.24.0.tgz", - "integrity": "sha512-Vpf9qnVW1RaDkoNKFUvfxqAbtI8ncb8OJlqZ9wwpXzWPEsvsB1nvdUi6oYrHIkQ1Y/tMDnr1h4nczS0VB9Xykg==", - "license": "MIT" - }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -1519,14 +1608,20 @@ "node": ">= 0.6" } }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/node-pty": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz", - "integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==", + "version": "1.2.0-beta.7", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.2.0-beta.7.tgz", + "integrity": "sha512-gHvC2HkwXDTqX931r7wBas2WISl7N26g6uOPHItA1OZmPlDwWZqTCWAjO8V3UShE9CEtd+VDawRr/0c8Uf+xiQ==", "hasInstallScript": true, "license": "MIT", "dependencies": { - "nan": "^2.17.0" + "node-addon-api": "^7.1.0" } }, "node_modules/normalize-path": { @@ -2122,6 +2217,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/packages/pluggable-widgets-mcp/package.json b/packages/pluggable-widgets-mcp/package.json index bd45aa5eb2..c38318be34 100644 --- a/packages/pluggable-widgets-mcp/package.json +++ b/packages/pluggable-widgets-mcp/package.json @@ -26,7 +26,7 @@ "@modelcontextprotocol/sdk": "^1.24.2", "cors": "^2.8.5", "express": "^5.1.0", - "node-pty": "^1.0.0", + "node-pty": "1.2.0-beta.7", "tsx": "^4.21.0", "zod": "^4.1.13" }, diff --git a/packages/pluggable-widgets-mcp/src/tools/utils/notifications.ts b/packages/pluggable-widgets-mcp/src/tools/utils/notifications.ts index f214154620..7ecbef307f 100644 --- a/packages/pluggable-widgets-mcp/src/tools/utils/notifications.ts +++ b/packages/pluggable-widgets-mcp/src/tools/utils/notifications.ts @@ -3,6 +3,19 @@ import type { LogLevel, ToolContext } from "@/tools/types"; /** * Sends a progress notification to the MCP client. * Only sends if the client provided a progressToken in the request. + * + * **Where this appears:** + * - ⚙️ Client UI indicators (spinners, progress bars, status indicators) + * - 🔍 MCP Inspector's Notifications panel (for debugging) + * - ❌ NOT in the chat conversation history + * + * **MCP Specification Behavior:** + * Progress notifications are routed to the client's UI layer, not the conversation. + * This is by design—use tool results for chat-visible output. + * + * @param context - Tool execution context with notification sender + * @param progress - Progress value (0-100) + * @param message - Optional progress description */ export async function sendProgress(context: ToolContext, progress: number, message?: string): Promise { const progressToken = context._meta?.progressToken; @@ -17,6 +30,22 @@ export async function sendProgress(context: ToolContext, progress: number, messa /** * Sends a logging message notification to the MCP client. * Works independently of progressToken and provides detailed context. + * + * **Where this appears:** + * - 🔍 MCP Inspector's Logs panel (for debugging) + * - 🛠️ Client developer consoles (if supported) + * - ❌ NOT in the chat conversation history + * + * **MCP Specification Behavior:** + * Log notifications are routed to debug/inspector layers, not the conversation. + * These are intended for developers debugging MCP servers, not end-user feedback. + * For chat-visible messages, use tool result content instead. + * + * @param context - Tool execution context with notification sender + * @param level - Log severity level (debug, info, warning, error) + * @param message - Human-readable log message + * @param data - Additional structured data for debugging + * @param logger - Logger name/category (defaults to "mcp-tools") */ export async function sendLogMessage( context: ToolContext, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4713a45961..5398ab660d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -420,8 +420,8 @@ importers: specifier: ^5.1.0 version: 5.2.1 node-pty: - specifier: ^1.0.0 - version: 1.1.0 + specifier: 1.2.0-beta.7 + version: 1.2.0-beta.7 tsx: specifier: ^4.21.0 version: 4.21.0 @@ -8938,8 +8938,8 @@ packages: node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - node-pty@1.1.0: - resolution: {integrity: sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==} + node-pty@1.2.0-beta.7: + resolution: {integrity: sha512-gHvC2HkwXDTqX931r7wBas2WISl7N26g6uOPHItA1OZmPlDwWZqTCWAjO8V3UShE9CEtd+VDawRr/0c8Uf+xiQ==} node-releases@2.0.23: resolution: {integrity: sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==} @@ -18512,7 +18512,7 @@ snapshots: node-int64@0.4.0: {} - node-pty@1.1.0: + node-pty@1.2.0-beta.7: dependencies: node-addon-api: 7.1.1 From 86124a7a278bf85808abff89bde88c9f7206d156 Mon Sep 17 00:00:00 2001 From: Rahman Date: Tue, 20 Jan 2026 09:26:23 +0100 Subject: [PATCH 09/10] feat(pluggable-widgets-mcp): add changelog --- packages/pluggable-widgets-mcp/CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 packages/pluggable-widgets-mcp/CHANGELOG.md diff --git a/packages/pluggable-widgets-mcp/CHANGELOG.md b/packages/pluggable-widgets-mcp/CHANGELOG.md new file mode 100644 index 0000000000..8c63b9710f --- /dev/null +++ b/packages/pluggable-widgets-mcp/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +All notable changes to this MCP server will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- We introduce pluggable-widgets-mcp. From 10ecd10e6abc3395a84722fb278e423d45f3a883 Mon Sep 17 00:00:00 2001 From: Rahman Date: Tue, 20 Jan 2026 09:35:47 +0100 Subject: [PATCH 10/10] feat(pluggable-widgets-mcp): remove redundant prettier config --- packages/pluggable-widgets-mcp/.prettierrc.js | 1 - 1 file changed, 1 deletion(-) delete mode 100644 packages/pluggable-widgets-mcp/.prettierrc.js diff --git a/packages/pluggable-widgets-mcp/.prettierrc.js b/packages/pluggable-widgets-mcp/.prettierrc.js deleted file mode 100644 index 0892704ab0..0000000000 --- a/packages/pluggable-widgets-mcp/.prettierrc.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("@mendix/prettier-config-web-widgets");