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
66 changes: 66 additions & 0 deletions packages/mcp-v2/src/tools/agents/run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { createMcpCaller } from "../../caller";
import { defineTool } from "../../define-tool";
import { hostServiceMutation } from "../../host-service-client";

export function register(server: McpServer): void {
defineTool(server, {
name: "agents_run",
description:
"Launch an agent inside an existing workspace. Resolves the host that owns the workspace, then runs the named agent preset (or HostAgentConfig instance) with the given prompt in a fresh terminal session. Use this to start a second agent in a workspace that already exists; for create-and-spawn in a single call, pass `agents` to workspaces_create instead.",
inputSchema: {
workspaceId: z
.string()
.uuid()
.describe("Workspace UUID to run the agent in."),
agent: z
.string()
.min(1)
.describe(
"Agent preset id (e.g. `claude`, `codex`) or HostAgentConfig instance UUID.",
),
prompt: z.string().min(1).describe("Prompt sent to the agent."),
attachmentIds: z
.array(z.string().uuid())
.optional()
.describe(
"Host-scoped attachment UUIDs. The host resolves these to absolute paths and appends them to the prompt.",
),
},
handler: async (input, ctx) => {
const caller = createMcpCaller(ctx);
const workspace = await caller.v2Workspace.getFromHost({
organizationId: ctx.organizationId,
id: input.workspaceId,
});
if (!workspace) {
throw new Error(`Workspace not found: ${input.workspaceId}`);
}

return hostServiceMutation<
{
workspaceId: string;
agent: string;
prompt: string;
attachmentIds?: string[];
},
{ sessionId: string; label: string }
>(
{
relayUrl: ctx.relayUrl,
organizationId: ctx.organizationId,
hostId: workspace.hostId,
jwt: ctx.bearerToken,
},
"agents.run",
{
workspaceId: input.workspaceId,
agent: input.agent,
prompt: input.prompt,
attachmentIds: input.attachmentIds,
},
);
Comment on lines +41 to +63
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Missing failure discriminant in response type

The agents.run host RPC can return { ok: false; error: string } on business-logic failures (e.g. unknown agent preset), as evidenced by the discriminated union workspaces_create now uses for the same per-agent results. Typing the return here as only { sessionId: string; label: string } means a { ok: false, error: "..." } payload from the host passes through silently: the MCP client receives a wrong shape with sessionId/label as undefined and never sees the error message.

The response generic should mirror the union, and the ok: false branch should surface the error:

return hostServiceMutation<
    { workspaceId: string; agent: string; prompt: string; attachmentIds?: string[] },
    | { ok: true; sessionId: string; label: string }
    | { ok: false; error: string }
>(
    { relayUrl: ctx.relayUrl, organizationId: ctx.organizationId, hostId: workspace.hostId, jwt: ctx.bearerToken },
    "agents.run",
    { workspaceId: input.workspaceId, agent: input.agent, prompt: input.prompt, attachmentIds: input.attachmentIds },
).then((result) => {
    if (!result.ok) throw new Error(result.error);
    return result;
});
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/mcp-v2/src/tools/agents/run.ts
Line: 41-63

Comment:
**Missing failure discriminant in response type**

The `agents.run` host RPC can return `{ ok: false; error: string }` on business-logic failures (e.g. unknown agent preset), as evidenced by the discriminated union `workspaces_create` now uses for the same per-agent results. Typing the return here as only `{ sessionId: string; label: string }` means a `{ ok: false, error: "..." }` payload from the host passes through silently: the MCP client receives a wrong shape with `sessionId`/`label` as `undefined` and never sees the error message.

The response generic should mirror the union, and the `ok: false` branch should surface the error:

```typescript
return hostServiceMutation<
    { workspaceId: string; agent: string; prompt: string; attachmentIds?: string[] },
    | { ok: true; sessionId: string; label: string }
    | { ok: false; error: string }
>(
    { relayUrl: ctx.relayUrl, organizationId: ctx.organizationId, hostId: workspace.hostId, jwt: ctx.bearerToken },
    "agents.run",
    { workspaceId: input.workspaceId, agent: input.agent, prompt: input.prompt, attachmentIds: input.attachmentIds },
).then((result) => {
    if (!result.ok) throw new Error(result.error);
    return result;
});
```

How can I resolve this? If you propose a fix, please make it concise.

},
});
}
2 changes: 2 additions & 0 deletions packages/mcp-v2/src/tools/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
setServerToolCallEmitter,
} from "../define-tool";

import * as agentsRun from "./agents/run";
import * as automationsCreate from "./automations/create";
import * as automationsDelete from "./automations/delete";
import * as automationsGet from "./automations/get";
Expand Down Expand Up @@ -46,6 +47,7 @@ const REGISTRARS = [
workspacesList,
workspacesCreate,
workspacesDelete,
agentsRun,
projectsList,
hostsList,
];
Expand Down
35 changes: 33 additions & 2 deletions packages/mcp-v2/src/tools/workspaces/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,27 @@ import { z } from "zod";
import { defineTool } from "../../define-tool";
import { hostServiceMutation } from "../../host-service-client";

const agentLaunchSchema = z.object({
agent: z
.string()
.min(1)
.describe(
"Agent preset id (e.g. `claude`, `codex`) or HostAgentConfig instance UUID.",
),
prompt: z.string().min(1).describe("Initial prompt the agent starts with."),
attachmentIds: z
.array(z.string().uuid())
.optional()
.describe(
"Host-scoped attachment UUIDs. The host resolves these to absolute paths and appends them to the prompt.",
),
});

export function register(server: McpServer): void {
defineTool(server, {
name: "workspaces_create",
description:
"Create a workspace on a host. A workspace is a branch-scoped working copy of a project. The host service materializes the git worktree on disk before returning. Provide exactly one of `branch` or `pr`. Use projects_list and hosts_list first to get the projectId and hostId.",
"Create a workspace on a host. A workspace is a branch-scoped working copy of a project. The host service materializes the git worktree on disk before returning. Provide exactly one of `branch` or `pr`. Optionally pass `agents` to spawn one or more agents in the workspace as soon as it is ready (each entry runs the equivalent of `agents_run` against the new workspace). Use projects_list and hosts_list first to get the projectId and hostId.",
inputSchema: {
projectId: z.string().uuid().describe("Project UUID."),
name: z.string().min(1).describe("Workspace name (display)."),
Expand Down Expand Up @@ -41,6 +57,12 @@ export function register(server: McpServer): void {
.uuid()
.optional()
.describe("Optional Superset task id to link to the new workspace."),
agents: z
.array(agentLaunchSchema)
.optional()
.describe(
"Agents to spawn in the workspace immediately after creation.",
),
},
handler: async (input, ctx) => {
return hostServiceMutation<
Expand All @@ -51,6 +73,11 @@ export function register(server: McpServer): void {
pr?: number;
baseBranch?: string;
taskId?: string;
agents?: Array<{
agent: string;
prompt: string;
attachmentIds?: string[];
}>;
},
{
workspace: {
Expand All @@ -60,7 +87,10 @@ export function register(server: McpServer): void {
branch: string;
};
terminals: Array<{ terminalId: string; label?: string }>;
agents: Array<unknown>;
agents: Array<
| { ok: true; sessionId: string; label: string }
| { ok: false; error: string }
>;
alreadyExists: boolean;
}
>(
Expand All @@ -78,6 +108,7 @@ export function register(server: McpServer): void {
pr: input.pr,
baseBranch: input.baseBranch,
taskId: input.taskId,
agents: input.agents,
},
);
},
Expand Down
Loading