From b3f44981a86c78e2bf2cd397c77ca6a572aa5de1 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 9 Jun 2026 20:22:54 +0700 Subject: [PATCH] Handle ACP providers without MCP support --- docs/custom-providers.md | 44 ++++-- packages/app/src/data/acp-provider-catalog.ts | 3 + .../hooks/use-acp-provider-catalog.test.ts | 8 + .../app/src/hooks/use-acp-provider-catalog.ts | 1 + .../server/agent/provider-registry.test.ts | 69 +++++++- .../src/server/agent/provider-registry.ts | 2 + .../server/agent/providers/acp-agent.test.ts | 48 ++++++ .../src/server/agent/providers/acp-agent.ts | 147 +++++++++++++----- .../agent/providers/cursor-acp-agent.ts | 2 + .../agent/providers/generic-acp-agent.test.ts | 39 +++++ .../agent/providers/generic-acp-agent.ts | 26 +++- 11 files changed, 332 insertions(+), 57 deletions(-) diff --git a/docs/custom-providers.md b/docs/custom-providers.md index b8b43f72c..4e166bbd1 100644 --- a/docs/custom-providers.md +++ b/docs/custom-providers.md @@ -438,6 +438,25 @@ Required fields for ACP providers: - `label` - `command` — the command to spawn the agent process (must support ACP over stdio) +By default, Paseo injects its internal MCP server into ACP providers so agents can use Paseo tools such as subagent creation. Some ACP adapters cannot create sessions when `mcpServers` is non-empty. Disable injected MCP for those providers with `params.supportsMcpServers: false`: + +```json +{ + "agents": { + "providers": { + "my-agent": { + "extends": "acp", + "label": "My Agent", + "command": ["my-agent", "acp"], + "params": { + "supportsMcpServers": false + } + } + } + } +} +``` + ### Generic ACP diagnostics Paseo diagnostics for `extends: "acp"` providers report the configured command, resolved launcher binary, version output, ACP `initialize`, ACP `session/new`, model count, modes, and final status. @@ -574,18 +593,19 @@ When an `additionalModels` entry has the same `id` as a discovered model, it upd Every entry under `agents.providers` accepts these fields: -| Field | Type | Required | Description | -| ------------------ | ------------------------ | ----------------- | ------------------------------------------------------------------ | -| `extends` | `string` | Yes (custom only) | Built-in provider ID to inherit from, or `"acp"` | -| `label` | `string` | Yes (custom only) | Display name in the UI | -| `description` | `string` | No | Short description shown in the UI | -| `command` | `string[]` | Yes (ACP only) | Command to spawn the agent process | -| `env` | `Record` | No | Environment variables to set for the agent process | -| `models` | `ProviderProfileModel[]` | No | Static model list (overrides runtime discovery) | -| `additionalModels` | `ProviderProfileModel[]` | No | Static model additions (merged with runtime discovery or `models`) | -| `disallowedTools` | `string[]` | No | Tool names to disable for this provider (e.g. `["WebSearch"]`) | -| `enabled` | `boolean` | No | Set to `false` to hide the provider (default: `true`) | -| `order` | `number` | No | Sort order in the provider list | +| Field | Type | Required | Description | +| ------------------ | ------------------------- | ----------------- | ------------------------------------------------------------------ | +| `extends` | `string` | Yes (custom only) | Built-in provider ID to inherit from, or `"acp"` | +| `label` | `string` | Yes (custom only) | Display name in the UI | +| `description` | `string` | No | Short description shown in the UI | +| `command` | `string[]` | Yes (ACP only) | Command to spawn the agent process | +| `env` | `Record` | No | Environment variables to set for the agent process | +| `params` | `Record` | No | Provider-specific options such as `supportsMcpServers: false` | +| `models` | `ProviderProfileModel[]` | No | Static model list (overrides runtime discovery) | +| `additionalModels` | `ProviderProfileModel[]` | No | Static model additions (merged with runtime discovery or `models`) | +| `disallowedTools` | `string[]` | No | Tool names to disable for this provider (e.g. `["WebSearch"]`) | +| `enabled` | `boolean` | No | Set to `false` to hide the provider (default: `true`) | +| `order` | `number` | No | Sort order in the provider list | ### Model definition diff --git a/packages/app/src/data/acp-provider-catalog.ts b/packages/app/src/data/acp-provider-catalog.ts index ac8ef6d36..cd817cac9 100644 --- a/packages/app/src/data/acp-provider-catalog.ts +++ b/packages/app/src/data/acp-provider-catalog.ts @@ -9,6 +9,7 @@ export interface AcpProviderCatalogEntry { installLink: string; command: readonly [string, ...string[]]; env?: Readonly>; + params?: Readonly>; } const CATALOG_DATA = [ @@ -166,6 +167,7 @@ const CATALOG_DATA = [ DROID_DISABLE_AUTO_UPDATE: "true", FACTORY_DROID_AUTO_UPDATE_ENABLED: "false", }, + params: { supportsMcpServers: false }, }, { id: "fast-agent", @@ -357,5 +359,6 @@ export const ACP_PROVIDER_CATALOG: AcpProviderCatalogEntry[] = CATALOG_DATA.map( installLink: entry.installLink, command: entry.command, env: "env" in entry ? entry.env : undefined, + params: "params" in entry ? entry.params : undefined, iconSvg: entry.iconId ? (ACP_PROVIDER_ICON_SVGS[entry.iconId] ?? null) : null, })); diff --git a/packages/app/src/hooks/use-acp-provider-catalog.test.ts b/packages/app/src/hooks/use-acp-provider-catalog.test.ts index d9c451d4b..68aa4057f 100644 --- a/packages/app/src/hooks/use-acp-provider-catalog.test.ts +++ b/packages/app/src/hooks/use-acp-provider-catalog.test.ts @@ -66,4 +66,12 @@ describe("ACP provider catalog", () => { AUGMENT_DISABLE_AUTO_UPDATE: "1", }); }); + + it("preserves provider params in the daemon config patch", () => { + const droidPatch = buildAcpProviderConfigPatch(findProvider("factory-droid")); + + expect(droidPatch.providers?.["factory-droid"]?.params).toEqual({ + supportsMcpServers: false, + }); + }); }); diff --git a/packages/app/src/hooks/use-acp-provider-catalog.ts b/packages/app/src/hooks/use-acp-provider-catalog.ts index 6e184ff7d..92f9ca07e 100644 --- a/packages/app/src/hooks/use-acp-provider-catalog.ts +++ b/packages/app/src/hooks/use-acp-provider-catalog.ts @@ -19,6 +19,7 @@ export function buildAcpProviderConfigPatch( description: entry.description, command: [...entry.command], env: entry.env ? { ...entry.env } : {}, + ...(entry.params ? { params: { ...entry.params } } : {}), }, }, }; diff --git a/packages/server/src/server/agent/provider-registry.test.ts b/packages/server/src/server/agent/provider-registry.test.ts index ad612bd4a..6f3846e2c 100644 --- a/packages/server/src/server/agent/provider-registry.test.ts +++ b/packages/server/src/server/agent/provider-registry.test.ts @@ -17,6 +17,7 @@ const mockState = vi.hoisted(() => { cursor: [] as Array<{ command: string[]; env?: Record; + providerParams?: unknown; }>, pi: [] as ConstructorEntry[], genericAcp: [] as Array<{ @@ -24,6 +25,7 @@ const mockState = vi.hoisted(() => { env?: Record; providerId?: string; label?: string; + providerParams?: unknown; }>, }, isCommandAvailable: vi.fn(async (_command: string) => false), @@ -239,7 +241,7 @@ vi.mock("./providers/pi/agent.js", () => ({ vi.mock("./providers/generic-acp-agent.js", () => ({ GenericACPAgentClient: class GenericACPAgentClient { - readonly capabilities = { + capabilities = { supportsStreaming: true, supportsSessionPersistence: true, supportsDynamicModes: true, @@ -255,7 +257,21 @@ vi.mock("./providers/generic-acp-agent.js", () => ({ env?: Record; providerId?: string; label?: string; + providerParams?: unknown; }) { + const providerParams = + options.providerParams && + typeof options.providerParams === "object" && + !Array.isArray(options.providerParams) + ? (options.providerParams as Record) + : {}; + this.capabilities = { + ...this.capabilities, + supportsMcpServers: + typeof providerParams.supportsMcpServers === "boolean" + ? providerParams.supportsMcpServers + : this.capabilities.supportsMcpServers, + }; this.runtimeSettings = { command: { mode: "replace", @@ -268,6 +284,7 @@ vi.mock("./providers/generic-acp-agent.js", () => ({ env: options.env, providerId: options.providerId, label: options.label, + providerParams: options.providerParams, }); } @@ -306,7 +323,11 @@ vi.mock("./providers/cursor-acp-agent.js", () => ({ readonly provider = "acp"; readonly runtimeSettings?: unknown; - constructor(options: { command: string[]; env?: Record }) { + constructor(options: { + command: string[]; + env?: Record; + providerParams?: unknown; + }) { this.runtimeSettings = { command: { mode: "replace", @@ -317,6 +338,7 @@ vi.mock("./providers/cursor-acp-agent.js", () => ({ mockState.constructorArgs.cursor.push({ command: options.command, env: options.env, + providerParams: options.providerParams, }); } @@ -517,6 +539,7 @@ test("new provider extending acp uses GenericACPAgentClient", () => { }, providerId: "my-agent", label: "My Agent", + providerParams: undefined, }, { command: ["my-agent", "--acp"], @@ -525,6 +548,46 @@ test("new provider extending acp uses GenericACPAgentClient", () => { }, providerId: "my-agent", label: "My Agent", + providerParams: undefined, + }, + ]); +}); + +test("ACP provider params can disable MCP support", () => { + const registry = buildProviderRegistry(logger, { + providerOverrides: { + "no-mcp-acp": { + extends: "acp", + label: "No MCP ACP", + command: ["no-mcp-acp", "serve"], + params: { + supportsMcpServers: false, + }, + }, + }, + }); + + const client = registry["no-mcp-acp"].createClient(logger); + + expect(client.capabilities.supportsMcpServers).toBe(false); + expect(mockState.constructorArgs.genericAcp).toEqual([ + { + command: ["no-mcp-acp", "serve"], + env: undefined, + providerId: "no-mcp-acp", + label: "No MCP ACP", + providerParams: { + supportsMcpServers: false, + }, + }, + { + command: ["no-mcp-acp", "serve"], + env: undefined, + providerId: "no-mcp-acp", + label: "No MCP ACP", + providerParams: { + supportsMcpServers: false, + }, }, ]); }); @@ -550,12 +613,14 @@ test("cursor provider extending acp uses CursorACPAgentClient", () => { env: { CURSOR_AGENT_LOG: "debug", }, + providerParams: undefined, }, { command: ["cursor-agent", "acp"], env: { CURSOR_AGENT_LOG: "debug", }, + providerParams: undefined, }, ]); expect(mockState.constructorArgs.genericAcp).toEqual([]); diff --git a/packages/server/src/server/agent/provider-registry.ts b/packages/server/src/server/agent/provider-registry.ts index ea48817f7..8e097ca28 100644 --- a/packages/server/src/server/agent/provider-registry.ts +++ b/packages/server/src/server/agent/provider-registry.ts @@ -602,6 +602,7 @@ function addDerivedProviders( env: override.env, providerId, label: override.label ?? providerId, + providerParams: override.params, }) : new GenericACPAgentClient({ logger, @@ -609,6 +610,7 @@ function addDerivedProviders( env: override.env, providerId, label: override.label ?? providerId, + providerParams: override.params, }), }); continue; diff --git a/packages/server/src/server/agent/providers/acp-agent.test.ts b/packages/server/src/server/agent/providers/acp-agent.test.ts index 3f87abc86..8b42e12c4 100644 --- a/packages/server/src/server/agent/providers/acp-agent.test.ts +++ b/packages/server/src/server/agent/providers/acp-agent.test.ts @@ -26,6 +26,7 @@ import { mapACPUsage, resolveACPModeSelection, resolveACPModelSelection, + summarizeACPRequestError, } from "./acp-agent.js"; import { COPILOT_ALLOW_ALL_MODE_ID, @@ -48,6 +49,7 @@ interface ACPSessionInternals { activeForegroundTurnId: string | null; configOptions: SessionConfigOption[]; translateSessionUpdate(update: SessionUpdate): AgentStreamEvent[]; + acpMcpServers(): unknown[]; } interface ACPModelSelectionInternals { @@ -1514,6 +1516,52 @@ describe("ACPAgentSession slash commands", () => { }); describe("ACPAgentSession", () => { + test("drops MCP servers from ACP requests when the provider does not support MCP", () => { + const session = new ACPAgentSession( + { + provider: "no-mcp-acp", + cwd: "/tmp/paseo-acp-test", + mcpServers: { + paseo: { + type: "http", + url: "http://127.0.0.1:6767/mcp/agents?callerAgentId=agent-1", + }, + }, + }, + { + provider: "no-mcp-acp", + logger: createTestLogger(), + defaultCommand: ["no-mcp-acp", "serve"], + defaultModes: [], + capabilities: { + supportsStreaming: true, + supportsSessionPersistence: true, + supportsDynamicModes: true, + supportsMcpServers: false, + supportsReasoningStream: true, + supportsToolInvocations: true, + }, + }, + ); + + expect(asInternals(session).acpMcpServers()).toEqual([]); + }); + + test("summarizes JSON-RPC error details without stringifying objects", () => { + const summary = summarizeACPRequestError( + new RequestError(-32603, "Internal error", { + details: "Droid process exited unexpectedly (exit code 1)", + }), + ); + + expect(summary).toMatchObject({ + message: "Internal error: Droid process exited unexpectedly (exit code 1)", + code: "-32603", + }); + expect(summary.message).not.toContain("[object Object]"); + expect(summary.diagnostic).toContain("Droid process exited unexpectedly"); + }); + test("accepts ACP extension notifications without failing the JSON-RPC connection", async () => { const logger = createTestLogger(); const trace = vi.spyOn(logger, "trace"); diff --git a/packages/server/src/server/agent/providers/acp-agent.ts b/packages/server/src/server/agent/providers/acp-agent.ts index 7dec971a8..5733abf54 100644 --- a/packages/server/src/server/agent/providers/acp-agent.ts +++ b/packages/server/src/server/agent/providers/acp-agent.ts @@ -112,7 +112,22 @@ function isACPError(value: unknown): value is ACPError { return isRecord(value) && typeof value.message === "string" && typeof value.code === "number"; } -function summarizeACPRequestError(error: unknown): { +function extractACPErrorDataMessage(data: unknown): string | null { + if (!isRecord(data)) { + return null; + } + + for (const key of ["details", "errorMessage", "message", "detail", "title"]) { + const value = data[key]; + if (typeof value === "string" && value.trim()) { + return value.trim(); + } + } + + return extractACPErrorDataMessage(data.error); +} + +export function summarizeACPRequestError(error: unknown): { message: string; code?: string; diagnostic?: string; @@ -120,11 +135,14 @@ function summarizeACPRequestError(error: unknown): { // Promise rejections are untyped, but the ACP SDK rejects JSON-RPC failures as response.error. if (isACPError(error)) { const code = String(error.code); + const detail = extractACPErrorDataMessage(error.data); + const message = + detail && detail !== error.message ? `${error.message}: ${detail}` : error.message; const data = error.data === undefined ? "" : ` | data=${JSON.stringify(error.data)}`; return { - message: error.message, + message, code, - diagnostic: `${error.message} | code=${code}${data}`, + diagnostic: `${message} | code=${code}${data}`, }; } @@ -135,6 +153,17 @@ function summarizeACPRequestError(error: unknown): { return { message: String(error) }; } +function toACPRequestError(error: unknown): Error { + if (!isACPError(error)) { + return error instanceof Error ? error : new Error(String(error)); + } + + const summary = summarizeACPRequestError(error); + const next = new Error(summary.message); + next.name = "ACPRequestError"; + return next; +} + function resolveTerminalCommand( command: string, args?: string[], @@ -151,7 +180,7 @@ function resolveTerminalCommand( return { command: shell.command, args: [...shell.flag, command] }; } -const DEFAULT_ACP_CAPABILITIES: AgentCapabilityFlags = { +export const DEFAULT_ACP_CAPABILITIES: AgentCapabilityFlags = { supportsStreaming: true, supportsSessionPersistence: true, supportsDynamicModes: true, @@ -673,10 +702,12 @@ export class ACPAgentClient implements AgentClient { const { cwd } = options; const probe = await this.spawnProcess(PROBE_ENV); try { - const response = await probe.connection.newSession({ - cwd, - mcpServers: [], - }); + const response = await this.runACPRequest(() => + probe.connection.newSession({ + cwd, + mcpServers: [], + }), + ); const transformed = this.transformSessionResponse(response); const models = deriveModelDefinitionsFromACP( this.provider, @@ -693,10 +724,12 @@ export class ACPAgentClient implements AgentClient { const { cwd } = options; const probe = await this.spawnProcess(PROBE_ENV); try { - const response = await probe.connection.newSession({ - cwd, - mcpServers: [], - }); + const response = await this.runACPRequest(() => + probe.connection.newSession({ + cwd, + mcpServers: [], + }), + ); const transformed = this.transformSessionResponse(response); const modeInfo = deriveModesFromACP( this.defaultModes, @@ -721,8 +754,8 @@ export class ACPAgentClient implements AgentClient { const sessions: PersistedAgentDescriptor[] = []; let cursor: string | null | undefined; for (;;) { - const page: ListSessionsResponse = await probe.connection.listSessions( - cursor ? { cursor } : {}, + const page: ListSessionsResponse = await this.runACPRequest(() => + probe.connection.listSessions(cursor ? { cursor } : {}), ); for (const session of page.sessions) { sessions.push({ @@ -809,15 +842,17 @@ export class ACPAgentClient implements AgentClient { let initialize: InitializeResponse; try { - initialize = await Promise.race([ - connection.initialize({ - protocolVersion: PROTOCOL_VERSION, - clientCapabilities: ACP_CLIENT_CAPABILITIES, - clientInfo: { name: "Paseo", version: "dev" }, - }), - spawnErrorPromise, - ...(initializeTimeoutPromise ? [initializeTimeoutPromise] : []), - ]); + initialize = await this.runACPRequest(() => + Promise.race([ + connection.initialize({ + protocolVersion: PROTOCOL_VERSION, + clientCapabilities: ACP_CLIENT_CAPABILITIES, + clientInfo: { name: "Paseo", version: "dev" }, + }), + spawnErrorPromise, + ...(initializeTimeoutPromise ? [initializeTimeoutPromise] : []), + ]), + ); } catch (error) { await terminateChildProcess(child, 2_000); throw error; @@ -861,6 +896,14 @@ export class ACPAgentClient implements AgentClient { } } + protected async runACPRequest(request: () => Promise): Promise { + try { + return await request(); + } catch (error) { + throw toACPRequestError(error); + } + } + protected async resolveLaunchCommand(): Promise<{ command: string; args: string[] }> { const prefix = await resolveProviderLaunch({ commandConfig: this.runtimeSettings?.command, @@ -998,10 +1041,12 @@ export class ACPAgentSession implements AgentSession, ACPClient { this.connection = spawned.connection; this.agentCapabilities = spawned.initialize.agentCapabilities ?? null; - const response = await this.connection.newSession({ - cwd: this.config.cwd, - mcpServers: normalizeMcpServers(this.config.mcpServers), - }); + const response = await this.runACPRequest(() => + this.connection!.newSession({ + cwd: this.config.cwd, + mcpServers: this.acpMcpServers(), + }), + ); this.sessionId = response.sessionId; this.bootstrapThreadEventPending = true; this.applySessionState(response); @@ -1024,20 +1069,24 @@ export class ACPAgentSession implements AgentSession, ACPClient { const sessionCapabilities = this.agentCapabilities?.sessionCapabilities; if (this.agentCapabilities?.loadSession) { this.replayingHistory = true; - const response = await this.connection.loadSession({ - sessionId: handle.sessionId, - cwd: this.config.cwd, - mcpServers: normalizeMcpServers(this.config.mcpServers), - }); + const response = await this.runACPRequest(() => + this.connection!.loadSession({ + sessionId: handle.sessionId, + cwd: this.config.cwd, + mcpServers: this.acpMcpServers(), + }), + ); this.replayingHistory = false; this.historyPending = this.persistedHistory.length > 0; this.applySessionState(response); } else if (sessionCapabilities?.resume) { - const response = await this.connection.unstable_resumeSession({ - sessionId: handle.sessionId, - cwd: this.config.cwd, - mcpServers: normalizeMcpServers(this.config.mcpServers), - }); + const response = await this.runACPRequest(() => + this.connection!.unstable_resumeSession({ + sessionId: handle.sessionId, + cwd: this.config.cwd, + mcpServers: this.acpMcpServers(), + }), + ); this.applySessionState(response); } else { throw new Error(`${this.provider} does not support ACP session resume`); @@ -1853,15 +1902,29 @@ export class ACPAgentSession implements AgentSession, ACPClient { { logger: this.logger, provider: this.provider }, ); const connection = new ClientSideConnection(() => this, stream); - const initialize = await connection.initialize({ - protocolVersion: PROTOCOL_VERSION, - clientCapabilities: ACP_CLIENT_CAPABILITIES, - clientInfo: { name: "Paseo", version: "dev" }, - }); + const initialize = await this.runACPRequest(() => + connection.initialize({ + protocolVersion: PROTOCOL_VERSION, + clientCapabilities: ACP_CLIENT_CAPABILITIES, + clientInfo: { name: "Paseo", version: "dev" }, + }), + ); return { child, connection, initialize }; } + private async runACPRequest(request: () => Promise): Promise { + try { + return await request(); + } catch (error) { + throw toACPRequestError(error); + } + } + + private acpMcpServers(): McpServer[] { + return this.capabilities.supportsMcpServers ? normalizeMcpServers(this.config.mcpServers) : []; + } + private applySessionState(response: SessionStateResponse): void { const transformed = this.sessionResponseTransformer ? this.sessionResponseTransformer(response) diff --git a/packages/server/src/server/agent/providers/cursor-acp-agent.ts b/packages/server/src/server/agent/providers/cursor-acp-agent.ts index ad253a4d1..47943a62d 100644 --- a/packages/server/src/server/agent/providers/cursor-acp-agent.ts +++ b/packages/server/src/server/agent/providers/cursor-acp-agent.ts @@ -11,6 +11,7 @@ interface CursorACPAgentClientOptions { env?: Record; providerId?: string; label?: string; + providerParams?: unknown; } const CURSOR_MODELS_TIMEOUT_MS = 10_000; @@ -28,6 +29,7 @@ export class CursorACPAgentClient extends GenericACPAgentClient { env: options.env, providerId: options.providerId, label: options.label, + providerParams: options.providerParams, // cursor-agent publishes slash commands asynchronously via available_commands_update. waitForInitialCommands: true, initialCommandsWaitTimeoutMs: CURSOR_INITIAL_COMMANDS_WAIT_TIMEOUT_MS, diff --git a/packages/server/src/server/agent/providers/generic-acp-agent.test.ts b/packages/server/src/server/agent/providers/generic-acp-agent.test.ts index 1985b2d03..19a1e9f8e 100644 --- a/packages/server/src/server/agent/providers/generic-acp-agent.test.ts +++ b/packages/server/src/server/agent/providers/generic-acp-agent.test.ts @@ -7,6 +7,17 @@ const mockState = vi.hoisted(() => ({ })); vi.mock("./acp-agent.js", () => ({ + DEFAULT_ACP_CAPABILITIES: { + supportsStreaming: true, + supportsSessionPersistence: true, + supportsDynamicModes: true, + supportsMcpServers: true, + supportsReasoningStream: true, + supportsToolInvocations: true, + supportsRewindConversation: false, + supportsRewindFiles: false, + supportsRewindBoth: false, + }, ACPAgentClient: class ACPAgentClient { readonly provider: string; @@ -40,7 +51,35 @@ describe("GenericACPAgentClient", () => { }, }, defaultCommand: ["hermes", "acp"], + capabilities: { + supportsStreaming: true, + supportsSessionPersistence: true, + supportsDynamicModes: true, + supportsMcpServers: true, + supportsReasoningStream: true, + supportsToolInvocations: true, + supportsRewindConversation: false, + supportsRewindFiles: false, + supportsRewindBoth: false, + }, }, ]); }); + + test("uses provider params to report MCP support", () => { + const _client = new GenericACPAgentClient({ + logger: createTestLogger(), + command: ["no-mcp-acp", "serve"], + providerParams: { + supportsMcpServers: false, + }, + }); + void _client; + + expect(mockState.superConstructorOptions.at(-1)).toMatchObject({ + capabilities: { + supportsMcpServers: false, + }, + }); + }); }); diff --git a/packages/server/src/server/agent/providers/generic-acp-agent.ts b/packages/server/src/server/agent/providers/generic-acp-agent.ts index 88d2c9525..a9ff355cd 100644 --- a/packages/server/src/server/agent/providers/generic-acp-agent.ts +++ b/packages/server/src/server/agent/providers/generic-acp-agent.ts @@ -1,10 +1,12 @@ import { homedir } from "node:os"; import type { Logger } from "pino"; +import { z } from "zod"; -import type { AgentProvider } from "../agent-sdk-types.js"; +import type { AgentCapabilityFlags, AgentProvider } from "../agent-sdk-types.js"; import { checkProviderLaunchAvailable, resolveProviderLaunch } from "../provider-launch-config.js"; import { ACPAgentClient, + DEFAULT_ACP_CAPABILITIES, deriveModelDefinitionsFromACP, deriveModesFromACP, type SessionStateResponse, @@ -20,12 +22,21 @@ import { const ACP_DIAGNOSTIC_INITIALIZE_TIMEOUT_MS = 8_000; const ACP_DIAGNOSTIC_SESSION_TIMEOUT_MS = 8_000; +export const GenericACPProviderParamsSchema = z + .object({ + supportsMcpServers: z.boolean().optional(), + }) + .passthrough(); + +type GenericACPProviderParams = z.infer; + interface GenericACPAgentClientOptions { logger: Logger; command: [string, ...string[]]; env?: Record; providerId?: string; label?: string; + providerParams?: unknown; waitForInitialCommands?: boolean; initialCommandsWaitTimeoutMs?: number; } @@ -43,6 +54,7 @@ export class GenericACPAgentClient extends ACPAgentClient { env: options.env, }, defaultCommand: options.command, + capabilities: buildGenericACPCapabilities(options), waitForInitialCommands: options.waitForInitialCommands, initialCommandsWaitTimeoutMs: options.initialCommandsWaitTimeoutMs, }); @@ -173,6 +185,18 @@ export class GenericACPAgentClient extends ACPAgentClient { } } +function buildGenericACPCapabilities(options: GenericACPAgentClientOptions): AgentCapabilityFlags { + const params = parseGenericACPProviderParams(options.providerParams); + return { + ...DEFAULT_ACP_CAPABILITIES, + supportsMcpServers: params.supportsMcpServers ?? DEFAULT_ACP_CAPABILITIES.supportsMcpServers, + }; +} + +function parseGenericACPProviderParams(params: unknown): GenericACPProviderParams { + return GenericACPProviderParamsSchema.parse(params ?? {}); +} + interface ACPDiagnosticProbeResult { status: string; initialize: string;