Compare commits

...

3 Commits

Author SHA1 Message Date
Mohamed Boudra
8d8bd6ec46 chore: retrigger CI 2026-05-13 19:11:38 +07:00
zbl
bfb8f5b640 test: cover provider feature discovery over MCP e2e 2026-05-13 19:10:38 +07:00
zbl
2b7002a878 Expose provider feature discovery through MCP 2026-05-13 19:10:38 +07:00
5 changed files with 123 additions and 4 deletions

View File

@@ -390,6 +390,26 @@ describe("Suite A: Core Fixes", () => {
}
});
test("list_provider_features returns draft provider features over MCP", async () => {
const payload = await callToolStructured(topLevelClient, "list_provider_features", {
provider: "claude",
cwd: parentAgentCwd,
model: "claude-test-model",
featureValues: { test_feature: true },
});
expect(payload.provider).toBe("claude");
expect(recordArr(payload.features)).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: "toggle",
id: "test_feature",
value: true,
}),
]),
);
});
test("create_agent accepts labels param", async () => {
let agentId: string | null = null;
try {

View File

@@ -133,6 +133,7 @@ function buildAgentManagerSpies() {
cancelAgentRun: vi.fn(),
getPendingPermissions: vi.fn(),
getRegisteredProviderIds: vi.fn().mockReturnValue(["claude"]),
listDraftFeatures: vi.fn(),
};
}
@@ -1793,6 +1794,57 @@ describe("provider listing MCP tool", () => {
describe("model listing MCP tool", () => {
const logger = createTestLogger();
it("lists provider features for a draft agent configuration", async () => {
const { agentManager, agentStorage, spies } = createTestDeps();
spies.agentManager.listDraftFeatures.mockResolvedValue([
{
type: "toggle",
id: "fast_mode",
label: "Fast mode",
value: false,
},
]);
const server = await createAgentMcpServer({
agentManager,
agentStorage,
logger,
});
const tool = registeredTool(server, "list_provider_features");
const input = {
provider: "codex",
cwd: "~/repo",
modeId: "full-access",
model: "gpt-5.4",
thinkingOptionId: "high",
featureValues: { fast_mode: true },
};
const parsed = await tool.inputSchema.safeParseAsync(input);
expect(parsed.success).toBe(true);
const response = await tool.handler(input);
expect(spies.agentManager.listDraftFeatures).toHaveBeenCalledWith({
provider: "codex",
cwd: expect.stringContaining("repo"),
modeId: "full-access",
model: "gpt-5.4",
thinkingOptionId: "high",
featureValues: { fast_mode: true },
});
expect(response.structuredContent).toEqual({
provider: "codex",
features: [
{
type: "toggle",
id: "fast_mode",
label: "Fast mode",
value: false,
},
],
});
});
it("rejects disabled providers without fetching models", async () => {
const { agentManager, agentStorage } = createTestDeps();
const fetchModels = vi.fn().mockResolvedValue([

View File

@@ -8,6 +8,7 @@ import type { ServerNotification, ServerRequest } from "@modelcontextprotocol/sd
import type { AgentProvider } from "./agent-sdk-types.js";
import type { AgentManager, WaitForAgentResult } from "./agent-manager.js";
import {
AgentFeatureSchema,
AgentPermissionRequestPayloadSchema,
AgentListItemPayloadSchema,
AgentPermissionResponseSchema,
@@ -587,6 +588,14 @@ 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(),
};
if (options.voiceOnly || options.enableVoiceTools || callerContext?.enableVoiceTools) {
server.registerTool(
@@ -1813,6 +1822,37 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
},
);
server.registerTool(
"list_provider_features",
{
title: "List provider features",
description:
"List provider-specific features available for a draft agent configuration, such as Codex fast_mode.",
inputSchema: listProviderFeaturesInputSchema,
outputSchema: {
provider: AgentProviderEnum,
features: z.array(AgentFeatureSchema),
},
},
async ({ provider, cwd, modeId, model, thinkingOptionId, featureValues }) => {
const features = await agentManager.listDraftFeatures({
provider,
cwd: expandUserPath(cwd),
...(modeId ? { modeId } : {}),
...(model ? { model } : {}),
...(thinkingOptionId ? { thinkingOptionId } : {}),
...(featureValues ? { featureValues } : {}),
});
return {
content: [],
structuredContent: ensureValidJson({
provider,
features,
}),
};
},
);
server.registerTool(
"list_worktrees",
{

View File

@@ -53,10 +53,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. |
| 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`. |
### Worktrees

View File

@@ -34,6 +34,12 @@ Compose: call `create_worktree` first, then `create_agent` with `cwd` set to the
**`archive_agent`** — `{ agentId }`. Interrupts if running, removes from active list.
## Provider features
**`list_provider_features`** — query provider-specific features before setting them. Required: `provider`, `cwd`. Optional: `model`, `modeId`, `thinkingOptionId`, `featureValues`.
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`.
## Heartbeats
**`create_schedule`** — required: `prompt`. Pick one of `cron` or `every` (`"5m"`, `"1h"`). Optional: `name`, `target` (`self` | `new-agent`), `provider`, `maxRuns`, `expiresIn`. Use for periodic checks on long-running work or recurring maintenance.