From 586d6ec1eb8061dfc725e0aa22f7d80a8ad757b1 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Fri, 30 Jan 2026 16:00:18 -0800 Subject: [PATCH 1/6] WIP --- .mcp.json | 6 +- .../hooks/useCommandWatcher/tools/index.ts | 2 + .../tools/start-claude-session.ts | 140 ++++++++++++++++++ .../hooks/useCommandWatcher/tools/types.ts | 4 + .../useCommandWatcher/useCommandWatcher.ts | 5 + .../devices/start-claude-session/index.ts | 1 + .../start-claude-session.ts | 43 ++++++ packages/mcp/src/tools/index.ts | 2 + 8 files changed, 200 insertions(+), 3 deletions(-) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/start-claude-session.ts create mode 100644 packages/mcp/src/tools/devices/start-claude-session/index.ts create mode 100644 packages/mcp/src/tools/devices/start-claude-session/start-claude-session.ts diff --git a/.mcp.json b/.mcp.json index 03b7976cc3f..ba029a2f9f6 100644 --- a/.mcp.json +++ b/.mcp.json @@ -2,11 +2,11 @@ "mcpServers": { "superset": { "type": "http", - "url": "https://api.superset.sh/api/agent/mcp", + "url": "http://localhost:3001/api/agent/mcp", "oauth": { "clientId": "claude-code", - "authorizationUrl": "https://api.superset.sh/api/auth/mcp/authorize", - "tokenUrl": "https://api.superset.sh/api/auth/mcp/token", + "authorizationUrl": "http://localhost:3001/api/auth/mcp/authorize", + "tokenUrl": "http://localhost:3001/api/auth/mcp/token", "scopes": ["openid", "profile", "email"] } } diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/index.ts b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/index.ts index ad5eec98c42..a1dbc34b2ab 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/index.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/index.ts @@ -4,6 +4,7 @@ import { getAppContext } from "./get-app-context"; import { listProjects } from "./list-projects"; import { listWorkspaces } from "./list-workspaces"; import { navigateToWorkspace } from "./navigate-to-workspace"; +import { startClaudeSession } from "./start-claude-session"; import { switchWorkspace } from "./switch-workspace"; import type { CommandResult, ToolContext, ToolDefinition } from "./types"; @@ -16,6 +17,7 @@ const tools: ToolDefinition[] = [ listProjects, listWorkspaces, navigateToWorkspace, + startClaudeSession, switchWorkspace, ]; diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/start-claude-session.ts b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/start-claude-session.ts new file mode 100644 index 00000000000..61de107c236 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/start-claude-session.ts @@ -0,0 +1,140 @@ +import type { SelectTask, SelectTaskStatus } from "@superset/db/schema"; +import { useWorkspaceInitStore } from "renderer/stores/workspace-init"; +import { z } from "zod"; +import type { CommandResult, ToolContext, ToolDefinition } from "./types"; + +const schema = z.object({ + taskId: z.string(), +}); + +function buildPrompt( + task: SelectTask, + status: SelectTaskStatus | null, +): string { + const lines: string[] = []; + + lines.push(`You are working on task "${task.title}" (${task.slug}).`); + lines.push(""); + + lines.push(`Priority: ${task.priority}`); + if (status) { + lines.push(`Status: ${status.name}`); + } + if (task.labels && task.labels.length > 0) { + lines.push(`Labels: ${task.labels.join(", ")}`); + } + lines.push(""); + + lines.push("## Task Description"); + lines.push(""); + lines.push(task.description || "No description provided."); + lines.push(""); + + lines.push("## Instructions"); + lines.push(""); + lines.push( + "You are running fully autonomously. Do not ask questions or wait for user feedback — make all decisions independently based on the codebase and task description.", + ); + lines.push(""); + lines.push( + "1. Explore the codebase to understand the relevant code and architecture", + ); + lines.push("2. Create a detailed execution plan for this task including:"); + lines.push(" - Purpose and scope of the changes"); + lines.push(" - Key assumptions"); + lines.push( + " - Concrete implementation steps with specific files to modify", + ); + lines.push(" - How to validate the changes work correctly"); + lines.push("3. Once your plan is solid, begin implementing it"); + lines.push( + `4. When you are done, use the Superset MCP \`update_task\` tool to update task "${task.id}" with a summary of your plan in the description field`, + ); + lines.push(""); + lines.push("Be thorough in your exploration before writing any code."); + + return lines.join("\n"); +} + +async function execute( + params: z.infer, + ctx: ToolContext, +): Promise { + // 1. Fetch task from local DB + const task = ctx.getTask(params.taskId); + if (!task) { + return { success: false, error: `Task not found: ${params.taskId}` }; + } + + const status = task.statusId ? ctx.getTaskStatus(task.statusId) : null; + + // 2. Construct prompt + const prompt = buildPrompt(task, status ?? null); + + // 3. Build claude command (heredoc for safe escaping) + const claudeCommand = `claude --allowedTools "mcp__superset*" --permission-mode plan "$(cat <<'SUPERSET_PROMPT'\n${prompt}\nSUPERSET_PROMPT\n)"`; + + // 4. Derive projectId from current workspace or most recent + const workspaces = ctx.getWorkspaces(); + if (!workspaces || workspaces.length === 0) { + return { success: false, error: "No workspaces available" }; + } + + let projectId: string | null = null; + const activeWorkspaceId = ctx.getActiveWorkspaceId(); + if (activeWorkspaceId) { + const activeWorkspace = workspaces.find( + (ws) => ws.id === activeWorkspaceId, + ); + if (activeWorkspace) { + projectId = activeWorkspace.projectId; + } + } + + if (!projectId) { + const sorted = [...workspaces].sort( + (a, b) => (b.lastOpenedAt ?? 0) - (a.lastOpenedAt ?? 0), + ); + projectId = sorted[0].projectId; + } + + try { + // 5. Create workspace + const result = await ctx.createWorktree.mutateAsync({ projectId }); + + // 6. Append claude command to pending terminal setup + const store = useWorkspaceInitStore.getState(); + const pending = store.pendingTerminalSetups[result.workspace.id]; + store.addPendingTerminalSetup({ + workspaceId: result.workspace.id, + projectId: pending?.projectId ?? projectId, + initialCommands: [...(pending?.initialCommands ?? []), claudeCommand], + defaultPreset: pending?.defaultPreset ?? null, + }); + + // 7. Navigate + await ctx.navigateToWorkspace(result.workspace.id); + + return { + success: true, + data: { + workspaceId: result.workspace.id, + branch: result.workspace.branch, + }, + }; + } catch (error) { + return { + success: false, + error: + error instanceof Error + ? error.message + : "Failed to start Claude session", + }; + } +} + +export const startClaudeSession: ToolDefinition = { + name: "start_claude_session", + schema, + execute, +}; diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/types.ts b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/types.ts index 5bccfd63f23..0101e8d9708 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/types.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/types.ts @@ -1,3 +1,4 @@ +import type { SelectTask, SelectTaskStatus } from "@superset/db/schema"; import type { SelectProject, SelectWorkspace } from "@superset/local-db"; import type { electronTrpc } from "renderer/lib/electron-trpc"; import type { z } from "zod"; @@ -21,6 +22,9 @@ export interface ToolContext { getWorkspaces: () => SelectWorkspace[] | undefined; getProjects: () => SelectProject[] | undefined; getActiveWorkspaceId: () => string | null; + // Collections for local data access + getTask: (taskId: string) => SelectTask | undefined; + getTaskStatus: (statusId: string) => SelectTaskStatus | undefined; // Navigation navigateToWorkspace: (workspaceId: string) => Promise; } diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/useCommandWatcher.ts b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/useCommandWatcher.ts index 7eccb303ccf..fd13b271a69 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/useCommandWatcher.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/useCommandWatcher.ts @@ -45,6 +45,9 @@ export function useCommandWatcher() { getWorkspaces: () => workspaces, getProjects: () => projects, getActiveWorkspaceId: getCurrentWorkspaceIdFromRoute, + getTask: (taskId: string) => collections.tasks.get(taskId), + getTaskStatus: (statusId: string) => + collections.taskStatuses.get(statusId), navigateToWorkspace: (workspaceId: string) => navigateToWorkspace(workspaceId, navigate), }), @@ -56,6 +59,8 @@ export function useCommandWatcher() { workspaces, projects, getCurrentWorkspaceIdFromRoute, + collections.tasks, + collections.taskStatuses, navigate, ], ); diff --git a/packages/mcp/src/tools/devices/start-claude-session/index.ts b/packages/mcp/src/tools/devices/start-claude-session/index.ts new file mode 100644 index 00000000000..4a758bd261c --- /dev/null +++ b/packages/mcp/src/tools/devices/start-claude-session/index.ts @@ -0,0 +1 @@ +export { register } from "./start-claude-session"; diff --git a/packages/mcp/src/tools/devices/start-claude-session/start-claude-session.ts b/packages/mcp/src/tools/devices/start-claude-session/start-claude-session.ts new file mode 100644 index 00000000000..bda4b619747 --- /dev/null +++ b/packages/mcp/src/tools/devices/start-claude-session/start-claude-session.ts @@ -0,0 +1,43 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { executeOnDevice, getMcpContext } from "../../utils"; + +export function register(server: McpServer) { + server.registerTool( + "start_claude_session", + { + description: + "Start an autonomous Claude Code session for a task. Creates a new workspace and launches Claude in plan mode with the task context.", + inputSchema: { + deviceId: z.string().describe("Target device ID"), + taskId: z.string().describe("Task ID to work on"), + }, + }, + async (args, extra) => { + const ctx = getMcpContext(extra); + const deviceId = args.deviceId as string; + const taskId = args.taskId as string; + + if (!deviceId) { + return { + content: [{ type: "text", text: "Error: deviceId is required" }], + isError: true, + }; + } + + if (!taskId) { + return { + content: [{ type: "text", text: "Error: taskId is required" }], + isError: true, + }; + } + + return executeOnDevice({ + ctx, + deviceId, + tool: "start_claude_session", + params: { taskId }, + }); + }, + ); +} diff --git a/packages/mcp/src/tools/index.ts b/packages/mcp/src/tools/index.ts index baaaf65dc6d..0e5053d24fb 100644 --- a/packages/mcp/src/tools/index.ts +++ b/packages/mcp/src/tools/index.ts @@ -6,6 +6,7 @@ import { register as listDevices } from "./devices/list-devices"; import { register as listProjects } from "./devices/list-projects"; import { register as listWorkspaces } from "./devices/list-workspaces"; import { register as navigateToWorkspace } from "./devices/navigate-to-workspace"; +import { register as startClaudeSession } from "./devices/start-claude-session"; import { register as switchWorkspace } from "./devices/switch-workspace"; import { register as listMembers } from "./organizations/list-members"; import { register as createTask } from "./tasks/create-task"; @@ -31,6 +32,7 @@ const allTools = [ createWorkspace, switchWorkspace, deleteWorkspace, + startClaudeSession, ]; export function registerTools(server: McpServer) { From f88fffdf0a72efa6a31700d321daa6185dd7aa92 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Fri, 30 Jan 2026 16:53:45 -0800 Subject: [PATCH 2/6] Move prompt building and task fetching to MCP server The desktop client was responsible for fetching task data and building the claude prompt, which coupled it to the DB schema. Now the MCP server builds the full command (prompt + heredoc escaping) and sends it to the desktop as an opaque string. The desktop tool accepts a required `name` param used for both workspace name and branch name. --- .../tools/start-claude-session.ts | 83 ++------------ .../hooks/useCommandWatcher/tools/types.ts | 4 - .../useCommandWatcher/useCommandWatcher.ts | 5 - .../start-claude-session.ts | 104 +++++++++++++++++- 4 files changed, 115 insertions(+), 81 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/start-claude-session.ts b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/start-claude-session.ts index 61de107c236..d8e1201f08d 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/start-claude-session.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/start-claude-session.ts @@ -1,80 +1,17 @@ -import type { SelectTask, SelectTaskStatus } from "@superset/db/schema"; import { useWorkspaceInitStore } from "renderer/stores/workspace-init"; import { z } from "zod"; import type { CommandResult, ToolContext, ToolDefinition } from "./types"; const schema = z.object({ - taskId: z.string(), + command: z.string(), + name: z.string(), }); -function buildPrompt( - task: SelectTask, - status: SelectTaskStatus | null, -): string { - const lines: string[] = []; - - lines.push(`You are working on task "${task.title}" (${task.slug}).`); - lines.push(""); - - lines.push(`Priority: ${task.priority}`); - if (status) { - lines.push(`Status: ${status.name}`); - } - if (task.labels && task.labels.length > 0) { - lines.push(`Labels: ${task.labels.join(", ")}`); - } - lines.push(""); - - lines.push("## Task Description"); - lines.push(""); - lines.push(task.description || "No description provided."); - lines.push(""); - - lines.push("## Instructions"); - lines.push(""); - lines.push( - "You are running fully autonomously. Do not ask questions or wait for user feedback — make all decisions independently based on the codebase and task description.", - ); - lines.push(""); - lines.push( - "1. Explore the codebase to understand the relevant code and architecture", - ); - lines.push("2. Create a detailed execution plan for this task including:"); - lines.push(" - Purpose and scope of the changes"); - lines.push(" - Key assumptions"); - lines.push( - " - Concrete implementation steps with specific files to modify", - ); - lines.push(" - How to validate the changes work correctly"); - lines.push("3. Once your plan is solid, begin implementing it"); - lines.push( - `4. When you are done, use the Superset MCP \`update_task\` tool to update task "${task.id}" with a summary of your plan in the description field`, - ); - lines.push(""); - lines.push("Be thorough in your exploration before writing any code."); - - return lines.join("\n"); -} - async function execute( params: z.infer, ctx: ToolContext, ): Promise { - // 1. Fetch task from local DB - const task = ctx.getTask(params.taskId); - if (!task) { - return { success: false, error: `Task not found: ${params.taskId}` }; - } - - const status = task.statusId ? ctx.getTaskStatus(task.statusId) : null; - - // 2. Construct prompt - const prompt = buildPrompt(task, status ?? null); - - // 3. Build claude command (heredoc for safe escaping) - const claudeCommand = `claude --allowedTools "mcp__superset*" --permission-mode plan "$(cat <<'SUPERSET_PROMPT'\n${prompt}\nSUPERSET_PROMPT\n)"`; - - // 4. Derive projectId from current workspace or most recent + // 1. Derive projectId from current workspace or most recent const workspaces = ctx.getWorkspaces(); if (!workspaces || workspaces.length === 0) { return { success: false, error: "No workspaces available" }; @@ -99,20 +36,24 @@ async function execute( } try { - // 5. Create workspace - const result = await ctx.createWorktree.mutateAsync({ projectId }); + // 2. Create workspace + const result = await ctx.createWorktree.mutateAsync({ + projectId, + name: params.name, + branchName: params.name, + }); - // 6. Append claude command to pending terminal setup + // 3. Append command to pending terminal setup const store = useWorkspaceInitStore.getState(); const pending = store.pendingTerminalSetups[result.workspace.id]; store.addPendingTerminalSetup({ workspaceId: result.workspace.id, projectId: pending?.projectId ?? projectId, - initialCommands: [...(pending?.initialCommands ?? []), claudeCommand], + initialCommands: [...(pending?.initialCommands ?? []), params.command], defaultPreset: pending?.defaultPreset ?? null, }); - // 7. Navigate + // 4. Navigate await ctx.navigateToWorkspace(result.workspace.id); return { diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/types.ts b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/types.ts index 0101e8d9708..5bccfd63f23 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/types.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/types.ts @@ -1,4 +1,3 @@ -import type { SelectTask, SelectTaskStatus } from "@superset/db/schema"; import type { SelectProject, SelectWorkspace } from "@superset/local-db"; import type { electronTrpc } from "renderer/lib/electron-trpc"; import type { z } from "zod"; @@ -22,9 +21,6 @@ export interface ToolContext { getWorkspaces: () => SelectWorkspace[] | undefined; getProjects: () => SelectProject[] | undefined; getActiveWorkspaceId: () => string | null; - // Collections for local data access - getTask: (taskId: string) => SelectTask | undefined; - getTaskStatus: (statusId: string) => SelectTaskStatus | undefined; // Navigation navigateToWorkspace: (workspaceId: string) => Promise; } diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/useCommandWatcher.ts b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/useCommandWatcher.ts index fd13b271a69..7eccb303ccf 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/useCommandWatcher.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/useCommandWatcher.ts @@ -45,9 +45,6 @@ export function useCommandWatcher() { getWorkspaces: () => workspaces, getProjects: () => projects, getActiveWorkspaceId: getCurrentWorkspaceIdFromRoute, - getTask: (taskId: string) => collections.tasks.get(taskId), - getTaskStatus: (statusId: string) => - collections.taskStatuses.get(statusId), navigateToWorkspace: (workspaceId: string) => navigateToWorkspace(workspaceId, navigate), }), @@ -59,8 +56,6 @@ export function useCommandWatcher() { workspaces, projects, getCurrentWorkspaceIdFromRoute, - collections.tasks, - collections.taskStatuses, navigate, ], ); diff --git a/packages/mcp/src/tools/devices/start-claude-session/start-claude-session.ts b/packages/mcp/src/tools/devices/start-claude-session/start-claude-session.ts index bda4b619747..3a46995ccd7 100644 --- a/packages/mcp/src/tools/devices/start-claude-session/start-claude-session.ts +++ b/packages/mcp/src/tools/devices/start-claude-session/start-claude-session.ts @@ -1,7 +1,70 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { db } from "@superset/db/client"; +import { taskStatuses, tasks } from "@superset/db/schema"; +import { and, eq, isNull } from "drizzle-orm"; +import { alias } from "drizzle-orm/pg-core"; import { z } from "zod"; import { executeOnDevice, getMcpContext } from "../../utils"; +function buildPrompt(task: { + id: string; + title: string; + slug: string; + description: string | null; + priority: string; + statusName: string | null; + labels: string[] | null; +}): string { + const lines: string[] = []; + + lines.push(`You are working on task "${task.title}" (${task.slug}).`); + lines.push(""); + + lines.push(`Priority: ${task.priority}`); + if (task.statusName) { + lines.push(`Status: ${task.statusName}`); + } + if (task.labels && task.labels.length > 0) { + lines.push(`Labels: ${task.labels.join(", ")}`); + } + lines.push(""); + + lines.push("## Task Description"); + lines.push(""); + lines.push(task.description || "No description provided."); + lines.push(""); + + lines.push("## Instructions"); + lines.push(""); + lines.push( + "You are running fully autonomously. Do not ask questions or wait for user feedback — make all decisions independently based on the codebase and task description.", + ); + lines.push(""); + lines.push( + "IMPORTANT: Do NOT write any code or make any changes to files. Your job is ONLY to explore and plan.", + ); + lines.push(""); + lines.push( + "1. Explore the codebase to understand the relevant code and architecture", + ); + lines.push("2. Create a detailed execution plan for this task including:"); + lines.push(" - Purpose and scope of the changes"); + lines.push(" - Key assumptions"); + lines.push( + " - Concrete implementation steps with specific files to modify", + ); + lines.push(" - How to validate the changes work correctly"); + lines.push( + `3. When your plan is complete, use the Superset MCP \`update_task\` tool to update task "${task.id}" with your full plan in the description field`, + ); + lines.push(""); + lines.push( + "Do NOT implement the plan. Only explore, plan, and update the task.", + ); + + return lines.join("\n"); +} + export function register(server: McpServer) { server.registerTool( "start_claude_session", @@ -32,11 +95,50 @@ export function register(server: McpServer) { }; } + // Fetch task data + const status = alias(taskStatuses, "status"); + const [task] = await db + .select({ + id: tasks.id, + slug: tasks.slug, + title: tasks.title, + description: tasks.description, + priority: tasks.priority, + statusName: status.name, + labels: tasks.labels, + }) + .from(tasks) + .leftJoin(status, eq(tasks.statusId, status.id)) + .where( + and( + eq(tasks.id, taskId), + eq(tasks.organizationId, ctx.organizationId), + isNull(tasks.deletedAt), + ), + ) + .limit(1); + + if (!task) { + return { + content: [{ type: "text", text: "Error: Task not found" }], + isError: true, + }; + } + + // Build the full claude command server-side + const prompt = buildPrompt(task); + const command = [ + "claude --dangerously-skip-permissions \"$(cat <<'SUPERSET_PROMPT'", + prompt, + "SUPERSET_PROMPT", + ')"', + ].join("\n"); + return executeOnDevice({ ctx, deviceId, tool: "start_claude_session", - params: { taskId }, + params: { command, name: task.slug }, }); }, ); From f21d1131a38d8458db875cd1153efe104f40c182 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Sat, 31 Jan 2026 12:05:51 -0800 Subject: [PATCH 3/6] Add start_claude_subagent tool and update prompt to implement Two changes: - New `start_claude_subagent` MCP + desktop tool that adds a terminal pane to the active workspace instead of creating a new one. This keeps it naturally hidden from Slack (which has no workspace context). - Update buildPrompt to have Claude implement after planning instead of stopping at the plan phase. --- .../hooks/useCommandWatcher/tools/index.ts | 2 + .../tools/start-claude-subagent.ts | 42 ++++ .../start-claude-session.ts | 179 +++++++++++------- 3 files changed, 156 insertions(+), 67 deletions(-) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/start-claude-subagent.ts diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/index.ts b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/index.ts index a1dbc34b2ab..e114b8304b1 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/index.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/index.ts @@ -5,6 +5,7 @@ import { listProjects } from "./list-projects"; import { listWorkspaces } from "./list-workspaces"; import { navigateToWorkspace } from "./navigate-to-workspace"; import { startClaudeSession } from "./start-claude-session"; +import { startClaudeSubagent } from "./start-claude-subagent"; import { switchWorkspace } from "./switch-workspace"; import type { CommandResult, ToolContext, ToolDefinition } from "./types"; @@ -18,6 +19,7 @@ const tools: ToolDefinition[] = [ listWorkspaces, navigateToWorkspace, startClaudeSession, + startClaudeSubagent, switchWorkspace, ]; diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/start-claude-subagent.ts b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/start-claude-subagent.ts new file mode 100644 index 00000000000..40770300d27 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/start-claude-subagent.ts @@ -0,0 +1,42 @@ +import { useTabsStore } from "renderer/stores/tabs/store"; +import { z } from "zod"; +import type { CommandResult, ToolContext, ToolDefinition } from "./types"; + +const schema = z.object({ + command: z.string(), +}); + +async function execute( + params: z.infer, + ctx: ToolContext, +): Promise { + const activeWorkspaceId = ctx.getActiveWorkspaceId(); + if (!activeWorkspaceId) { + return { success: false, error: "No active workspace" }; + } + + const tabsStore = useTabsStore.getState(); + const activeTabId = tabsStore.activeTabIds[activeWorkspaceId]; + if (!activeTabId) { + return { success: false, error: "No active tab in workspace" }; + } + + const paneId = tabsStore.addPane(activeTabId, { + initialCommands: [params.command], + }); + + if (!paneId) { + return { success: false, error: "Failed to add pane" }; + } + + return { + success: true, + data: { workspaceId: activeWorkspaceId, paneId }, + }; +} + +export const startClaudeSubagent: ToolDefinition = { + name: "start_claude_subagent", + schema, + execute, +}; diff --git a/packages/mcp/src/tools/devices/start-claude-session/start-claude-session.ts b/packages/mcp/src/tools/devices/start-claude-session/start-claude-session.ts index 3a46995ccd7..ec5c0b035ca 100644 --- a/packages/mcp/src/tools/devices/start-claude-session/start-claude-session.ts +++ b/packages/mcp/src/tools/devices/start-claude-session/start-claude-session.ts @@ -6,7 +6,7 @@ import { alias } from "drizzle-orm/pg-core"; import { z } from "zod"; import { executeOnDevice, getMcpContext } from "../../utils"; -function buildPrompt(task: { +interface TaskData { id: string; title: string; slug: string; @@ -14,7 +14,9 @@ function buildPrompt(task: { priority: string; statusName: string | null; labels: string[] | null; -}): string { +} + +function buildPrompt(task: TaskData): string { const lines: string[] = []; lines.push(`You are working on task "${task.title}" (${task.slug}).`); @@ -40,10 +42,6 @@ function buildPrompt(task: { "You are running fully autonomously. Do not ask questions or wait for user feedback — make all decisions independently based on the codebase and task description.", ); lines.push(""); - lines.push( - "IMPORTANT: Do NOT write any code or make any changes to files. Your job is ONLY to explore and plan.", - ); - lines.push(""); lines.push( "1. Explore the codebase to understand the relevant code and architecture", ); @@ -54,23 +52,87 @@ function buildPrompt(task: { " - Concrete implementation steps with specific files to modify", ); lines.push(" - How to validate the changes work correctly"); + lines.push("3. Implement the plan"); lines.push( - `3. When your plan is complete, use the Superset MCP \`update_task\` tool to update task "${task.id}" with your full plan in the description field`, + "4. Verify your changes work correctly (run relevant tests, typecheck, lint)", ); - lines.push(""); lines.push( - "Do NOT implement the plan. Only explore, plan, and update the task.", + `5. When done, use the Superset MCP \`update_task\` tool to update task "${task.id}" with a summary of what was done`, ); return lines.join("\n"); } +function buildCommand(task: TaskData): string { + const prompt = buildPrompt(task); + return [ + "claude --dangerously-skip-permissions \"$(cat <<'SUPERSET_PROMPT'", + prompt, + "SUPERSET_PROMPT", + ')"', + ].join("\n"); +} + +async function fetchTask({ + taskId, + organizationId, +}: { + taskId: string; + organizationId: string; +}): Promise { + const status = alias(taskStatuses, "status"); + const [task] = await db + .select({ + id: tasks.id, + slug: tasks.slug, + title: tasks.title, + description: tasks.description, + priority: tasks.priority, + statusName: status.name, + labels: tasks.labels, + }) + .from(tasks) + .leftJoin(status, eq(tasks.statusId, status.id)) + .where( + and( + eq(tasks.id, taskId), + eq(tasks.organizationId, organizationId), + isNull(tasks.deletedAt), + ), + ) + .limit(1); + + return task ?? null; +} + +function validateArgs(args: Record): { + deviceId: string; + taskId: string; +} | null { + const deviceId = args.deviceId as string; + const taskId = args.taskId as string; + if (!deviceId || !taskId) return null; + return { deviceId, taskId }; +} + +const ERROR_DEVICE_AND_TASK_REQUIRED = { + content: [ + { type: "text" as const, text: "Error: deviceId and taskId are required" }, + ], + isError: true, +}; + +const ERROR_TASK_NOT_FOUND = { + content: [{ type: "text" as const, text: "Error: Task not found" }], + isError: true, +}; + export function register(server: McpServer) { server.registerTool( "start_claude_session", { description: - "Start an autonomous Claude Code session for a task. Creates a new workspace and launches Claude in plan mode with the task context.", + "Start an autonomous Claude Code session for a task. Creates a new workspace with its own git branch and launches Claude with the task context.", inputSchema: { deviceId: z.string().describe("Target device ID"), taskId: z.string().describe("Task ID to work on"), @@ -78,67 +140,50 @@ export function register(server: McpServer) { }, async (args, extra) => { const ctx = getMcpContext(extra); - const deviceId = args.deviceId as string; - const taskId = args.taskId as string; - - if (!deviceId) { - return { - content: [{ type: "text", text: "Error: deviceId is required" }], - isError: true, - }; - } - - if (!taskId) { - return { - content: [{ type: "text", text: "Error: taskId is required" }], - isError: true, - }; - } - - // Fetch task data - const status = alias(taskStatuses, "status"); - const [task] = await db - .select({ - id: tasks.id, - slug: tasks.slug, - title: tasks.title, - description: tasks.description, - priority: tasks.priority, - statusName: status.name, - labels: tasks.labels, - }) - .from(tasks) - .leftJoin(status, eq(tasks.statusId, status.id)) - .where( - and( - eq(tasks.id, taskId), - eq(tasks.organizationId, ctx.organizationId), - isNull(tasks.deletedAt), - ), - ) - .limit(1); - - if (!task) { - return { - content: [{ type: "text", text: "Error: Task not found" }], - isError: true, - }; - } - - // Build the full claude command server-side - const prompt = buildPrompt(task); - const command = [ - "claude --dangerously-skip-permissions \"$(cat <<'SUPERSET_PROMPT'", - prompt, - "SUPERSET_PROMPT", - ')"', - ].join("\n"); + const validated = validateArgs(args); + if (!validated) return ERROR_DEVICE_AND_TASK_REQUIRED; + + const task = await fetchTask({ + taskId: validated.taskId, + organizationId: ctx.organizationId, + }); + if (!task) return ERROR_TASK_NOT_FOUND; return executeOnDevice({ ctx, - deviceId, + deviceId: validated.deviceId, tool: "start_claude_session", - params: { command, name: task.slug }, + params: { command: buildCommand(task), name: task.slug }, + }); + }, + ); + + server.registerTool( + "start_claude_subagent", + { + description: + "Start a Claude Code subagent for a task in an existing workspace. Adds a new terminal pane to the active workspace instead of creating a new one. Use this when you want to run Claude alongside your current work.", + inputSchema: { + deviceId: z.string().describe("Target device ID"), + taskId: z.string().describe("Task ID to work on"), + }, + }, + async (args, extra) => { + const ctx = getMcpContext(extra); + const validated = validateArgs(args); + if (!validated) return ERROR_DEVICE_AND_TASK_REQUIRED; + + const task = await fetchTask({ + taskId: validated.taskId, + organizationId: ctx.organizationId, + }); + if (!task) return ERROR_TASK_NOT_FOUND; + + return executeOnDevice({ + ctx, + deviceId: validated.deviceId, + tool: "start_claude_subagent", + params: { command: buildCommand(task) }, }); }, ); From 8691d2a9f914f07302b5ede4616b034cbbacea1b Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Sat, 31 Jan 2026 12:07:07 -0800 Subject: [PATCH 4/6] Revert .mcp.json localhost URLs --- .mcp.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.mcp.json b/.mcp.json index ba029a2f9f6..03b7976cc3f 100644 --- a/.mcp.json +++ b/.mcp.json @@ -2,11 +2,11 @@ "mcpServers": { "superset": { "type": "http", - "url": "http://localhost:3001/api/agent/mcp", + "url": "https://api.superset.sh/api/agent/mcp", "oauth": { "clientId": "claude-code", - "authorizationUrl": "http://localhost:3001/api/auth/mcp/authorize", - "tokenUrl": "http://localhost:3001/api/auth/mcp/token", + "authorizationUrl": "https://api.superset.sh/api/auth/mcp/authorize", + "tokenUrl": "https://api.superset.sh/api/auth/mcp/token", "scopes": ["openid", "profile", "email"] } } From 9556e1a4dbcad4fc78a5dd896c19e2b566a1356c Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Sat, 31 Jan 2026 12:09:46 -0800 Subject: [PATCH 5/6] Simplify buildPrompt to template literal, drop TaskData interface --- .../start-claude-session.ts | 86 +++++++------------ 1 file changed, 29 insertions(+), 57 deletions(-) diff --git a/packages/mcp/src/tools/devices/start-claude-session/start-claude-session.ts b/packages/mcp/src/tools/devices/start-claude-session/start-claude-session.ts index ec5c0b035ca..a44d812c9f4 100644 --- a/packages/mcp/src/tools/devices/start-claude-session/start-claude-session.ts +++ b/packages/mcp/src/tools/devices/start-claude-session/start-claude-session.ts @@ -6,65 +6,37 @@ import { alias } from "drizzle-orm/pg-core"; import { z } from "zod"; import { executeOnDevice, getMcpContext } from "../../utils"; -interface TaskData { - id: string; - title: string; - slug: string; - description: string | null; - priority: string; - statusName: string | null; - labels: string[] | null; -} +function buildCommand(task: NonNullable>>): string { + const metadata = [ + `Priority: ${task.priority}`, + task.statusName && `Status: ${task.statusName}`, + task.labels?.length && `Labels: ${task.labels.join(", ")}`, + ] + .filter(Boolean) + .join("\n"); -function buildPrompt(task: TaskData): string { - const lines: string[] = []; - - lines.push(`You are working on task "${task.title}" (${task.slug}).`); - lines.push(""); - - lines.push(`Priority: ${task.priority}`); - if (task.statusName) { - lines.push(`Status: ${task.statusName}`); - } - if (task.labels && task.labels.length > 0) { - lines.push(`Labels: ${task.labels.join(", ")}`); - } - lines.push(""); - - lines.push("## Task Description"); - lines.push(""); - lines.push(task.description || "No description provided."); - lines.push(""); - - lines.push("## Instructions"); - lines.push(""); - lines.push( - "You are running fully autonomously. Do not ask questions or wait for user feedback — make all decisions independently based on the codebase and task description.", - ); - lines.push(""); - lines.push( - "1. Explore the codebase to understand the relevant code and architecture", - ); - lines.push("2. Create a detailed execution plan for this task including:"); - lines.push(" - Purpose and scope of the changes"); - lines.push(" - Key assumptions"); - lines.push( - " - Concrete implementation steps with specific files to modify", - ); - lines.push(" - How to validate the changes work correctly"); - lines.push("3. Implement the plan"); - lines.push( - "4. Verify your changes work correctly (run relevant tests, typecheck, lint)", - ); - lines.push( - `5. When done, use the Superset MCP \`update_task\` tool to update task "${task.id}" with a summary of what was done`, - ); + const prompt = `You are working on task "${task.title}" (${task.slug}). - return lines.join("\n"); -} +${metadata} + +## Task Description + +${task.description || "No description provided."} + +## Instructions + +You are running fully autonomously. Do not ask questions or wait for user feedback — make all decisions independently based on the codebase and task description. + +1. Explore the codebase to understand the relevant code and architecture +2. Create a detailed execution plan for this task including: + - Purpose and scope of the changes + - Key assumptions + - Concrete implementation steps with specific files to modify + - How to validate the changes work correctly +3. Implement the plan +4. Verify your changes work correctly (run relevant tests, typecheck, lint) +5. When done, use the Superset MCP \`update_task\` tool to update task "${task.id}" with a summary of what was done`; -function buildCommand(task: TaskData): string { - const prompt = buildPrompt(task); return [ "claude --dangerously-skip-permissions \"$(cat <<'SUPERSET_PROMPT'", prompt, @@ -79,7 +51,7 @@ async function fetchTask({ }: { taskId: string; organizationId: string; -}): Promise { +}) { const status = alias(taskStatuses, "status"); const [task] = await db .select({ From a17cbcd715e10a3e93b90ec9cdd0b8f5635e8be1 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Sat, 31 Jan 2026 12:11:59 -0800 Subject: [PATCH 6/6] Fix lint --- .../devices/start-claude-session/start-claude-session.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/mcp/src/tools/devices/start-claude-session/start-claude-session.ts b/packages/mcp/src/tools/devices/start-claude-session/start-claude-session.ts index a44d812c9f4..02a145cdbf3 100644 --- a/packages/mcp/src/tools/devices/start-claude-session/start-claude-session.ts +++ b/packages/mcp/src/tools/devices/start-claude-session/start-claude-session.ts @@ -6,7 +6,9 @@ import { alias } from "drizzle-orm/pg-core"; import { z } from "zod"; import { executeOnDevice, getMcpContext } from "../../utils"; -function buildCommand(task: NonNullable>>): string { +function buildCommand( + task: NonNullable>>, +): string { const metadata = [ `Priority: ${task.priority}`, task.statusName && `Status: ${task.statusName}`,