Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ 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 { startClaudeSubagent } from "./start-claude-subagent";
import { switchWorkspace } from "./switch-workspace";
import type { CommandResult, ToolContext, ToolDefinition } from "./types";

Expand All @@ -16,6 +18,8 @@ const tools: ToolDefinition<any>[] = [
listProjects,
listWorkspaces,
navigateToWorkspace,
startClaudeSession,
startClaudeSubagent,
switchWorkspace,
];

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { useWorkspaceInitStore } from "renderer/stores/workspace-init";
import { z } from "zod";
import type { CommandResult, ToolContext, ToolDefinition } from "./types";

const schema = z.object({
command: z.string(),
name: z.string(),
});

async function execute(
params: z.infer<typeof schema>,
ctx: ToolContext,
): Promise<CommandResult> {
// 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" };
}

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 {
// 2. Create workspace
const result = await ctx.createWorktree.mutateAsync({
projectId,
name: params.name,
branchName: params.name,
});

// 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 ?? []), params.command],
defaultPreset: pending?.defaultPreset ?? null,
});

// 4. 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<typeof schema> = {
name: "start_claude_session",
schema,
execute,
};
Original file line number Diff line number Diff line change
@@ -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<typeof schema>,
ctx: ToolContext,
): Promise<CommandResult> {
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<typeof schema> = {
name: "start_claude_subagent",
schema,
execute,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { register } from "./start-claude-session";
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
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 buildCommand(
task: NonNullable<Awaited<ReturnType<typeof fetchTask>>>,
): string {
const metadata = [
`Priority: ${task.priority}`,
task.statusName && `Status: ${task.statusName}`,
task.labels?.length && `Labels: ${task.labels.join(", ")}`,
]
.filter(Boolean)
.join("\n");

const prompt = `You are working on task "${task.title}" (${task.slug}).

${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`;

return [
"claude --dangerously-skip-permissions \"$(cat <<'SUPERSET_PROMPT'",
prompt,
"SUPERSET_PROMPT",
')"',
].join("\n");
}

async function fetchTask({
taskId,
organizationId,
}: {
taskId: string;
organizationId: string;
}) {
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<string, unknown>): {
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 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"),
},
},
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_session",
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) },
});
},
);
}
2 changes: 2 additions & 0 deletions packages/mcp/src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -31,6 +32,7 @@ const allTools = [
createWorkspace,
switchWorkspace,
deleteWorkspace,
startClaudeSession,
];

export function registerTools(server: McpServer) {
Expand Down