feat(mcp): consolidate provider settings tools (#1011)

Closes #984
This commit is contained in:
Bolun Zhang
2026-05-19 12:56:03 +08:00
committed by GitHub
parent bc32b16b02
commit e38d0e0fa9
5 changed files with 319 additions and 144 deletions

View File

@@ -154,7 +154,7 @@ async function createTopLevelAgent(args?: Partial<StructuredContent>): Promise<s
title: "Parity agent",
provider: "claude/claude-test-model",
initialPrompt: "say done and stop",
mode: "bypassPermissions",
settings: { modeId: "bypassPermissions" },
background: true,
...args,
});
@@ -240,7 +240,7 @@ beforeAll(async () => {
title: "MCP parity parent",
provider: "claude/claude-test-model",
initialPrompt: "say done and stop",
mode: "bypassPermissions",
settings: { modeId: "bypassPermissions" },
background: true,
});
parentAgentId = str(parentPayload.agentId);
@@ -339,7 +339,7 @@ describe("Suite A: Core Fixes", () => {
test("create_agent accepts provider features over MCP", async () => {
let agentId: string | null = null;
try {
agentId = await createTopLevelAgent({ features: { test_feature: true } });
agentId = await createTopLevelAgent({ settings: { features: { test_feature: true } } });
const internalSnapshot = daemonHandle.daemon.agentManager.getAgent(agentId);
expect(internalSnapshot?.config.featureValues).toEqual({ test_feature: true });
@@ -356,7 +356,7 @@ describe("Suite A: Core Fixes", () => {
try {
agentId = await createChildAgent({
provider: "claude/claude-test-model",
features: { test_feature: true },
settings: { features: { test_feature: true } },
});
const internalSnapshot = daemonHandle.daemon.agentManager.getAgent(agentId);
expect(internalSnapshot?.config.featureValues).toEqual({ test_feature: true });
@@ -369,14 +369,13 @@ describe("Suite A: Core Fixes", () => {
}
});
test("set_agent_feature updates provider features over MCP", async () => {
test("update_agent updates provider features over MCP", async () => {
let agentId: string | null = null;
try {
agentId = await createTopLevelAgent({ features: { test_feature: false } });
const updated = await callToolStructured(topLevelClient, "set_agent_feature", {
agentId = await createTopLevelAgent({ settings: { features: { test_feature: false } } });
const updated = await callToolStructured(topLevelClient, "update_agent", {
agentId,
featureId: "test_feature",
value: true,
settings: { features: { test_feature: true } },
});
expect(updated.success).toBe(true);
const internalSnapshot = daemonHandle.daemon.agentManager.getAgent(agentId);
@@ -390,15 +389,18 @@ describe("Suite A: Core Fixes", () => {
}
});
test("list_provider_features returns draft provider features over MCP", async () => {
const payload = await callToolStructured(topLevelClient, "list_provider_features", {
test("inspect_provider returns draft provider features over MCP", async () => {
const payload = await callToolStructured(topLevelClient, "inspect_provider", {
provider: "claude",
cwd: parentAgentCwd,
model: "claude-test-model",
featureValues: { test_feature: true },
settings: {
model: "claude-test-model",
features: { test_feature: true },
},
});
expect(payload.provider).toBe("claude");
expect(payload.selectedModel).toBe("claude-test-model");
expect(recordArr(payload.features)).toEqual(
expect.arrayContaining([
expect.objectContaining({

View File

@@ -117,7 +117,9 @@ function buildAgentManagerSpies() {
createAgent: vi.fn(),
waitForAgentEvent: vi.fn(),
recordUserMessage: vi.fn(),
setAgentMode: vi.fn(),
setAgentMode: vi.fn().mockResolvedValue(undefined),
setAgentModel: vi.fn().mockResolvedValue(undefined),
setAgentThinkingOption: vi.fn().mockResolvedValue(undefined),
setAgentFeature: vi.fn().mockResolvedValue(undefined),
setLabels: vi.fn().mockResolvedValue(undefined),
setTitle: vi.fn().mockResolvedValue(undefined),
@@ -446,7 +448,7 @@ describe("create_agent MCP tool", () => {
const missingTitle = await tool.inputSchema.safeParseAsync({
cwd: existingCwd,
mode: "default",
settings: { modeId: "default" },
provider: "codex/gpt-5.4",
initialPrompt: "test",
});
@@ -455,7 +457,7 @@ describe("create_agent MCP tool", () => {
const tooLong = await tool.inputSchema.safeParseAsync({
cwd: existingCwd,
mode: "default",
settings: { modeId: "default" },
provider: "codex/gpt-5.4",
title: "x".repeat(61),
initialPrompt: "test",
@@ -465,7 +467,7 @@ describe("create_agent MCP tool", () => {
const ok = await tool.inputSchema.safeParseAsync({
cwd: existingCwd,
mode: "default",
settings: { modeId: "default" },
provider: "codex/gpt-5.4",
title: "Short title",
initialPrompt: "test",
@@ -479,7 +481,7 @@ describe("create_agent MCP tool", () => {
const tool = registeredTool(server, "create_agent");
const parsed = await tool.inputSchema.safeParseAsync({
cwd: existingCwd,
mode: "default",
settings: { modeId: "default" },
provider: "codex/gpt-5.4",
title: "Short title",
});
@@ -510,7 +512,7 @@ describe("create_agent MCP tool", () => {
provider: "codex/gpt-5.4",
initialPrompt: "Do work",
background: true,
features: { fast_mode: true },
settings: { features: { fast_mode: true } },
};
const parsed = await tool.inputSchema.safeParseAsync(input);
@@ -536,7 +538,7 @@ describe("create_agent MCP tool", () => {
const missingProvider = await tool.inputSchema.safeParseAsync({
cwd: existingCwd,
mode: "default",
settings: { modeId: "default" },
title: "Short title",
initialPrompt: "test",
});
@@ -549,7 +551,7 @@ describe("create_agent MCP tool", () => {
const providerWithoutModel = await tool.inputSchema.safeParseAsync({
cwd: existingCwd,
mode: "default",
settings: { modeId: "default" },
title: "Short title",
provider: "codex",
initialPrompt: "test",
@@ -558,7 +560,7 @@ describe("create_agent MCP tool", () => {
const providerWithEmptyModel = await tool.inputSchema.safeParseAsync({
cwd: existingCwd,
mode: "default",
settings: { modeId: "default" },
title: "Short title",
provider: "codex/",
initialPrompt: "test",
@@ -567,7 +569,7 @@ describe("create_agent MCP tool", () => {
const providerWithEmptyProvider = await tool.inputSchema.safeParseAsync({
cwd: existingCwd,
mode: "default",
settings: { modeId: "default" },
title: "Short title",
provider: "/gpt-5.4",
initialPrompt: "test",
@@ -577,7 +579,7 @@ describe("create_agent MCP tool", () => {
await expect(
tool.handler({
cwd: existingCwd,
mode: "default",
settings: { modeId: "default" },
title: "Short title",
provider: "codex/gpt-5.4",
model: "gpt-5.4",
@@ -722,10 +724,9 @@ describe("create_agent MCP tool", () => {
await tool.handler({
cwd: existingCwd,
title: "Config test",
mode: "auto",
initialPrompt: "Do work",
provider: "codex/gpt-5.4",
thinking: "think-hard",
settings: { modeId: "auto", thinkingOptionId: "think-hard" },
labels: { source: "mcp" },
});
@@ -1269,7 +1270,7 @@ describe("create_agent MCP tool", () => {
const parsed = await tool.inputSchema.safeParseAsync({
cwd: existingCwd,
title: "Custom provider agent",
mode: "default",
settings: { modeId: "default" },
provider: "zai/custom-model",
initialPrompt: "Do work",
});
@@ -1360,7 +1361,7 @@ describe("create_agent MCP tool", () => {
provider: "codex/gpt-5.4",
initialPrompt: "Do work",
background: true,
features: { fast_mode: true },
settings: { features: { fast_mode: true } },
};
const parsed = await tool.inputSchema.safeParseAsync(input);
@@ -1403,7 +1404,7 @@ describe("create_agent MCP tool", () => {
await tool.handler({
cwd: existingCwd,
title: "Injected config test",
mode: "auto",
settings: { modeId: "auto" },
provider: "codex/gpt-5.4",
initialPrompt: "Do work",
});
@@ -1428,7 +1429,7 @@ describe("create_agent MCP tool", () => {
cwd: existingCwd,
title: "Bad mode",
provider: "opencode/gpt-5.4",
mode: "bypassPermissions",
settings: { modeId: "bypassPermissions" },
initialPrompt: "Do work",
}),
).rejects.toThrow(
@@ -1567,7 +1568,7 @@ describe("create_agent MCP tool", () => {
await tool.handler({
title: "Child",
provider: "opencode/gpt-5.4",
mode: "build",
settings: { modeId: "build" },
initialPrompt: "Do work",
});
@@ -1579,17 +1580,31 @@ describe("create_agent MCP tool", () => {
});
});
describe("set_agent_feature MCP tool", () => {
describe("update_agent MCP tool", () => {
const logger = createTestLogger();
it("sets a provider feature on an existing agent", async () => {
const { agentManager, agentStorage, spies } = createTestDeps();
it("does not register the replaced feature-specific MCP tool", async () => {
const { agentManager, agentStorage } = createTestDeps();
const server = await createAgentMcpServer({ agentManager, agentStorage, logger });
const tool = registeredTool(server, "set_agent_feature");
expect(lookupTool(server, "set_agent_feature")).toBeUndefined();
});
it("updates runtime settings before metadata", async () => {
const { agentManager, agentStorage, spies } = createTestDeps();
spies.agentStorage.get.mockResolvedValue(createStoredRecord({ id: "agent-1" }));
const server = await createAgentMcpServer({ agentManager, agentStorage, logger });
const tool = registeredTool(server, "update_agent");
const input = {
agentId: "agent-1",
featureId: "fast_mode",
value: true,
name: "Updated agent",
labels: { role: "worker" },
settings: {
modeId: "full-access",
model: "gpt-5.4",
thinkingOptionId: "high",
features: { fast_mode: true },
},
};
const parsed = await tool.inputSchema.safeParseAsync(input);
@@ -1597,9 +1612,39 @@ describe("set_agent_feature MCP tool", () => {
const response = await tool.handler(input);
expect(spies.agentManager.setAgentMode).toHaveBeenCalledWith("agent-1", "full-access");
expect(spies.agentManager.setAgentModel).toHaveBeenCalledWith("agent-1", "gpt-5.4");
expect(spies.agentManager.setAgentThinkingOption).toHaveBeenCalledWith("agent-1", "high");
expect(spies.agentManager.setAgentFeature).toHaveBeenCalledWith("agent-1", "fast_mode", true);
expect(spies.agentStorage.upsert).toHaveBeenCalledWith(
expect.objectContaining({
id: "agent-1",
title: "Updated agent",
}),
);
expect(spies.agentManager.setLabels).toHaveBeenCalledWith("agent-1", { role: "worker" });
expect(response.structuredContent).toEqual({ success: true });
});
it("does not update metadata when runtime settings fail", async () => {
const { agentManager, agentStorage, spies } = createTestDeps();
spies.agentManager.setAgentFeature.mockRejectedValue(new Error("unsupported feature"));
const server = await createAgentMcpServer({ agentManager, agentStorage, logger });
const tool = registeredTool(server, "update_agent");
await expect(
tool.handler({
agentId: "agent-1",
name: "Should not persist",
labels: { role: "worker" },
settings: { features: { fast_mode: true } },
}),
).rejects.toThrow("unsupported feature");
expect(spies.agentStorage.get).not.toHaveBeenCalled();
expect(spies.agentStorage.upsert).not.toHaveBeenCalled();
expect(spies.agentManager.setLabels).not.toHaveBeenCalled();
});
});
describe("create_schedule MCP tool", () => {
@@ -2033,10 +2078,17 @@ describe("provider listing MCP tool", () => {
});
});
describe("model listing MCP tool", () => {
describe("provider MCP tools", () => {
const logger = createTestLogger();
it("lists provider features for a draft agent configuration", async () => {
it("does not register the replaced feature-specific provider discovery MCP tool", async () => {
const { agentManager, agentStorage } = createTestDeps();
const server = await createAgentMcpServer({ agentManager, agentStorage, logger });
expect(lookupTool(server, "list_provider_features")).toBeUndefined();
});
it("inspects provider features for a draft agent configuration", async () => {
const { agentManager, agentStorage, spies } = createTestDeps();
spies.agentManager.listDraftFeatures.mockResolvedValue([
{
@@ -2046,19 +2098,29 @@ describe("model listing MCP tool", () => {
value: false,
},
]);
const providerRegistry = {
codex: createProviderDefinition({
id: "codex",
label: "Codex",
description: "OpenAI coding agent",
modes: [{ id: "full-access", label: "Full Access", description: "Can edit files" }],
}),
};
const server = await createAgentMcpServer({
agentManager,
agentStorage,
providerRegistry,
logger,
});
const tool = registeredTool(server, "list_provider_features");
const tool = registeredTool(server, "inspect_provider");
const input = {
provider: "codex",
provider: "codex/gpt-5.4",
cwd: "~/repo",
modeId: "full-access",
model: "gpt-5.4",
thinkingOptionId: "high",
featureValues: { fast_mode: true },
settings: {
modeId: "full-access",
thinkingOptionId: "high",
features: { fast_mode: true },
},
};
const parsed = await tool.inputSchema.safeParseAsync(input);
@@ -2076,6 +2138,12 @@ describe("model listing MCP tool", () => {
});
expect(response.structuredContent).toEqual({
provider: "codex",
label: "Codex",
description: "OpenAI coding agent",
enabled: true,
status: "available",
modes: [{ id: "full-access", label: "Full Access", description: "Can edit files" }],
selectedModel: "gpt-5.4",
features: [
{
type: "toggle",
@@ -2118,6 +2186,38 @@ describe("model listing MCP tool", () => {
);
expect(fetchModels).not.toHaveBeenCalled();
});
it("inspect_provider rejects disabled providers without fetching models", async () => {
const { agentManager, agentStorage } = createTestDeps();
const fetchModels = vi.fn().mockResolvedValue([
{
provider: "codex",
id: "gpt-5.4",
label: "GPT-5.4",
},
]);
const providerRegistry = {
codex: createProviderDefinition({
id: "codex",
label: "Codex",
enabled: false,
fetchModels,
}),
};
const server = await createAgentMcpServer({
agentManager,
agentStorage,
providerRegistry,
logger,
});
const tool = registeredTool(server, "inspect_provider");
await expect(tool.handler({ provider: "codex", cwd: "~/repo" })).rejects.toThrow(
"Provider 'codex' is disabled",
);
expect(fetchModels).not.toHaveBeenCalled();
});
});
describe("speak MCP tool", () => {

View File

@@ -62,6 +62,7 @@ import {
AgentModelSchema,
AgentProviderEnum,
AgentStatusEnum,
ProviderModeSchema,
ProviderSummarySchema,
parseDurationString,
resolveRequiredProviderModel,
@@ -587,6 +588,55 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
},
{ message: "provider must be provider/model, for example codex/gpt-5.4" },
);
const ProviderOrProviderModelInputSchema = AgentProviderEnum.trim()
.min(1, "provider is required")
.refine(
(value) => {
if (!value.includes("/")) {
return true;
}
try {
resolveRequiredProviderModel(value);
return true;
} catch {
return false;
}
},
{ message: "provider must be provider or provider/model, for example codex/gpt-5.4" },
);
const CreateAgentSettingsInputSchema = z
.object({
modeId: z.string().optional().describe("Session mode to configure before the first run."),
thinkingOptionId: 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."),
})
.strict();
const UpdateAgentSettingsInputSchema = z
.object({
modeId: z.string().optional().describe("Session mode ID."),
model: z.string().nullable().optional().describe("Model ID. Pass null to clear."),
thinkingOptionId: z
.string()
.nullable()
.optional()
.describe("Thinking option ID. Pass null to clear."),
features: z
.record(z.unknown())
.optional()
.describe("Provider-specific feature values, for example { fast_mode: true } for Codex."),
})
.strict();
const InspectProviderSettingsInputSchema = z
.object({
modeId: z.string().optional().describe("Draft session mode ID."),
model: z.string().optional().describe("Draft model ID."),
thinkingOptionId: z.string().optional().describe("Draft thinking option ID."),
features: z.record(z.unknown()).optional().describe("Draft provider feature values."),
})
.strict();
const agentToAgentInputSchema = {
cwd: z
.string()
@@ -601,23 +651,15 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
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"),
settings: CreateAgentSettingsInputSchema.optional().describe(
"Initial runtime settings for the new 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()
@@ -647,21 +689,15 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
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"),
settings: CreateAgentSettingsInputSchema.optional().describe(
"Initial runtime settings for the new 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()
@@ -700,13 +736,17 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
const createAgentInputSchema = callerAgentId ? agentToAgentInputSchema : topLevelInputSchema;
const agentToAgentCreateAgentArgsSchema = z.object(agentToAgentInputSchema).strict();
const topLevelCreateAgentArgsSchema = z.object(topLevelInputSchema).strict();
const listProviderFeaturesInputSchema = {
provider: AgentProviderEnum,
cwd: z.string().describe("Working directory used to resolve provider feature availability."),
modeId: z.string().optional(),
model: z.string().optional(),
thinkingOptionId: z.string().optional(),
featureValues: z.record(z.unknown()).optional(),
const inspectProviderInputSchema = {
provider: ProviderOrProviderModelInputSchema.describe(
"Provider ID, optionally with a model ID (for example codex or codex/gpt-5.4).",
),
cwd: z
.string()
.optional()
.describe("Working directory used to resolve provider feature availability."),
settings: InspectProviderSettingsInputSchema.optional().describe(
"Draft provider settings used to compute available features.",
),
};
if (options.voiceOnly || options.enableVoiceTools || callerContext?.enableVoiceTools) {
@@ -758,7 +798,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
background: boolean;
normalizedTitle: string | null;
model: string | undefined;
thinking: string | undefined;
thinkingOptionId: string | undefined;
features: Record<string, unknown> | undefined;
labels: Record<string, string> | undefined;
notifyOnFinish: boolean;
@@ -805,6 +845,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
throw new Error(`Parent agent ${parentAgentId} not found`);
}
const provider = resolvedProviderModel.provider;
const settings = callerArgs.settings;
const resolvedCwd = resolveChildAgentCwd({
parentCwd: parentAgent.cwd,
requestedCwd: callerArgs.cwd,
@@ -812,7 +853,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
allowCustomCwd: callerContext?.allowCustomCwd ?? true,
});
const resolvedMode = resolveAndValidateCreateAgentMode({
requestedMode: callerArgs.mode,
requestedMode: settings?.modeId,
targetProvider: provider,
parent: {
provider: parentAgent.provider,
@@ -828,8 +869,8 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
background: callerArgs.background ?? false,
normalizedTitle: callerArgs.title.trim(),
model: resolvedProviderModel.model,
thinking: callerArgs.thinking,
features: callerArgs.features,
thinkingOptionId: settings?.thinkingOptionId,
features: settings?.features,
labels: callerArgs.labels,
notifyOnFinish: callerArgs.notifyOnFinish ?? false,
resolvedCwd,
@@ -843,9 +884,10 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
): Promise<ResolvedCreateAgentArgs> => {
const topLevelArgs = topLevelCreateAgentArgsSchema.parse(args);
const resolvedProviderModel = resolveRequiredProviderModel(topLevelArgs.provider);
const { cwd, mode, worktreeName, baseBranch, refName, action, githubPrNumber } = topLevelArgs;
const { cwd, settings, worktreeName, baseBranch, refName, action, githubPrNumber } =
topLevelArgs;
const resolvedMode = resolveAndValidateCreateAgentMode({
requestedMode: mode,
requestedMode: settings?.modeId,
targetProvider: resolvedProviderModel.provider,
parent: null,
availableModes: getAvailableModeIds(resolvedProviderModel.provider),
@@ -902,8 +944,8 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
background: topLevelArgs.background ?? false,
normalizedTitle: topLevelArgs.title.trim(),
model: resolvedProviderModel.model,
thinking: topLevelArgs.thinking,
features: topLevelArgs.features,
thinkingOptionId: settings?.thinkingOptionId,
features: settings?.features,
labels: topLevelArgs.labels,
notifyOnFinish: topLevelArgs.notifyOnFinish ?? false,
resolvedCwd,
@@ -946,7 +988,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
background,
normalizedTitle,
model,
thinking,
thinkingOptionId,
features,
labels,
notifyOnFinish,
@@ -968,7 +1010,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
modeId: resolvedMode,
title: normalizedTitle ?? undefined,
model,
thinkingOptionId: thinking,
thinkingOptionId,
featureValues: features,
},
undefined,
@@ -1057,29 +1099,6 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
},
);
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",
{
@@ -1440,17 +1459,35 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
"update_agent",
{
title: "Update agent",
description: "Update an agent name and/or labels.",
description: "Update an agent name, labels, and/or runtime settings.",
inputSchema: {
agentId: z.string(),
name: z.string().optional(),
labels: z.record(z.string(), z.string()).optional().describe("Labels to set on the agent"),
settings: UpdateAgentSettingsInputSchema.optional().describe(
"Runtime settings to apply to the agent.",
),
},
outputSchema: {
success: z.boolean(),
},
},
async ({ agentId, name, labels }) => {
async ({ agentId, name, labels, settings }) => {
if (settings?.modeId !== undefined) {
await agentManager.setAgentMode(agentId, settings.modeId);
}
if (settings?.model !== undefined) {
await agentManager.setAgentModel(agentId, settings.model);
}
if (settings?.thinkingOptionId !== undefined) {
await agentManager.setAgentThinkingOption(agentId, settings.thinkingOptionId);
}
if (settings?.features) {
for (const [featureId, value] of Object.entries(settings.features)) {
await agentManager.setAgentFeature(agentId, featureId, value);
}
}
const trimmedName = name?.trim();
if (trimmedName) {
const record = await agentStorage.get(agentId);
@@ -2022,30 +2059,63 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
);
server.registerTool(
"list_provider_features",
"inspect_provider",
{
title: "List provider features",
title: "Inspect provider",
description:
"List provider-specific features available for a draft agent configuration, such as Codex fast_mode.",
inputSchema: listProviderFeaturesInputSchema,
"Inspect compact provider capabilities for orchestration, including modes and draft feature settings. Use list_models for the full model list.",
inputSchema: inspectProviderInputSchema,
outputSchema: {
provider: AgentProviderEnum,
label: z.string().nullable().optional(),
description: z.string().nullable().optional(),
enabled: z.boolean(),
status: z.string(),
modes: z.array(ProviderModeSchema).nullish(),
selectedModel: z.string().nullable(),
features: z.array(AgentFeatureSchema),
},
},
async ({ provider, cwd, modeId, model, thinkingOptionId, featureValues }) => {
const features = await agentManager.listDraftFeatures({
async ({ provider, cwd, settings }) => {
const resolvedProviderModel = resolveScheduleProviderAndModel({
provider,
cwd: expandUserPath(cwd),
...(modeId ? { modeId } : {}),
...(model ? { model } : {}),
...(thinkingOptionId ? { thinkingOptionId } : {}),
...(featureValues ? { featureValues } : {}),
defaultProvider: provider,
});
const providerId = resolvedProviderModel.provider;
if (!providerRegistry) {
throw new Error("Provider registry is not configured");
}
const definition = providerRegistry[providerId];
if (!definition) {
throw new Error(`Provider ${providerId} is not configured`);
}
const summary = await resolveProviderSummary(definition, childLogger);
if (!definition.enabled) {
throw new Error(`Provider '${providerId}' is disabled`);
}
if (summary.status !== "available") {
throw new Error(summary.error ?? `Provider '${providerId}' is unavailable`);
}
const resolvedCwd = resolveScopedCwd(cwd, { required: true });
const selectedModel = settings?.model ?? resolvedProviderModel.model;
const features = await agentManager.listDraftFeatures({
provider: providerId,
cwd: resolvedCwd,
...(settings?.modeId ? { modeId: settings.modeId } : {}),
...(selectedModel ? { model: selectedModel } : {}),
...(settings?.thinkingOptionId ? { thinkingOptionId: settings.thinkingOptionId } : {}),
...(settings?.features ? { featureValues: settings.features } : {}),
});
return {
content: [],
structuredContent: ensureValidJson({
provider,
provider: providerId,
label: summary.label,
description: summary.description,
enabled: summary.enabled,
status: summary.status,
modes: summary.modes,
selectedModel: selectedModel ?? null,
features,
}),
};

View File

@@ -15,20 +15,19 @@ The MCP server itself is controlled by `daemon.mcp.enabled`. Existing agents may
### Agents
| Tool | Function |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `create_agent` | Create an agent tied to a working directory, optionally with an initial prompt, provider features, or a new git worktree. |
| `wait_for_agent` | Block until an agent requests permission or finishes its current run. |
| `send_agent_prompt` | Send a task to a running agent. |
| `get_agent_status` | Return the latest snapshot for an agent. |
| `list_agents` | List recent agents as compact metadata. |
| `cancel_agent` | Abort an agent's current run but keep the agent alive. |
| `archive_agent` | Soft-delete an agent and remove it from the active list. |
| `kill_agent` | Terminate an agent session permanently. |
| `update_agent` | Update an agent name or labels. |
| `get_agent_activity` | Return recent agent timeline entries as a curated summary. |
| `set_agent_mode` | Switch an agent's session mode. |
| `set_agent_feature` | Set a provider-specific feature on an existing agent, for example Codex `fast_mode`. |
| Tool | Function |
| -------------------- | ---------------------------------------------------------------------------------------------------- |
| `create_agent` | Create an agent tied to a working directory, optionally with initial settings or a new git worktree. |
| `wait_for_agent` | Block until an agent requests permission or finishes its current run. |
| `send_agent_prompt` | Send a task to a running agent. |
| `get_agent_status` | Return the latest snapshot for an agent. |
| `list_agents` | List recent agents as compact metadata. |
| `cancel_agent` | Abort an agent's current run but keep the agent alive. |
| `archive_agent` | Soft-delete an agent and remove it from the active list. |
| `kill_agent` | Terminate an agent session permanently. |
| `update_agent` | Update an agent name, labels, or runtime settings such as mode/model/thinking/features. |
| `get_agent_activity` | Return recent agent timeline entries as a curated summary. |
| `set_agent_mode` | Switch an agent's session mode. |
### Terminals
@@ -53,11 +52,11 @@ The MCP server itself is controlled by `daemon.mcp.enabled`. Existing agents may
### Providers
| Tool | Function |
| ------------------------ | ------------------------------------------------------------------------------------------- |
| `list_providers` | List configured agent providers, availability, and modes. |
| `list_models` | List models for an agent provider. |
| `list_provider_features` | List provider-specific features for a draft agent configuration, such as Codex `fast_mode`. |
| Tool | Function |
| ------------------ | ----------------------------------------------------------------- |
| `list_providers` | List configured agent providers, availability, and modes. |
| `list_models` | List models for an agent provider. |
| `inspect_provider` | Inspect compact provider capabilities and draft feature settings. |
### Worktrees

View File

@@ -20,25 +20,29 @@ Returns `{ branchName, worktreePath }`. Pass `cwd` to target a specific repo.
## Agents
**`create_agent`** — required: `title`, `provider` (`claude/opus`, `codex/gpt-5.4`, …), `initialPrompt`. Common: `cwd` (often a `worktreePath`), `background` (default `false` — blocks until completion or permission), `notifyOnFinish`, `features`. Returns `{ agentId, … }`.
**`create_agent`** — required: `title`, `provider` (`claude/opus`, `codex/gpt-5.4`, …), `initialPrompt`. Common: `cwd` (often a `worktreePath`), `background` (default `false` — blocks until completion or permission), `notifyOnFinish`, `settings`. Returns `{ agentId, … }`.
Provider features are provider-specific. For Codex fast mode, pass `features: { "fast_mode": true }` when creating the agent.
Initial runtime settings live under `settings`: `modeId`, `thinkingOptionId`, and provider-specific `features`. For Codex fast mode, pass `settings: { features: { "fast_mode": true } }` when creating the agent.
Compose: call `create_worktree` first, then `create_agent` with `cwd` set to the returned `worktreePath`.
**`send_agent_prompt`** — `{ agentId, prompt }`. Blocks by default; pass `background: true` to fire-and-forget.
**`set_agent_feature`** — `{ agentId, featureId, value }`. Use for provider-specific toggles on an existing agent, for example `{ agentId, featureId: "fast_mode", value: true }` for Codex.
**`update_agent`** — `{ agentId, name?, labels?, settings? }`. Use `settings` for runtime changes on an existing agent: `modeId`, `model`, `thinkingOptionId`, and provider-specific `features`. For Codex fast mode, pass `settings: { features: { "fast_mode": true } }`.
**`list_agents`** — filter by `cwd`, `statuses`, `sinceHours`, `includeArchived`.
**`archive_agent`** — `{ agentId }`. Interrupts if running, removes from active list.
## Provider features
## Provider discovery
**`list_provider_features`** — query provider-specific features before setting them. Required: `provider`, `cwd`. Optional: `model`, `modeId`, `thinkingOptionId`, `featureValues`.
**`list_providers`** — compact provider availability and modes.
Only set feature IDs returned by `list_provider_features`. For Codex fast mode, look for `fast_mode` and pass `features: { "fast_mode": true }` to `create_agent`.
**`list_models`** — full model list for one provider. Use only when you need model IDs or thinking options; the list can be large.
**`inspect_provider`** — compact provider capability and feature inspection. Required: `provider`; pass `cwd` when you are not in an agent-scoped session. Optional: `settings` with draft `model`, `modeId`, `thinkingOptionId`, and `features`.
Only set feature IDs returned by `inspect_provider`. For Codex fast mode, look for `fast_mode` and pass `settings: { features: { "fast_mode": true } }` to `create_agent` or `update_agent`.
## Heartbeats