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"
+
+