Files
paseo/packages/server/src/server/agent/mcp-server.ts
2026-05-13 20:05:52 +08:00

2204 lines
67 KiB
TypeScript

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { ensureValidJson } from "../json-utils.js";
import type { Logger } from "pino";
import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
import type { ServerNotification, ServerRequest } from "@modelcontextprotocol/sdk/types.js";
import type { AgentProvider } from "./agent-sdk-types.js";
import type { AgentManager, WaitForAgentResult } from "./agent-manager.js";
import {
AgentPermissionRequestPayloadSchema,
AgentListItemPayloadSchema,
AgentPermissionResponseSchema,
AgentSnapshotPayloadSchema,
} from "../messages.js";
import type { AgentListItemPayload } from "../messages.js";
import {
buildStoredAgentPayload,
toAgentListItemPayload,
toAgentPayload,
} from "./agent-projections.js";
import { curateAgentActivity } from "./activity-curator.js";
import { selectItemsByProjectedLimit } from "./timeline-projection.js";
import type { AgentStorage } from "./agent-storage.js";
import { ensureAgentLoaded } from "./agent-loading.js";
import { isStoredAgentProviderAvailable } from "../persistence-hooks.js";
import {
appendTimelineItemIfAgentKnown,
emitLiveTimelineItemIfAgentKnown,
} from "./timeline-append.js";
import { getPaseoWorktreesRoot } from "../../utils/worktree.js";
import {
archivePaseoWorktree,
killTerminalsUnderPath,
type ArchivePaseoWorktreeDependencies,
} from "../paseo-worktree-archive-service.js";
import { WaitForAgentTracker } from "./wait-for-agent-tracker.js";
import { scheduleAgentMetadataGeneration } from "./agent-metadata-generator.js";
import type { VoiceCallerContext, VoiceSpeakHandler } from "../voice-types.js";
import { expandUserPath, isSameOrDescendantPath, resolvePathFromBase } from "../path-utils.js";
import { PARENT_AGENT_ID_LABEL } from "../../shared/agent-labels.js";
import type { TerminalManager } from "../../terminal/terminal-manager.js";
import type {
AgentWorktreeSetupContinuation,
CreatePaseoWorktreeSetupContinuationInput,
CreatePaseoWorktreeWorkflowFn,
CreatePaseoWorktreeWorkflowResult,
} from "../worktree-session.js";
import type { ScheduleService } from "../schedule/service.js";
import { ScheduleSummarySchema, StoredScheduleSchema } from "../schedule/types.js";
import type { ProviderDefinition } from "./provider-registry.js";
import { getAgentProviderDefinition } from "./provider-manifest.js";
import { resolveAndValidateCreateAgentMode } from "./create-agent-mode.js";
import { resolveSnapshotCwd } from "./provider-snapshot-manager.js";
import {
AgentModelSchema,
AgentProviderEnum,
AgentStatusEnum,
ProviderSummarySchema,
parseDurationString,
resolveRequiredProviderModel,
sanitizePermissionRequest,
serializeSnapshotWithMetadata,
toScheduleSummary,
waitForAgentWithTimeout,
} from "./mcp-shared.js";
import { sendPromptToAgent, setupFinishNotification, startAgentRun } from "./agent-prompt.js";
import type { GitHubService } from "../../services/github-service.js";
import type { WorkspaceGitService } from "../workspace-git-service.js";
import type { CreatePaseoWorktreeInput } from "../paseo-worktree-service.js";
import { toWorktreeRequestError } from "../worktree-errors.js";
import { join } from "node:path";
export interface AgentMcpServerOptions {
agentManager: AgentManager;
agentStorage: AgentStorage;
terminalManager?: TerminalManager | null;
getDaemonTcpPort?: () => number | null;
scheduleService?: ScheduleService | null;
providerRegistry?: Record<AgentProvider, ProviderDefinition> | null;
github?: GitHubService;
workspaceGitService?: Pick<
WorkspaceGitService,
"getSnapshot" | "listWorktrees" | "resolveRepoRoot"
>;
archiveWorkspaceRecord?: ArchivePaseoWorktreeDependencies["archiveWorkspaceRecord"];
emitWorkspaceUpdatesForWorkspaceIds?: ArchivePaseoWorktreeDependencies["emitWorkspaceUpdatesForWorkspaceIds"];
markWorkspaceArchiving?: ArchivePaseoWorktreeDependencies["markWorkspaceArchiving"];
clearWorkspaceArchiving?: ArchivePaseoWorktreeDependencies["clearWorkspaceArchiving"];
emitSessionMessage?: ArchivePaseoWorktreeDependencies["emit"];
createPaseoWorktree?: CreatePaseoWorktreeWorkflowFn;
paseoHome?: string;
/**
* ID of the agent that is connecting to this MCP server.
* Used for cwd/mode inheritance when agents spawn child agents.
*/
callerAgentId?: string;
/**
* Optional resolver for session-bound speak handlers.
* Used by hidden voice agents to narrate through daemon-managed TTS.
*/
resolveSpeakHandler?: (callerAgentId: string) => VoiceSpeakHandler | null;
resolveCallerContext?: (callerAgentId: string) => VoiceCallerContext | null;
enableVoiceTools?: boolean;
voiceOnly?: boolean;
logger: Logger;
}
const CLAUDE_TO_CODEX_MODE: Record<string, string> = {
plan: "read-only",
default: "auto",
acceptEdits: "auto",
bypassPermissions: "full-access",
};
const CODEX_TO_CLAUDE_MODE: Record<string, string> = {
"read-only": "plan",
auto: "default",
"full-access": "bypassPermissions",
};
function mapModeAcrossProviders(
sourceMode: string,
sourceProvider: AgentProvider,
targetProvider: AgentProvider,
): string {
if (sourceProvider === targetProvider) {
return sourceMode;
}
if (sourceProvider === "claude" && targetProvider === "codex") {
const mapped = CLAUDE_TO_CODEX_MODE[sourceMode];
if (mapped) {
return mapped;
}
return "auto";
}
if (sourceProvider === "codex" && targetProvider === "claude") {
const mapped = CODEX_TO_CLAUDE_MODE[sourceMode];
if (mapped) {
return mapped;
}
return "default";
}
return sourceMode;
}
type McpToolContext = RequestHandlerExtra<ServerRequest, ServerNotification>;
function parseTimestamp(value: string | null | undefined): number {
if (!value) {
return 0;
}
const parsed = Date.parse(value);
return Number.isNaN(parsed) ? 0 : parsed;
}
function resolveAgentListActivityTime(agent: AgentListItemPayload): number {
return Math.max(
parseTimestamp(agent.updatedAt),
parseTimestamp(agent.lastUserMessageAt),
parseTimestamp(agent.attentionTimestamp),
parseTimestamp(agent.archivedAt),
parseTimestamp(agent.createdAt),
);
}
function resolveRegisteredProviderIds(
agentManager: AgentManager,
providerRegistry: Record<AgentProvider, ProviderDefinition> | null | undefined,
): AgentProvider[] {
return providerRegistry ? Object.keys(providerRegistry) : agentManager.getRegisteredProviderIds();
}
interface ProviderSummary {
id: AgentProvider;
label: string;
description: string;
enabled: boolean;
modes: ProviderDefinition["modes"];
status: string;
error?: string;
}
async function resolveProviderSummary(
provider: ProviderDefinition,
logger: Logger,
): Promise<ProviderSummary> {
const base = {
id: provider.id,
label: provider.label,
description: provider.description,
modes: provider.modes,
};
if (!provider.enabled) {
return { ...base, enabled: false, status: "unavailable" };
}
try {
const available = await provider.createClient(logger).isAvailable();
return { ...base, enabled: true, status: available ? "available" : "unavailable" };
} catch (availabilityError) {
return {
...base,
enabled: true,
status: "unavailable",
error:
availabilityError instanceof Error ? availabilityError.message : String(availabilityError),
};
}
}
function compareAgentListItems(a: AgentListItemPayload, b: AgentListItemPayload): number {
const attentionDelta =
Number(b.requiresAttention ?? false) - Number(a.requiresAttention ?? false);
if (attentionDelta !== 0) {
return attentionDelta;
}
const statusOrder = {
running: 0,
initializing: 1,
idle: 2,
error: 3,
closed: 4,
} as Record<string, number>;
const statusDelta = (statusOrder[a.status] ?? 999) - (statusOrder[b.status] ?? 999);
if (statusDelta !== 0) {
return statusDelta;
}
return resolveAgentListActivityTime(b) - resolveAgentListActivityTime(a);
}
function resolveScheduleProviderAndModel(params: {
provider?: string;
defaultProvider: AgentProvider;
}): { provider: AgentProvider; model?: string } {
const providerInput = params.provider?.trim() || params.defaultProvider;
const slashIndex = providerInput.indexOf("/");
if (slashIndex === -1) {
return { provider: providerInput };
}
const provider = providerInput.slice(0, slashIndex).trim();
const model = providerInput.slice(slashIndex + 1).trim();
if (!provider || !model) {
throw new Error("provider must be <provider> or <provider>/<model>");
}
return {
provider: provider,
model,
};
}
function resolveChildAgentCwd(params: {
parentCwd: string;
requestedCwd?: string;
lockedCwd?: string;
allowCustomCwd: boolean;
}): string {
const lockedCwd = params.lockedCwd?.trim();
if (lockedCwd) {
return expandUserPath(lockedCwd);
}
const requestedCwd = params.requestedCwd?.trim();
if (!requestedCwd || !params.allowCustomCwd) {
return params.parentCwd;
}
return resolvePathFromBase(params.parentCwd, requestedCwd);
}
const TerminalSummarySchema = z.object({
id: z.string(),
name: z.string(),
cwd: z.string(),
});
const WorktreeSummarySchema = z.object({
path: z.string(),
createdAt: z.string(),
branchName: z.string().optional(),
head: z.string().optional(),
});
function resolveTerminalKeyToken(key: string, literal: boolean): string {
if (literal) {
return key;
}
switch (key) {
case "Enter":
return "\r";
case "Tab":
return "\t";
case "Escape":
return "\u001b";
case "Space":
return " ";
case "BSpace":
return "\u007f";
case "C-c":
return "\u0003";
case "C-d":
return "\u0004";
case "C-z":
return "\u001a";
case "C-l":
return "\u000c";
case "C-a":
return "\u0001";
case "C-e":
return "\u0005";
default:
return key;
}
}
export async function createAgentMcpServer(options: AgentMcpServerOptions): Promise<McpServer> {
const {
agentManager,
agentStorage,
terminalManager,
scheduleService,
providerRegistry,
callerAgentId,
resolveSpeakHandler,
resolveCallerContext,
logger,
} = options;
const childLogger = logger.child({ module: "agent", component: "mcp-server" });
const waitTracker = new WaitForAgentTracker(logger);
const callerContext = callerAgentId ? (resolveCallerContext?.(callerAgentId) ?? null) : null;
const server = new McpServer({
name: "agent-mcp",
version: "2.0.0",
});
const resolveCallerAgent = () => {
if (!callerAgentId) {
return null;
}
const parentAgent = agentManager.getAgent(callerAgentId);
if (!parentAgent) {
throw new Error(`Parent agent ${callerAgentId} not found`);
}
return parentAgent;
};
const resolveScopedCwd = (requestedCwd?: string, opts?: { required?: boolean }): string => {
const callerAgent = resolveCallerAgent();
if (callerAgent) {
return resolveChildAgentCwd({
parentCwd: callerAgent.cwd,
requestedCwd,
lockedCwd: callerContext?.lockedCwd,
allowCustomCwd: callerContext?.allowCustomCwd ?? true,
});
}
const trimmedCwd = requestedCwd?.trim();
if (!trimmedCwd) {
if (opts?.required) {
throw new Error("cwd is required");
}
throw new Error("cwd is required when no caller agent is available");
}
return expandUserPath(trimmedCwd);
};
const buildCallerAgentScheduleConfigExtras = (
callerAgent: NonNullable<ReturnType<typeof resolveCallerAgent>>,
): Record<string, unknown> => {
return {
...(callerAgent.config.thinkingOptionId
? { thinkingOptionId: callerAgent.config.thinkingOptionId }
: {}),
...(callerAgent.config.approvalPolicy
? { approvalPolicy: callerAgent.config.approvalPolicy }
: {}),
...(callerAgent.config.sandboxMode ? { sandboxMode: callerAgent.config.sandboxMode } : {}),
...(typeof callerAgent.config.networkAccess === "boolean"
? { networkAccess: callerAgent.config.networkAccess }
: {}),
...(typeof callerAgent.config.webSearch === "boolean"
? { webSearch: callerAgent.config.webSearch }
: {}),
...(callerAgent.config.title ? { title: callerAgent.config.title } : {}),
...(callerAgent.config.extra ? { extra: callerAgent.config.extra } : {}),
...(callerAgent.config.systemPrompt ? { systemPrompt: callerAgent.config.systemPrompt } : {}),
...(callerAgent.config.mcpServers ? { mcpServers: callerAgent.config.mcpServers } : {}),
};
};
const buildCallerAgentScheduleConfig = (
callerAgent: NonNullable<ReturnType<typeof resolveCallerAgent>>,
params?: { provider?: string; cwd?: string },
) => {
const hasProviderOverride = params?.provider !== undefined;
const resolvedProviderModel = hasProviderOverride
? resolveScheduleProviderAndModel({
provider: params?.provider,
defaultProvider: callerAgent.provider,
})
: null;
const resolvedProvider = resolvedProviderModel?.provider ?? callerAgent.provider;
let resolvedModel: string | undefined;
if (resolvedProviderModel?.model) {
resolvedModel = resolvedProviderModel.model;
} else if (!hasProviderOverride && callerAgent.config.model) {
resolvedModel = callerAgent.config.model;
}
return {
provider: resolvedProvider,
cwd: params?.cwd?.trim() ? expandUserPath(params.cwd) : callerAgent.cwd,
...(callerAgent.currentModeId
? {
modeId: mapModeAcrossProviders(
callerAgent.currentModeId,
callerAgent.provider,
resolvedProvider,
),
}
: {}),
...(resolvedModel ? { model: resolvedModel } : {}),
...buildCallerAgentScheduleConfigExtras(callerAgent),
};
};
const resolveNewAgentScheduleTarget = (params?: { provider?: string; cwd?: string }) => {
if (!params?.provider?.trim()) {
throw new Error("provider is required when target is new-agent");
}
const callerAgent = resolveCallerAgent();
if (callerAgent) {
return {
type: "new-agent" as const,
config: buildCallerAgentScheduleConfig(callerAgent, params),
};
}
const resolvedProviderModel = resolveScheduleProviderAndModel({
provider: params?.provider,
defaultProvider: params.provider,
});
return {
type: "new-agent" as const,
config: {
provider: resolvedProviderModel.provider,
cwd: params?.cwd?.trim() ? expandUserPath(params.cwd) : process.cwd(),
...(resolvedProviderModel.model ? { model: resolvedProviderModel.model } : {}),
},
};
};
const ProviderModelInputSchema = AgentProviderEnum.trim()
.refine((value) => value.includes("/"), {
message: "provider must be provider/model, for example codex/gpt-5.4",
})
.refine(
(value) => {
try {
resolveRequiredProviderModel(value);
return true;
} catch {
return false;
}
},
{ message: "provider must be provider/model, for example codex/gpt-5.4" },
);
const agentToAgentInputSchema = {
cwd: z
.string()
.optional()
.describe("Optional working directory. Defaults to the caller agent working directory."),
title: z
.string()
.trim()
.min(1, "Title is required")
.max(60, "Title must be 60 characters or fewer")
.describe("Short descriptive title (<= 60 chars) summarizing the agent's focus."),
provider: ProviderModelInputSchema.describe(
"Required provider/model pair, for example codex/gpt-5.4.",
),
thinking: z.string().optional().describe("Thinking option ID"),
features: z
.record(z.unknown())
.optional()
.describe("Provider-specific feature values, for example { fast_mode: true } for Codex."),
labels: z.record(z.string(), z.string()).optional().describe("Labels to set on the agent"),
initialPrompt: z
.string()
.trim()
.min(1, "initialPrompt is required")
.describe("Required first task to run immediately after creation."),
mode: z
.string()
.optional()
.describe(
"Optional session mode for the new agent. Required when the new agent uses a different provider than the caller agent.",
),
background: z
.boolean()
.optional()
.default(false)
.describe(
"Run agent in background. If false (default), waits for completion or permission request. If true, returns immediately.",
),
notifyOnFinish: z
.boolean()
.optional()
.default(false)
.describe(
"Send a notification prompt to the caller agent when this agent finishes, errors, or needs permission. Requires a caller agent context.",
),
};
const topLevelInputSchema = {
cwd: z
.string()
.describe("Required working directory for the agent (absolute, relative, or ~)."),
title: z
.string()
.trim()
.min(1, "Title is required")
.max(60, "Title must be 60 characters or fewer")
.describe("Short descriptive title (<= 60 chars) summarizing the agent's focus."),
provider: ProviderModelInputSchema.describe(
"Required provider/model pair, for example codex/gpt-5.4.",
),
thinking: z.string().optional().describe("Thinking option ID"),
features: z
.record(z.unknown())
.optional()
.describe("Provider-specific feature values, for example { fast_mode: true } for Codex."),
labels: z.record(z.string(), z.string()).optional().describe("Labels to set on the agent"),
initialPrompt: z
.string()
.trim()
.min(1, "initialPrompt is required")
.describe("Required first task to run immediately after creation."),
mode: z
.string()
.optional()
.describe("Optional session mode to configure before the first run."),
worktreeName: z
.string()
.optional()
.describe("Optional git worktree branch name (lowercase alphanumerics + hyphen)."),
baseBranch: z
.string()
.optional()
.describe("Required when worktreeName is set: the base branch to diff/merge against."),
refName: z.string().min(1).optional().describe("Optional source ref for worktree creation."),
action: z
.enum(["branch-off", "checkout"])
.optional()
.describe("Optional worktree creation action."),
githubPrNumber: z
.number()
.int()
.positive()
.optional()
.describe("Optional GitHub pull request number to checkout."),
background: z
.boolean()
.optional()
.default(false)
.describe(
"Run agent in background. If false (default), waits for completion or permission request. If true, returns immediately.",
),
notifyOnFinish: z
.boolean()
.optional()
.default(false)
.describe(
"Send a notification prompt to the caller agent when this agent finishes, errors, or needs permission. Requires a caller agent context.",
),
};
const createAgentInputSchema = callerAgentId ? agentToAgentInputSchema : topLevelInputSchema;
const agentToAgentCreateAgentArgsSchema = z.object(agentToAgentInputSchema).strict();
const topLevelCreateAgentArgsSchema = z.object(topLevelInputSchema).strict();
if (options.voiceOnly || options.enableVoiceTools || callerContext?.enableVoiceTools) {
server.registerTool(
"speak",
{
title: "Speak",
description:
"Speak text to the user via daemon-managed voice output. Blocks until playback completes.",
inputSchema: {
text: z
.string()
.trim()
.min(1, "text is required")
.max(4000, "text must be 4000 characters or fewer"),
},
outputSchema: {
ok: z.boolean(),
},
},
async (args, context?: McpToolContext) => {
if (!callerAgentId) {
throw new Error("speak is only available to agent-scoped MCP sessions");
}
const handler = resolveSpeakHandler?.(callerAgentId) ?? null;
if (!handler) {
throw new Error(`No speak handler registered for caller agent '${callerAgentId}'`);
}
await handler({
text: args.text,
callerAgentId,
signal: context?.signal,
});
return {
content: [],
structuredContent: ensureValidJson({ ok: true }),
};
},
);
}
if (options.voiceOnly) {
return server;
}
interface ResolvedCreateAgentArgs {
provider: AgentProvider;
initialPrompt: string;
background: boolean;
normalizedTitle: string | null;
model: string | undefined;
thinking: string | undefined;
features: Record<string, unknown> | undefined;
labels: Record<string, string> | undefined;
notifyOnFinish: boolean;
resolvedCwd: string;
resolvedMode: string | undefined;
setupContinuation: AgentWorktreeSetupContinuation | undefined;
}
const getProviderModes = (provider: AgentProvider) => {
const fromRegistry = providerRegistry?.[provider];
if (fromRegistry) {
return fromRegistry.modes;
}
try {
return getAgentProviderDefinition(provider).modes;
} catch {
return undefined;
}
};
const getAvailableModeIds = (provider: AgentProvider): string[] | undefined => {
return getProviderModes(provider)?.map((mode) => mode.id);
};
const getUnattendedModeId = (provider: AgentProvider): string | undefined => {
return getProviderModes(provider)?.find((mode) => mode.isUnattended)?.id;
};
const isParentInUnattendedMode = (provider: AgentProvider, modeId: string | null): boolean => {
if (modeId === null) return false;
const modes = getProviderModes(provider);
if (!modes) return false;
return modes.some((mode) => mode.id === modeId && mode.isUnattended === true);
};
const resolveCallerCreateAgentArgs = (
args: unknown,
parentAgentId: string,
): ResolvedCreateAgentArgs => {
const callerArgs = agentToAgentCreateAgentArgsSchema.parse(args);
const resolvedProviderModel = resolveRequiredProviderModel(callerArgs.provider);
const parentAgent = agentManager.getAgent(parentAgentId);
if (!parentAgent) {
throw new Error(`Parent agent ${parentAgentId} not found`);
}
const provider = resolvedProviderModel.provider;
const resolvedCwd = resolveChildAgentCwd({
parentCwd: parentAgent.cwd,
requestedCwd: callerArgs.cwd,
lockedCwd: callerContext?.lockedCwd,
allowCustomCwd: callerContext?.allowCustomCwd ?? true,
});
const resolvedMode = resolveAndValidateCreateAgentMode({
requestedMode: callerArgs.mode,
targetProvider: provider,
parent: {
provider: parentAgent.provider,
modeId: parentAgent.currentModeId,
isUnattended: isParentInUnattendedMode(parentAgent.provider, parentAgent.currentModeId),
},
availableModes: getAvailableModeIds(provider),
targetUnattendedMode: getUnattendedModeId(provider),
});
return {
provider,
initialPrompt: callerArgs.initialPrompt,
background: callerArgs.background ?? false,
normalizedTitle: callerArgs.title.trim(),
model: resolvedProviderModel.model,
thinking: callerArgs.thinking,
features: callerArgs.features,
labels: callerArgs.labels,
notifyOnFinish: callerArgs.notifyOnFinish ?? false,
resolvedCwd,
resolvedMode,
setupContinuation: undefined,
};
};
const resolveTopLevelCreateAgentArgs = async (
args: unknown,
): Promise<ResolvedCreateAgentArgs> => {
const topLevelArgs = topLevelCreateAgentArgsSchema.parse(args);
const resolvedProviderModel = resolveRequiredProviderModel(topLevelArgs.provider);
const { cwd, mode, worktreeName, baseBranch, refName, action, githubPrNumber } = topLevelArgs;
const resolvedMode = resolveAndValidateCreateAgentMode({
requestedMode: mode,
targetProvider: resolvedProviderModel.provider,
parent: null,
availableModes: getAvailableModeIds(resolvedProviderModel.provider),
targetUnattendedMode: getUnattendedModeId(resolvedProviderModel.provider),
});
let resolvedCwd = expandUserPath(cwd);
let setupContinuation: AgentWorktreeSetupContinuation | undefined;
const shouldCreateWorktree = Boolean(worktreeName || refName || action || githubPrNumber);
if (shouldCreateWorktree) {
if (worktreeName && !baseBranch && !refName && !action && githubPrNumber === undefined) {
throw new Error("baseBranch is required when creating a worktree");
}
const createdWorktree = await createMcpWorktree({
input: {
cwd: resolvedCwd,
worktreeSlug: worktreeName,
refName,
action,
githubPrNumber,
...(topLevelArgs.initialPrompt
? { firstAgentContext: { prompt: topLevelArgs.initialPrompt } }
: {}),
runSetup: false,
paseoHome: options.paseoHome,
},
createPaseoWorktree: options.createPaseoWorktree,
resolveDefaultBranch: baseBranch ? async () => baseBranch : undefined,
setupContinuation: {
kind: "agent",
terminalManager: terminalManager ?? null,
appendTimelineItem: ({ agentId, item }) =>
appendTimelineItemIfAgentKnown({
agentManager,
agentId,
item,
}),
emitLiveTimelineItem: ({ agentId, item }) =>
emitLiveTimelineItemIfAgentKnown({
agentManager,
agentId,
item,
}),
logger: childLogger,
},
});
resolvedCwd = createdWorktree.worktree.worktreePath;
setupContinuation = createdWorktree.setupContinuation;
}
return {
provider: resolvedProviderModel.provider,
initialPrompt: topLevelArgs.initialPrompt,
background: topLevelArgs.background ?? false,
normalizedTitle: topLevelArgs.title.trim(),
model: resolvedProviderModel.model,
thinking: topLevelArgs.thinking,
features: topLevelArgs.features,
labels: topLevelArgs.labels,
notifyOnFinish: topLevelArgs.notifyOnFinish ?? false,
resolvedCwd,
resolvedMode,
setupContinuation,
};
};
server.registerTool(
"create_agent",
{
title: "Create agent",
description:
"Create an agent tied to a working directory. Requires provider/model, for example codex/gpt-5.4. Do not guess; call list_providers and list_models first if uncertain. Optionally run an initial prompt immediately or create a git worktree for the agent.",
inputSchema: createAgentInputSchema,
outputSchema: {
agentId: z.string(),
type: AgentProviderEnum,
status: AgentStatusEnum,
cwd: z.string(),
currentModeId: z.string().nullable(),
availableModes: z.array(
z.object({
id: z.string(),
label: z.string(),
description: z.string().nullable().optional(),
}),
),
lastMessage: z.string().nullable().optional(),
permission: AgentPermissionRequestPayloadSchema.nullable().optional(),
},
},
async (args: unknown) => {
const resolved = callerAgentId
? resolveCallerCreateAgentArgs(args, callerAgentId)
: await resolveTopLevelCreateAgentArgs(args);
const {
provider,
initialPrompt,
background,
normalizedTitle,
model,
thinking,
features,
labels,
notifyOnFinish,
resolvedCwd,
resolvedMode,
setupContinuation,
} = resolved;
const childAgentDefaultLabels = callerContext?.childAgentDefaultLabels;
const mergedLabels = {
...(callerAgentId ? { [PARENT_AGENT_ID_LABEL]: callerAgentId } : {}),
...childAgentDefaultLabels,
...labels,
};
const snapshot = await agentManager.createAgent(
{
provider,
cwd: resolvedCwd,
modeId: resolvedMode,
title: normalizedTitle ?? undefined,
model,
thinkingOptionId: thinking,
featureValues: features,
},
undefined,
Object.keys(mergedLabels).length > 0 ? { labels: mergedLabels } : undefined,
);
setupContinuation?.startAfterAgentCreate({
agentId: snapshot.id,
});
const trimmedPrompt = initialPrompt.trim();
scheduleAgentMetadataGeneration({
agentManager,
agentId: snapshot.id,
cwd: snapshot.cwd,
workspaceGitService: options.workspaceGitService,
initialPrompt: trimmedPrompt,
explicitTitle: snapshot.config.title,
paseoHome: options.paseoHome,
logger: childLogger,
});
try {
agentManager.recordUserMessage(snapshot.id, trimmedPrompt, {
emitState: false,
});
} catch (error) {
childLogger.error({ err: error, agentId: snapshot.id }, "Failed to record initial prompt");
}
try {
startAgentRun(agentManager, snapshot.id, trimmedPrompt, childLogger);
if (notifyOnFinish && callerAgentId) {
setupFinishNotification({
agentManager,
agentStorage,
childAgentId: snapshot.id,
callerAgentId,
logger: childLogger,
});
}
// If not running in background, wait for completion
if (!background) {
const result = await waitForAgentWithTimeout(agentManager, snapshot.id, {
waitForActive: true,
});
const responseData = {
agentId: snapshot.id,
type: provider,
status: result.status,
cwd: snapshot.cwd,
currentModeId: snapshot.currentModeId,
availableModes: snapshot.availableModes,
lastMessage: result.lastMessage,
permission: sanitizePermissionRequest(result.permission),
};
const validJson = ensureValidJson(responseData);
const response = {
content: [],
structuredContent: validJson,
};
return response;
}
} catch (error) {
childLogger.error({ err: error, agentId: snapshot.id }, "Failed to run initial prompt");
}
// Return immediately if background=true
const response = {
content: [],
structuredContent: ensureValidJson({
agentId: snapshot.id,
type: provider,
status: snapshot.lifecycle,
cwd: snapshot.cwd,
currentModeId: snapshot.currentModeId,
availableModes: snapshot.availableModes,
lastMessage: null,
permission: null,
}),
};
return response;
},
);
server.registerTool(
"set_agent_feature",
{
title: "Set agent feature",
description: "Set a provider-specific feature on an existing agent, such as Codex fast_mode.",
inputSchema: {
agentId: z.string(),
featureId: z.string().trim().min(1),
value: z.unknown(),
},
outputSchema: {
success: z.boolean(),
},
},
async ({ agentId, featureId, value }) => {
await agentManager.setAgentFeature(agentId, featureId, value);
return {
content: [],
structuredContent: ensureValidJson({ success: true }),
};
},
);
server.registerTool(
"wait_for_agent",
{
title: "Wait for agent",
description:
"Block until the agent requests permission or the current run completes. Returns the pending permission (if any) and recent activity summary.",
inputSchema: {
agentId: z.string().describe("Agent identifier returned by the create_agent tool"),
},
outputSchema: {
agentId: z.string(),
status: AgentStatusEnum,
permission: AgentPermissionRequestPayloadSchema.nullable(),
lastMessage: z.string().nullable(),
},
},
async ({ agentId }, { signal }) => {
const abortController = new AbortController();
const cleanupFns: Array<() => void> = [];
const cleanup = () => {
while (cleanupFns.length) {
const fn = cleanupFns.pop();
try {
fn?.();
} catch {
// ignore cleanup errors
}
}
};
const forwardExternalAbort = () => {
if (!abortController.signal.aborted) {
const reason = signal?.reason ?? new Error("wait_for_agent aborted");
abortController.abort(reason);
}
};
if (signal) {
if (signal.aborted) {
forwardExternalAbort();
} else {
signal.addEventListener("abort", forwardExternalAbort, { once: true });
cleanupFns.push(() => signal.removeEventListener("abort", forwardExternalAbort));
}
}
const unregister = waitTracker.register(agentId, (reason) => {
if (!abortController.signal.aborted) {
abortController.abort(new Error(reason ?? "wait_for_agent cancelled"));
}
});
cleanupFns.push(unregister);
try {
const result: WaitForAgentResult = await waitForAgentWithTimeout(agentManager, agentId, {
signal: abortController.signal,
});
const validJson = ensureValidJson({
agentId,
status: result.status,
permission: sanitizePermissionRequest(result.permission),
lastMessage: result.lastMessage,
});
const response = {
content: [],
structuredContent: validJson,
};
return response;
} finally {
cleanup();
}
},
);
server.registerTool(
"send_agent_prompt",
{
title: "Send agent prompt",
description:
"Send a task to a running agent. Returns immediately after the agent begins processing.",
inputSchema: {
agentId: z.string(),
prompt: z.string(),
sessionMode: z
.string()
.optional()
.describe("Optional mode to set before running the prompt."),
background: z
.boolean()
.optional()
.default(false)
.describe(
"Run agent in background. If false (default), waits for completion or permission request. If true, returns immediately.",
),
notifyOnFinish: z
.boolean()
.optional()
.default(false)
.describe(
"Send a notification prompt to the caller agent when this agent finishes, errors, or needs permission.",
),
},
outputSchema: {
success: z.boolean(),
status: AgentStatusEnum,
lastMessage: z.string().nullable().optional(),
permission: AgentPermissionRequestPayloadSchema.nullable().optional(),
},
},
async ({ agentId, prompt, sessionMode, background = false, notifyOnFinish = false }) => {
if (agentManager.hasInFlightRun(agentId)) {
waitTracker.cancel(agentId, "Agent run interrupted by new prompt");
}
await sendPromptToAgent({
agentManager,
agentStorage,
agentId,
userMessageText: prompt,
prompt,
sessionMode,
logger: childLogger,
});
if (notifyOnFinish && callerAgentId) {
setupFinishNotification({
agentManager,
agentStorage,
childAgentId: agentId,
callerAgentId,
logger: childLogger,
});
}
// If not running in background, wait for completion
if (!background) {
const result = await waitForAgentWithTimeout(agentManager, agentId, {
waitForActive: true,
});
const responseData = {
success: true,
status: result.status,
lastMessage: result.lastMessage,
permission: sanitizePermissionRequest(result.permission),
};
const validJson = ensureValidJson(responseData);
const response = {
content: [],
structuredContent: validJson,
};
return response;
}
// Return immediately if background=true
// Re-fetch snapshot since the state may have changed
const currentSnapshot = agentManager.getAgent(agentId);
const responseData = {
success: true,
status: currentSnapshot?.lifecycle ?? "idle",
lastMessage: null,
permission: null,
};
const validJson = ensureValidJson(responseData);
const response = {
content: [],
structuredContent: validJson,
};
return response;
},
);
server.registerTool(
"get_agent_status",
{
title: "Get agent status",
description:
"Return the latest snapshot for an agent, including lifecycle state, capabilities, and pending permissions.",
inputSchema: {
agentId: z.string(),
},
outputSchema: {
status: AgentStatusEnum,
snapshot: AgentSnapshotPayloadSchema,
},
},
async ({ agentId }) => {
const snapshot = agentManager.getAgent(agentId);
if (snapshot) {
const structuredSnapshot = await serializeSnapshotWithMetadata(
agentStorage,
snapshot,
childLogger,
);
return {
content: [],
structuredContent: ensureValidJson({
status: snapshot.lifecycle,
snapshot: structuredSnapshot,
}),
};
}
const record = await agentStorage.get(agentId);
if (!record || record.internal) {
throw new Error(`Agent ${agentId} not found`);
}
const structuredSnapshot = buildStoredAgentPayload(
record,
resolveRegisteredProviderIds(agentManager, providerRegistry),
);
return {
content: [],
structuredContent: ensureValidJson({
status: structuredSnapshot.status,
snapshot: structuredSnapshot,
}),
};
},
);
server.registerTool(
"list_agents",
{
title: "List agents",
description: "List recent agents as compact metadata.",
inputSchema: {
includeArchived: z.boolean().optional().default(false),
cwd: z.string().optional(),
sinceHours: z
.number()
.int()
.positive()
.max(24 * 30)
.optional()
.default(48),
statuses: z.array(AgentStatusEnum).optional(),
limit: z.number().int().positive().max(200).optional().default(50),
},
outputSchema: {
agents: z.array(AgentListItemPayloadSchema),
},
},
async ({ includeArchived = false, cwd, sinceHours = 48, statuses, limit = 50 }) => {
const callerCwd = callerAgentId ? resolveCallerAgent()?.cwd : undefined;
const requestedCwd = cwd?.trim() ? expandUserPath(cwd) : callerCwd;
const statusFilter = statuses && statuses.length > 0 ? new Set(statuses) : null;
const sinceMs = Date.now() - sinceHours * 60 * 60 * 1000;
const liveSnapshots = agentManager.listAgents();
const liveAgents = await Promise.all(
liveSnapshots.map((snapshot) =>
serializeSnapshotWithMetadata(agentStorage, snapshot, childLogger),
),
);
const liveIds = new Set(liveSnapshots.map((snapshot) => snapshot.id));
const storedRecords = await agentStorage.list();
const registeredProviderIds = resolveRegisteredProviderIds(agentManager, providerRegistry);
const storedAgents = storedRecords
.filter((record) => !record.internal && !liveIds.has(record.id))
.filter((record) => includeArchived || !record.archivedAt)
.filter(
(record) =>
includeArchived || isStoredAgentProviderAvailable(record, registeredProviderIds),
)
.map((record) => buildStoredAgentPayload(record, registeredProviderIds));
const agents = [...liveAgents, ...storedAgents]
.map(toAgentListItemPayload)
.filter((agent) => !requestedCwd || isSameOrDescendantPath(requestedCwd, agent.cwd))
.filter((agent) => !statusFilter || statusFilter.has(agent.status))
.filter((agent) => !agent.archivedAt || resolveAgentListActivityTime(agent) >= sinceMs)
.sort(compareAgentListItems)
.slice(0, limit);
return {
content: [],
structuredContent: ensureValidJson({ agents }),
};
},
);
server.registerTool(
"cancel_agent",
{
title: "Cancel agent run",
description: "Abort the agent's current run but keep the agent alive for future tasks.",
inputSchema: {
agentId: z.string(),
},
outputSchema: {
success: z.boolean(),
},
},
async ({ agentId }) => {
const success = await agentManager.cancelAgentRun(agentId);
if (success) {
waitTracker.cancel(agentId, "Agent run cancelled");
}
return {
content: [],
structuredContent: ensureValidJson({ success }),
};
},
);
server.registerTool(
"archive_agent",
{
title: "Archive agent",
description:
"Archive an agent (soft-delete). The agent is interrupted if running and removed from the active list.",
inputSchema: {
agentId: z.string(),
},
outputSchema: {
success: z.boolean(),
},
},
async ({ agentId }) => {
await agentManager.archiveAgent(agentId);
waitTracker.cancel(agentId, "Agent archived");
return {
content: [],
structuredContent: ensureValidJson({ success: true }),
};
},
);
server.registerTool(
"kill_agent",
{
title: "Kill agent",
description: "Terminate an agent session permanently.",
inputSchema: {
agentId: z.string(),
},
outputSchema: {
success: z.boolean(),
},
},
async ({ agentId }) => {
await agentManager.closeAgent(agentId);
waitTracker.cancel(agentId, "Agent terminated");
return {
content: [],
structuredContent: ensureValidJson({ success: true }),
};
},
);
server.registerTool(
"update_agent",
{
title: "Update agent",
description: "Update an agent name and/or labels.",
inputSchema: {
agentId: z.string(),
name: z.string().optional(),
labels: z.record(z.string(), z.string()).optional().describe("Labels to set on the agent"),
},
outputSchema: {
success: z.boolean(),
},
},
async ({ agentId, name, labels }) => {
const trimmedName = name?.trim();
if (trimmedName) {
const record = await agentStorage.get(agentId);
if (!record) {
throw new Error(`Agent ${agentId} not found`);
}
await agentStorage.upsert({
...record,
title: trimmedName,
updatedAt: new Date().toISOString(),
});
agentManager.notifyAgentState(agentId);
}
if (labels) {
await agentManager.setLabels(agentId, labels);
}
return {
content: [],
structuredContent: ensureValidJson({ success: true }),
};
},
);
server.registerTool(
"list_terminals",
{
title: "List terminals",
description: "List terminals for a working directory or across all working directories.",
inputSchema: {
cwd: z
.string()
.optional()
.describe("Optional working directory. Defaults to the caller agent cwd."),
all: z.boolean().optional().describe("List terminals across all working directories."),
},
outputSchema: {
terminals: z.array(TerminalSummarySchema),
},
},
async ({ cwd, all }) => {
if (!terminalManager) {
throw new Error("Terminal manager is not configured");
}
const terminals = all
? (
await Promise.all(
terminalManager.listDirectories().map(async (directory) =>
(await terminalManager.getTerminals(directory)).map((terminal) => ({
id: terminal.id,
name: terminal.name,
cwd: terminal.cwd,
})),
),
)
).flat()
: (await terminalManager.getTerminals(resolveScopedCwd(cwd, { required: true }))).map(
(terminal) => ({
id: terminal.id,
name: terminal.name,
cwd: terminal.cwd,
}),
);
return {
content: [],
structuredContent: ensureValidJson({ terminals }),
};
},
);
server.registerTool(
"create_terminal",
{
title: "Create terminal",
description: "Create a terminal session for a working directory.",
inputSchema: {
cwd: z
.string()
.optional()
.describe("Optional working directory. Defaults to the caller agent cwd."),
name: z.string().optional().describe("Optional terminal name."),
},
outputSchema: TerminalSummarySchema.shape,
},
async ({ cwd, name }) => {
if (!terminalManager) {
throw new Error("Terminal manager is not configured");
}
const terminal = await terminalManager.createTerminal({
cwd: resolveScopedCwd(cwd, { required: true }),
...(name?.trim() ? { name: name.trim() } : {}),
});
return {
content: [],
structuredContent: ensureValidJson({
id: terminal.id,
name: terminal.name,
cwd: terminal.cwd,
}),
};
},
);
server.registerTool(
"kill_terminal",
{
title: "Kill terminal",
description: "Kill an existing terminal session.",
inputSchema: {
terminalId: z.string(),
},
outputSchema: {
success: z.boolean(),
},
},
async ({ terminalId }) => {
if (!terminalManager) {
throw new Error("Terminal manager is not configured");
}
const terminal = terminalManager.getTerminal(terminalId);
if (!terminal) {
throw new Error(`Terminal ${terminalId} not found`);
}
terminal.kill();
return {
content: [],
structuredContent: ensureValidJson({ success: true }),
};
},
);
server.registerTool(
"capture_terminal",
{
title: "Capture terminal",
description: "Capture plain-text terminal output lines from a terminal session.",
inputSchema: {
terminalId: z.string(),
start: z.number().optional(),
end: z.number().optional(),
scrollback: z.boolean().optional(),
stripAnsi: z.boolean().optional().default(true),
},
outputSchema: {
terminalId: z.string(),
lines: z.array(z.string()),
totalLines: z.number().int().nonnegative(),
},
},
async ({ terminalId, start, end, scrollback, stripAnsi = true }) => {
if (!terminalManager) {
throw new Error("Terminal manager is not configured");
}
if (!terminalManager.getTerminal(terminalId)) {
throw new Error(`Terminal ${terminalId} not found`);
}
const capture = await terminalManager.captureTerminal(terminalId, {
start: scrollback ? 0 : start,
end,
stripAnsi,
});
return {
content: [],
structuredContent: ensureValidJson({
terminalId,
lines: capture.lines,
totalLines: capture.totalLines,
}),
};
},
);
server.registerTool(
"send_terminal_keys",
{
title: "Send terminal keys",
description: "Send literal text or special key tokens to a terminal session.",
inputSchema: {
terminalId: z.string(),
keys: z.string(),
literal: z.boolean().optional(),
},
outputSchema: {
success: z.boolean(),
},
},
async ({ terminalId, keys, literal = false }) => {
if (!terminalManager) {
throw new Error("Terminal manager is not configured");
}
const terminal = terminalManager.getTerminal(terminalId);
if (!terminal) {
throw new Error(`Terminal ${terminalId} not found`);
}
terminal.send({
type: "input",
data: resolveTerminalKeyToken(keys, literal),
});
return {
content: [],
structuredContent: ensureValidJson({ success: true }),
};
},
);
server.registerTool(
"create_schedule",
{
title: "Create schedule",
description: "Create a recurring schedule that runs on an agent or a new agent.",
inputSchema: {
prompt: z.string().trim().min(1, "prompt is required"),
every: z.string().optional(),
cron: z.string().optional(),
name: z.string().optional(),
target: z.enum(["self", "new-agent"]).optional(),
provider: AgentProviderEnum.optional().describe(
"Provider, or provider/model (for example: codex or codex/gpt-5.4).",
),
cwd: z.string().optional(),
maxRuns: z.number().int().positive().optional(),
expiresIn: z.string().optional(),
},
outputSchema: ScheduleSummarySchema.shape,
},
async ({ prompt, every, cron, name, target, provider, cwd, maxRuns, expiresIn }) => {
if (!scheduleService) {
throw new Error("Schedule service is not configured");
}
const cadenceCount = Number(every !== undefined) + Number(cron !== undefined);
if (cadenceCount !== 1) {
throw new Error("Specify exactly one of every or cron");
}
const scheduleTarget =
target === "self"
? (() => {
const callerAgent = resolveCallerAgent();
if (!callerAgentId || !callerAgent) {
throw new Error("target=self requires a caller agent");
}
const trimmedCwd = cwd?.trim();
if (trimmedCwd && expandUserPath(trimmedCwd) !== callerAgent.cwd) {
throw new Error("cwd can only differ from the caller agent when target=new-agent");
}
if (provider !== undefined) {
const resolved = resolveScheduleProviderAndModel({
provider,
defaultProvider: callerAgent.provider,
});
if (
resolved.provider !== callerAgent.provider ||
(resolved.model !== undefined && resolved.model !== callerAgent.config.model)
) {
throw new Error(
"provider can only differ from the caller agent when target=new-agent",
);
}
}
return { type: "agent" as const, agentId: callerAgentId };
})()
: (() => {
return resolveNewAgentScheduleTarget({ provider, cwd });
})();
const schedule = await scheduleService.create({
prompt: prompt.trim(),
cadence: every
? { type: "every" as const, everyMs: parseDurationString(every) }
: { type: "cron" as const, expression: cron!.trim() },
target: scheduleTarget,
...(name?.trim() ? { name: name.trim() } : {}),
...(maxRuns === undefined ? {} : { maxRuns }),
...(expiresIn === undefined
? {}
: { expiresAt: new Date(Date.now() + parseDurationString(expiresIn)).toISOString() }),
});
return {
content: [],
structuredContent: ensureValidJson(toScheduleSummary(schedule)),
};
},
);
server.registerTool(
"list_schedules",
{
title: "List schedules",
description: "List all schedules managed by the daemon.",
inputSchema: {},
outputSchema: {
schedules: z.array(ScheduleSummarySchema),
},
},
async () => {
if (!scheduleService) {
throw new Error("Schedule service is not configured");
}
const schedules = (await scheduleService.list()).map((schedule) =>
toScheduleSummary(schedule),
);
return {
content: [],
structuredContent: ensureValidJson({ schedules }),
};
},
);
server.registerTool(
"inspect_schedule",
{
title: "Inspect schedule",
description: "Inspect a schedule and its run history.",
inputSchema: {
id: z.string(),
},
outputSchema: StoredScheduleSchema.shape,
},
async ({ id }) => {
if (!scheduleService) {
throw new Error("Schedule service is not configured");
}
const schedule = await scheduleService.inspect(id);
return {
content: [],
structuredContent: ensureValidJson(schedule),
};
},
);
server.registerTool(
"pause_schedule",
{
title: "Pause schedule",
description: "Pause an active schedule.",
inputSchema: {
id: z.string(),
},
outputSchema: {
success: z.boolean(),
},
},
async ({ id }) => {
if (!scheduleService) {
throw new Error("Schedule service is not configured");
}
await scheduleService.pause(id);
return {
content: [],
structuredContent: ensureValidJson({ success: true }),
};
},
);
server.registerTool(
"resume_schedule",
{
title: "Resume schedule",
description: "Resume a paused schedule.",
inputSchema: {
id: z.string(),
},
outputSchema: {
success: z.boolean(),
},
},
async ({ id }) => {
if (!scheduleService) {
throw new Error("Schedule service is not configured");
}
await scheduleService.resume(id);
return {
content: [],
structuredContent: ensureValidJson({ success: true }),
};
},
);
server.registerTool(
"delete_schedule",
{
title: "Delete schedule",
description: "Delete a schedule permanently.",
inputSchema: {
id: z.string(),
},
outputSchema: {
success: z.boolean(),
},
},
async ({ id }) => {
if (!scheduleService) {
throw new Error("Schedule service is not configured");
}
await scheduleService.delete(id);
return {
content: [],
structuredContent: ensureValidJson({ success: true }),
};
},
);
server.registerTool(
"list_providers",
{
title: "List providers",
description: "List configured agent providers, availability, and their modes.",
inputSchema: {},
outputSchema: {
providers: z.array(ProviderSummarySchema),
},
},
async () => {
const providers = await Promise.all(
Object.values(providerRegistry ?? {}).map((provider) =>
resolveProviderSummary(provider, childLogger),
),
);
return {
content: [],
structuredContent: ensureValidJson({ providers }),
};
},
);
server.registerTool(
"list_models",
{
title: "List models",
description: "List models for an agent provider.",
inputSchema: {
provider: AgentProviderEnum,
},
outputSchema: {
provider: z.string(),
models: z.array(AgentModelSchema),
},
},
async ({ provider }) => {
if (!providerRegistry) {
throw new Error("Provider registry is not configured");
}
const definition = providerRegistry[provider];
if (!definition) {
throw new Error(`Provider ${provider} is not configured`);
}
if (!definition.enabled) {
throw new Error(`Provider '${provider}' is disabled`);
}
const models = await definition.fetchModels({ cwd: resolveSnapshotCwd(), force: false });
return {
content: [],
structuredContent: ensureValidJson({
provider,
models,
}),
};
},
);
server.registerTool(
"list_worktrees",
{
title: "List worktrees",
description: "List Paseo-managed git worktrees for a repository.",
inputSchema: {
cwd: z
.string()
.optional()
.describe("Optional repository cwd. Defaults to the caller agent cwd."),
},
outputSchema: {
worktrees: z.array(WorktreeSummarySchema),
},
},
async ({ cwd }) => {
const resolvedCwd = resolveScopedCwd(cwd, { required: true });
if (!options.workspaceGitService) {
throw new Error("WorkspaceGitService is required to list worktrees");
}
const worktrees = await options.workspaceGitService.listWorktrees(resolvedCwd, {
reason: "mcp:list-worktrees",
});
return {
content: [],
structuredContent: ensureValidJson({ worktrees }),
};
},
);
server.registerTool(
"create_worktree",
{
title: "Create worktree",
description:
"Create a Paseo-managed git worktree. Branch off a new branch, check out an existing branch, or check out a GitHub PR.",
inputSchema: {
cwd: z.string().optional().describe("Repository directory. Defaults to the agent's cwd."),
target: z
.discriminatedUnion("mode", [
z
.object({
mode: z.literal("branch-off"),
newBranch: z.string().min(1).describe("Name for the new branch."),
base: z
.string()
.min(1)
.optional()
.describe("Base ref. Defaults to the repo's default branch."),
})
.describe("Create a new branch off a base."),
z
.object({
mode: z.literal("checkout-branch"),
branch: z.string().min(1).describe("Existing branch to check out."),
})
.describe("Check out an existing branch."),
z
.object({
mode: z.literal("checkout-pr"),
prNumber: z.number().int().positive().describe("Pull request number."),
})
.describe("Check out a GitHub pull request."),
])
.describe("What the worktree should contain."),
},
outputSchema: {
branchName: z.string(),
worktreePath: z.string(),
},
},
async ({ cwd, target }) => {
const repoRoot = resolveScopedCwd(cwd, { required: true });
const mcpInput = mcpCreateWorktreeInput(repoRoot, target, options.paseoHome);
const createdWorktree = await createMcpWorktree({
input: mcpInput.input,
createPaseoWorktree: options.createPaseoWorktree,
resolveDefaultBranch: mcpInput.resolveDefaultBranch,
});
const { worktree } = createdWorktree;
await options.workspaceGitService?.listWorktrees?.(repoRoot, {
force: true,
reason: "mcp:create-worktree",
});
return {
content: [],
structuredContent: ensureValidJson({
branchName: worktree.branchName,
worktreePath: worktree.worktreePath,
}),
};
},
);
server.registerTool(
"archive_worktree",
{
title: "Archive worktree",
description: "Delete a Paseo-managed git worktree.",
inputSchema: {
cwd: z
.string()
.optional()
.describe("Optional repository cwd. Defaults to the caller agent cwd."),
worktreePath: z.string().optional(),
worktreeSlug: z.string().optional(),
},
outputSchema: {
success: z.boolean(),
},
},
async ({ cwd, worktreePath, worktreeSlug }) => {
const resolvedCwd = resolveScopedCwd(cwd, { required: true });
if (!worktreePath && !worktreeSlug) {
throw new Error("worktreePath or worktreeSlug is required");
}
if (!options.github) {
throw new Error("GitHub service is required to archive worktrees");
}
if (!options.workspaceGitService) {
throw new Error("WorkspaceGitService is required to archive worktrees");
}
if (!options.archiveWorkspaceRecord) {
throw new Error("Workspace registry archiver is required to archive worktrees");
}
if (!options.emitWorkspaceUpdatesForWorkspaceIds) {
throw new Error("Workspace update emitter is required to archive worktrees");
}
if (!options.markWorkspaceArchiving) {
throw new Error("Workspace archiving marker is required to archive worktrees");
}
if (!options.clearWorkspaceArchiving) {
throw new Error("Workspace archiving clearer is required to archive worktrees");
}
if (!options.emitSessionMessage) {
throw new Error("Session message emitter is required to archive worktrees");
}
const repoRoot = await options.workspaceGitService.resolveRepoRoot(resolvedCwd);
const targetPath =
worktreePath ??
join(await getPaseoWorktreesRoot(repoRoot, options.paseoHome), worktreeSlug!);
await archivePaseoWorktree(
{
paseoHome: options.paseoHome,
github: options.github,
workspaceGitService: options.workspaceGitService,
agentManager,
agentStorage,
archiveWorkspaceRecord: options.archiveWorkspaceRecord,
emit: options.emitSessionMessage,
emitWorkspaceUpdatesForWorkspaceIds: options.emitWorkspaceUpdatesForWorkspaceIds,
markWorkspaceArchiving: options.markWorkspaceArchiving,
clearWorkspaceArchiving: options.clearWorkspaceArchiving,
isPathWithinRoot: isSameOrDescendantPath,
killTerminalsUnderPath: (rootPath) =>
killTerminalsUnderPath(
{
terminalManager: terminalManager ?? null,
isPathWithinRoot: isSameOrDescendantPath,
killTrackedTerminal: () => {},
sessionLogger: childLogger,
},
rootPath,
),
sessionLogger: childLogger,
},
{
targetPath,
repoRoot,
requestId: "mcp:archive_worktree",
},
);
await options.workspaceGitService.listWorktrees(repoRoot, {
force: true,
reason: "mcp:archive-worktree",
});
return {
content: [],
structuredContent: ensureValidJson({ success: true }),
};
},
);
server.registerTool(
"get_agent_activity",
{
title: "Get agent activity",
description: "Return recent agent timeline entries as a curated summary.",
inputSchema: {
agentId: z.string(),
limit: z
.number()
.optional()
.describe("Optional limit for number of activities to include (most recent first)."),
},
outputSchema: {
agentId: z.string(),
updateCount: z.number(),
currentModeId: z.string().nullable(),
content: z.string(),
},
},
async ({ agentId, limit }) => {
await ensureAgentLoaded(agentId, {
agentManager,
agentStorage,
logger: childLogger,
});
const timeline = agentManager.getTimeline(agentId);
const snapshot = agentManager.getAgent(agentId);
const selection = selectItemsByProjectedLimit({
items: timeline,
direction: "tail",
limit: limit ?? 0,
});
const curatedContent = curateAgentActivity(selection.items);
const { totalProjected, shownProjected } = selection;
const noun = totalProjected === 1 ? "activity" : "activities";
const countHeader =
limit && shownProjected < totalProjected
? `Showing ${shownProjected} of ${totalProjected} ${noun} (limited to ${limit})`
: `Showing all ${totalProjected} ${noun}`;
const contentWithCount = `${countHeader}\n\n${curatedContent}`;
return {
content: [],
structuredContent: ensureValidJson({
agentId,
updateCount: timeline.length,
currentModeId: snapshot?.currentModeId ?? null,
content: contentWithCount,
}),
};
},
);
server.registerTool(
"set_agent_mode",
{
title: "Set agent session mode",
description:
"Switch the agent's session mode (plan, bypassPermissions, read-only, auto, etc.).",
inputSchema: {
agentId: z.string(),
modeId: z.string(),
},
outputSchema: {
success: z.boolean(),
newMode: z.string(),
},
},
async ({ agentId, modeId }) => {
await agentManager.setAgentMode(agentId, modeId);
return {
content: [],
structuredContent: ensureValidJson({ success: true, newMode: modeId }),
};
},
);
server.registerTool(
"list_pending_permissions",
{
title: "List pending permissions",
description:
"Return all pending permission requests across all agents with the normalized payloads.",
inputSchema: {},
outputSchema: {
permissions: z.array(
z.object({
agentId: z.string(),
status: AgentStatusEnum,
request: AgentPermissionRequestPayloadSchema,
}),
),
},
},
async () => {
const permissions = agentManager.listAgents().flatMap((agent) => {
const payload = toAgentPayload(agent);
return payload.pendingPermissions.map((request) => ({
agentId: agent.id,
status: payload.status,
request,
}));
});
return {
content: [],
structuredContent: ensureValidJson({ permissions }),
};
},
);
server.registerTool(
"respond_to_permission",
{
title: "Respond to permission",
description:
"Approve or deny a pending permission request with an AgentManager-compatible response payload.",
inputSchema: {
agentId: z.string(),
requestId: z.string(),
response: AgentPermissionResponseSchema,
},
outputSchema: {
success: z.boolean(),
},
},
async ({ agentId, requestId, response }) => {
await agentManager.respondToPermission(agentId, requestId, response);
return {
content: [],
structuredContent: ensureValidJson({ success: true }),
};
},
);
return server;
}
type McpCreateWorktreeTarget =
| { mode: "branch-off"; newBranch: string; base?: string }
| { mode: "checkout-branch"; branch: string }
| { mode: "checkout-pr"; prNumber: number };
function mcpCreateWorktreeInput(
repoRoot: string,
target: McpCreateWorktreeTarget,
paseoHome: string | undefined,
): { input: CreatePaseoWorktreeInput; resolveDefaultBranch?: (root: string) => Promise<string> } {
const base = { cwd: repoRoot, runSetup: false, paseoHome } as const;
switch (target.mode) {
case "branch-off":
return {
input: {
...base,
worktreeSlug: target.newBranch,
action: "branch-off",
...(target.base ? { refName: target.base } : {}),
},
};
case "checkout-branch":
return {
input: { ...base, action: "checkout", refName: target.branch },
};
case "checkout-pr":
return {
input: { ...base, action: "checkout", githubPrNumber: target.prNumber },
};
default:
throw new Error("unreachable");
}
}
interface CreateMcpWorktreeOptions {
input: CreatePaseoWorktreeInput;
createPaseoWorktree: CreatePaseoWorktreeWorkflowFn | undefined;
resolveDefaultBranch?: (repoRoot: string) => Promise<string>;
setupContinuation?: CreatePaseoWorktreeSetupContinuationInput;
}
async function createMcpWorktree(
options: CreateMcpWorktreeOptions,
): Promise<CreatePaseoWorktreeWorkflowResult> {
try {
if (!options.createPaseoWorktree) {
throw new Error("Paseo worktree service is not configured");
}
const result = await options.createPaseoWorktree(options.input, {
...(options.resolveDefaultBranch
? { resolveDefaultBranch: options.resolveDefaultBranch }
: {}),
...(options.setupContinuation ? { setupContinuation: options.setupContinuation } : {}),
});
return result;
} catch (error) {
throw toWorktreeRequestError(error);
}
}