diff --git a/drizzle/0012_bouncy_fenris.sql b/drizzle/0012_bouncy_fenris.sql new file mode 100644 index 0000000..77cb9b8 --- /dev/null +++ b/drizzle/0012_bouncy_fenris.sql @@ -0,0 +1,23 @@ +CREATE TABLE `mcp_servers` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text NOT NULL, + `transport` text NOT NULL, + `command` text, + `args` text, + `env_json` text, + `url` text, + `enabled` integer DEFAULT 0 NOT NULL, + `created_at` integer DEFAULT (unixepoch()) NOT NULL, + `updated_at` integer DEFAULT (unixepoch()) NOT NULL +); +--> statement-breakpoint +CREATE TABLE `mcp_tool_consents` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `server_id` integer NOT NULL, + `tool_name` text NOT NULL, + `consent` text DEFAULT 'ask' NOT NULL, + `updated_at` integer DEFAULT (unixepoch()) NOT NULL, + FOREIGN KEY (`server_id`) REFERENCES `mcp_servers`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `uniq_mcp_consent` ON `mcp_tool_consents` (`server_id`,`tool_name`); \ No newline at end of file diff --git a/drizzle/meta/0012_snapshot.json b/drizzle/meta/0012_snapshot.json new file mode 100644 index 0000000..5d61c2b --- /dev/null +++ b/drizzle/meta/0012_snapshot.json @@ -0,0 +1,731 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "8c77d7f5-9f88-4186-8aff-8385e060e59f", + "prevId": "6ac2fe61-675b-4e3f-baf7-0f7d5f76bb2c", + "tables": { + "apps": { + "name": "apps", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "github_org": { + "name": "github_org", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_repo": { + "name": "github_repo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_branch": { + "name": "github_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "supabase_project_id": { + "name": "supabase_project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "neon_project_id": { + "name": "neon_project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "neon_development_branch_id": { + "name": "neon_development_branch_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "neon_preview_branch_id": { + "name": "neon_preview_branch_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vercel_project_id": { + "name": "vercel_project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vercel_project_name": { + "name": "vercel_project_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vercel_team_id": { + "name": "vercel_team_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vercel_deployment_url": { + "name": "vercel_deployment_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "install_command": { + "name": "install_command", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "start_command": { + "name": "start_command", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "chat_context": { + "name": "chat_context", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "chats": { + "name": "chats", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "app_id": { + "name": "app_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "initial_commit_hash": { + "name": "initial_commit_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "chats_app_id_apps_id_fk": { + "name": "chats_app_id_apps_id_fk", + "tableFrom": "chats", + "tableTo": "apps", + "columnsFrom": [ + "app_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "language_model_providers": { + "name": "language_model_providers", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "api_base_url": { + "name": "api_base_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "env_var_name": { + "name": "env_var_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "language_models": { + "name": "language_models", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "api_name": { + "name": "api_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "builtin_provider_id": { + "name": "builtin_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_provider_id": { + "name": "custom_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "max_output_tokens": { + "name": "max_output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "context_window": { + "name": "context_window", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "language_models_custom_provider_id_language_model_providers_id_fk": { + "name": "language_models_custom_provider_id_language_model_providers_id_fk", + "tableFrom": "language_models", + "tableTo": "language_model_providers", + "columnsFrom": [ + "custom_provider_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "mcp_servers": { + "name": "mcp_servers", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "args": { + "name": "args", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "env_json": { + "name": "env_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "0" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "mcp_tool_consents": { + "name": "mcp_tool_consents", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "server_id": { + "name": "server_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "consent": { + "name": "consent", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ask'" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "uniq_mcp_consent": { + "name": "uniq_mcp_consent", + "columns": [ + "server_id", + "tool_name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "mcp_tool_consents_server_id_mcp_servers_id_fk": { + "name": "mcp_tool_consents_server_id_mcp_servers_id_fk", + "tableFrom": "mcp_tool_consents", + "tableTo": "mcp_servers", + "columnsFrom": [ + "server_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "chat_id": { + "name": "chat_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "approval_state": { + "name": "approval_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commit_hash": { + "name": "commit_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "messages_chat_id_chats_id_fk": { + "name": "messages_chat_id_chats_id_fk", + "tableFrom": "messages", + "tableTo": "chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "prompts": { + "name": "prompts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "versions": { + "name": "versions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "app_id": { + "name": "app_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "commit_hash": { + "name": "commit_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "neon_db_timestamp": { + "name": "neon_db_timestamp", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "versions_app_commit_unique": { + "name": "versions_app_commit_unique", + "columns": [ + "app_id", + "commit_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "versions_app_id_apps_id_fk": { + "name": "versions_app_id_apps_id_fk", + "tableFrom": "versions", + "tableTo": "apps", + "columnsFrom": [ + "app_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 6662c49..dc2fbb2 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -85,6 +85,13 @@ "when": 1755545060076, "tag": "0011_light_zeigeist", "breakpoints": true + }, + { + "idx": 12, + "version": "6", + "when": 1758320228637, + "tag": "0012_bouncy_fenris", + "breakpoints": true } ] } \ No newline at end of file diff --git a/e2e-tests/helpers/test_helper.ts b/e2e-tests/helpers/test_helper.ts index 8d3c6b7..ec22aee 100644 --- a/e2e-tests/helpers/test_helper.ts +++ b/e2e-tests/helpers/test_helper.ts @@ -330,7 +330,7 @@ export class PageObject { await this.page.getByRole("button", { name: "Import" }).click(); } - async selectChatMode(mode: "build" | "ask") { + async selectChatMode(mode: "build" | "ask" | "agent") { await this.page.getByTestId("chat-mode-selector").click(); await this.page.getByRole("option", { name: mode }).click(); } @@ -685,11 +685,16 @@ export class PageObject { await this.page.getByRole("button", { name: "Back" }).click(); } - async sendPrompt(prompt: string) { + async sendPrompt( + prompt: string, + { skipWaitForCompletion = false }: { skipWaitForCompletion?: boolean } = {}, + ) { await this.getChatInput().click(); await this.getChatInput().fill(prompt); await this.page.getByRole("button", { name: "Send message" }).click(); - await this.waitForChatCompletion(); + if (!skipWaitForCompletion) { + await this.waitForChatCompletion(); + } } async selectModel({ provider, model }: { provider: string; model: string }) { diff --git a/e2e-tests/mcp.spec.ts b/e2e-tests/mcp.spec.ts new file mode 100644 index 0000000..3c2a72c --- /dev/null +++ b/e2e-tests/mcp.spec.ts @@ -0,0 +1,49 @@ +import path from "path"; +import { test } from "./helpers/test_helper"; +import { expect } from "@playwright/test"; + +test("mcp - call calculator", async ({ po }) => { + await po.setUp(); + await po.goToSettingsTab(); + await po.page.getByRole("button", { name: "Tools (MCP)" }).click(); + + await po.page + .getByRole("textbox", { name: "My MCP Server" }) + .fill("testing-mcp-server"); + await po.page.getByRole("textbox", { name: "node" }).fill("node"); + const testMcpServerPath = path.join( + __dirname, + "..", + "testing", + "fake-stdio-mcp-server.mjs", + ); + console.log("testMcpServerPath", testMcpServerPath); + await po.page + .getByRole("textbox", { name: "path/to/mcp-server.js --flag" }) + .fill(testMcpServerPath); + await po.page.getByRole("button", { name: "Add Server" }).click(); + await po.page + .getByRole("button", { name: "Add Environment Variable" }) + .click(); + await po.page.getByRole("textbox", { name: "Key" }).fill("testKey1"); + await po.page.getByRole("textbox", { name: "Value" }).fill("testValue1"); + await po.page.getByRole("button", { name: "Save" }).click(); + await po.goToAppsTab(); + await po.selectChatMode("agent"); + await po.sendPrompt("[call_tool=calculator_add]", { + skipWaitForCompletion: true, + }); + + // Wait for consent dialog to appear + const alwaysAllowButton = po.page.getByRole("button", { + name: "Always allow", + }); + await expect(alwaysAllowButton).toBeVisible(); + + // Make sure the tool call doesn't execute until consent is given + await po.snapshotMessages(); + await alwaysAllowButton.click(); + + await po.sendPrompt("[dump]"); + await po.snapshotServerDump("all-messages"); +}); diff --git a/e2e-tests/snapshots/mcp.spec.ts_mcp---call-calculator-1.aria.yml b/e2e-tests/snapshots/mcp.spec.ts_mcp---call-calculator-1.aria.yml new file mode 100644 index 0000000..80161cb --- /dev/null +++ b/e2e-tests/snapshots/mcp.spec.ts_mcp---call-calculator-1.aria.yml @@ -0,0 +1,7 @@ +- paragraph: "[call_tool=calculator_add]" +- img +- text: Tool Call +- img +- text: testing-mcp-server calculator_add +- img +- text: less than a minute ago diff --git a/e2e-tests/snapshots/mcp.spec.ts_mcp---call-calculator-1.txt b/e2e-tests/snapshots/mcp.spec.ts_mcp---call-calculator-1.txt new file mode 100644 index 0000000..24f510a --- /dev/null +++ b/e2e-tests/snapshots/mcp.spec.ts_mcp---call-calculator-1.txt @@ -0,0 +1,80 @@ +=== +role: system +message: +You are an AI App Builder Agent. Your role is to analyze app development requests and gather all necessary information before the actual coding phase begins. + +## Core Mission +Determine what tools, APIs, data, or external resources are needed to build the requested application. Prepare everything needed for successful app development without writing any code yourself. + +## Tool Usage Decision Framework + +### Use Tools When The App Needs: +- **External APIs or services** (payment processing, authentication, maps, social media, etc.) +- **Real-time data** (weather, stock prices, news, current events) +- **Third-party integrations** (Firebase, Supabase, cloud services) +- **Current framework/library documentation** or best practices + +### Use Tools To Research: +- Available APIs and their documentation +- Authentication methods and implementation approaches +- Database options and setup requirements +- UI/UX frameworks and component libraries +- Deployment platforms and requirements +- Performance optimization strategies +- Security best practices for the app type + +### When Tools Are NOT Needed +If the app request is straightforward and can be built with standard web technologies without external dependencies, respond with: + +**"Ok, looks like I don't need any tools, I can start building."** + +This applies to simple apps like: +- Basic calculators or converters +- Simple games (tic-tac-toe, memory games) +- Static information displays +- Basic form interfaces +- Simple data visualization with static data + +## Critical Constraints + +- ABSOLUTELY NO CODE GENERATION +- **Never write HTML, CSS, JavaScript, TypeScript, or any programming code** +- **Do not create component examples or code snippets** +- **Do not provide implementation details or syntax** +- Your job ends with information gathering and requirement analysis +- All actual development happens in the next phase + +## Output Structure + +When tools are used, provide a brief human-readable summary of the information gathered from the tools. + +When tools are not used, simply state: **"Ok, looks like I don't need any tools, I can start building."** + + +=== +role: user +message: [call_tool=calculator_add] + +=== +role: assistant +message: +{"a":1,"b":2} + + +{"content":[{"type":"text","text":"3"}],"isError":false} + + + + A file (2) + + More + EOM + + A file (2) + + More + EOM + +=== +role: user +message: [dump] \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4cf1bb9..387aada 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dyad", - "version": "0.20.0", + "version": "0.21.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dyad", - "version": "0.20.0", + "version": "0.21.0", "license": "MIT", "dependencies": { "@ai-sdk/amazon-bedrock": "^3.0.15", @@ -21,6 +21,7 @@ "@biomejs/biome": "^1.9.4", "@dyad-sh/supabase-management-js": "v1.0.0", "@lexical/react": "^0.33.1", + "@modelcontextprotocol/sdk": "^1.17.5", "@monaco-editor/react": "^4.7.0-rc.0", "@neondatabase/api-client": "^2.1.0", "@neondatabase/serverless": "^1.0.1", @@ -105,6 +106,7 @@ "@playwright/test": "^1.52.0", "@testing-library/react": "^16.3.0", "@types/better-sqlite3": "^7.6.13", + "@types/fs-extra": "^11.0.4", "@types/glob": "^8.1.0", "@types/kill-port": "^2.0.3", "@types/node": "^22.14.0", @@ -2727,6 +2729,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@eslint/js": { "version": "8.57.1", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", @@ -3952,6 +3967,29 @@ "node": ">= 12.13.0" } }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.5.tgz", + "integrity": "sha512-QakrKIGniGuRVfWBdMsDea/dx1PNE739QJ7gCM41s9q+qaCYTHCdsIBXQVVXry3mfWAiaM9kT22Hyz53Uw8mfg==", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.6", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@monaco-editor/loader": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.5.0.tgz", @@ -6895,13 +6933,13 @@ } }, "node_modules/@types/fs-extra": { - "version": "9.0.13", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", - "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", + "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { + "@types/jsonfile": "*", "@types/node": "*" } }, @@ -7012,6 +7050,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonfile": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", + "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/keyv": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", @@ -7582,6 +7630,49 @@ "node": ">=6.5" } }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -7662,7 +7753,6 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -8064,6 +8154,26 @@ "dev": true, "license": "MIT" }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/boolean": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", @@ -8181,6 +8291,15 @@ "dev": true, "license": "MIT" }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -8636,6 +8755,56 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-cursor/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-cursor/node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, "node_modules/cli-spinners": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", @@ -8892,18 +9061,57 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "license": "MIT" }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-es": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.2.tgz", "integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==", "license": "MIT" }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, "node_modules/core-js": { "version": "3.45.1", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.1.tgz", @@ -8915,6 +9123,19 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", @@ -9235,6 +9456,15 @@ "node": ">=0.4.0" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/deprecation": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", @@ -9326,6 +9556,16 @@ "node": ">=8" } }, + "node_modules/dir-glob/node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -10023,6 +10263,12 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, "node_modules/electron": { "version": "35.1.4", "resolved": "https://registry.npmjs.org/electron/-/electron-35.1.4.tgz", @@ -10042,431 +10288,6 @@ "node": ">= 12.20.55" } }, - "node_modules/electron-installer-common": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/electron-installer-common/-/electron-installer-common-0.10.4.tgz", - "integrity": "sha512-8gMNPXfAqUE5CfXg8RL0vXpLE9HAaPkgLXVoHE3BMUzogMWenf4LmwQ27BdCUrEhkjrKl+igs2IHJibclR3z3Q==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@electron/asar": "^3.2.5", - "@malept/cross-spawn-promise": "^1.0.0", - "debug": "^4.1.1", - "fs-extra": "^9.0.0", - "glob": "^7.1.4", - "lodash": "^4.17.15", - "parse-author": "^2.0.0", - "semver": "^7.1.1", - "tmp-promise": "^3.0.2" - }, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "url": "https://github.com/electron-userland/electron-installer-common?sponsor=1" - }, - "optionalDependencies": { - "@types/fs-extra": "^9.0.1" - } - }, - "node_modules/electron-installer-common/node_modules/@malept/cross-spawn-promise": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", - "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/malept" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" - } - ], - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "cross-spawn": "^7.0.1" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/electron-installer-common/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/electron-installer-common/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/electron-installer-debian": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/electron-installer-debian/-/electron-installer-debian-3.2.0.tgz", - "integrity": "sha512-58ZrlJ1HQY80VucsEIG9tQ//HrTlG6sfofA3nRGr6TmkX661uJyu4cMPPh6kXW+aHdq/7+q25KyQhDrXvRL7jw==", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin", - "linux" - ], - "dependencies": { - "@malept/cross-spawn-promise": "^1.0.0", - "debug": "^4.1.1", - "electron-installer-common": "^0.10.2", - "fs-extra": "^9.0.0", - "get-folder-size": "^2.0.1", - "lodash": "^4.17.4", - "word-wrap": "^1.2.3", - "yargs": "^16.0.2" - }, - "bin": { - "electron-installer-debian": "src/cli.js" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/electron-installer-debian/node_modules/@malept/cross-spawn-promise": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", - "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/malept" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" - } - ], - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "cross-spawn": "^7.0.1" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/electron-installer-debian/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/electron-installer-debian/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/electron-installer-debian/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/electron-installer-debian/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/electron-installer-debian/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/electron-installer-debian/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/electron-installer-debian/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/electron-installer-debian/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "license": "ISC", - "optional": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/electron-installer-redhat": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/electron-installer-redhat/-/electron-installer-redhat-3.4.0.tgz", - "integrity": "sha512-gEISr3U32Sgtj+fjxUAlSDo3wyGGq6OBx7rF5UdpIgbnpUvMN4W5uYb0ThpnAZ42VEJh/3aODQXHbFS4f5J3Iw==", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin", - "linux" - ], - "dependencies": { - "@malept/cross-spawn-promise": "^1.0.0", - "debug": "^4.1.1", - "electron-installer-common": "^0.10.2", - "fs-extra": "^9.0.0", - "lodash": "^4.17.15", - "word-wrap": "^1.2.3", - "yargs": "^16.0.2" - }, - "bin": { - "electron-installer-redhat": "src/cli.js" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/electron-installer-redhat/node_modules/@malept/cross-spawn-promise": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", - "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/malept" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" - } - ], - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "cross-spawn": "^7.0.1" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/electron-installer-redhat/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/electron-installer-redhat/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/electron-installer-redhat/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/electron-installer-redhat/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/electron-installer-redhat/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/electron-installer-redhat/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/electron-installer-redhat/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/electron-installer-redhat/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "license": "ISC", - "optional": true, - "engines": { - "node": ">=10" - } - }, "node_modules/electron-log": { "version": "5.4.3", "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-5.4.3.tgz", @@ -10648,6 +10469,15 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/encoding": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", @@ -10964,6 +10794,12 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -11305,6 +11141,15 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -11321,6 +11166,18 @@ "dev": true, "license": "MIT" }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/eventsource-parser": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", @@ -11422,6 +11279,84 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -11453,7 +11388,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -11490,7 +11424,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { @@ -11603,6 +11536,23 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -11796,6 +11746,15 @@ "node": ">= 12.20" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/framer-motion": { "version": "12.23.12", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.12.tgz", @@ -11823,6 +11782,15 @@ } } }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -11952,15 +11920,6 @@ "node": ">= 12" } }, - "node_modules/gar": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/gar/-/gar-1.0.4.tgz", - "integrity": "sha512-w4n9cPWyP7aHxKxYHFQMegj7WIAsL/YX/C4Bs5Rr8s1H9M1rNtRWRsw+ovYMkXDQ5S4ZbYHsHAPmevPjPgw44w==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "dev": true, - "license": "MIT", - "optional": true - }, "node_modules/gaxios": { "version": "6.7.1", "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", @@ -12093,9 +12052,9 @@ } }, "node_modules/get-east-asian-width": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.1.tgz", - "integrity": "sha512-R1QfovbPsKmosqTnPoRFiJ7CF9MLRgb53ChvMZm+r4p76/+8yKDy17qLL2PKInORy2RkZZekuK0efYgmzTkXyQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", "dev": true, "license": "MIT", "engines": { @@ -12105,21 +12064,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-folder-size": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/get-folder-size/-/get-folder-size-2.0.1.tgz", - "integrity": "sha512-+CEb+GDCM7tkOS2wdMKTn9vU7DgnKUTuDlehkNJKNSovdCOVxs14OfKCk4cvSaR3za4gj+OBdl9opPN9xrJ0zA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "gar": "^1.0.4", - "tiny-each-async": "2.0.3" - }, - "bin": { - "get-folder-size": "bin/get-folder-size" - } - }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -12747,6 +12691,31 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -12842,7 +12811,6 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "license": "MIT", - "optional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -12991,6 +12959,15 @@ "node": ">= 12" } }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", @@ -13338,6 +13315,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/is-reference": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", @@ -13887,7 +13870,6 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { @@ -14315,9 +14297,9 @@ } }, "node_modules/lint-staged/node_modules/ansi-escapes": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", - "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.1.0.tgz", + "integrity": "sha512-YdhtCd19sKRKfAAUsrcC1wzm4JuzJoiX4pOJqIoW2qmKj5WzG/dL8uUJ0361zaXtHqK7gEhOwtAtz7t3Yq3X5g==", "dev": true, "license": "MIT", "dependencies": { @@ -14490,39 +14472,6 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/lint-staged/node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/lint-staged/node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", @@ -15212,6 +15161,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/mem": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", @@ -15237,6 +15195,18 @@ "node": ">=6" } }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -16454,11 +16424,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -16551,6 +16529,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -16973,6 +16963,15 @@ "node": ">=0.10.0" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -17039,14 +17038,37 @@ "node": "20 || >=22" } }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha512-dUnb5dXUf+kzhC/W/F4e5/SkluXIFf5VUHolW1Eg1irn1hGWjPGdsRcvYJ1nD6lhk8Ir7VM0bHJKsYTx8Jx9OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-type/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, "node_modules/pathe": { @@ -17159,6 +17181,15 @@ "node": ">=6" } }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/playwright": { "version": "1.55.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0.tgz", @@ -17297,9 +17328,9 @@ } }, "node_modules/posthog-js": { - "version": "1.261.8", - "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.261.8.tgz", - "integrity": "sha512-HohKQ5Fuvei/3ZLIdayq6lDpeXsG891t2y2izpHu6q/1SoCS+HlYjViz3WCu9KlE7AfjfpwvN1kjnFNNPWeOig==", + "version": "1.262.0", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.262.0.tgz", + "integrity": "sha512-RPbm+0qLVsgKQEN3KjfYAK+0qOwBPT28RHDg4WIstN8/z2m6PczMqSirOIXSqbvDwSCQQQRPTKS6fSurevqJMA==", "license": "SEE LICENSE IN LICENSE", "dependencies": { "@posthog/core": "1.0.2", @@ -17505,6 +17536,19 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -17525,12 +17569,26 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -17565,6 +17623,46 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.7.0", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -17586,15 +17684,6 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/react": { "version": "19.1.1", "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", @@ -17883,29 +17972,6 @@ "node": ">=4" } }, - "node_modules/read-pkg/node_modules/path-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", - "integrity": "sha512-dUnb5dXUf+kzhC/W/F4e5/SkluXIFf5VUHolW1Eg1irn1hGWjPGdsRcvYJ1nD6lhk8Ir7VM0bHJKsYTx8Jx9OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/read-pkg/node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -18157,55 +18223,38 @@ } }, "node_modules/restore-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", - "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", "dev": true, "license": "MIT", "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/restore-cursor/node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/restore-cursor/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", "dev": true, "license": "MIT", "dependencies": { - "mimic-fn": "^2.1.0" + "mimic-function": "^5.0.0" }, "engines": { - "node": ">=6" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/restore-cursor/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -18313,6 +18362,22 @@ "fsevents": "~2.3.2" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -18416,8 +18481,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/scheduler": { "version": "0.26.0", @@ -18445,6 +18509,49 @@ "license": "MIT", "optional": true }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/send/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/serialize-error": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", @@ -18497,6 +18604,21 @@ "seroval": "^1.0" } }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -18545,6 +18667,12 @@ "node": ">= 0.4" } }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/sha.js": { "version": "2.4.12", "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", @@ -18827,7 +18955,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -18847,7 +18974,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -18864,7 +18990,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -18883,7 +19008,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -19296,6 +19420,15 @@ "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", "license": "MIT" }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/std-env": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", @@ -19542,16 +19675,12 @@ } }, "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "license": "MIT", "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, "node_modules/strip-literal": { @@ -19863,14 +19992,6 @@ "dev": true, "license": "MIT" }, - "node_modules/tiny-each-async": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/tiny-each-async/-/tiny-each-async-2.0.3.tgz", - "integrity": "sha512-5ROII7nElnAirvFn8g7H7MtpfV1daMcyfTGQwsn/x2VtyV+VPiO5CjReCJtWLvoKTDEDmZocf3cNPraiMnBXLA==", - "dev": true, - "license": "MIT", - "optional": true - }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -19944,28 +20065,6 @@ "node": ">=14.0.0" } }, - "node_modules/tmp": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", - "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/tmp-promise": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", - "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tmp": "^0.2.0" - } - }, "node_modules/to-buffer": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", @@ -19992,6 +20091,15 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -20162,6 +20270,41 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -20408,6 +20551,15 @@ "node": ">= 10.0.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", @@ -20452,7 +20604,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" @@ -20689,6 +20840,15 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", diff --git a/package.json b/package.json index 57e7050..debe060 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "@playwright/test": "^1.52.0", "@testing-library/react": "^16.3.0", "@types/better-sqlite3": "^7.6.13", + "@types/fs-extra": "^11.0.4", "@types/glob": "^8.1.0", "@types/kill-port": "^2.0.3", "@types/node": "^22.14.0", @@ -97,6 +98,7 @@ "@biomejs/biome": "^1.9.4", "@dyad-sh/supabase-management-js": "v1.0.0", "@lexical/react": "^0.33.1", + "@modelcontextprotocol/sdk": "^1.17.5", "@monaco-editor/react": "^4.7.0-rc.0", "@neondatabase/api-client": "^2.1.0", "@neondatabase/serverless": "^1.0.1", @@ -170,5 +172,10 @@ "lint-staged": { "**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx,vue,astro,svelte}": "oxlint", "*.{js,css,md,ts,tsx,jsx,json}": "prettier --write" + }, + "overrides": { + "@vercel/sdk": { + "@modelcontextprotocol/sdk": "$@modelcontextprotocol/sdk" + } } } diff --git a/playwright.config.ts b/playwright.config.ts index 510529d..b58f5c3 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -42,10 +42,12 @@ const config: PlaywrightTestConfig = { // video: "retain-on-failure", }, - webServer: { - command: `cd testing/fake-llm-server && npm run build && npm start`, - url: "http://localhost:3500/health", - }, + webServer: [ + { + command: `cd testing/fake-llm-server && npm run build && npm start`, + url: "http://localhost:3500/health", + }, + ], }; export default config; diff --git a/src/components/ChatInputControls.tsx b/src/components/ChatInputControls.tsx index 25d28f0..2c28731 100644 --- a/src/components/ChatInputControls.tsx +++ b/src/components/ChatInputControls.tsx @@ -2,15 +2,25 @@ import { ContextFilesPicker } from "./ContextFilesPicker"; import { ModelPicker } from "./ModelPicker"; import { ProModeSelector } from "./ProModeSelector"; import { ChatModeSelector } from "./ChatModeSelector"; +import { McpToolsPicker } from "@/components/McpToolsPicker"; +import { useSettings } from "@/hooks/useSettings"; export function ChatInputControls({ showContextFilesPicker = false, }: { showContextFilesPicker?: boolean; }) { + const { settings } = useSettings(); + return (
+ {settings?.selectedChatMode === "agent" && ( + <> +
+ + + )}
diff --git a/src/components/ChatModeSelector.tsx b/src/components/ChatModeSelector.tsx index c0e4aca..8ebda49 100644 --- a/src/components/ChatModeSelector.tsx +++ b/src/components/ChatModeSelector.tsx @@ -29,6 +29,8 @@ export function ChatModeSelector() { return "Build"; case "ask": return "Ask"; + case "agent": + return "Agent"; default: return "Build"; } @@ -70,6 +72,14 @@ export function ChatModeSelector() {
+ +
+ Agent (experimental) + + Agent can use tools (MCP) and generate code + +
+
); diff --git a/src/components/McpConsentToast.tsx b/src/components/McpConsentToast.tsx new file mode 100644 index 0000000..2394510 --- /dev/null +++ b/src/components/McpConsentToast.tsx @@ -0,0 +1,200 @@ +import React from "react"; +import { Button } from "./ui/button"; +import { X, ShieldAlert } from "lucide-react"; +import { toast } from "sonner"; + +interface McpConsentToastProps { + toastId: string | number; + serverName: string; + toolName: string; + toolDescription?: string | null; + inputPreview?: string | null; + onDecision: (decision: "accept-once" | "accept-always" | "decline") => void; +} + +export function McpConsentToast({ + toastId, + serverName, + toolName, + toolDescription, + inputPreview, + onDecision, +}: McpConsentToastProps) { + const handleClose = () => toast.dismiss(toastId); + + const handle = (d: "accept-once" | "accept-always" | "decline") => { + onDecision(d); + toast.dismiss(toastId); + }; + + // Collapsible tool description state + const [isExpanded, setIsExpanded] = React.useState(false); + const [collapsedMaxHeight, setCollapsedMaxHeight] = React.useState(0); + const [hasOverflow, setHasOverflow] = React.useState(false); + const descRef = React.useRef(null); + + // Collapsible input preview state + const [isInputExpanded, setIsInputExpanded] = React.useState(false); + const [inputCollapsedMaxHeight, setInputCollapsedMaxHeight] = + React.useState(0); + const [inputHasOverflow, setInputHasOverflow] = React.useState(false); + const inputRef = React.useRef(null); + + React.useEffect(() => { + if (!toolDescription) { + setHasOverflow(false); + return; + } + + const element = descRef.current; + if (!element) return; + + const compute = () => { + const computedStyle = window.getComputedStyle(element); + const lineHeight = parseFloat(computedStyle.lineHeight || "20"); + const maxLines = 4; // show first few lines by default + const maxHeightPx = Math.max(0, Math.round(lineHeight * maxLines)); + setCollapsedMaxHeight(maxHeightPx); + // Overflow if full height exceeds our collapsed height + setHasOverflow(element.scrollHeight > maxHeightPx + 1); + }; + + // Compute initially and on resize + compute(); + const onResize = () => compute(); + window.addEventListener("resize", onResize); + return () => window.removeEventListener("resize", onResize); + }, [toolDescription]); + + React.useEffect(() => { + if (!inputPreview) { + setInputHasOverflow(false); + return; + } + + const element = inputRef.current; + if (!element) return; + + const compute = () => { + const computedStyle = window.getComputedStyle(element); + const lineHeight = parseFloat(computedStyle.lineHeight || "16"); + const maxLines = 6; // show first few lines by default + const maxHeightPx = Math.max(0, Math.round(lineHeight * maxLines)); + setInputCollapsedMaxHeight(maxHeightPx); + setInputHasOverflow(element.scrollHeight > maxHeightPx + 1); + }; + + compute(); + const onResize = () => compute(); + window.addEventListener("resize", onResize); + return () => window.removeEventListener("resize", onResize); + }, [inputPreview]); + + return ( +
+
+
+
+
+
+
+ +
+
+

+ Tool wants to run +

+ +
+
+

+ {toolName} from + {serverName} requests + your consent. +

+ {toolDescription && ( +
+

+ {toolDescription} +

+ {hasOverflow && ( + + )} +
+ )} + {inputPreview && ( +
+
+                    {inputPreview}
+                  
+ {inputHasOverflow && ( + + )} +
+ )} +
+
+ + + +
+
+
+
+
+ ); +} diff --git a/src/components/McpToolsPicker.tsx b/src/components/McpToolsPicker.tsx new file mode 100644 index 0000000..9c07264 --- /dev/null +++ b/src/components/McpToolsPicker.tsx @@ -0,0 +1,130 @@ +import React, { useState } from "react"; +import { Button } from "@/components/ui/button"; + +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { Badge } from "@/components/ui/badge"; +import { Wrench } from "lucide-react"; +import { useMcp } from "@/hooks/useMcp"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +export function McpToolsPicker() { + const [isOpen, setIsOpen] = useState(false); + const { servers, toolsByServer, consentsMap, setToolConsent } = useMcp(); + + // Removed activation toggling – consent governs execution time behavior + + return ( + + + + + + + + + Tools + + + +
+
+

Tools (MCP)

+

+ Enable tools from your configured MCP servers. +

+
+ {servers.length === 0 ? ( +
+ No MCP servers configured. Configure them in Settings → Tools + (MCP). +
+ ) : ( +
+ {servers.map((s) => ( +
+
+
{s.name}
+ {s.enabled ? ( + Enabled + ) : ( + Disabled + )} +
+
+ {(toolsByServer[s.id] || []).map((t) => ( +
+
+
+ {t.name} +
+ {t.description && ( +
+ {t.description} +
+ )} +
+ +
+ ))} + {(toolsByServer[s.id] || []).length === 0 && ( +
+ No tools discovered. +
+ )} +
+
+ ))} +
+ )} +
+
+
+ ); +} diff --git a/src/components/SettingsList.tsx b/src/components/SettingsList.tsx index 95610f1..a15a60c 100644 --- a/src/components/SettingsList.tsx +++ b/src/components/SettingsList.tsx @@ -10,6 +10,7 @@ const SETTINGS_SECTIONS = [ { id: "provider-settings", label: "Model Providers" }, { id: "telemetry", label: "Telemetry" }, { id: "integrations", label: "Integrations" }, + { id: "tools-mcp", label: "Tools (MCP)" }, { id: "experiments", label: "Experiments" }, { id: "danger-zone", label: "Danger Zone" }, ]; diff --git a/src/components/chat/DyadMarkdownParser.tsx b/src/components/chat/DyadMarkdownParser.tsx index a9ce828..2d0c1ee 100644 --- a/src/components/chat/DyadMarkdownParser.tsx +++ b/src/components/chat/DyadMarkdownParser.tsx @@ -17,6 +17,8 @@ import { CustomTagState } from "./stateTypes"; import { DyadOutput } from "./DyadOutput"; import { DyadProblemSummary } from "./DyadProblemSummary"; import { IpcClient } from "@/ipc/ipc_client"; +import { DyadMcpToolCall } from "./DyadMcpToolCall"; +import { DyadMcpToolResult } from "./DyadMcpToolResult"; interface DyadMarkdownParserProps { content: string; @@ -124,6 +126,8 @@ function preprocessUnclosedTags(content: string): { "dyad-codebase-context", "think", "dyad-command", + "dyad-mcp-tool-call", + "dyad-mcp-tool-result", ]; let processedContent = content; @@ -191,6 +195,8 @@ function parseCustomTags(content: string): ContentPiece[] { "dyad-codebase-context", "think", "dyad-command", + "dyad-mcp-tool-call", + "dyad-mcp-tool-result", ]; const tagPattern = new RegExp( @@ -399,6 +405,34 @@ function renderCustomTag( ); + case "dyad-mcp-tool-call": + return ( + + {content} + + ); + + case "dyad-mcp-tool-result": + return ( + + {content} + + ); + case "dyad-output": return ( = ({ + node, + children, +}) => { + const serverName: string = node?.properties?.serverName || ""; + const toolName: string = node?.properties?.toolName || ""; + const [expanded, setExpanded] = useState(false); + + const raw = typeof children === "string" ? children : String(children ?? ""); + + const prettyJson = useMemo(() => { + if (!expanded) return ""; + try { + const parsed = JSON.parse(raw); + return JSON.stringify(parsed, null, 2); + } catch (e) { + console.error("Error parsing JSON for dyad-mcp-tool-call", e); + return raw; + } + }, [expanded, raw]); + + return ( +
setExpanded((v) => !v)} + > + {/* Top-left label badge */} +
+ + Tool Call +
+ + {/* Right chevron */} +
+ {expanded ? : } +
+ + {/* Header content */} +
+ {serverName ? ( + + {serverName} + + ) : null} + {toolName ? ( + + {toolName} + + ) : null} + {/* Intentionally no preview or content when collapsed */} +
+ + {/* JSON content */} + {expanded ? ( +
+ {prettyJson} +
+ ) : null} +
+ ); +}; diff --git a/src/components/chat/DyadMcpToolResult.tsx b/src/components/chat/DyadMcpToolResult.tsx new file mode 100644 index 0000000..998a0c5 --- /dev/null +++ b/src/components/chat/DyadMcpToolResult.tsx @@ -0,0 +1,73 @@ +import React, { useMemo, useState } from "react"; +import { CheckCircle, ChevronsUpDown, ChevronsDownUp } from "lucide-react"; +import { CodeHighlight } from "./CodeHighlight"; + +interface DyadMcpToolResultProps { + node?: any; + children?: React.ReactNode; +} + +export const DyadMcpToolResult: React.FC = ({ + node, + children, +}) => { + const serverName: string = node?.properties?.serverName || ""; + const toolName: string = node?.properties?.toolName || ""; + const [expanded, setExpanded] = useState(false); + + const raw = typeof children === "string" ? children : String(children ?? ""); + + const prettyJson = useMemo(() => { + if (!expanded) return ""; + try { + const parsed = JSON.parse(raw); + return JSON.stringify(parsed, null, 2); + } catch (e) { + console.error("Error parsing JSON for dyad-mcp-tool-result", e); + return raw; + } + }, [expanded, raw]); + + return ( +
setExpanded((v) => !v)} + > + {/* Top-left label badge */} +
+ + Tool Result +
+ + {/* Right chevron */} +
+ {expanded ? : } +
+ + {/* Header content */} +
+ {serverName ? ( + + {serverName} + + ) : null} + {toolName ? ( + + {toolName} + + ) : null} + {/* Intentionally no preview or content when collapsed */} +
+ + {/* JSON content */} + {expanded ? ( +
+ {prettyJson} +
+ ) : null} +
+ ); +}; diff --git a/src/components/settings/ToolsMcpSettings.tsx b/src/components/settings/ToolsMcpSettings.tsx new file mode 100644 index 0000000..cf2f5a2 --- /dev/null +++ b/src/components/settings/ToolsMcpSettings.tsx @@ -0,0 +1,504 @@ +import React, { useMemo, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { useMcp, type Transport } from "@/hooks/useMcp"; +import { showError, showSuccess } from "@/lib/toast"; +import { Edit2, Plus, Save, Trash2, X } from "lucide-react"; + +type KeyValue = { key: string; value: string }; + +function parseEnvJsonToArray( + envJson?: Record | string | null, +): KeyValue[] { + if (!envJson) return []; + try { + const obj = + typeof envJson === "string" + ? (JSON.parse(envJson) as unknown as Record) + : (envJson as Record); + return Object.entries(obj).map(([key, value]) => ({ + key, + value: String(value ?? ""), + })); + } catch { + return []; + } +} + +function arrayToEnvObject(envVars: KeyValue[]): Record { + const env: Record = {}; + for (const { key, value } of envVars) { + if (key.trim().length === 0) continue; + env[key.trim()] = value; + } + return env; +} + +function EnvVarsEditor({ + serverId, + envJson, + disabled, + onSave, + isSaving, +}: { + serverId: number; + envJson?: Record | null; + disabled?: boolean; + onSave: (envVars: KeyValue[]) => Promise; + isSaving: boolean; +}) { + const initial = useMemo(() => parseEnvJsonToArray(envJson), [envJson]); + const [envVars, setEnvVars] = useState(initial); + const [editingKey, setEditingKey] = useState(null); + const [editingKeyValue, setEditingKeyValue] = useState(""); + const [editingValue, setEditingValue] = useState(""); + const [newKey, setNewKey] = useState(""); + const [newValue, setNewValue] = useState(""); + const [isAddingNew, setIsAddingNew] = useState(false); + + React.useEffect(() => { + setEnvVars(initial); + }, [serverId, initial]); + + const saveAll = async (next: KeyValue[]) => { + await onSave(next); + setEnvVars(next); + }; + + const handleAdd = async () => { + if (!newKey.trim() || !newValue.trim()) { + showError("Both key and value are required"); + return; + } + if (envVars.some((e) => e.key === newKey.trim())) { + showError("Environment variable with this key already exists"); + return; + } + const next = [...envVars, { key: newKey.trim(), value: newValue.trim() }]; + await saveAll(next); + setNewKey(""); + setNewValue(""); + setIsAddingNew(false); + showSuccess("Environment variables saved"); + }; + + const handleEdit = (kv: KeyValue) => { + setEditingKey(kv.key); + setEditingKeyValue(kv.key); + setEditingValue(kv.value); + }; + + const handleSaveEdit = async () => { + if (!editingKey) return; + if (!editingKeyValue.trim() || !editingValue.trim()) { + showError("Both key and value are required"); + return; + } + if ( + envVars.some( + (e) => e.key === editingKeyValue.trim() && e.key !== editingKey, + ) + ) { + showError("Environment variable with this key already exists"); + return; + } + const next = envVars.map((e) => + e.key === editingKey + ? { key: editingKeyValue.trim(), value: editingValue.trim() } + : e, + ); + await saveAll(next); + setEditingKey(null); + setEditingKeyValue(""); + setEditingValue(""); + showSuccess("Environment variables saved"); + }; + + const handleCancelEdit = () => { + setEditingKey(null); + setEditingKeyValue(""); + setEditingValue(""); + }; + + const handleDelete = async (key: string) => { + const next = envVars.filter((e) => e.key !== key); + await saveAll(next); + showSuccess("Environment variables saved"); + }; + + return ( +
+ {isAddingNew ? ( +
+
+ + setNewKey(e.target.value)} + autoFocus + disabled={disabled || isSaving} + /> +
+
+ + setNewValue(e.target.value)} + disabled={disabled || isSaving} + /> +
+
+ + +
+
+ ) : ( + + )} + +
+ {envVars.length === 0 ? ( +

+ No environment variables configured +

+ ) : ( + envVars.map((kv) => ( +
+ {editingKey === kv.key ? ( + <> +
+ setEditingKeyValue(e.target.value)} + placeholder="Key" + className="h-8" + disabled={disabled || isSaving} + /> + setEditingValue(e.target.value)} + placeholder="Value" + className="h-8" + disabled={disabled || isSaving} + /> +
+
+ + +
+ + ) : ( + <> +
+
{kv.key}
+
+ {kv.value} +
+
+
+ + +
+ + )} +
+ )) + )} +
+
+ ); +} + +export function ToolsMcpSettings() { + const { + servers, + toolsByServer, + consentsMap, + createServer, + toggleEnabled: toggleServerEnabled, + deleteServer, + setToolConsent: updateToolConsent, + updateServer, + isUpdatingServer, + } = useMcp(); + const [consents, setConsents] = useState>({}); + const [name, setName] = useState(""); + const [transport, setTransport] = useState("stdio"); + const [command, setCommand] = useState(""); + const [args, setArgs] = useState(""); + const [url, setUrl] = useState(""); + const [enabled, setEnabled] = useState(true); + + React.useEffect(() => { + setConsents(consentsMap); + }, [consentsMap]); + + const onCreate = async () => { + const parsedArgs = (() => { + const trimmed = args.trim(); + if (!trimmed) return null; + if (trimmed.startsWith("[")) { + try { + const arr = JSON.parse(trimmed); + return Array.isArray(arr) && arr.every((x) => typeof x === "string") + ? (arr as string[]) + : null; + } catch { + // fall through + } + } + return trimmed.split(" ").filter(Boolean); + })(); + await createServer({ + name, + transport, + command: command || null, + args: parsedArgs, + url: url || null, + enabled, + }); + setName(""); + setCommand(""); + setArgs(""); + setUrl(""); + setEnabled(true); + }; + + // Removed activation toggling – tools are used dynamically with consent checks + + const onSetToolConsent = async ( + serverId: number, + toolName: string, + consent: "ask" | "always" | "denied", + ) => { + await updateToolConsent(serverId, toolName, consent); + setConsents((prev) => ({ ...prev, [`${serverId}:${toolName}`]: consent })); + }; + + return ( +
+
+
+
+ + setName(e.target.value)} + placeholder="My MCP Server" + /> +
+
+ + +
+ {transport === "stdio" && ( + <> +
+ + setCommand(e.target.value)} + placeholder="node" + /> +
+
+ + setArgs(e.target.value)} + placeholder="path/to/mcp-server.js --flag" + /> +
+ + )} + {transport === "http" && ( +
+ + setUrl(e.target.value)} + placeholder="http://localhost:3000" + /> +
+ )} +
+ + +
+
+
+ +
+
+ +
+ {servers.map((s) => ( +
+
+
+
{s.name}
+
+ {s.transport} + {s.url ? ` · ${s.url}` : ""} + {s.command ? ` · ${s.command}` : ""} + {Array.isArray(s.args) && s.args.length + ? ` · ${s.args.join(" ")}` + : ""} +
+
+
+ toggleServerEnabled(s.id, !!s.enabled)} + /> + +
+
+ {s.transport === "stdio" && ( +
+
+ Environment Variables +
+ { + await updateServer({ + id: s.id, + envJson: arrayToEnvObject(pairs), + }); + }} + /> +
+ )} +
+ {(toolsByServer[s.id] || []).map((t) => ( +
+
+
{t.name}
+
+ +
+
+ {t.description && ( +
+ {t.description} +
+ )} +
+ ))} + {(toolsByServer[s.id] || []).length === 0 && ( +
+ No tools discovered. +
+ )} +
+
+ ))} + {servers.length === 0 && ( +
+ No servers configured yet. +
+ )} +
+
+ ); +} diff --git a/src/db/schema.ts b/src/db/schema.ts index f19ad17..f841a4c 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -172,3 +172,43 @@ export const versionsRelations = relations(versions, ({ one }) => ({ references: [apps.id], }), })); + +// --- MCP (Model Context Protocol) tables --- +export const mcpServers = sqliteTable("mcp_servers", { + id: integer("id").primaryKey({ autoIncrement: true }), + name: text("name").notNull(), + transport: text("transport").notNull(), + command: text("command"), + // Store typed JSON for args and environment variables + args: text("args", { mode: "json" }).$type(), + envJson: text("env_json", { mode: "json" }).$type | null>(), + url: text("url"), + enabled: integer("enabled", { mode: "boolean" }) + .notNull() + .default(sql`0`), + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), +}); + +export const mcpToolConsents = sqliteTable( + "mcp_tool_consents", + { + id: integer("id").primaryKey({ autoIncrement: true }), + serverId: integer("server_id") + .notNull() + .references(() => mcpServers.id, { onDelete: "cascade" }), + toolName: text("tool_name").notNull(), + consent: text("consent").notNull().default("ask"), // ask | always | denied + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .default(sql`(unixepoch())`), + }, + (table) => [unique("uniq_mcp_consent").on(table.serverId, table.toolName)], +); diff --git a/src/hooks/useMcp.ts b/src/hooks/useMcp.ts new file mode 100644 index 0000000..7b67d95 --- /dev/null +++ b/src/hooks/useMcp.ts @@ -0,0 +1,173 @@ +import { useMemo } from "react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { IpcClient } from "@/ipc/ipc_client"; +import type { + McpServer, + McpServerUpdate, + McpTool, + McpToolConsent, + CreateMcpServer, +} from "@/ipc/ipc_types"; + +export type Transport = "stdio" | "http"; + +export function useMcp() { + const queryClient = useQueryClient(); + + const serversQuery = useQuery({ + queryKey: ["mcp", "servers"], + queryFn: async () => { + const ipc = IpcClient.getInstance(); + const list = await ipc.listMcpServers(); + return (list || []) as McpServer[]; + }, + meta: { showErrorToast: true }, + }); + + const serverIds = useMemo( + () => (serversQuery.data || []).map((s) => s.id).sort((a, b) => a - b), + [serversQuery.data], + ); + + const toolsByServerQuery = useQuery, Error>({ + queryKey: ["mcp", "tools-by-server", serverIds], + enabled: serverIds.length > 0, + queryFn: async () => { + const ipc = IpcClient.getInstance(); + const entries = await Promise.all( + serverIds.map(async (id) => [id, await ipc.listMcpTools(id)] as const), + ); + return Object.fromEntries(entries) as Record; + }, + meta: { showErrorToast: true }, + }); + + const consentsQuery = useQuery({ + queryKey: ["mcp", "consents"], + queryFn: async () => { + const ipc = IpcClient.getInstance(); + const list = await ipc.getMcpToolConsents(); + return (list || []) as McpToolConsent[]; + }, + meta: { showErrorToast: true }, + }); + + const consentsMap = useMemo(() => { + const map: Record = {}; + for (const c of consentsQuery.data || []) { + map[`${c.serverId}:${c.toolName}`] = c.consent; + } + return map; + }, [consentsQuery.data]); + + const createServerMutation = useMutation({ + mutationFn: async (params: CreateMcpServer) => { + const ipc = IpcClient.getInstance(); + return ipc.createMcpServer(params); + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ["mcp", "servers"] }); + await queryClient.invalidateQueries({ + queryKey: ["mcp", "tools-by-server"], + }); + }, + meta: { showErrorToast: true }, + }); + + const updateServerMutation = useMutation({ + mutationFn: async (params: McpServerUpdate) => { + const ipc = IpcClient.getInstance(); + return ipc.updateMcpServer(params); + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ["mcp", "servers"] }); + await queryClient.invalidateQueries({ + queryKey: ["mcp", "tools-by-server"], + }); + }, + meta: { showErrorToast: true }, + }); + + const deleteServerMutation = useMutation({ + mutationFn: async (id: number) => { + const ipc = IpcClient.getInstance(); + return ipc.deleteMcpServer(id); + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ["mcp", "servers"] }); + await queryClient.invalidateQueries({ + queryKey: ["mcp", "tools-by-server"], + }); + }, + meta: { showErrorToast: true }, + }); + + const setConsentMutation = useMutation({ + mutationFn: async (params: { + serverId: number; + toolName: string; + consent: McpToolConsent["consent"]; + }) => { + const ipc = IpcClient.getInstance(); + return ipc.setMcpToolConsent(params); + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ["mcp", "consents"] }); + }, + meta: { showErrorToast: true }, + }); + + const createServer = async (params: CreateMcpServer) => + createServerMutation.mutateAsync(params); + + const toggleEnabled = async (id: number, currentEnabled: boolean) => + updateServerMutation.mutateAsync({ id, enabled: !currentEnabled }); + + const updateServer = async (params: McpServerUpdate) => + updateServerMutation.mutateAsync(params); + + const deleteServer = async (id: number) => + deleteServerMutation.mutateAsync(id); + + const setToolConsent = async ( + serverId: number, + toolName: string, + consent: McpToolConsent["consent"], + ) => setConsentMutation.mutateAsync({ serverId, toolName, consent }); + + const refetchAll = async () => { + await Promise.all([ + queryClient.invalidateQueries({ queryKey: ["mcp", "servers"] }), + queryClient.invalidateQueries({ queryKey: ["mcp", "tools-by-server"] }), + queryClient.invalidateQueries({ queryKey: ["mcp", "consents"] }), + ]); + }; + + return { + servers: serversQuery.data || [], + toolsByServer: toolsByServerQuery.data || {}, + consentsList: consentsQuery.data || [], + consentsMap, + isLoading: + serversQuery.isLoading || + toolsByServerQuery.isLoading || + consentsQuery.isLoading, + error: + serversQuery.error || toolsByServerQuery.error || consentsQuery.error, + refetchAll, + + // Mutations + createServer, + toggleEnabled, + updateServer, + deleteServer, + setToolConsent, + + // Status flags + isCreating: createServerMutation.isPending, + isToggling: updateServerMutation.isPending, + isUpdatingServer: updateServerMutation.isPending, + isDeleting: deleteServerMutation.isPending, + isSettingConsent: setConsentMutation.isPending, + } as const; +} diff --git a/src/ipc/handlers/chat_stream_handlers.ts b/src/ipc/handlers/chat_stream_handlers.ts index fece036..5a37cd1 100644 --- a/src/ipc/handlers/chat_stream_handlers.ts +++ b/src/ipc/handlers/chat_stream_handlers.ts @@ -1,5 +1,5 @@ import { v4 as uuidv4 } from "uuid"; -import { ipcMain } from "electron"; +import { ipcMain, IpcMainInvokeEvent } from "electron"; import { ModelMessage, TextPart, @@ -7,7 +7,9 @@ import { streamText, ToolSet, TextStreamPart, + stepCountIs, } from "ai"; + import { db } from "../../db"; import { chats, messages } from "../../db/schema"; import { and, eq, isNull } from "drizzle-orm"; @@ -42,6 +44,8 @@ import { getMaxTokens, getTemperature } from "../utils/token_utils"; import { MAX_CHAT_TURNS_IN_CONTEXT } from "@/constants/settings_constants"; import { validateChatContext } from "../utils/context_paths_utils"; import { GoogleGenerativeAIProviderOptions } from "@ai-sdk/google"; +import { mcpServers } from "../../db/schema"; +import { requireMcpToolConsent } from "../utils/mcp_consent"; import { getExtraProviderOptions } from "../utils/thinking_utils"; @@ -64,6 +68,7 @@ import { parseAppMentions } from "@/shared/parse_mention_apps"; import { prompts as promptsTable } from "../../db/schema"; import { inArray } from "drizzle-orm"; import { replacePromptReference } from "../utils/replacePromptReference"; +import { mcpManager } from "../utils/mcp_manager"; type AsyncIterableStream = AsyncIterable & ReadableStream; @@ -103,6 +108,23 @@ function escapeXml(unsafe: string): string { .replace(/"/g, """); } +// Safely parse an MCP tool key that combines server and tool names. +// We split on the LAST occurrence of "__" to avoid ambiguity if either +// side contains "__" as part of its sanitized name. +function parseMcpToolKey(toolKey: string): { + serverName: string; + toolName: string; +} { + const separator = "__"; + const lastIndex = toolKey.lastIndexOf(separator); + if (lastIndex === -1) { + return { serverName: "", toolName: toolKey }; + } + const serverName = toolKey.slice(0, lastIndex); + const toolName = toolKey.slice(lastIndex + separator.length); + return { serverName, toolName }; +} + // Ensure the temp directory exists if (!fs.existsSync(TEMP_DIR)) { fs.mkdirSync(TEMP_DIR, { recursive: true }); @@ -129,11 +151,16 @@ async function processStreamChunks({ for await (const part of fullStream) { let chunk = ""; + if ( + inThinkingBlock && + !["reasoning-delta", "reasoning-end", "reasoning-start"].includes( + part.type, + ) + ) { + chunk = ""; + inThinkingBlock = false; + } if (part.type === "text-delta") { - if (inThinkingBlock) { - chunk = ""; - inThinkingBlock = false; - } chunk += part.text; } else if (part.type === "reasoning-delta") { if (!inThinkingBlock) { @@ -142,6 +169,14 @@ async function processStreamChunks({ } chunk += escapeDyadTags(part.text); + } else if (part.type === "tool-call") { + const { serverName, toolName } = parseMcpToolKey(part.toolName); + const content = escapeDyadTags(JSON.stringify(part.input)); + chunk = `\n${content}\n\n`; + } else if (part.type === "tool-result") { + const { serverName, toolName } = parseMcpToolKey(part.toolName); + const content = escapeDyadTags(part.output); + chunk = `\n${content}\n\n`; } if (!chunk) { @@ -496,7 +531,10 @@ ${componentSnippet} let systemPrompt = constructSystemPrompt({ aiRules: await readAiRules(getDyadAppPath(updatedChat.app.path)), - chatMode: settings.selectedChatMode, + chatMode: + settings.selectedChatMode === "agent" + ? "build" + : settings.selectedChatMode, }); // Add information about mentioned apps if any @@ -603,19 +641,21 @@ This conversation includes one or more image attachments. When the user uploads ] as const) : []; + const limitedHistoryChatMessages = limitedMessageHistory.map((msg) => ({ + role: msg.role as "user" | "assistant" | "system", + // Why remove thinking tags? + // Thinking tags are generally not critical for the context + // and eats up extra tokens. + content: + settings.selectedChatMode === "ask" + ? removeDyadTags(removeNonEssentialTags(msg.content)) + : removeNonEssentialTags(msg.content), + })); + let chatMessages: ModelMessage[] = [ ...codebasePrefix, ...otherCodebasePrefix, - ...limitedMessageHistory.map((msg) => ({ - role: msg.role as "user" | "assistant" | "system", - // Why remove thinking tags? - // Thinking tags are generally not critical for the context - // and eats up extra tokens. - content: - settings.selectedChatMode === "ask" - ? removeDyadTags(removeNonEssentialTags(msg.content)) - : removeNonEssentialTags(msg.content), - })), + ...limitedHistoryChatMessages, ]; // Check if the last message should include attachments @@ -654,9 +694,15 @@ This conversation includes one or more image attachments. When the user uploads const simpleStreamText = async ({ chatMessages, modelClient, + tools, + systemPromptOverride = systemPrompt, + dyadDisableFiles = false, }: { chatMessages: ModelMessage[]; modelClient: ModelClient; + tools?: ToolSet; + systemPromptOverride?: string; + dyadDisableFiles?: boolean; }) => { const dyadRequestId = uuidv4(); if (isEngineEnabled) { @@ -671,6 +717,7 @@ This conversation includes one or more image attachments. When the user uploads const providerOptions: Record = { "dyad-engine": { dyadRequestId, + dyadDisableFiles, }, "dyad-gateway": getExtraProviderOptions( modelClient.builtinProviderId, @@ -708,6 +755,7 @@ This conversation includes one or more image attachments. When the user uploads }, } satisfies GoogleGenerativeAIProviderOptions; } + return streamText({ headers: isAnthropic ? { @@ -718,8 +766,10 @@ This conversation includes one or more image attachments. When the user uploads temperature: await getTemperature(settings.selectedModel), maxRetries: 2, model: modelClient.model, + stopWhen: stepCountIs(3), providerOptions, - system: systemPrompt, + system: systemPromptOverride, + tools, messages: chatMessages.filter((m) => m.content), onError: (error: any) => { logger.error("Error streaming text:", error); @@ -780,6 +830,38 @@ This conversation includes one or more image attachments. When the user uploads return fullResponse; }; + if (settings.selectedChatMode === "agent") { + const tools = await getMcpTools(event); + + const { fullStream } = await simpleStreamText({ + chatMessages: limitedHistoryChatMessages, + modelClient, + tools, + systemPromptOverride: constructSystemPrompt({ + aiRules: await readAiRules(getDyadAppPath(updatedChat.app.path)), + chatMode: "agent", + }), + dyadDisableFiles: true, + }); + + const result = await processStreamChunks({ + fullStream, + fullResponse, + abortController, + chatId: req.chatId, + processResponseChunkUpdate, + }); + fullResponse = result.fullResponse; + chatMessages.push({ + role: "assistant", + content: fullResponse, + }); + chatMessages.push({ + role: "user", + content: "OK.", + }); + } + // When calling streamText, the messages need to be properly formatted for mixed content const { fullStream } = await simpleStreamText({ chatMessages, @@ -1316,3 +1398,48 @@ These are the other apps that I've mentioned in my prompt. These other apps' cod ${otherAppsCodebaseInfo} `; } + +async function getMcpTools(event: IpcMainInvokeEvent): Promise { + const mcpToolSet: ToolSet = {}; + try { + const servers = await db + .select() + .from(mcpServers) + .where(eq(mcpServers.enabled, true as any)); + for (const s of servers) { + const client = await mcpManager.getClient(s.id); + const toolSet = await client.tools(); + for (const [name, tool] of Object.entries(toolSet)) { + const key = `${String(s.name || "").replace(/[^a-zA-Z0-9_-]/g, "-")}__${String(name).replace(/[^a-zA-Z0-9_-]/g, "-")}`; + const original = tool; + mcpToolSet[key] = { + description: original?.description, + inputSchema: original?.inputSchema, + execute: async (args: any, execCtx: any) => { + const inputPreview = + typeof args === "string" + ? args + : Array.isArray(args) + ? args.join(" ") + : JSON.stringify(args).slice(0, 500); + const ok = await requireMcpToolConsent(event, { + serverId: s.id, + serverName: s.name, + toolName: name, + toolDescription: original?.description, + inputPreview, + }); + + if (!ok) throw new Error(`User declined running tool ${key}`); + const res = await original.execute?.(args, execCtx); + + return typeof res === "string" ? res : JSON.stringify(res); + }, + }; + } + } + } catch (e) { + logger.warn("Failed building MCP toolset", e); + } + return mcpToolSet; +} diff --git a/src/ipc/handlers/mcp_handlers.ts b/src/ipc/handlers/mcp_handlers.ts new file mode 100644 index 0000000..81d78af --- /dev/null +++ b/src/ipc/handlers/mcp_handlers.ts @@ -0,0 +1,163 @@ +import { IpcMainInvokeEvent } from "electron"; +import log from "electron-log"; +import { db } from "../../db"; +import { mcpServers, mcpToolConsents } from "../../db/schema"; +import { eq, and } from "drizzle-orm"; +import { createLoggedHandler } from "./safe_handle"; + +import { resolveConsent } from "../utils/mcp_consent"; +import { getStoredConsent } from "../utils/mcp_consent"; +import { mcpManager } from "../utils/mcp_manager"; +import { CreateMcpServer, McpServerUpdate, McpTool } from "../ipc_types"; + +const logger = log.scope("mcp_handlers"); +const handle = createLoggedHandler(logger); + +type ConsentDecision = "accept-once" | "accept-always" | "decline"; + +export function registerMcpHandlers() { + // CRUD for MCP servers + handle("mcp:list-servers", async () => { + return await db.select().from(mcpServers); + }); + + handle( + "mcp:create-server", + async (_event: IpcMainInvokeEvent, params: CreateMcpServer) => { + const { name, transport, command, args, envJson, url, enabled } = params; + const result = await db + .insert(mcpServers) + .values({ + name, + transport, + command: command || null, + args: args || null, + envJson: envJson || null, + url: url || null, + enabled: !!enabled, + }) + .returning(); + return result[0]; + }, + ); + + handle( + "mcp:update-server", + async (_event: IpcMainInvokeEvent, params: McpServerUpdate) => { + const update: any = {}; + if (params.name !== undefined) update.name = params.name; + if (params.transport !== undefined) update.transport = params.transport; + if (params.command !== undefined) update.command = params.command; + if (params.args !== undefined) update.args = params.args || null; + if (params.cwd !== undefined) update.cwd = params.cwd; + if (params.envJson !== undefined) update.envJson = params.envJson || null; + if (params.url !== undefined) update.url = params.url; + if (params.enabled !== undefined) update.enabled = !!params.enabled; + + const result = await db + .update(mcpServers) + .set(update) + .where(eq(mcpServers.id, params.id)) + .returning(); + // If server config changed, dispose cached client to be recreated on next use + try { + mcpManager.dispose(params.id); + } catch {} + return result[0]; + }, + ); + + handle( + "mcp:delete-server", + async (_event: IpcMainInvokeEvent, id: number) => { + try { + mcpManager.dispose(id); + } catch {} + await db.delete(mcpServers).where(eq(mcpServers.id, id)); + return { success: true }; + }, + ); + + // Tools listing (dynamic) + handle( + "mcp:list-tools", + async ( + _event: IpcMainInvokeEvent, + serverId: number, + ): Promise => { + try { + const client = await mcpManager.getClient(serverId); + const remoteTools = await client.tools(); + const tools = await Promise.all( + Object.entries(remoteTools).map(async ([name, tool]) => ({ + name, + description: tool.description ?? null, + consent: await getStoredConsent(serverId, name), + })), + ); + return tools; + } catch (e) { + logger.error("Failed to list tools", e); + return []; + } + }, + ); + // Consents + handle("mcp:get-tool-consents", async () => { + return await db.select().from(mcpToolConsents); + }); + + handle( + "mcp:set-tool-consent", + async ( + _event: IpcMainInvokeEvent, + params: { + serverId: number; + toolName: string; + consent: "ask" | "always" | "denied"; + }, + ) => { + const existing = await db + .select() + .from(mcpToolConsents) + .where( + and( + eq(mcpToolConsents.serverId, params.serverId), + eq(mcpToolConsents.toolName, params.toolName), + ), + ); + if (existing.length > 0) { + const result = await db + .update(mcpToolConsents) + .set({ consent: params.consent }) + .where( + and( + eq(mcpToolConsents.serverId, params.serverId), + eq(mcpToolConsents.toolName, params.toolName), + ), + ) + .returning(); + return result[0]; + } else { + const result = await db + .insert(mcpToolConsents) + .values({ + serverId: params.serverId, + toolName: params.toolName, + consent: params.consent, + }) + .returning(); + return result[0]; + } + }, + ); + + // Tool consent request/response handshake + // Receive consent response from renderer + handle( + "mcp:tool-consent-response", + async (_event, data: { requestId: string; decision: ConsentDecision }) => { + resolveConsent(data.requestId, data.decision); + }, + ); +} diff --git a/src/ipc/ipc_client.ts b/src/ipc/ipc_client.ts index 2963ea6..8be2f60 100644 --- a/src/ipc/ipc_client.ts +++ b/src/ipc/ipc_client.ts @@ -63,6 +63,8 @@ import type { PromptDto, CreatePromptParamsDto, UpdatePromptParamsDto, + McpServerUpdate, + CreateMcpServer, } from "./ipc_types"; import type { Template } from "../shared/templates"; import type { @@ -119,11 +121,13 @@ export class IpcClient { onError: (error: string) => void; } >; + private mcpConsentHandlers: Map void>; private constructor() { this.ipcRenderer = (window as any).electron.ipcRenderer as IpcRenderer; this.chatStreams = new Map(); this.appStreams = new Map(); this.helpStreams = new Map(); + this.mcpConsentHandlers = new Map(); // Set up listeners for stream events this.ipcRenderer.on("chat:response:chunk", (data) => { if ( @@ -238,6 +242,12 @@ export class IpcClient { this.helpStreams.delete(sessionId); } }); + + // MCP tool consent request from main + this.ipcRenderer.on("mcp:tool-consent-request", (payload) => { + const handler = this.mcpConsentHandlers.get("consent"); + if (handler) handler(payload); + }); } public static getInstance(): IpcClient { @@ -814,6 +824,67 @@ export class IpcClient { return result.version as string; } + // --- MCP Client Methods --- + public async listMcpServers() { + return this.ipcRenderer.invoke("mcp:list-servers"); + } + + public async createMcpServer(params: CreateMcpServer) { + return this.ipcRenderer.invoke("mcp:create-server", params); + } + + public async updateMcpServer(params: McpServerUpdate) { + return this.ipcRenderer.invoke("mcp:update-server", params); + } + + public async deleteMcpServer(id: number) { + return this.ipcRenderer.invoke("mcp:delete-server", id); + } + + public async listMcpTools(serverId: number) { + return this.ipcRenderer.invoke("mcp:list-tools", serverId); + } + + // Removed: upsertMcpTools and setMcpToolActive – tools are fetched dynamically at runtime + + public async getMcpToolConsents() { + return this.ipcRenderer.invoke("mcp:get-tool-consents"); + } + + public async setMcpToolConsent(params: { + serverId: number; + toolName: string; + consent: "ask" | "always" | "denied"; + }) { + return this.ipcRenderer.invoke("mcp:set-tool-consent", params); + } + + public onMcpToolConsentRequest( + handler: (payload: { + requestId: string; + serverId: number; + serverName: string; + toolName: string; + toolDescription?: string | null; + inputPreview?: string | null; + }) => void, + ) { + this.mcpConsentHandlers.set("consent", handler as any); + return () => { + this.mcpConsentHandlers.delete("consent"); + }; + } + + public respondToMcpConsentRequest( + requestId: string, + decision: "accept-once" | "accept-always" | "decline", + ) { + this.ipcRenderer.invoke("mcp:tool-consent-response", { + requestId, + decision, + }); + } + // Get proposal details public async getProposal(chatId: number): Promise { try { diff --git a/src/ipc/ipc_host.ts b/src/ipc/ipc_host.ts index d9cbc41..557ac64 100644 --- a/src/ipc/ipc_host.ts +++ b/src/ipc/ipc_host.ts @@ -30,6 +30,7 @@ import { registerTemplateHandlers } from "./handlers/template_handlers"; import { registerPortalHandlers } from "./handlers/portal_handlers"; import { registerPromptHandlers } from "./handlers/prompt_handlers"; import { registerHelpBotHandlers } from "./handlers/help_bot_handlers"; +import { registerMcpHandlers } from "./handlers/mcp_handlers"; export function registerIpcHandlers() { // Register all IPC handlers by category @@ -65,4 +66,5 @@ export function registerIpcHandlers() { registerPortalHandlers(); registerPromptHandlers(); registerHelpBotHandlers(); + registerMcpHandlers(); } diff --git a/src/ipc/ipc_types.ts b/src/ipc/ipc_types.ts index 9efabe3..2005eea 100644 --- a/src/ipc/ipc_types.ts +++ b/src/ipc/ipc_types.ts @@ -450,3 +450,37 @@ export interface HelpChatResponseError { sessionId: string; error: string; } + +// --- MCP Types --- +export interface McpServer { + id: number; + name: string; + transport: string; + command?: string | null; + args?: string[] | null; + cwd?: string | null; + envJson?: Record | null; + url?: string | null; + enabled: boolean; + createdAt: number; + updatedAt: number; +} + +export interface CreateMcpServer + extends Omit {} +export type McpServerUpdate = Partial & Pick; +export type McpToolConsentType = "ask" | "always" | "denied"; + +export interface McpTool { + name: string; + description?: string | null; + consent: McpToolConsentType; +} + +export interface McpToolConsent { + id: number; + serverId: number; + toolName: string; + consent: McpToolConsentType; + updatedAt: number; +} diff --git a/src/ipc/utils/llm_engine_provider.ts b/src/ipc/utils/llm_engine_provider.ts index b670253..d9ca698 100644 --- a/src/ipc/utils/llm_engine_provider.ts +++ b/src/ipc/utils/llm_engine_provider.ts @@ -137,6 +137,10 @@ export function createDyadEngine( if ("dyadRequestId" in parsedBody) { delete parsedBody.dyadRequestId; } + const dyadDisableFiles = parsedBody.dyadDisableFiles; + if ("dyadDisableFiles" in parsedBody) { + delete parsedBody.dyadDisableFiles; + } // Track and modify requestId with attempt number let modifiedRequestId = requestId; @@ -147,7 +151,7 @@ export function createDyadEngine( } // Add files to the request if they exist - if (files?.length) { + if (files?.length && !dyadDisableFiles) { parsedBody.dyad_options = { files, enable_lazy_edits: options.dyadOptions.enableLazyEdits, diff --git a/src/ipc/utils/mcp_consent.ts b/src/ipc/utils/mcp_consent.ts new file mode 100644 index 0000000..b05f07f --- /dev/null +++ b/src/ipc/utils/mcp_consent.ts @@ -0,0 +1,108 @@ +import { db } from "../../db"; +import { mcpToolConsents } from "../../db/schema"; +import { and, eq } from "drizzle-orm"; +import { IpcMainInvokeEvent } from "electron"; + +export type Consent = "ask" | "always" | "denied"; + +const pendingConsentResolvers = new Map< + string, + (d: "accept-once" | "accept-always" | "decline") => void +>(); + +export function waitForConsent( + requestId: string, +): Promise<"accept-once" | "accept-always" | "decline"> { + return new Promise((resolve) => { + pendingConsentResolvers.set(requestId, resolve); + }); +} + +export function resolveConsent( + requestId: string, + decision: "accept-once" | "accept-always" | "decline", +) { + const resolver = pendingConsentResolvers.get(requestId); + if (resolver) { + pendingConsentResolvers.delete(requestId); + resolver(decision); + } +} + +export async function getStoredConsent( + serverId: number, + toolName: string, +): Promise { + const rows = await db + .select() + .from(mcpToolConsents) + .where( + and( + eq(mcpToolConsents.serverId, serverId), + eq(mcpToolConsents.toolName, toolName), + ), + ); + if (rows.length === 0) return "ask"; + return (rows[0].consent as Consent) ?? "ask"; +} + +export async function setStoredConsent( + serverId: number, + toolName: string, + consent: Consent, +): Promise { + const rows = await db + .select() + .from(mcpToolConsents) + .where( + and( + eq(mcpToolConsents.serverId, serverId), + eq(mcpToolConsents.toolName, toolName), + ), + ); + if (rows.length > 0) { + await db + .update(mcpToolConsents) + .set({ consent }) + .where( + and( + eq(mcpToolConsents.serverId, serverId), + eq(mcpToolConsents.toolName, toolName), + ), + ); + } else { + await db.insert(mcpToolConsents).values({ serverId, toolName, consent }); + } +} + +export async function requireMcpToolConsent( + event: IpcMainInvokeEvent, + params: { + serverId: number; + serverName: string; + toolName: string; + toolDescription?: string | null; + inputPreview?: string | null; + }, +): Promise { + const current = await getStoredConsent(params.serverId, params.toolName); + if (current === "always") return true; + if (current === "denied") return false; + + // Ask renderer for a decision via event bridge + const requestId = `${params.serverId}:${params.toolName}:${Date.now()}`; + (event.sender as any).send("mcp:tool-consent-request", { + requestId, + ...params, + }); + const response = await waitForConsent(requestId); + + if (response === "accept-always") { + await setStoredConsent(params.serverId, params.toolName, "always"); + return true; + } + if (response === "decline") { + return false; + } + return response === "accept-once"; +} diff --git a/src/ipc/utils/mcp_manager.ts b/src/ipc/utils/mcp_manager.ts new file mode 100644 index 0000000..791db55 --- /dev/null +++ b/src/ipc/utils/mcp_manager.ts @@ -0,0 +1,59 @@ +import { db } from "../../db"; +import { mcpServers } from "../../db/schema"; +import { experimental_createMCPClient, experimental_MCPClient } from "ai"; +import { eq } from "drizzle-orm"; + +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; + +class McpManager { + private static _instance: McpManager; + static get instance(): McpManager { + if (!this._instance) this._instance = new McpManager(); + return this._instance; + } + + private clients = new Map(); + + async getClient(serverId: number): Promise { + const existing = this.clients.get(serverId); + if (existing) return existing; + const server = await db + .select() + .from(mcpServers) + .where(eq(mcpServers.id, serverId)); + const s = server.find((x) => x.id === serverId); + if (!s) throw new Error(`MCP server not found: ${serverId}`); + let transport: StdioClientTransport | StreamableHTTPClientTransport; + if (s.transport === "stdio") { + const args = s.args ?? []; + const env = s.envJson ?? undefined; + if (!s.command) throw new Error("MCP server command is required"); + transport = new StdioClientTransport({ + command: s.command, + args, + env, + }); + } else if (s.transport === "http") { + if (!s.url) throw new Error("HTTP MCP requires url"); + transport = new StreamableHTTPClientTransport(new URL(s.url as string)); + } else { + throw new Error(`Unsupported MCP transport: ${s.transport}`); + } + const client = await experimental_createMCPClient({ + transport, + }); + this.clients.set(serverId, client); + return client; + } + + dispose(serverId: number) { + const c = this.clients.get(serverId); + if (c) { + c.close(); + this.clients.delete(serverId); + } + } +} + +export const mcpManager = McpManager.instance; diff --git a/src/lib/schemas.ts b/src/lib/schemas.ts index cd76476..e02efb9 100644 --- a/src/lib/schemas.ts +++ b/src/lib/schemas.ts @@ -128,7 +128,7 @@ export type RuntimeMode = z.infer; export const RuntimeMode2Schema = z.enum(["host", "docker"]); export type RuntimeMode2 = z.infer; -export const ChatModeSchema = z.enum(["build", "ask"]); +export const ChatModeSchema = z.enum(["build", "ask", "agent"]); export type ChatMode = z.infer; export const GitHubSecretsSchema = z.object({ diff --git a/src/lib/toast.tsx b/src/lib/toast.tsx index d71a30f..757fa3c 100644 --- a/src/lib/toast.tsx +++ b/src/lib/toast.tsx @@ -3,6 +3,7 @@ import { PostHog } from "posthog-js"; import React from "react"; import { CustomErrorToast } from "../components/CustomErrorToast"; import { InputRequestToast } from "../components/InputRequestToast"; +import { McpConsentToast } from "../components/McpConsentToast"; /** * Toast utility functions for consistent notifications across the app @@ -111,6 +112,29 @@ export const showInputRequest = ( return toastId; }; +export function showMcpConsentToast(args: { + serverName: string; + toolName: string; + toolDescription?: string | null; + inputPreview?: string | null; + onDecision: (d: "accept-once" | "accept-always" | "decline") => void; +}) { + const toastId = toast.custom( + (t) => ( + + ), + { duration: Infinity }, + ); + return toastId; +} + export const showExtraFilesToast = ({ files, error, diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index 5303fe4..929166b 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -24,6 +24,7 @@ import { AutoUpdateSwitch } from "@/components/AutoUpdateSwitch"; import { ReleaseChannelSelector } from "@/components/ReleaseChannelSelector"; import { NeonIntegration } from "@/components/NeonIntegration"; import { RuntimeModeSelector } from "@/components/RuntimeModeSelector"; +import { ToolsMcpSettings } from "@/components/settings/ToolsMcpSettings"; export default function SettingsPage() { const [isResetDialogOpen, setIsResetDialogOpen] = useState(false); @@ -119,6 +120,17 @@ export default function SettingsPage() { + {/* Tools (MCP) */} +
+

+ Tools (MCP) +

+ +
+ {/* Experiments Section */}
{ - const systemPrompt = - chatMode === "ask" ? ASK_MODE_SYSTEM_PROMPT : BUILD_SYSTEM_PROMPT; - + const systemPrompt = getSystemPromptForChatMode(chatMode); return systemPrompt.replace("[[AI_RULES]]", aiRules ?? DEFAULT_AI_RULES); }; +export const getSystemPromptForChatMode = ( + chatMode: "build" | "ask" | "agent", +) => { + if (chatMode === "agent") { + return AGENT_MODE_SYSTEM_PROMPT; + } + if (chatMode === "ask") { + return ASK_MODE_SYSTEM_PROMPT; + } + return BUILD_SYSTEM_PROMPT; +}; + export const readAiRules = async (dyadAppPath: string) => { const aiRulesPath = path.join(dyadAppPath, "AI_RULES.md"); try { diff --git a/src/renderer.tsx b/src/renderer.tsx index c36f8c1..8fdc38c 100644 --- a/src/renderer.tsx +++ b/src/renderer.tsx @@ -11,7 +11,8 @@ import { QueryClientProvider, MutationCache, } from "@tanstack/react-query"; -import { showError } from "./lib/toast"; +import { showError, showMcpConsentToast } from "./lib/toast"; +import { IpcClient } from "./ipc/ipc_client"; // @ts-ignore console.log("Running in mode:", import.meta.env.MODE); @@ -109,6 +110,20 @@ function App() { }; }, []); + useEffect(() => { + const ipc = IpcClient.getInstance(); + const unsubscribe = ipc.onMcpToolConsentRequest((payload) => { + showMcpConsentToast({ + serverName: payload.serverName, + toolName: payload.toolName, + toolDescription: payload.toolDescription, + inputPreview: payload.inputPreview, + onDecision: (d) => ipc.respondToMcpConsentRequest(payload.requestId, d), + }); + }); + return () => unsubscribe(); + }, []); + return ; } diff --git a/testing/README.md b/testing/README.md new file mode 100644 index 0000000..6d19def --- /dev/null +++ b/testing/README.md @@ -0,0 +1,50 @@ +### Fake stdio MCP server + +This directory contains a minimal stdio MCP server for local testing. + +- **Tools**: + - **calculator_add**: adds two numbers. Inputs: `a` (number), `b` (number). + - **print_envs**: returns all environment variables visible to the server as pretty JSON. + +### Requirements + +- **Node 20+** (same as the repo engines) +- Uses the repo dependency `@modelcontextprotocol/sdk` and `zod` + +### Launch + +- **Via Node**: + + ```bash + node testing/fake-stdio-mcp-server.mjs + ``` + +- **Via script** (adds a stable entrypoint path): + + ```bash + testing/run-fake-stdio-mcp-server.sh + ``` + +### Passing environment variables + +Environment variables provided when launching (either from your shell or by the app) will be visible to the `print_envs` tool. + +```bash +export FOO=bar +export SECRET_TOKEN=example +testing/run-fake-stdio-mcp-server.sh +``` + +### Integrating with Dyad (stdio MCP) + +When adding a stdio MCP server in the app, use: + +- **Command**: `testing/run-fake-stdio-mcp-server.sh` (absolute path recommended) +- **Transport**: `stdio` +- **Args**: leave empty (not required) +- **Env**: optional key/values (e.g., `FOO=bar`) + +Once connected, you should see the two tools listed: + +- `calculator_add` +- `print_envs` diff --git a/testing/fake-llm-server/chatCompletionHandler.ts b/testing/fake-llm-server/chatCompletionHandler.ts index f4bb39d..a943e21 100644 --- a/testing/fake-llm-server/chatCompletionHandler.ts +++ b/testing/fake-llm-server/chatCompletionHandler.ts @@ -180,9 +180,46 @@ export default Index; messageContent = `[[STRING_IS_FINISHED]]";\nFinished writing file.`; messageContent += "\n\n" + generateDump(req); } + const isToolCall = !!( + lastMessage && + lastMessage.content && + lastMessage.content.includes("[call_tool=calculator_add]") + ); + let message = { + role: "assistant", + content: messageContent, + } as any; // Non-streaming response if (!stream) { + if (isToolCall) { + const toolCallId = `call_${Date.now()}`; + return res.json({ + id: `chatcmpl-${Date.now()}`, + object: "chat.completion", + created: Math.floor(Date.now() / 1000), + model: "fake-model", + choices: [ + { + index: 0, + message: { + role: "assistant", + tool_calls: [ + { + id: toolCallId, + type: "function", + function: { + name: "calculator_add", + arguments: JSON.stringify({ a: 1, b: 2 }), + }, + }, + ], + }, + finish_reason: "tool_calls", + }, + ], + }); + } return res.json({ id: `chatcmpl-${Date.now()}`, object: "chat.completion", @@ -191,10 +228,7 @@ export default Index; choices: [ { index: 0, - message: { - role: "assistant", - content: messageContent, - }, + message, finish_reason: "stop", }, ], @@ -206,9 +240,73 @@ export default Index; res.setHeader("Cache-Control", "no-cache"); res.setHeader("Connection", "keep-alive"); + // Tool call streaming (OpenAI-style) + if (isToolCall) { + const now = Date.now(); + const mkChunk = (delta: any, finish: null | string = null) => { + const chunk = { + id: `chatcmpl-${now}`, + object: "chat.completion.chunk", + created: Math.floor(now / 1000), + model: "fake-model", + choices: [ + { + index: 0, + delta, + finish_reason: finish, + }, + ], + }; + return `data: ${JSON.stringify(chunk)}\n\n`; + }; + + // 1) Send role + res.write(mkChunk({ role: "assistant" })); + + // 2) Send tool_calls init with id + name + empty args + const toolCallId = `call_${now}`; + res.write( + mkChunk({ + tool_calls: [ + { + index: 0, + id: toolCallId, + type: "function", + function: { + name: "testing-mcp-server__calculator_add", + arguments: "", + }, + }, + ], + }), + ); + + // 3) Stream arguments gradually + const args = JSON.stringify({ a: 1, b: 2 }); + let i = 0; + const argBatchSize = 6; + const argInterval = setInterval(() => { + if (i < args.length) { + const part = args.slice(i, i + argBatchSize); + i += argBatchSize; + res.write( + mkChunk({ + tool_calls: [{ index: 0, function: { arguments: part } }], + }), + ); + } else { + // 4) Finalize with finish_reason tool_calls and [DONE] + res.write(mkChunk({}, "tool_calls")); + res.write("data: [DONE]\n\n"); + clearInterval(argInterval); + res.end(); + } + }, 10); + return; + } + // Split the message into characters to simulate streaming - const message = messageContent; - const messageChars = message.split(""); + const messageChars = messageContent.split(""); // Stream each character with a delay let index = 0; diff --git a/testing/fake-stdio-mcp-server.mjs b/testing/fake-stdio-mcp-server.mjs new file mode 100644 index 0000000..f1262f2 --- /dev/null +++ b/testing/fake-stdio-mcp-server.mjs @@ -0,0 +1,44 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { z } from "zod"; + +const server = new McpServer({ + name: "fake-stdio-mcp", + version: "0.1.0", +}); + +server.registerTool( + "calculator_add", + { + title: "Calculator Add", + description: "Add two numbers and return the sum", + inputSchema: { a: z.number(), b: z.number() }, + }, + async ({ a, b }) => { + const sum = a + b; + return { + content: [{ type: "text", text: String(sum) }], + }; + }, +); + +server.registerTool( + "print_envs", + { + title: "Print Envs", + description: "Print the environment variables received by the server", + inputSchema: {}, + }, + async () => { + const envObject = Object.fromEntries( + Object.entries(process.env).map(([key, value]) => [key, value ?? ""]), + ); + const pretty = JSON.stringify(envObject, null, 2); + return { + content: [{ type: "text", text: pretty }], + }; + }, +); + +const transport = new StdioServerTransport(); +await server.connect(transport); diff --git a/testing/run-fake-stdio-mcp-server.sh b/testing/run-fake-stdio-mcp-server.sh new file mode 100755 index 0000000..609030d --- /dev/null +++ b/testing/run-fake-stdio-mcp-server.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Launch the fake stdio MCP server with Node. +# Usage: testing/run-fake-stdio-mcp-server.sh + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +NODE_BIN="node" + +exec "$NODE_BIN" "$SCRIPT_DIR/fake-stdio-mcp-server.mjs" + +