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/.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/AGENTS.md b/packages/pluggable-widgets-mcp/AGENTS.md new file mode 100644 index 0000000000..9b34567841 --- /dev/null +++ b/packages/pluggable-widgets-mcp/AGENTS.md @@ -0,0 +1,113 @@ +# Pluggable Widgets MCP Server + +MCP server enabling AI assistants to scaffold and manage Mendix pluggable widgets via STDIO (default) or HTTP transport. + +## Quick Reference + +```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/ # 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 Tools + +1. Create `src/tools/my-feature.tools.ts`: + +```typescript +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; + +export function registerMyTools(server: McpServer): void { + server.tool( + "my-tool", + "Description shown to LLM", + z.object({ param: z.string().describe("Parameter description") }), + async ({ param }) => ({ + content: [{ type: "text", text: "Success" }] + }) + ); +} +``` + +2. Register in `src/tools/index.ts`: + +```typescript +import { registerMyTools } from "./my-feature.tools"; + +export function registerAllTools(server: McpServer): void { + // ... existing registrations + registerMyTools(server); +} +``` + +## Code Patterns + +- **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` + +## Notification Behavior (Important for AI Agents) + +When using this MCP server, understand where different types of output appear: + +| 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 | + +**Key Implications for AI Agents:** + +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. + +2. **Tool results are authoritative**: Only tool result content appears in the conversation history. Use this for: + + - Success confirmations with file paths + - Structured error messages with suggestions + - Any information the AI needs to continue the workflow + +3. **Progress tracking is for humans**: `sendProgress()` and `sendLogMessage()` are for human observers using MCP Inspector or UI indicators, not for AI decision-making. + +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 + +**Example Workflow:** + +```typescript +// ❌ This progress won't appear in AI's context +await sendProgress(context, 50, "Scaffolding widget..."); + +// ✅ This result WILL appear in AI's context +return createToolResponse(`Widget created at ${widgetPath}`); +``` + +## Testing + +```bash +npx @modelcontextprotocol/inspector node dist/index.js +``` + +## Security + +**Read before implementing file operations**: [docs/agent/security.md](docs/agent/security.md) + +All file operation tools must use `validateFilePath()` from `@/security` to prevent path traversal attacks. 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. diff --git a/packages/pluggable-widgets-mcp/README.md b/packages/pluggable-widgets-mcp/README.md new file mode 100644 index 0000000000..39ac713ce2 --- /dev/null +++ b/packages/pluggable-widgets-mcp/README.md @@ -0,0 +1,267 @@ +# 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 build # Build the server +pnpm start # STDIO mode (default) +pnpm start:stdio # HTTP 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 + +### STDIO Mode (default) + +Runs via stdin/stdout for CLI-based MCP clients (Claude Desktop, etc.). + +```bash +pnpm start +pnpm start:stdio +``` + +### HTTP Mode + +Runs an HTTP server for web-based MCP clients. + +```bash +pnpm start:http +``` + +- Server runs on `http://localhost:3100` (override with `PORT` env var) +- Health check: `GET /health` +- MCP endpoint: `POST /mcp` + +## 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)_** + +**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": { + "pluggable-widgets-mcp": { + "command": "node", + "args": ["/path/to/pluggable-widgets-mcp/dist/index.js", "stdio"] + } + } +} +``` + +> **Note:** After rebuilding the server, you may need to restart/reconnect your MCP client to pick up changes. + +## 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. + +### 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:** All file operations are protected by `src/security/guardrails.ts`: + +- Path traversal is blocked (no `..` escapes) +- Extension whitelist: `.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 +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. + +## 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`) +- [x] HTTP transport +- [x] STDIO transport +- [x] Progress notifications +- [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 + +Apache-2.0 - Mendix Technology BV 2025 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/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/generators/types.ts b/packages/pluggable-widgets-mcp/src/generators/types.ts new file mode 100644 index 0000000000..efab114623 --- /dev/null +++ b/packages/pluggable-widgets-mcp/src/generators/types.ts @@ -0,0 +1,161 @@ +/** + * Type definitions for widget generation. + * These types define the JSON schema that AI fills in, + * which is then transformed into valid XML by the generator. + */ + +/** + * Mendix property types supported in widget XML. + */ +export type MendixPropertyType = + | "string" + | "boolean" + | "integer" + | "decimal" + | "textTemplate" + | "expression" + | "action" + | "attribute" + | "datasource" + | "association" + | "entity" + | "enumeration" + | "icon" + | "image" + | "file" + | "widgets" + | "object"; + +/** + * Attribute types for attribute properties. + */ +export type AttributeType = + | "String" + | "Integer" + | "Long" + | "Decimal" + | "Boolean" + | "DateTime" + | "Enum" + | "HashString" + | "Binary" + | "AutoNumber"; + +/** + * Enumeration value for enumeration properties. + */ +export interface EnumValue { + key: string; + caption: string; +} + +/** + * Definition for a single widget property. + * This is the JSON structure AI fills in. + */ +export interface PropertyDefinition { + /** Property key (camelCase identifier) */ + key: string; + + /** Property type */ + type: MendixPropertyType; + + /** Display caption in Studio Pro */ + caption: string; + + /** Optional description shown in Studio Pro */ + description?: string; + + /** Whether the property is required */ + required?: boolean; + + /** Default value (for string, boolean, integer, decimal, enumeration) */ + defaultValue?: string | number | boolean; + + /** For enumeration type: list of allowed values */ + enumValues?: EnumValue[]; + + /** For attribute type: allowed attribute types */ + attributeTypes?: AttributeType[]; + + /** For datasource type: whether it returns a list */ + isList?: boolean; + + /** For widgets type: reference to datasource property key */ + dataSource?: string; + + /** For object type: nested properties */ + properties?: PropertyDefinition[]; + + /** For expression type: return type */ + returnType?: "String" | "Integer" | "Decimal" | "Boolean" | "DateTime"; +} + +/** + * System properties available in Mendix widgets. + */ +export type SystemProperty = "Name" | "TabIndex" | "Visibility"; + +/** + * Property group for organizing properties in Studio Pro. + */ +export interface PropertyGroup { + /** Group caption displayed in Studio Pro */ + caption: string; + + /** Property keys in this group */ + properties: string[]; +} + +/** + * Complete widget definition. + * This is the full JSON structure AI provides for widget generation. + */ +export interface WidgetDefinition { + /** Widget name (PascalCase) */ + name: string; + + /** Widget description */ + description: string; + + /** Widget ID (e.g., "com.mendix.widget.custom.mywidget.MyWidget") */ + id?: string; + + /** Organization namespace */ + organization?: string; + + /** Studio Pro category */ + studioCategory?: string; + + /** Whether widget needs entity context */ + needsEntityContext?: boolean; + + /** Whether widget supports offline */ + offlineCapable?: boolean; + + /** Help URL */ + helpUrl?: string; + + /** Property definitions */ + properties: PropertyDefinition[]; + + /** System properties to include */ + systemProperties?: SystemProperty[]; + + /** Optional property grouping */ + propertyGroups?: PropertyGroup[]; +} + +/** + * Result of XML generation. + */ +export interface GeneratorResult { + /** Whether generation succeeded */ + success: boolean; + + /** Generated XML content (if success) */ + xml?: string; + + /** Error message (if failed) */ + error?: string; +} diff --git a/packages/pluggable-widgets-mcp/src/generators/xml-generator.ts b/packages/pluggable-widgets-mcp/src/generators/xml-generator.ts new file mode 100644 index 0000000000..c1ac043dc3 --- /dev/null +++ b/packages/pluggable-widgets-mcp/src/generators/xml-generator.ts @@ -0,0 +1,256 @@ +/** + * XML Generator for Mendix Widget Definitions. + * + * Transforms a WidgetDefinition JSON structure into valid Mendix widget XML. + * This is a deterministic transformation - same input always produces same output. + */ + +import type { EnumValue, GeneratorResult, PropertyDefinition, SystemProperty, WidgetDefinition } from "./types"; + +/** + * Escapes special XML characters in text content. + */ +function escapeXml(text: string): string { + return text + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +/** + * Generates XML for enumeration values. + */ +function generateEnumValues(values: EnumValue[], indent: string): string { + const lines = values.map( + v => `${indent} ${escapeXml(v.caption)}` + ); + return `${indent}\n${lines.join("\n")}\n${indent}`; +} + +/** + * Generates XML for attribute types. + */ +function generateAttributeTypes(types: string[], indent: string): string { + const lines = types.map(t => `${indent} `); + return `${indent}\n${lines.join("\n")}\n${indent}`; +} + +/** + * Generates XML for a single property. + */ +function generateProperty(prop: PropertyDefinition, indent: string): string { + const attrs: string[] = [`key="${escapeXml(prop.key)}"`, `type="${prop.type}"`]; + + // Add optional attributes + if (prop.required !== undefined) { + attrs.push(`required="${prop.required}"`); + } + if (prop.defaultValue !== undefined) { + attrs.push(`defaultValue="${escapeXml(String(prop.defaultValue))}"`); + } + if (prop.isList) { + attrs.push(`isList="true"`); + } + if (prop.dataSource) { + attrs.push(`dataSource="${escapeXml(prop.dataSource)}"`); + } + + const attrString = attrs.join(" "); + const innerIndent = indent + " "; + const lines: string[] = []; + + lines.push(`${indent}`); + lines.push(`${innerIndent}${escapeXml(prop.caption)}`); + + if (prop.description) { + lines.push(`${innerIndent}${escapeXml(prop.description)}`); + } else { + lines.push(`${innerIndent}`); + } + + // Type-specific content + if (prop.type === "enumeration" && prop.enumValues) { + lines.push(generateEnumValues(prop.enumValues, innerIndent)); + } + + if (prop.type === "attribute" && prop.attributeTypes) { + lines.push(generateAttributeTypes(prop.attributeTypes, innerIndent)); + } + + if (prop.type === "expression" && prop.returnType) { + lines.push(`${innerIndent}`); + } + + if (prop.type === "object" && prop.properties) { + lines.push(`${innerIndent}`); + lines.push(`${innerIndent} `); + for (const nestedProp of prop.properties) { + lines.push(generateProperty(nestedProp, innerIndent + " ")); + } + lines.push(`${innerIndent} `); + lines.push(`${innerIndent}`); + } + + lines.push(`${indent}`); + + return lines.join("\n"); +} + +/** + * Generates XML for system properties. + */ +function generateSystemProperty(key: SystemProperty, indent: string): string { + return `${indent}`; +} + +/** + * Generates the complete widget XML from a widget definition. + */ +export function generateWidgetXml(widget: WidgetDefinition): GeneratorResult { + try { + // Derive defaults + const organization = widget.organization ?? "mendix"; + const widgetNameLower = widget.name.toLowerCase(); + const widgetId = widget.id ?? `com.${organization}.widget.custom.${widgetNameLower}.${widget.name}`; + const studioCategory = widget.studioCategory ?? "Display"; + const needsEntityContext = widget.needsEntityContext ?? false; + const offlineCapable = widget.offlineCapable ?? true; + + // Build widget attributes + const widgetAttrs = [ + `id="${escapeXml(widgetId)}"`, + `pluginWidget="true"`, + needsEntityContext ? `needsEntityContext="true"` : null, + `offlineCapable="${offlineCapable}"`, + `xmlns="http://www.mendix.com/widget/1.0/"`, + `xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"`, + `xsi:schemaLocation="http://www.mendix.com/widget/1.0/ ../../../../node_modules/mendix/custom_widget.xsd"` + ] + .filter(Boolean) + .join(" "); + + const lines: string[] = []; + lines.push(``); + lines.push(``); + lines.push(` ${escapeXml(widget.name)}`); + lines.push(` ${widget.description ? escapeXml(widget.description) : ""}`); + lines.push(` ${escapeXml(studioCategory)}`); + lines.push(` ${escapeXml(studioCategory)}`); + + if (widget.helpUrl) { + lines.push(` ${escapeXml(widget.helpUrl)}`); + } + + lines.push(` `); + lines.push(` `); + + // Group properties if grouping is specified + if (widget.propertyGroups && widget.propertyGroups.length > 0) { + for (const group of widget.propertyGroups) { + lines.push(` `); + for (const propKey of group.properties) { + const prop = widget.properties.find(p => p.key === propKey); + if (prop) { + lines.push(generateProperty(prop, " ")); + } + } + lines.push(` `); + } + } else { + // Default grouping: General for all properties + lines.push(` `); + for (const prop of widget.properties.filter(p => p.type !== "action")) { + lines.push(generateProperty(prop, " ")); + } + lines.push(` `); + + // Events group for actions + const actionProps = widget.properties.filter(p => p.type === "action"); + if (actionProps.length > 0) { + lines.push(` `); + for (const prop of actionProps) { + lines.push(generateProperty(prop, " ")); + } + lines.push(` `); + } + } + + // System properties + if (widget.systemProperties && widget.systemProperties.length > 0) { + const visibilityProps = widget.systemProperties.filter(p => p === "Visibility"); + const commonProps = widget.systemProperties.filter(p => p !== "Visibility"); + + if (visibilityProps.length > 0) { + lines.push(` `); + for (const sysProp of visibilityProps) { + lines.push(generateSystemProperty(sysProp, " ")); + } + lines.push(` `); + } + + if (commonProps.length > 0) { + lines.push(` `); + for (const sysProp of commonProps) { + lines.push(generateSystemProperty(sysProp, " ")); + } + lines.push(` `); + } + } + + lines.push(` `); + lines.push(` `); + lines.push(``); + + return { + success: true, + xml: lines.join("\n") + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error) + }; + } +} + +/** + * Validates a widget definition before generation. + * Returns an array of validation errors (empty if valid). + */ +export function validateWidgetDefinition(widget: WidgetDefinition): string[] { + const errors: string[] = []; + + if (!widget.name || widget.name.trim() === "") { + errors.push("Widget name is required"); + } else if (!/^[A-Z][a-zA-Z0-9]*$/.test(widget.name)) { + errors.push("Widget name must be PascalCase (e.g., MyWidget)"); + } + + if (!widget.properties || widget.properties.length === 0) { + errors.push("Widget must have at least one property"); + } + + for (const prop of widget.properties ?? []) { + if (!prop.key || prop.key.trim() === "") { + errors.push("Property key is required"); + } else if (!/^[a-z][a-zA-Z0-9]*$/.test(prop.key)) { + errors.push(`Property key "${prop.key}" must be camelCase`); + } + + if (!prop.caption || prop.caption.trim() === "") { + errors.push(`Property "${prop.key}" must have a caption`); + } + + if (prop.type === "enumeration" && (!prop.enumValues || prop.enumValues.length === 0)) { + errors.push(`Enumeration property "${prop.key}" must have enumValues`); + } + + if (prop.type === "attribute" && (!prop.attributeTypes || prop.attributeTypes.length === 0)) { + errors.push(`Attribute property "${prop.key}" must have attributeTypes`); + } + } + + return errors; +} diff --git a/packages/pluggable-widgets-mcp/src/index.ts b/packages/pluggable-widgets-mcp/src/index.ts new file mode 100644 index 0000000000..5e4fe00721 --- /dev/null +++ b/packages/pluggable-widgets-mcp/src/index.ts @@ -0,0 +1,16 @@ +#!/usr/bin/env node +import { startHttpServer } from "@/server/http"; +import { startStdioServer } from "@/server/stdio"; + +type TransportMode = "http" | "stdio"; + +const mode = (process.argv[2] as TransportMode) || "stdio"; + +if (mode === "http") { + startHttpServer(); +} else { + startStdioServer().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..49e7cb7947 --- /dev/null +++ b/packages/pluggable-widgets-mcp/src/resources/guidelines.ts @@ -0,0 +1,78 @@ +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: "property-types", + uri: "mendix://guidelines/property-types", + title: "Property Types Reference", + description: + "Complete reference for all Mendix widget property types (string, boolean, action, attribute, etc.) with JSON schema and XML output examples", + filename: "property-types.md" + }, + { + name: "widget-patterns", + uri: "mendix://guidelines/widget-patterns", + title: "Widget Patterns", + description: + "Reusable patterns for common widget types (button, input, display, container, data list) with TSX and SCSS templates", + filename: "widget-patterns.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/security/guardrails.ts b/packages/pluggable-widgets-mcp/src/security/guardrails.ts new file mode 100644 index 0000000000..b538b18af3 --- /dev/null +++ b/packages/pluggable-widgets-mcp/src/security/guardrails.ts @@ -0,0 +1,123 @@ +/** + * Security Guardrails for File Operations + * + * This module centralizes all security validation to: + * 1. Prevent path traversal attacks (directory escape) + * 2. Restrict file operations to safe extensions + * 3. Ensure operations stay within widget boundaries + * + * @module security/guardrails + */ + +import { extname, resolve } from "node:path"; + +// ============================================================================= +// Configuration +// ============================================================================= + +/** + * Allowed file extensions for write operations. + * Only widget-related source files are permitted. + */ +export const ALLOWED_EXTENSIONS = [".tsx", ".ts", ".xml", ".scss", ".css", ".json", ".md", ".editorConfig.ts"]; + +/** + * Config files allowed without extensions (e.g., .gitignore) + */ +const ALLOWED_EXTENSIONLESS_PATTERNS = ["package", "tsconfig", "eslintrc", ".gitignore", ".prettierrc"]; + +// ============================================================================= +// Path Traversal Prevention +// ============================================================================= + +/** + * Validates that a file path is within the allowed widget directory. + * Prevents directory traversal attacks. + * + * Uses path.resolve() to canonicalize paths, catching tricks like: + * - /widget/../../../etc/passwd + * - encoded sequences + * + * @param basePath - The base widget directory path + * @param relativePath - The relative file path to validate + * @returns true if the path is safe, false otherwise + * + * @example + * isPathWithinDirectory("/widgets/foo", "src/Bar.tsx") // true + * isPathWithinDirectory("/widgets/foo", "../secret.txt") // false + */ +export 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; +} + +// ============================================================================= +// Extension Whitelist +// ============================================================================= + +/** + * 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 + * + * @example + * isExtensionAllowed("Button.tsx") // true + * isExtensionAllowed("script.exe") // false + */ +export 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 ALLOWED_EXTENSIONLESS_PATTERNS.some(name => filename.includes(name) || filename.startsWith(".")); + } + return ALLOWED_EXTENSIONS.includes(ext); +} + +// ============================================================================= +// Combined Validation (High-Level API) +// ============================================================================= + +/** + * Validates widget path and file path for security. + * Throws an error if validation fails. + * + * @param widgetPath - The base widget directory path + * @param filePath - The relative file path to validate + * @param checkExtension - Whether to also validate file extension (for write operations) + * + * @throws {Error} If path traversal detected ('..' in path) + * @throws {Error} If path escapes widget directory + * @throws {Error} If extension not allowed (when checkExtension=true) + * + * @example + * // Read operation (no extension check) + * validateFilePath("/widgets/foo", "src/Bar.tsx"); + * + * // Write operation (with extension check) + * validateFilePath("/widgets/foo", "src/Bar.tsx", true); + */ +export function validateFilePath(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(", ")}`); + } +} diff --git a/packages/pluggable-widgets-mcp/src/security/index.ts b/packages/pluggable-widgets-mcp/src/security/index.ts new file mode 100644 index 0000000000..708da7c7b7 --- /dev/null +++ b/packages/pluggable-widgets-mcp/src/security/index.ts @@ -0,0 +1,14 @@ +/** + * Security module exports. + * + * This module provides security guardrails for file operations: + * - Path traversal prevention + * - Extension whitelist validation + * + * @example + * import { validateFilePath, ALLOWED_EXTENSIONS } from "@/security"; + * + * validateFilePath(widgetPath, filePath, true); // Throws if invalid + */ + +export { ALLOWED_EXTENSIONS, isExtensionAllowed, isPathWithinDirectory, validateFilePath } from "./guardrails"; 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..3f54654dba --- /dev/null +++ b/packages/pluggable-widgets-mcp/src/server/http.ts @@ -0,0 +1,39 @@ +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 function startHttpServer(): void { + const app = createMcpExpressApp(); + app.use( + cors({ + origin: true, + methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD"], + allowedHeaders: "*", + exposedHeaders: ["mcp-session-id"], + credentials: true + }) + ); + + setupRoutes(app); + + 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`); + }); + + const shutdown = async (): Promise => { + console.log("\n[HTTP] Shutting down server..."); + await sessionManager.closeAll(); + server.close(() => 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..38cdbc1d64 --- /dev/null +++ b/packages/pluggable-widgets-mcp/src/server/routes.ts @@ -0,0 +1,93 @@ +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 { + // Handle CORS preflight explicitly + app.options("/mcp", (_req: Request, res: Response) => { + res.status(204).end(); + }); + + 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 via POST with initialize request + if (req.method === "POST" && !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: GET request for SSE - create new session + // StreamableHTTP uses GET for server-to-client event streams + if (req.method === "GET") { + const transport = sessionManager.createTransport(); + const server = createMcpServer(); + await server.connect(transport); + await transport.handleRequest(req, res); + return; + } + + // Case 4: Invalid request + sendJsonRpcError(res, 400, "Bad Request: No valid session ID provided"); + } catch (error) { + console.error("[MCP] Route error:", error); + 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..a0c2630ecf --- /dev/null +++ b/packages/pluggable-widgets-mcp/src/server/server.ts @@ -0,0 +1,32 @@ +import { SERVER_ICON, SERVER_INSTRUCTIONS, SERVER_NAME, SERVER_VERSION, SERVER_WEBSITE_URL } from "@/config"; +import { registerResources } from "@/resources"; +import { registerAllTools } from "@/tools"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; + +/** + * Creates and configures a new MCP server instance with all registered tools and resources. + */ +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 + } + ); + + registerAllTools(server); + registerResources(server); + + return server; +} 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/build.tools.ts b/packages/pluggable-widgets-mcp/src/tools/build.tools.ts new file mode 100644 index 0000000000..b3faba539f --- /dev/null +++ b/packages/pluggable-widgets-mcp/src/tools/build.tools.ts @@ -0,0 +1,442 @@ +/** + * Build tools for Mendix pluggable widgets. + * Wraps pluggable-widget-tools for building and validating widgets. + */ + +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { spawn } from "node:child_process"; +import { existsSync, readdirSync } from "node:fs"; +import { join } from "node:path"; +import { z } from "zod"; +import type { ToolContext, ToolResponse } from "./types"; +import { ProgressTracker } from "./utils/progress-tracker"; +import { + createStructuredError, + createStructuredErrorResponse, + createToolResponse, + type StructuredError +} from "./utils/response"; + +/** + * Input schema for build-widget tool. + */ +const buildWidgetSchema = z.object({ + widgetPath: z.string().describe("Absolute path to the widget directory (containing package.json)") +}); + +type BuildWidgetInput = z.infer; + +/** + * Parsed error with location information. + */ +interface ParsedError { + message: string; + file?: string; + line?: number; + column?: number; + tsCode?: string; // e.g., "TS2339" + category: "typescript" | "xml" | "dependency" | "unknown"; +} + +/** + * Result of parsing build output. + */ +interface BuildResult { + success: boolean; + mpkPath?: string; + errors: ParsedError[]; + warnings: string[]; + output: string; +} + +/** + * TypeScript error pattern: src/Component.tsx(42,5): error TS2339: Property 'x' does not exist + * Also matches: src/Component.tsx:42:5 - error TS2339: Property 'x' does not exist + */ +const TS_ERROR_PATTERN = /^(.+?)[:(](\d+)[,:](\d+)[):]?\s*[-:]?\s*error\s+(TS\d+):\s*(.+)$/; + +/** + * Simple TS error pattern: error TS2339: Property 'x' does not exist + */ +const TS_ERROR_SIMPLE_PATTERN = /^error\s+(TS\d+):\s*(.+)$/; + +/** + * XML error patterns + */ +const XML_ERROR_PATTERNS = [/XML.*error/i, /invalid.*xml/i, /schema.*validation.*failed/i, /widget\.xml.*error/i]; + +/** + * Dependency error patterns + */ +const DEP_ERROR_PATTERNS = [/cannot find module/i, /module not found/i, /npm ERR!/i, /ENOENT.*node_modules/i]; + +/** + * Parses a TypeScript error line to extract file, line, column, code, and message. + */ +function parseTypeScriptError(line: string): ParsedError | null { + // Try full pattern with file location + const fullMatch = line.match(TS_ERROR_PATTERN); + if (fullMatch) { + return { + file: fullMatch[1], + line: parseInt(fullMatch[2], 10), + column: parseInt(fullMatch[3], 10), + tsCode: fullMatch[4], + message: fullMatch[5], + category: "typescript" + }; + } + + // Try simple pattern without file location + const simpleMatch = line.match(TS_ERROR_SIMPLE_PATTERN); + if (simpleMatch) { + return { + tsCode: simpleMatch[1], + message: simpleMatch[2], + category: "typescript" + }; + } + + return null; +} + +/** + * Parses the build output to extract meaningful errors and warnings. + */ +function parseBuildOutput(stdout: string, stderr: string): BuildResult { + const output = stdout + "\n" + stderr; + const errors: ParsedError[] = []; + const warnings: string[] = []; + let mpkPath: string | undefined; + + const lines = output.split("\n"); + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed) continue; + + // TypeScript errors (try to parse with location) + if (trimmed.includes("error TS") || trimmed.match(/:\s*error\s+TS/)) { + const parsed = parseTypeScriptError(trimmed); + if (parsed) { + errors.push(parsed); + continue; + } + } + + // XML validation errors + if (XML_ERROR_PATTERNS.some(pattern => pattern.test(trimmed))) { + errors.push({ + message: trimmed, + category: "xml" + }); + continue; + } + + // Dependency errors + if (DEP_ERROR_PATTERNS.some(pattern => pattern.test(trimmed))) { + errors.push({ + message: trimmed, + category: "dependency" + }); + continue; + } + + // General errors (fallback) + if (trimmed.startsWith("error:") || trimmed.startsWith("Error:")) { + errors.push({ + message: trimmed.replace(/^[Ee]rror:\s*/, ""), + category: "unknown" + }); + continue; + } + + // Warnings + if (trimmed.startsWith("warning:") || trimmed.startsWith("Warning:")) { + warnings.push(trimmed); + } + + // MPK output path + if (trimmed.includes(".mpk")) { + const mpkMatch = trimmed.match(/([^\s]+\.mpk)/); + if (mpkMatch) { + mpkPath = mpkMatch[1]; + } + } + } + + // Check for success indicators + // pluggable-widgets-tools outputs "created dist/..." when successful + const hasCreatedOutput = output.includes("created dist/") || output.includes("created dist\\"); + const success = + errors.length === 0 && + (output.includes("Build completed") || output.includes("successfully") || hasCreatedOutput || mpkPath); + + return { + success: !!success, + mpkPath, + errors, + warnings, + output + }; +} + +/** + * Build progress phases for user-friendly messages. + */ +const BUILD_PHASES = { + START: { progress: 0, message: "Starting build process..." }, + VALIDATING: { progress: 20, message: "Validating widget XML schema..." }, + GENERATING_TYPES: { progress: 40, message: "Generating TypeScript types from XML..." }, + COMPILING: { progress: 60, message: "Compiling TypeScript..." }, + BUNDLING: { progress: 80, message: "Bundling widget..." }, + COMPLETE: { progress: 100, message: "Build complete!" } +} as const; + +/** + * Runs the build command and returns the result. + */ +async function runBuild(widgetPath: string, tracker?: ProgressTracker): Promise { + return new Promise(resolve => { + // Report start + tracker?.progress(BUILD_PHASES.START.progress, BUILD_PHASES.START.message); + + // Use npm run build to run pluggable-widgets-tools (correct package name) + const buildProcess = spawn("npm", ["run", "build"], { + cwd: widgetPath, + shell: true, + env: { + ...globalThis.process.env, + FORCE_COLOR: "0" // Disable colors for easier parsing + } + }); + + let stdout = ""; + let stderr = ""; + + buildProcess.stdout?.on("data", (data: Buffer) => { + const chunk = data.toString(); + stdout += chunk; + + // Update progress based on build output + if (tracker) { + if (chunk.includes("Validating") || chunk.includes("XML")) { + tracker.progress(BUILD_PHASES.VALIDATING.progress, BUILD_PHASES.VALIDATING.message); + tracker.updateStep("validating"); + } else if (chunk.includes("Generating") || chunk.includes("types")) { + tracker.progress(BUILD_PHASES.GENERATING_TYPES.progress, BUILD_PHASES.GENERATING_TYPES.message); + tracker.updateStep("generating-types"); + } else if (chunk.includes("Compiling") || chunk.includes("tsc")) { + tracker.progress(BUILD_PHASES.COMPILING.progress, BUILD_PHASES.COMPILING.message); + tracker.updateStep("compiling"); + } else if (chunk.includes("Bundling") || chunk.includes("rollup") || chunk.includes("webpack")) { + tracker.progress(BUILD_PHASES.BUNDLING.progress, BUILD_PHASES.BUNDLING.message); + tracker.updateStep("bundling"); + } + } + }); + + buildProcess.stderr?.on("data", (data: Buffer) => { + stderr += data.toString(); + }); + + buildProcess.on("close", code => { + const result = parseBuildOutput(stdout, stderr); + + // If exit code is non-zero and we didn't detect errors, add generic error + if (code !== 0 && result.errors.length === 0) { + result.errors.push({ + message: `Build failed with exit code ${code}`, + category: "unknown" + }); + result.success = false; + } + + // Report completion + if (tracker) { + if (result.success) { + tracker.progress(BUILD_PHASES.COMPLETE.progress, BUILD_PHASES.COMPLETE.message); + tracker.markComplete(); + } else { + tracker.error(`Build failed with ${result.errors.length} error(s)`); + } + } + + resolve(result); + }); + + buildProcess.on("error", err => { + tracker?.error(`Failed to start build: ${err.message}`); + resolve({ + success: false, + errors: [{ message: `Failed to start build process: ${err.message}`, category: "unknown" }], + warnings: [], + output: "" + }); + }); + }); +} + +/** + * Converts a parsed error to a structured error with suggestions. + */ +function toStructuredError(error: ParsedError): StructuredError { + const suggestions: Record = { + typescript: + "Check the TypeScript code at the specified location. Ensure props match the generated types from widget XML.", + xml: "Verify your widget.xml follows the Mendix schema. Check property types and required attributes.", + dependency: + "Run 'npm install' in the widget directory. If the issue persists, check that all dependencies are listed in package.json.", + unknown: "Review the build output for more details. Try running 'npx pluggable-widget-tools build' manually." + }; + + const codeMap: Record = { + typescript: "ERR_BUILD_TS", + xml: "ERR_BUILD_XML", + dependency: "ERR_BUILD_MISSING_DEP", + unknown: "ERR_BUILD_UNKNOWN" + }; + + return createStructuredError(codeMap[error.category], error.message, { + suggestion: suggestions[error.category], + file: error.file, + line: error.line, + column: error.column + }); +} + +/** + * Finds the most recently created MPK file in the widget's dist directory. + */ +function findMpkFile(widgetPath: string): string | undefined { + const distPath = join(widgetPath, "dist"); + if (!existsSync(distPath)) return undefined; + + try { + // Search for .mpk files recursively (usually in dist/x.x.x/) + const searchDir = (dir: string): string | undefined => { + const entries = readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = join(dir, entry.name); + if (entry.isDirectory()) { + const found = searchDir(fullPath); + if (found) return found; + } else if (entry.name.endsWith(".mpk")) { + return fullPath; + } + } + return undefined; + }; + return searchDir(distPath); + } catch { + return undefined; + } +} + +/** + * Handler for the build-widget tool. + */ +async function handleBuildWidget(args: BuildWidgetInput, context: ToolContext): Promise { + const { widgetPath } = args; + + // Validate path exists + if (!existsSync(widgetPath)) { + return createStructuredErrorResponse( + createStructuredError("ERR_NOT_FOUND", `Widget directory not found: ${widgetPath}`, { + suggestion: "Verify the widget path is correct and the directory exists." + }) + ); + } + + // Check for package.json + const packageJsonPath = join(widgetPath, "package.json"); + if (!existsSync(packageJsonPath)) { + return createStructuredErrorResponse( + createStructuredError("ERR_NOT_FOUND", `No package.json found in ${widgetPath}`, { + suggestion: "Ensure this is a valid widget directory created with create-widget tool." + }) + ); + } + + // Create progress tracker + const tracker = new ProgressTracker({ + context, + logger: "build", + totalSteps: 5 // 5 phases: start, validate, generate, compile, bundle + }); + + tracker.start("initializing"); + + try { + // Run build with progress tracking + const result = await runBuild(widgetPath, tracker); + + // Try to find MPK file if not already detected + const mpkPath = result.mpkPath || findMpkFile(widgetPath); + + if (result.success) { + let message = `✅ Build successful!`; + + if (mpkPath) { + message += `\n\n📦 MPK output: ${mpkPath}`; + } + + if (result.warnings.length > 0) { + message += `\n\n⚠️ Warnings:\n${result.warnings.map(w => ` - ${w}`).join("\n")}`; + } + + return createToolResponse(message); + } else { + // Return first error as structured error (most relevant) + if (result.errors.length > 0) { + const primaryError = toStructuredError(result.errors[0]); + + // Add additional errors to raw output if multiple + if (result.errors.length > 1) { + const additionalErrors = result.errors + .slice(1) + .map(e => { + const loc = e.file ? `${e.file}${e.line ? `:${e.line}` : ""}` : ""; + return loc ? `[${loc}] ${e.message}` : e.message; + }) + .join("\n"); + + primaryError.details = { + ...primaryError.details, + rawOutput: `Additional errors (${result.errors.length - 1}):\n${additionalErrors}` + }; + } + + return createStructuredErrorResponse(primaryError); + } + + // Fallback for unknown failures + return createStructuredErrorResponse( + createStructuredError("ERR_BUILD_UNKNOWN", "Build failed with unknown error", { + suggestion: "Check the raw build output for details.", + rawOutput: result.output.slice(0, 1000) + }) + ); + } + } finally { + tracker.stop(); + } +} + +/** + * Registers the build tools with the MCP server. + */ +export function registerBuildTools(server: McpServer): void { + server.registerTool( + "build-widget", + { + title: "Build Widget", + description: + "Builds a Mendix pluggable widget using pluggable-widget-tools. " + + "Validates XML, compiles TypeScript, generates types, and produces an .mpk file. " + + "Returns build errors if any, which can be used to fix issues.", + inputSchema: buildWidgetSchema + }, + handleBuildWidget + ); +} 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/file-operations.tools.ts b/packages/pluggable-widgets-mcp/src/tools/file-operations.tools.ts new file mode 100644 index 0000000000..72104ce7ef --- /dev/null +++ b/packages/pluggable-widgets-mcp/src/tools/file-operations.tools.ts @@ -0,0 +1,318 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises"; +import { dirname, extname, join } from "node:path"; +import { z } from "zod"; +import { ALLOWED_EXTENSIONS, validateFilePath } from "@/security"; +import type { ToolResponse } from "@/tools/types"; +import { createErrorResponse, createToolResponse } from "@/tools/utils/response"; + +// ============================================================================= +// 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") +}); + +const fileEntrySchema = z.object({ + relativePath: z.string().min(1).describe("Relative path within the widget directory"), + content: z.string().describe("File content to write") +}); + +const batchWriteWidgetFilesSchema = z.object({ + widgetPath: z.string().min(1).describe("Absolute path to the widget directory"), + files: z.array(fileEntrySchema).min(1).describe("Array of files to write") +}); + +type ListWidgetFilesInput = z.infer; +type ReadWidgetFileInput = z.infer; +type WriteWidgetFileInput = z.infer; +type BatchWriteWidgetFilesInput = 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 { + validateFilePath(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 { + validateFilePath(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}`); + } +} + +async function handleBatchWriteWidgetFiles(args: BatchWriteWidgetFilesInput): Promise { + const results: Array<{ path: string; success: boolean; error?: string }> = []; + + // Validate all paths first before writing anything + for (const file of args.files) { + try { + validateFilePath(args.widgetPath, file.relativePath, true); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return createErrorResponse(`Validation failed for ${file.relativePath}: ${message}`); + } + } + + // Write all files + for (const file of args.files) { + try { + const fullPath = join(args.widgetPath, file.relativePath); + + // Ensure parent directory exists + const parentDir = dirname(fullPath); + await mkdir(parentDir, { recursive: true }); + + // Write the file + await writeFile(fullPath, file.content, "utf-8"); + + console.error(`[file-operations] Wrote file: ${fullPath}`); + results.push({ path: file.relativePath, success: true }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + results.push({ path: file.relativePath, success: false, error: message }); + } + } + + const successful = results.filter(r => r.success); + const failed = results.filter(r => !r.success); + + if (failed.length === 0) { + return createToolResponse( + [`Successfully wrote ${successful.length} files:`, "", ...successful.map(r => ` - ${r.path}`)].join("\n") + ); + } else if (successful.length === 0) { + return createErrorResponse( + [`Failed to write all ${failed.length} files:`, "", ...failed.map(r => ` - ${r.path}: ${r.error}`)].join( + "\n" + ) + ); + } else { + return createToolResponse( + [ + `Partial success: ${successful.length} written, ${failed.length} failed`, + "", + "Written:", + ...successful.map(r => ` - ${r.path}`), + "", + "Failed:", + ...failed.map(r => ` - ${r.path}: ${r.error}`) + ].join("\n") + ); + } +} + +// ============================================================================= +// 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(", ")}`; + +const BATCH_WRITE_WIDGET_FILES_DESCRIPTION = `Writes multiple files to a widget directory in a single operation. + +Use this for atomic writes when updating XML, TSX, and SCSS together. +Validates all paths before writing to ensure consistency. +Creates parent directories if they don't exist. + +Example use case: After generating XML from a widget definition, +write the XML, TSX component, and SCSS files together. + +Allowed file types: ${ALLOWED_EXTENSIONS.join(", ")}`; + +/** + * Registers 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 registerFileOperationTools(server: McpServer): void { + server.registerTool( + "list-widget-files", + { + title: "List Widget Files", + description: LIST_WIDGET_FILES_DESCRIPTION, + inputSchema: listWidgetFilesSchema + }, + handleListWidgetFiles + ); + + server.registerTool( + "read-widget-file", + { + title: "Read Widget File", + description: READ_WIDGET_FILE_DESCRIPTION, + inputSchema: readWidgetFileSchema + }, + handleReadWidgetFile + ); + + server.registerTool( + "write-widget-file", + { + title: "Write Widget File", + description: WRITE_WIDGET_FILE_DESCRIPTION, + inputSchema: writeWidgetFileSchema + }, + handleWriteWidgetFile + ); + + server.registerTool( + "batch-write-widget-files", + { + title: "Batch Write Widget Files", + description: BATCH_WRITE_WIDGET_FILES_DESCRIPTION, + inputSchema: batchWriteWidgetFilesSchema + }, + handleBatchWriteWidgetFiles + ); +} 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..31e508e651 --- /dev/null +++ b/packages/pluggable-widgets-mcp/src/tools/index.ts @@ -0,0 +1,24 @@ +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"; + +/** + * Registers all tools 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, 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. + */ +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 new file mode 100644 index 0000000000..640e0a8e25 --- /dev/null +++ b/packages/pluggable-widgets-mcp/src/tools/scaffolding.tools.ts @@ -0,0 +1,204 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { GENERATIONS_DIR } from "@/config"; +import { DEFAULT_WIDGET_OPTIONS, type ToolContext, 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 { + createStructuredError, + createStructuredErrorResponse, + createToolResponse, + type ErrorCode +} from "@/tools/utils/response"; +import { access, mkdir } from "node:fs/promises"; +import { dirname } from "node:path"; +import { z } from "zod"; + +/** + * Schema for create-widget tool input. + * Extends the base widgetOptionsSchema with tool-specific options like outputPath. + */ +const createWidgetSchema = widgetOptionsSchema.extend({ + outputPath: z + .string() + .optional() + .describe( + "[OPTIONAL] Directory where widget will be created. Defaults to ./generations/ in the current working directory. For desktop clients without a clear working directory, ask the user for their preferred location." + ) +}); + +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}) + • outputPath: Directory where widget will be created (default: ./generations/) + +Ask the user if they want to customize any options before proceeding.`; + +/** + * Registers scaffolding-related tools for widget creation and management. + * + * Currently registers the create-widget tool. 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 registerScaffoldingTools(server: McpServer): void { + server.registerTool( + "create-widget", + { + title: "Create Widget", + description: CREATE_WIDGET_DESCRIPTION, + inputSchema: createWidgetSchema + }, + handleCreateWidget + ); +} + +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", + totalSteps: GENERATOR_PROMPTS.length + }); + + 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}"...`, { + widgetName: options.name, + template: options.template, + organization: options.organization, + outputDir + }); + + // Ensure output directory exists + await mkdir(outputDir, { recursive: true }); + + // Create widget folder - we control the folder name (matches user's input) + const widgetFolder = options.name; + const widgetPath = `${outputDir}/${widgetFolder}`; + await mkdir(widgetPath, { recursive: true }); + + // Run generator inside the widget folder (it outputs files directly there) + await runWidgetGenerator(options, tracker, widgetPath); + + 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}`, + "", + "=== 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. npm install", + "3. npm start (builds and watches 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 + }); + + // Categorize the error for structured response + let code: ErrorCode = "ERR_SCAFFOLD_FAILED"; + let suggestion = "Check the error details and try again. Ensure you have npm/npx available."; + + if (message.includes("timed out")) { + code = "ERR_SCAFFOLD_TIMEOUT"; + suggestion = + "The generator took too long. Check your network connection and npm registry access. Try running 'npx @mendix/generator-widget' manually."; + } else if (message.includes("prompt") || message.includes("expected")) { + code = "ERR_SCAFFOLD_PROMPT"; + suggestion = + "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"; + // 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( + createStructuredError(code, `Failed to create widget "${options.name}"`, { + suggestion, + rawOutput: 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..ab5084024f --- /dev/null +++ b/packages/pluggable-widgets-mcp/src/tools/types.ts @@ -0,0 +1,134 @@ +import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js"; +import type { ServerNotification, ServerRequest } from "@modelcontextprotocol/sdk/types.js"; +import { z } 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. + * Used for typing handlers that are passed to McpServer.registerTool(). + */ +export type ToolHandler = (args: T, context: ToolContext) => Promise; + +/** + * Log levels supported by MCP logging notifications. + */ +export type LogLevel = "debug" | "info" | "notice" | "warning" | "error"; + +// ============================================================================= +// Widget Generator Types +// ============================================================================= + +/** + * 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; + 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..f6317ee4ea --- /dev/null +++ b/packages/pluggable-widgets-mcp/src/tools/utils/generator.ts @@ -0,0 +1,351 @@ +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"; + +// Re-export for backward compatibility with existing imports +export { DEFAULT_WIDGET_OPTIONS }; + +/** + * 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; + +/** + * Buffer size for prompt detection in terminal output. + * Increased from 500 to improve reliability with terminal buffering. + */ +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. + */ +interface GeneratorLocalState { + output: string; + 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: WidgetOptionsInput): 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: 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, 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, + "© Mendix Technology BV 2025", // Copyright + options.license, + options.version, + options.author, + 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, + options.unitTests ? "yes" : "no", + options.e2eTests ? "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() + ); +} + +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. + */ +function handleGeneratorOutput( + state: GeneratorLocalState, + tracker: ProgressTracker, + 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) { + return; + } + + const expectedPattern = GENERATOR_PROMPTS[state.answerIndex]; + const recentOutput = state.output.slice(-PROMPT_DETECTION_BUFFER_SIZE).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, 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(); + } +} + +/** + * Gets the path to npx based on the current node executable. + * This ensures we use the correct npx even when PATH is not fully available. + */ +function getNpxPath(): string { + const nodePath = process.execPath; + const nodeDir = nodePath.substring(0, nodePath.lastIndexOf("/")); + return `${nodeDir}/npx`; +} + +/** + * 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 async function runWidgetGenerator( + options: WidgetOptions, + tracker: ProgressTracker, + outputDir: string = GENERATIONS_DIR +): Promise { + const pty = await loadNodePty(); + const answers = buildGeneratorAnswers(options, outputDir); + const npxPath = getNpxPath(); + + return new Promise((resolve, reject) => { + const state: GeneratorLocalState = { + output: "", + answerIndex: 0, + promptMatchedIndex: -1, + allPromptsAnswered: false, + lastActivityTime: Date.now() + }; + + tracker.start("initializing"); + + // Use full path to npx to avoid PATH issues in STDIO mode + const ptyProcess = pty.spawn(npxPath, ["@mendix/generator-widget", options.name], { + name: "xterm-color", + cols: 120, + rows: 30, + 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.startsWith("\x1b") ? "(Arrow+Enter)" : `"${answer}"`; + const idx = state.answerIndex + 1; + console.error(`[create-widget] [${idx}/${answers.length}] Sending: ${displayAnswer}`); + state.answerIndex++; + ptyProcess.write(answer + "\r"); + } + }; + + // 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, () => { + 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 }) => { + clearInterval(stuckCheckInterval); + tracker.stop(); + if (exitCode === 0) { + // Generator creates folder with exact widget name (preserves case) + const widgetFolder = options.name; + 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(() => { + clearInterval(stuckCheckInterval); + 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..7ecbef307f --- /dev/null +++ b/packages/pluggable-widgets-mcp/src/tools/utils/notifications.ts @@ -0,0 +1,65 @@ +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; + 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. + * + * **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, + 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..0a968776f6 --- /dev/null +++ b/packages/pluggable-widgets-mcp/src/tools/utils/response.ts @@ -0,0 +1,124 @@ +import type { ToolResponse } from "@/tools/types"; + +/** + * Error codes for structured error responses. + * These help clients categorize and handle errors appropriately. + */ +export type ErrorCode = + | "ERR_BUILD_TS" // TypeScript compilation error + | "ERR_BUILD_XML" // XML validation error + | "ERR_BUILD_MISSING_DEP" // Missing dependency + | "ERR_BUILD_UNKNOWN" // Unknown build error + | "ERR_SCAFFOLD_TIMEOUT" // Scaffolding timed out + | "ERR_SCAFFOLD_PROMPT" // Generator prompt mismatch + | "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_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. + * Provides actionable information for debugging and fixing issues. + */ +export interface StructuredError { + code: ErrorCode; + message: string; + suggestion?: string; + details?: { + file?: string; + line?: number; + column?: number; + rawOutput?: string; + }; +} + +/** + * 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 }] + }; +} + +/** + * Creates a structured error response with code, message, and details. + * Formats the error for both human readability and machine parsing. + */ +export function createStructuredErrorResponse(error: StructuredError): ToolResponse { + const lines: string[] = []; + + // Header with error code + lines.push(`❌ [${error.code}] ${error.message}`); + + // File location if available + if (error.details?.file) { + let location = ` 📁 File: ${error.details.file}`; + if (error.details.line) { + location += `:${error.details.line}`; + if (error.details.column) { + location += `:${error.details.column}`; + } + } + lines.push(location); + } + + // Suggestion for fixing + if (error.suggestion) { + lines.push(` 💡 Suggestion: ${error.suggestion}`); + } + + // Raw output for debugging (truncated) + if (error.details?.rawOutput) { + const truncated = + error.details.rawOutput.length > 500 + ? error.details.rawOutput.slice(0, 500) + "...(truncated)" + : error.details.rawOutput; + lines.push(` 📝 Details:\n${truncated}`); + } + + return { + content: [{ type: "text", text: lines.join("\n") }] + }; +} + +/** + * Creates a structured error object (for use with createStructuredErrorResponse). + */ +export function createStructuredError( + code: ErrorCode, + message: string, + options?: { + suggestion?: string; + file?: string; + line?: number; + column?: number; + rawOutput?: string; + } +): StructuredError { + return { + code, + message, + suggestion: options?.suggestion, + details: + options?.file || options?.line || options?.rawOutput + ? { + file: options?.file, + line: options?.line, + column: options?.column, + rawOutput: options?.rawOutput + } + : undefined + }; +} 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..5398ab660d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -408,6 +408,43 @@ importers: specifier: ^16.0.0 version: 16.4.0 + packages/pluggable-widgets-mcp: + dependencies: + '@modelcontextprotocol/sdk': + specifier: ^1.24.2 + version: 1.25.2(hono@4.11.3)(zod@4.3.5) + cors: + specifier: ^2.8.5 + version: 2.8.5 + express: + specifier: ^5.1.0 + version: 5.2.1 + node-pty: + specifier: 1.2.0-beta.7 + version: 1.2.0-beta.7 + tsx: + specifier: ^4.21.0 + version: 4.21.0 + zod: + specifier: ^4.1.13 + version: 4.3.5 + devDependencies: + '@types/cors': + specifier: ^2.8.19 + version: 2.8.19 + '@types/express': + specifier: ^5.0.6 + version: 5.0.6 + '@types/node': + specifier: ~22.14.0 + version: 22.14.1 + tsc-alias: + specifier: ^1.8.16 + version: 1.8.16 + typescript: + specifier: '>5.8.0' + version: 5.9.3 + packages/pluggableWidgets/accessibility-helper-web: dependencies: '@mendix/widget-plugin-component-kit': @@ -3898,6 +3935,162 @@ packages: resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} engines: {node: '>=10.0.0'} + '@esbuild/aix-ppc64@0.27.2': + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.2': + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.2': + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.2': + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.2': + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.2': + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.2': + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.2': + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.2': + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.2': + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.2': + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.2': + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.2': + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.2': + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.2': + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.2': + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.2': + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.2': + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.2': + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.2': + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.2': + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.2': + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.2': + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.2': + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.2': + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.2': + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.0': resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3968,6 +4161,12 @@ packages: resolution: {integrity: sha512-Tt1oSC7yBwM05j6/SOLagOAJ/NW7XrXKKqUwcuBY++OZO9YyEWF/i72jFSc3DGW4ZAHfc6HHsTIkhayxyy+DsA==} engines: {node: '>=20.0.0'} + '@hono/node-server@1.19.7': + resolution: {integrity: sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -4043,10 +4242,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 +4250,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 +4258,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} @@ -4216,6 +4403,16 @@ packages: engines: {node: '>=20'} hasBin: true + '@modelcontextprotocol/sdk@1.25.2': + resolution: {integrity: sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -4818,12 +5015,21 @@ packages: '@types/big.js@6.2.2': resolution: {integrity: sha512-e2cOW9YlVzFY2iScnGBBkplKsrn2CsObHQ2Hiw4V1sSyiGbgWL8IyqE3zFi1Pt5o1pdAtYkDAIsF3KKUPjdzaA==} + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + '@types/cheerio@0.22.35': resolution: {integrity: sha512-yD57BchKRvTV+JD53UZ6PD8KWY5g5rvvMLRnZR3EQBCZXiDT/HR+pKpMzFGlWNhFrXlo7VPZXtKvIEwZkAWOIA==} + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/conventional-commits-parser@5.0.1': resolution: {integrity: sha512-7uz5EHdzz2TqoMfV7ee61Egf5y6NkcO4FB/1iCCQnbeiI1F3xzv3vK5dBCXUCLQgGYS+mUeigK1iKQzvED+QnQ==} + '@types/cors@2.8.19': + resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + '@types/cross-zip@4.0.2': resolution: {integrity: sha512-yvTQ6/tWlGdykh6qkVigwmq42gi51qHPdi7e60KRmPCxeYj5QcX8RX0T6jCDIWcHNWLMVw1IuoMehGcwDuzrYw==} @@ -4848,6 +5054,12 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/express-serve-static-core@5.1.0': + resolution: {integrity: sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==} + + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + '@types/fs-extra@8.1.5': resolution: {integrity: sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==} @@ -4866,6 +5078,9 @@ packages: '@types/graceful-fs@4.1.9': resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + '@types/istanbul-lib-coverage@2.0.6': resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} @@ -4878,9 +5093,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==} @@ -4935,6 +5147,12 @@ packages: '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + '@types/qs@6.14.0': + resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + '@types/rc-slider@8.6.6': resolution: {integrity: sha512-2Q3vwKrSm3PbgiMNwzxMkOaMtcAGi0xQ8WPeVKoabk1vNYHiVR44DMC3mr9jC2lhbxCBgGBJWF9sBhmnSDQ8Bg==} @@ -4983,6 +5201,12 @@ packages: '@types/semver@7.7.1': resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} @@ -5301,6 +5525,10 @@ packages: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-globals@7.0.1: resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} @@ -5345,6 +5573,14 @@ packages: ajv: optional: true + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv-keywords@5.1.0: resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} peerDependencies: @@ -5623,6 +5859,10 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -5669,6 +5909,10 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -5899,6 +6143,10 @@ packages: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} + commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + commenting@1.1.0: resolution: {integrity: sha512-YeNK4tavZwtH7jEgK1ZINXzLKm6DZdEMfsaaieOsCAN0S8vsY7UeuO3Q7d/M018EFgE+IeUAuBOKkFccBZsUZA==} @@ -5940,6 +6188,14 @@ packages: resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} engines: {node: '>= 0.10.0'} + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + conventional-changelog-angular@7.0.0: resolution: {integrity: sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==} engines: {node: '>=16'} @@ -5956,6 +6212,14 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + copy-and-watch@0.1.8: resolution: {integrity: sha512-Prw3k4Za+C/m/OutNtjy1+7Fq+JTiryrFc5JiR0wRrYQ+yPUnsXV8DNPna7plzEcmNbm8x87fqegp9+ogNqKNQ==} engines: {node: '>=10'} @@ -5976,6 +6240,10 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + cosmiconfig-typescript-loader@6.2.0: resolution: {integrity: sha512-GEN39v7TgdxgIoNcdkRE3uiAzQt3UXLyHbRHD6YoL048XAeOomyxaP+Hh/+2C6C2wYjxJ2onhJcsQp+L4YEkVQ==} engines: {node: '>=v18'} @@ -6603,6 +6871,11 @@ packages: es6-weak-map@2.0.3: resolution: {integrity: sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==} + esbuild@0.27.2: + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -6861,6 +7134,14 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -6877,13 +7158,19 @@ 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==} + express-rate-limit@7.5.1: + resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + ext@1.7.0: resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} @@ -6961,6 +7248,10 @@ packages: resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} engines: {node: '>= 0.8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + find-free-port@2.0.0: resolution: {integrity: sha512-J1j8gfEVf5FN4PR5w5wrZZ7NYs2IvqsHcd03cAeQx3Ec/mo+lKceaVNhpsRKoZpZKbId88o8qh+dwUwzBV6WCg==} @@ -7015,10 +7306,18 @@ packages: resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + fresh@0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + from2@2.3.0: resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==} @@ -7107,6 +7406,9 @@ packages: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} + get-tsconfig@4.13.0: + resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + git-hooks-list@4.1.1: resolution: {integrity: sha512-cmP497iLq54AZnv4YRAEMnEyQ1eIn4tGKbmswqwmFV4GBnAqE8NLtWxxdXa++AalfgL5EBH4IxTPyquEuGY/jA==} @@ -7335,6 +7637,10 @@ packages: hoist-non-react-statics@2.5.5: resolution: {integrity: sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==} + hono@4.11.3: + resolution: {integrity: sha512-PmQi306+M/ct/m5s66Hrg+adPnkD5jiO6IjA7WhWw0gSBSo1EcRegwuI1deZ+wd5pzCGynCcn2DprnE4/yEV4w==} + engines: {node: '>=16.9.0'} + html-element-map@1.3.1: resolution: {integrity: sha512-6XMlxrAFX4UEEGxctfFnmrFaaZFNf9i5fNuV5wZ3WWQ4FVaNP1aX1LkX9j2mfEx1NpjeE/rL3nmgEn23GdFmrg==} @@ -7355,6 +7661,10 @@ packages: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + http-proxy-agent@5.0.0: resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} engines: {node: '>= 6'} @@ -7384,6 +7694,10 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.1: + resolution: {integrity: sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==} + engines: {node: '>=0.10.0'} + icss-replace-symbols@1.1.0: resolution: {integrity: sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg==} @@ -7487,6 +7801,10 @@ packages: ip@1.1.9: resolution: {integrity: sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==} + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + is-arguments@1.2.0: resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} engines: {node: '>= 0.4'} @@ -7634,6 +7952,9 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} @@ -7801,10 +8122,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 +8167,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'} @@ -7948,6 +8253,9 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + jose@6.1.3: + resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + js-beautify@1.15.4: resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==} engines: {node: '>=14'} @@ -8000,6 +8308,9 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -8290,6 +8601,10 @@ packages: mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + memoize-one@5.2.1: resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} @@ -8306,6 +8621,10 @@ packages: resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} engines: {node: '>=16.10'} + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge-refs@1.3.0: resolution: {integrity: sha512-nqXPXbso+1dcKDpPCXvwZyJILz+vSLqGGOnDrYHQYE+B8n9JTCekVLC65AfCpR4ggVyA/45Y0iR9LDyS2iI+zA==} peerDependencies: @@ -8393,10 +8712,18 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} @@ -8544,6 +8871,10 @@ packages: murmurhash-js@1.0.0: resolution: {integrity: sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==} + mylas@2.1.14: + resolution: {integrity: sha512-BzQguy9W9NJgoVn2mRWzbFrFWWztGCcng2QI9+41frfk+Athwgx3qhqhvStz7ExeUUu7Kzw427sNzHpEZNINog==} + engines: {node: '>=16.0.0'} + nanoevents@9.1.0: resolution: {integrity: sha512-Jd0fILWG44a9luj8v5kED4WI+zfkkgwKyRQKItTtlPfEsh7Lznfi1kr8/iZ+XAIss4Qq5GqRB0qtWbaz9ceO/A==} engines: {node: ^18.0.0 || >=20.0.0} @@ -8578,6 +8909,10 @@ packages: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} @@ -8603,6 +8938,9 @@ packages: node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + 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==} @@ -8837,6 +9175,9 @@ packages: resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} engines: {node: 20 || >=22} + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -8887,6 +9228,10 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + pkg-dir@4.2.0: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} @@ -8904,6 +9249,10 @@ packages: engines: {node: '>=18'} hasBin: true + plimit-lit@1.6.1: + resolution: {integrity: sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==} + engines: {node: '>=12'} + plotly.js-dist-min@3.1.1: resolution: {integrity: sha512-eyuiESylUXW4kaF+v9J2gy9eZ+YT2uSVLILM4w1Afxnuv9u4UX9OnZnHR1OdF9ybq4x7+9chAzWUUbQ6HvBb3g==} @@ -9196,10 +9545,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'} @@ -9245,6 +9590,10 @@ packages: protocol-buffers-schema@3.6.0: resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==} + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + prr@1.0.1: resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} @@ -9270,9 +9619,17 @@ packages: peerDependencies: react: '>=18.0.0 <19.0.0' + qs@6.14.1: + resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} + engines: {node: '>=0.6'} + querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + queue-lit@1.5.2: + resolution: {integrity: sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==} + engines: {node: '>=12'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -9313,6 +9670,10 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -9586,6 +9947,9 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve-protobuf-schema@2.1.0: resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==} @@ -9677,6 +10041,10 @@ packages: engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + rst-selector-parser@2.2.3: resolution: {integrity: sha512-nDG1rZeP6oFTLN6yNDV/uiAvs1+FS/KlrEwh7+y7dpuApDBy6bI2HTBcc0/V8lv9OTqfyD34eF7au2pm8aBbhA==} @@ -9768,6 +10136,10 @@ packages: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + serialize-error@2.1.0: resolution: {integrity: sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==} engines: {node: '>=0.10.0'} @@ -9779,6 +10151,10 @@ packages: resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} engines: {node: '>= 0.8.0'} + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -9982,6 +10358,10 @@ packages: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -10323,6 +10703,11 @@ packages: '@swc/wasm': optional: true + tsc-alias@1.8.16: + resolution: {integrity: sha512-QjCyu55NFyRSBAl6+MTFwplpFcnm2Pq01rR/uxfqJoLMm6X3O14KEGtaSDZpJYaE1bJBGDjD0eSuiIWPe2T58g==} + engines: {node: '>=16.20.2'} + hasBin: true + tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} @@ -10335,6 +10720,11 @@ packages: peerDependencies: typescript: '>5.8.0' + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} @@ -10396,6 +10786,10 @@ packages: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + type@2.7.3: resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} @@ -10545,6 +10939,10 @@ packages: resolution: {integrity: sha512-IUoow1YUtvoBBC06dXs8bR8B9vuA3aJfmQNKMoaPG/OFsPmoQvw8xh+6Ye25Gx9DQhoEom3Pcu9MKHerm/NpUQ==} engines: {node: ^18.17.0 || >=20.5.0} + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + vlq@0.2.3: resolution: {integrity: sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==} @@ -10840,9 +11238,17 @@ packages: resolution: {integrity: sha512-EkXc2JGcKhO5N5aZ7TmuNo45budRaFGHOmz24wtJR7znbNqDPmdZtUauKX6et8KAVseAMBOyWJqEpXcHTBsh7Q==} engines: {node: '>= 6'} + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + peerDependencies: + zod: ^3.25 || ^4 + zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.3.5: + resolution: {integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==} + snapshots: '@adobe/css-tools@4.4.4': {} @@ -11976,6 +12382,84 @@ snapshots: '@discoveryjs/json-ext@0.5.7': {} + '@esbuild/aix-ppc64@0.27.2': + optional: true + + '@esbuild/android-arm64@0.27.2': + optional: true + + '@esbuild/android-arm@0.27.2': + optional: true + + '@esbuild/android-x64@0.27.2': + optional: true + + '@esbuild/darwin-arm64@0.27.2': + optional: true + + '@esbuild/darwin-x64@0.27.2': + optional: true + + '@esbuild/freebsd-arm64@0.27.2': + optional: true + + '@esbuild/freebsd-x64@0.27.2': + optional: true + + '@esbuild/linux-arm64@0.27.2': + optional: true + + '@esbuild/linux-arm@0.27.2': + optional: true + + '@esbuild/linux-ia32@0.27.2': + optional: true + + '@esbuild/linux-loong64@0.27.2': + optional: true + + '@esbuild/linux-mips64el@0.27.2': + optional: true + + '@esbuild/linux-ppc64@0.27.2': + optional: true + + '@esbuild/linux-riscv64@0.27.2': + optional: true + + '@esbuild/linux-s390x@0.27.2': + optional: true + + '@esbuild/linux-x64@0.27.2': + optional: true + + '@esbuild/netbsd-arm64@0.27.2': + optional: true + + '@esbuild/netbsd-x64@0.27.2': + optional: true + + '@esbuild/openbsd-arm64@0.27.2': + optional: true + + '@esbuild/openbsd-x64@0.27.2': + optional: true + + '@esbuild/openharmony-arm64@0.27.2': + optional: true + + '@esbuild/sunos-x64@0.27.2': + optional: true + + '@esbuild/win32-arm64@0.27.2': + optional: true + + '@esbuild/win32-ia32@0.27.2': + optional: true + + '@esbuild/win32-x64@0.27.2': + optional: true + '@eslint-community/eslint-utils@4.9.0(eslint@7.32.0)': dependencies: eslint: 7.32.0 @@ -12079,6 +12563,10 @@ snapshots: jest-mock: 29.7.0 jest-util: 29.7.0 + '@hono/node-server@1.19.7(hono@4.11.3)': + dependencies: + hono: 4.11.3 + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -12183,8 +12671,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 +12682,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 +12698,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 @@ -12486,7 +12966,7 @@ snapshots: identity-obj-proxy: 3.0.0 jasmine: 3.99.0 jasmine-core: 3.99.1 - jest: 29.7.0(@types/node@22.14.1) + jest: 29.7.0(@types/node@22.14.1)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.14.1)(typescript@5.9.3)) jest-environment-jsdom: 29.7.0 jest-jasmine2: 29.7.0 jest-junit: 13.2.0 @@ -12541,6 +13021,28 @@ snapshots: - tslib - utf-8-validate + '@modelcontextprotocol/sdk@1.25.2(hono@4.11.3)(zod@4.3.5)': + dependencies: + '@hono/node-server': 1.19.7(hono@4.11.3) + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + content-type: 1.0.5 + cors: 2.8.5 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 7.5.1(express@5.2.1) + jose: 6.1.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.3.5 + zod-to-json-schema: 3.25.1(zod@4.3.5) + transitivePeerDependencies: + - hono + - supports-color + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -13145,14 +13647,27 @@ snapshots: '@types/big.js@6.2.2': {} + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 22.14.1 + '@types/cheerio@0.22.35': dependencies: '@types/node': 22.14.1 + '@types/connect@3.4.38': + dependencies: + '@types/node': 22.14.1 + '@types/conventional-commits-parser@5.0.1': dependencies: '@types/node': 22.14.1 + '@types/cors@2.8.19': + dependencies: + '@types/node': 22.14.1 + '@types/cross-zip@4.0.2': {} '@types/date-arithmetic@4.1.4': {} @@ -13178,6 +13693,19 @@ snapshots: '@types/estree@1.0.8': {} + '@types/express-serve-static-core@5.1.0': + dependencies: + '@types/node': 22.14.1 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@5.0.6': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.1.0 + '@types/serve-static': 2.2.0 + '@types/fs-extra@8.1.5': dependencies: '@types/node': 22.14.1 @@ -13199,6 +13727,8 @@ snapshots: dependencies: '@types/node': 22.14.1 + '@types/http-errors@2.0.5': {} + '@types/istanbul-lib-coverage@2.0.6': {} '@types/istanbul-lib-report@3.0.3': @@ -13214,11 +13744,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': @@ -13275,6 +13800,10 @@ snapshots: '@types/prop-types@15.7.15': {} + '@types/qs@6.14.0': {} + + '@types/range-parser@1.2.7': {} + '@types/rc-slider@8.6.6': dependencies: '@types/rc-tooltip': 3.7.14 @@ -13341,6 +13870,15 @@ snapshots: '@types/semver@7.7.1': {} + '@types/send@1.2.1': + dependencies: + '@types/node': 22.14.1 + + '@types/serve-static@2.2.0': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 22.14.1 + '@types/stack-utils@2.0.3': {} '@types/supercluster@7.1.3': @@ -13349,7 +13887,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': {} @@ -13758,6 +14296,11 @@ snapshots: mime-types: 2.1.35(patch_hash=f54449b9273bc9e74fb67a14fcd001639d788d038b7eb0b5f43c10dff2b1adfb) negotiator: 0.6.3 + accepts@2.0.0: + dependencies: + mime-types: 3.0.2(patch_hash=f54449b9273bc9e74fb67a14fcd001639d788d038b7eb0b5f43c10dff2b1adfb) + negotiator: 1.0.0 + acorn-globals@7.0.1: dependencies: acorn: 8.15.0 @@ -13795,6 +14338,10 @@ snapshots: optionalDependencies: ajv: 8.17.1 + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + ajv-keywords@5.1.0(ajv@8.17.1): dependencies: ajv: 8.17.1 @@ -14126,6 +14673,20 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.0 + iconv-lite: 0.7.1 + on-finished: 2.4.1 + qs: 6.14.1 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + boolbase@1.0.0: {} brace-expansion@1.1.12: @@ -14175,6 +14736,8 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + bytes@3.1.2: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -14308,7 +14871,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: {} @@ -14432,6 +14996,8 @@ snapshots: commander@8.3.0: {} + commander@9.5.0: {} + commenting@1.1.0: {} commondir@1.0.1: {} @@ -14490,6 +15056,10 @@ snapshots: transitivePeerDependencies: - supports-color + content-disposition@1.0.1: {} + + content-type@1.0.5: {} + conventional-changelog-angular@7.0.0: dependencies: compare-func: 2.0.0 @@ -14507,6 +15077,10 @@ snapshots: convert-source-map@2.0.0: {} + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + copy-and-watch@0.1.8: dependencies: chokidar: 3.6.0 @@ -14532,6 +15106,11 @@ snapshots: core-util-is@1.0.3: {} + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + cosmiconfig-typescript-loader@6.2.0(@types/node@22.14.1)(cosmiconfig@9.0.0(typescript@5.9.3))(typescript@5.9.3): dependencies: '@types/node': 22.14.1 @@ -15285,6 +15864,35 @@ snapshots: es6-iterator: 2.0.3 es6-symbol: 3.1.4 + esbuild@0.27.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.2 + '@esbuild/android-arm': 0.27.2 + '@esbuild/android-arm64': 0.27.2 + '@esbuild/android-x64': 0.27.2 + '@esbuild/darwin-arm64': 0.27.2 + '@esbuild/darwin-x64': 0.27.2 + '@esbuild/freebsd-arm64': 0.27.2 + '@esbuild/freebsd-x64': 0.27.2 + '@esbuild/linux-arm': 0.27.2 + '@esbuild/linux-arm64': 0.27.2 + '@esbuild/linux-ia32': 0.27.2 + '@esbuild/linux-loong64': 0.27.2 + '@esbuild/linux-mips64el': 0.27.2 + '@esbuild/linux-ppc64': 0.27.2 + '@esbuild/linux-riscv64': 0.27.2 + '@esbuild/linux-s390x': 0.27.2 + '@esbuild/linux-x64': 0.27.2 + '@esbuild/netbsd-arm64': 0.27.2 + '@esbuild/netbsd-x64': 0.27.2 + '@esbuild/openbsd-arm64': 0.27.2 + '@esbuild/openbsd-x64': 0.27.2 + '@esbuild/openharmony-arm64': 0.27.2 + '@esbuild/sunos-x64': 0.27.2 + '@esbuild/win32-arm64': 0.27.2 + '@esbuild/win32-ia32': 0.27.2 + '@esbuild/win32-x64': 0.27.2 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -15613,6 +16221,12 @@ snapshots: events@3.3.0: {} + eventsource-parser@3.0.6: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -15638,16 +16252,44 @@ snapshots: jest-message-util: 29.7.0 jest-util: 29.7.0 - expect@30.2.0: + exponential-backoff@3.1.3: {} + + express-rate-limit@7.5.1(express@5.2.1): 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 + express: 5.2.1 - exponential-backoff@3.1.3: {} + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.0 + merge-descriptors: 2.0.0 + mime-types: 3.0.2(patch_hash=f54449b9273bc9e74fb67a14fcd001639d788d038b7eb0b5f43c10dff2b1adfb) + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.1 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.1 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color ext@1.7.0: dependencies: @@ -15728,6 +16370,17 @@ snapshots: transitivePeerDependencies: - supports-color + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + find-free-port@2.0.0: {} find-up@4.1.0: @@ -15792,8 +16445,12 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35(patch_hash=f54449b9273bc9e74fb67a14fcd001639d788d038b7eb0b5f43c10dff2b1adfb) + forwarded@0.2.0: {} + fresh@0.5.2: {} + fresh@2.0.0: {} + from2@2.3.0: dependencies: inherits: 2.0.4 @@ -15883,6 +16540,10 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 + get-tsconfig@4.13.0: + dependencies: + resolve-pkg-maps: 1.0.0 + git-hooks-list@4.1.1: {} git-raw-commits@4.0.0: @@ -16194,6 +16855,8 @@ snapshots: hoist-non-react-statics@2.5.5: {} + hono@4.11.3: {} + html-element-map@1.3.1: dependencies: array.prototype.filter: 1.0.4 @@ -16227,6 +16890,14 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + http-proxy-agent@5.0.0: dependencies: '@tootallnate/once': 2.0.0 @@ -16261,6 +16932,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.1: + dependencies: + safer-buffer: 2.1.2 + icss-replace-symbols@1.1.0: {} icss-utils@5.1.0(postcss@8.5.6): @@ -16340,6 +17015,8 @@ snapshots: ip@1.1.9: {} + ipaddr.js@1.9.1: {} + is-arguments@1.2.0: dependencies: call-bound: 1.0.4 @@ -16460,6 +17137,8 @@ snapshots: is-potential-custom-element-name@1.0.1: {} + is-promise@4.0.0: {} + is-reference@1.2.1: dependencies: '@types/estree': 1.0.8 @@ -16700,13 +17379,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 +17474,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 +17486,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 +17618,7 @@ snapshots: ci-info: 4.3.1 graceful-fs: 4.2.11 picomatch: 4.0.3 + optional: true jest-validate@29.7.0: dependencies: @@ -17005,18 +17653,6 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@22.14.1): - dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.14.1)(typescript@5.9.3)) - '@jest/types': 29.6.3 - import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@22.14.1)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.14.1)(typescript@5.9.3)) - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - jest@29.7.0(@types/node@22.14.1)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.14.1)(typescript@5.9.3)): dependencies: '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.14.1)(typescript@5.9.3)) @@ -17031,6 +17667,8 @@ snapshots: jiti@2.6.1: {} + jose@6.1.3: {} + js-beautify@1.15.4: dependencies: config-chain: 1.1.13 @@ -17099,6 +17737,8 @@ snapshots: json-schema-traverse@1.0.0: {} + json-schema-typed@8.0.2: {} + json-stable-stringify-without-jsonify@1.0.1: {} json-stringify-pretty-compact@4.0.0: {} @@ -17410,6 +18050,8 @@ snapshots: mdurl@2.0.0: {} + media-typer@1.1.0: {} + memoize-one@5.2.1: {} memoize-one@6.0.0: {} @@ -17426,6 +18068,8 @@ snapshots: meow@12.1.1: {} + merge-descriptors@2.0.0: {} + merge-refs@1.3.0(@types/react@19.2.2): optionalDependencies: '@types/react': 19.2.2 @@ -17659,10 +18303,16 @@ snapshots: mime-db@1.52.0: {} + mime-db@1.54.0: {} + mime-types@2.1.35(patch_hash=f54449b9273bc9e74fb67a14fcd001639d788d038b7eb0b5f43c10dff2b1adfb): dependencies: mime-db: 1.52.0 + mime-types@3.0.2(patch_hash=f54449b9273bc9e74fb67a14fcd001639d788d038b7eb0b5f43c10dff2b1adfb): + dependencies: + mime-db: 1.54.0 + mime@1.6.0: {} mime@2.5.2: {} @@ -17811,6 +18461,8 @@ snapshots: murmurhash-js@1.0.0: {} + mylas@2.1.14: {} + nanoevents@9.1.0: {} nanoid@3.3.11: {} @@ -17841,6 +18493,8 @@ snapshots: negotiator@0.6.3: {} + negotiator@1.0.0: {} + neo-async@2.6.2: {} next-tick@1.1.0: {} @@ -17850,8 +18504,7 @@ snapshots: semver: 7.7.3 optional: true - node-addon-api@7.1.1: - optional: true + node-addon-api@7.1.1: {} node-fetch@2.7.0: dependencies: @@ -17859,6 +18512,10 @@ snapshots: node-int64@0.4.0: {} + node-pty@1.2.0-beta.7: + dependencies: + node-addon-api: 7.1.1 + node-releases@2.0.23: {} nopt@7.2.1: @@ -18100,6 +18757,8 @@ snapshots: lru-cache: 11.2.2 minipass: 7.1.2 + path-to-regexp@8.3.0: {} + path-type@4.0.0: {} path2d@0.2.2: @@ -18133,6 +18792,8 @@ snapshots: pirates@4.0.7: {} + pkce-challenge@5.0.1: {} + pkg-dir@4.2.0: dependencies: find-up: 4.1.0 @@ -18147,6 +18808,10 @@ snapshots: optionalDependencies: fsevents: 2.3.2 + plimit-lit@1.6.1: + dependencies: + queue-lit: 1.5.2 + plotly.js-dist-min@3.1.1: {} plotly.js@3.1.1(mapbox-gl@1.13.3): @@ -18487,12 +19152,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 @@ -18547,6 +19206,11 @@ snapshots: protocol-buffers-schema@3.6.0: {} + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + prr@1.0.1: {} psl@1.15.0: @@ -18569,8 +19233,14 @@ snapshots: dependencies: react: 18.3.1 + qs@6.14.1: + dependencies: + side-channel: 1.1.0 + querystringify@2.2.0: {} + queue-lit@1.5.2: {} + queue-microtask@1.2.3: {} queue@6.0.2: @@ -18613,6 +19283,13 @@ snapshots: range-parser@1.2.1: {} + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.1 + unpipe: 1.0.0 + rc@1.2.8: dependencies: deep-extend: 0.6.0 @@ -19054,6 +19731,8 @@ snapshots: resolve-from@5.0.0: {} + resolve-pkg-maps@1.0.0: {} + resolve-protobuf-schema@2.1.0: dependencies: protocol-buffers-schema: 3.6.0 @@ -19169,6 +19848,16 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + rst-selector-parser@2.2.3: dependencies: lodash.flattendeep: 4.4.0 @@ -19267,6 +19956,22 @@ snapshots: transitivePeerDependencies: - supports-color + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2(patch_hash=f54449b9273bc9e74fb67a14fcd001639d788d038b7eb0b5f43c10dff2b1adfb) + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + serialize-error@2.1.0: {} serialize-javascript@6.0.2: @@ -19282,6 +19987,15 @@ snapshots: transitivePeerDependencies: - supports-color + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -19491,6 +20205,8 @@ snapshots: statuses@2.0.1: {} + statuses@2.0.2: {} + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -19818,7 +20534,7 @@ snapshots: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 handlebars: 4.7.8 - jest: 29.7.0(@types/node@22.14.1) + jest: 29.7.0(@types/node@22.14.1)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.14.1)(typescript@5.9.3)) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 @@ -19863,6 +20579,16 @@ snapshots: optionalDependencies: '@swc/core': 1.13.5 + tsc-alias@1.8.16: + dependencies: + chokidar: 3.6.0 + commander: 9.5.0 + get-tsconfig: 4.13.0 + globby: 11.1.0 + mylas: 2.1.14 + normalize-path: 3.0.0 + plimit-lit: 1.6.1 + tslib@1.14.1: {} tslib@2.8.1: {} @@ -19872,6 +20598,13 @@ snapshots: tslib: 1.14.1 typescript: 5.9.3 + tsx@4.21.0: + dependencies: + esbuild: 0.27.2 + get-tsconfig: 4.13.0 + optionalDependencies: + fsevents: 2.3.3 + tunnel-agent@0.6.0: dependencies: safe-buffer: 5.2.1 @@ -19918,6 +20651,12 @@ snapshots: type-fest@4.41.0: {} + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2(patch_hash=f54449b9273bc9e74fb67a14fcd001639d788d038b7eb0b5f43c10dff2b1adfb) + type@2.7.3: {} typed-array-buffer@1.0.3: @@ -20067,6 +20806,8 @@ snapshots: validate-npm-package-name@6.0.2: {} + vary@1.1.2: {} + vlq@0.2.3: {} vlq@1.0.1: {} @@ -20378,4 +21119,10 @@ snapshots: compress-commons: 2.1.1 readable-stream: 3.6.2 + zod-to-json-schema@3.25.1(zod@4.3.5): + dependencies: + zod: 4.3.5 + zod@3.25.76: {} + + zod@4.3.5: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4c37007e1c..5a173015b8 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,6 @@ packages: - - "packages/*/*" - - "automation/*" + - "packages/*/*" + - "packages/pluggable-widgets-mcp/" + - "automation/*" catalog: - rollup: "3.29" + rollup: "3.29"