Handle ACP providers without MCP support

This commit is contained in:
Mohamed Boudra
2026-06-10 11:29:26 +07:00
parent e9f9759ba8
commit b3f44981a8
11 changed files with 332 additions and 57 deletions
+32 -12
View File
@@ -438,6 +438,25 @@ Required fields for ACP providers:
- `label` - `label`
- `command` — the command to spawn the agent process (must support ACP over stdio) - `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 ### 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. 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: Every entry under `agents.providers` accepts these fields:
| Field | Type | Required | Description | | Field | Type | Required | Description |
| ------------------ | ------------------------ | ----------------- | ------------------------------------------------------------------ | | ------------------ | ------------------------- | ----------------- | ------------------------------------------------------------------ |
| `extends` | `string` | Yes (custom only) | Built-in provider ID to inherit from, or `"acp"` | | `extends` | `string` | Yes (custom only) | Built-in provider ID to inherit from, or `"acp"` |
| `label` | `string` | Yes (custom only) | Display name in the UI | | `label` | `string` | Yes (custom only) | Display name in the UI |
| `description` | `string` | No | Short description shown in the UI | | `description` | `string` | No | Short description shown in the UI |
| `command` | `string[]` | Yes (ACP only) | Command to spawn the agent process | | `command` | `string[]` | Yes (ACP only) | Command to spawn the agent process |
| `env` | `Record<string, string>` | No | Environment variables to set for the agent process | | `env` | `Record<string, string>` | No | Environment variables to set for the agent process |
| `models` | `ProviderProfileModel[]` | No | Static model list (overrides runtime discovery) | | `params` | `Record<string, unknown>` | No | Provider-specific options such as `supportsMcpServers: false` |
| `additionalModels` | `ProviderProfileModel[]` | No | Static model additions (merged with runtime discovery or `models`) | | `models` | `ProviderProfileModel[]` | No | Static model list (overrides runtime discovery) |
| `disallowedTools` | `string[]` | No | Tool names to disable for this provider (e.g. `["WebSearch"]`) | | `additionalModels` | `ProviderProfileModel[]` | No | Static model additions (merged with runtime discovery or `models`) |
| `enabled` | `boolean` | No | Set to `false` to hide the provider (default: `true`) | | `disallowedTools` | `string[]` | No | Tool names to disable for this provider (e.g. `["WebSearch"]`) |
| `order` | `number` | No | Sort order in the provider list | | `enabled` | `boolean` | No | Set to `false` to hide the provider (default: `true`) |
| `order` | `number` | No | Sort order in the provider list |
### Model definition ### Model definition
@@ -9,6 +9,7 @@ export interface AcpProviderCatalogEntry {
installLink: string; installLink: string;
command: readonly [string, ...string[]]; command: readonly [string, ...string[]];
env?: Readonly<Record<string, string>>; env?: Readonly<Record<string, string>>;
params?: Readonly<Record<string, unknown>>;
} }
const CATALOG_DATA = [ const CATALOG_DATA = [
@@ -166,6 +167,7 @@ const CATALOG_DATA = [
DROID_DISABLE_AUTO_UPDATE: "true", DROID_DISABLE_AUTO_UPDATE: "true",
FACTORY_DROID_AUTO_UPDATE_ENABLED: "false", FACTORY_DROID_AUTO_UPDATE_ENABLED: "false",
}, },
params: { supportsMcpServers: false },
}, },
{ {
id: "fast-agent", id: "fast-agent",
@@ -357,5 +359,6 @@ export const ACP_PROVIDER_CATALOG: AcpProviderCatalogEntry[] = CATALOG_DATA.map(
installLink: entry.installLink, installLink: entry.installLink,
command: entry.command, command: entry.command,
env: "env" in entry ? entry.env : undefined, 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, iconSvg: entry.iconId ? (ACP_PROVIDER_ICON_SVGS[entry.iconId] ?? null) : null,
})); }));
@@ -66,4 +66,12 @@ describe("ACP provider catalog", () => {
AUGMENT_DISABLE_AUTO_UPDATE: "1", 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,
});
});
}); });
@@ -19,6 +19,7 @@ export function buildAcpProviderConfigPatch(
description: entry.description, description: entry.description,
command: [...entry.command], command: [...entry.command],
env: entry.env ? { ...entry.env } : {}, env: entry.env ? { ...entry.env } : {},
...(entry.params ? { params: { ...entry.params } } : {}),
}, },
}, },
}; };
@@ -17,6 +17,7 @@ const mockState = vi.hoisted(() => {
cursor: [] as Array<{ cursor: [] as Array<{
command: string[]; command: string[];
env?: Record<string, string>; env?: Record<string, string>;
providerParams?: unknown;
}>, }>,
pi: [] as ConstructorEntry[], pi: [] as ConstructorEntry[],
genericAcp: [] as Array<{ genericAcp: [] as Array<{
@@ -24,6 +25,7 @@ const mockState = vi.hoisted(() => {
env?: Record<string, string>; env?: Record<string, string>;
providerId?: string; providerId?: string;
label?: string; label?: string;
providerParams?: unknown;
}>, }>,
}, },
isCommandAvailable: vi.fn(async (_command: string) => false), isCommandAvailable: vi.fn(async (_command: string) => false),
@@ -239,7 +241,7 @@ vi.mock("./providers/pi/agent.js", () => ({
vi.mock("./providers/generic-acp-agent.js", () => ({ vi.mock("./providers/generic-acp-agent.js", () => ({
GenericACPAgentClient: class GenericACPAgentClient { GenericACPAgentClient: class GenericACPAgentClient {
readonly capabilities = { capabilities = {
supportsStreaming: true, supportsStreaming: true,
supportsSessionPersistence: true, supportsSessionPersistence: true,
supportsDynamicModes: true, supportsDynamicModes: true,
@@ -255,7 +257,21 @@ vi.mock("./providers/generic-acp-agent.js", () => ({
env?: Record<string, string>; env?: Record<string, string>;
providerId?: string; providerId?: string;
label?: string; label?: string;
providerParams?: unknown;
}) { }) {
const providerParams =
options.providerParams &&
typeof options.providerParams === "object" &&
!Array.isArray(options.providerParams)
? (options.providerParams as Record<string, unknown>)
: {};
this.capabilities = {
...this.capabilities,
supportsMcpServers:
typeof providerParams.supportsMcpServers === "boolean"
? providerParams.supportsMcpServers
: this.capabilities.supportsMcpServers,
};
this.runtimeSettings = { this.runtimeSettings = {
command: { command: {
mode: "replace", mode: "replace",
@@ -268,6 +284,7 @@ vi.mock("./providers/generic-acp-agent.js", () => ({
env: options.env, env: options.env,
providerId: options.providerId, providerId: options.providerId,
label: options.label, label: options.label,
providerParams: options.providerParams,
}); });
} }
@@ -306,7 +323,11 @@ vi.mock("./providers/cursor-acp-agent.js", () => ({
readonly provider = "acp"; readonly provider = "acp";
readonly runtimeSettings?: unknown; readonly runtimeSettings?: unknown;
constructor(options: { command: string[]; env?: Record<string, string> }) { constructor(options: {
command: string[];
env?: Record<string, string>;
providerParams?: unknown;
}) {
this.runtimeSettings = { this.runtimeSettings = {
command: { command: {
mode: "replace", mode: "replace",
@@ -317,6 +338,7 @@ vi.mock("./providers/cursor-acp-agent.js", () => ({
mockState.constructorArgs.cursor.push({ mockState.constructorArgs.cursor.push({
command: options.command, command: options.command,
env: options.env, env: options.env,
providerParams: options.providerParams,
}); });
} }
@@ -517,6 +539,7 @@ test("new provider extending acp uses GenericACPAgentClient", () => {
}, },
providerId: "my-agent", providerId: "my-agent",
label: "My Agent", label: "My Agent",
providerParams: undefined,
}, },
{ {
command: ["my-agent", "--acp"], command: ["my-agent", "--acp"],
@@ -525,6 +548,46 @@ test("new provider extending acp uses GenericACPAgentClient", () => {
}, },
providerId: "my-agent", providerId: "my-agent",
label: "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: { env: {
CURSOR_AGENT_LOG: "debug", CURSOR_AGENT_LOG: "debug",
}, },
providerParams: undefined,
}, },
{ {
command: ["cursor-agent", "acp"], command: ["cursor-agent", "acp"],
env: { env: {
CURSOR_AGENT_LOG: "debug", CURSOR_AGENT_LOG: "debug",
}, },
providerParams: undefined,
}, },
]); ]);
expect(mockState.constructorArgs.genericAcp).toEqual([]); expect(mockState.constructorArgs.genericAcp).toEqual([]);
@@ -602,6 +602,7 @@ function addDerivedProviders(
env: override.env, env: override.env,
providerId, providerId,
label: override.label ?? providerId, label: override.label ?? providerId,
providerParams: override.params,
}) })
: new GenericACPAgentClient({ : new GenericACPAgentClient({
logger, logger,
@@ -609,6 +610,7 @@ function addDerivedProviders(
env: override.env, env: override.env,
providerId, providerId,
label: override.label ?? providerId, label: override.label ?? providerId,
providerParams: override.params,
}), }),
}); });
continue; continue;
@@ -26,6 +26,7 @@ import {
mapACPUsage, mapACPUsage,
resolveACPModeSelection, resolveACPModeSelection,
resolveACPModelSelection, resolveACPModelSelection,
summarizeACPRequestError,
} from "./acp-agent.js"; } from "./acp-agent.js";
import { import {
COPILOT_ALLOW_ALL_MODE_ID, COPILOT_ALLOW_ALL_MODE_ID,
@@ -48,6 +49,7 @@ interface ACPSessionInternals {
activeForegroundTurnId: string | null; activeForegroundTurnId: string | null;
configOptions: SessionConfigOption[]; configOptions: SessionConfigOption[];
translateSessionUpdate(update: SessionUpdate): AgentStreamEvent[]; translateSessionUpdate(update: SessionUpdate): AgentStreamEvent[];
acpMcpServers(): unknown[];
} }
interface ACPModelSelectionInternals { interface ACPModelSelectionInternals {
@@ -1514,6 +1516,52 @@ describe("ACPAgentSession slash commands", () => {
}); });
describe("ACPAgentSession", () => { 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<ACPSessionInternals>(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 () => { test("accepts ACP extension notifications without failing the JSON-RPC connection", async () => {
const logger = createTestLogger(); const logger = createTestLogger();
const trace = vi.spyOn(logger, "trace"); const trace = vi.spyOn(logger, "trace");
@@ -112,7 +112,22 @@ function isACPError(value: unknown): value is ACPError {
return isRecord(value) && typeof value.message === "string" && typeof value.code === "number"; 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; message: string;
code?: string; code?: string;
diagnostic?: 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. // Promise rejections are untyped, but the ACP SDK rejects JSON-RPC failures as response.error.
if (isACPError(error)) { if (isACPError(error)) {
const code = String(error.code); 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)}`; const data = error.data === undefined ? "" : ` | data=${JSON.stringify(error.data)}`;
return { return {
message: error.message, message,
code, code,
diagnostic: `${error.message} | code=${code}${data}`, diagnostic: `${message} | code=${code}${data}`,
}; };
} }
@@ -135,6 +153,17 @@ function summarizeACPRequestError(error: unknown): {
return { message: String(error) }; 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( function resolveTerminalCommand(
command: string, command: string,
args?: string[], args?: string[],
@@ -151,7 +180,7 @@ function resolveTerminalCommand(
return { command: shell.command, args: [...shell.flag, command] }; return { command: shell.command, args: [...shell.flag, command] };
} }
const DEFAULT_ACP_CAPABILITIES: AgentCapabilityFlags = { export const DEFAULT_ACP_CAPABILITIES: AgentCapabilityFlags = {
supportsStreaming: true, supportsStreaming: true,
supportsSessionPersistence: true, supportsSessionPersistence: true,
supportsDynamicModes: true, supportsDynamicModes: true,
@@ -673,10 +702,12 @@ export class ACPAgentClient implements AgentClient {
const { cwd } = options; const { cwd } = options;
const probe = await this.spawnProcess(PROBE_ENV); const probe = await this.spawnProcess(PROBE_ENV);
try { try {
const response = await probe.connection.newSession({ const response = await this.runACPRequest(() =>
cwd, probe.connection.newSession({
mcpServers: [], cwd,
}); mcpServers: [],
}),
);
const transformed = this.transformSessionResponse(response); const transformed = this.transformSessionResponse(response);
const models = deriveModelDefinitionsFromACP( const models = deriveModelDefinitionsFromACP(
this.provider, this.provider,
@@ -693,10 +724,12 @@ export class ACPAgentClient implements AgentClient {
const { cwd } = options; const { cwd } = options;
const probe = await this.spawnProcess(PROBE_ENV); const probe = await this.spawnProcess(PROBE_ENV);
try { try {
const response = await probe.connection.newSession({ const response = await this.runACPRequest(() =>
cwd, probe.connection.newSession({
mcpServers: [], cwd,
}); mcpServers: [],
}),
);
const transformed = this.transformSessionResponse(response); const transformed = this.transformSessionResponse(response);
const modeInfo = deriveModesFromACP( const modeInfo = deriveModesFromACP(
this.defaultModes, this.defaultModes,
@@ -721,8 +754,8 @@ export class ACPAgentClient implements AgentClient {
const sessions: PersistedAgentDescriptor[] = []; const sessions: PersistedAgentDescriptor[] = [];
let cursor: string | null | undefined; let cursor: string | null | undefined;
for (;;) { for (;;) {
const page: ListSessionsResponse = await probe.connection.listSessions( const page: ListSessionsResponse = await this.runACPRequest(() =>
cursor ? { cursor } : {}, probe.connection.listSessions(cursor ? { cursor } : {}),
); );
for (const session of page.sessions) { for (const session of page.sessions) {
sessions.push({ sessions.push({
@@ -809,15 +842,17 @@ export class ACPAgentClient implements AgentClient {
let initialize: InitializeResponse; let initialize: InitializeResponse;
try { try {
initialize = await Promise.race([ initialize = await this.runACPRequest(() =>
connection.initialize({ Promise.race([
protocolVersion: PROTOCOL_VERSION, connection.initialize({
clientCapabilities: ACP_CLIENT_CAPABILITIES, protocolVersion: PROTOCOL_VERSION,
clientInfo: { name: "Paseo", version: "dev" }, clientCapabilities: ACP_CLIENT_CAPABILITIES,
}), clientInfo: { name: "Paseo", version: "dev" },
spawnErrorPromise, }),
...(initializeTimeoutPromise ? [initializeTimeoutPromise] : []), spawnErrorPromise,
]); ...(initializeTimeoutPromise ? [initializeTimeoutPromise] : []),
]),
);
} catch (error) { } catch (error) {
await terminateChildProcess(child, 2_000); await terminateChildProcess(child, 2_000);
throw error; throw error;
@@ -861,6 +896,14 @@ export class ACPAgentClient implements AgentClient {
} }
} }
protected async runACPRequest<T>(request: () => Promise<T>): Promise<T> {
try {
return await request();
} catch (error) {
throw toACPRequestError(error);
}
}
protected async resolveLaunchCommand(): Promise<{ command: string; args: string[] }> { protected async resolveLaunchCommand(): Promise<{ command: string; args: string[] }> {
const prefix = await resolveProviderLaunch({ const prefix = await resolveProviderLaunch({
commandConfig: this.runtimeSettings?.command, commandConfig: this.runtimeSettings?.command,
@@ -998,10 +1041,12 @@ export class ACPAgentSession implements AgentSession, ACPClient {
this.connection = spawned.connection; this.connection = spawned.connection;
this.agentCapabilities = spawned.initialize.agentCapabilities ?? null; this.agentCapabilities = spawned.initialize.agentCapabilities ?? null;
const response = await this.connection.newSession({ const response = await this.runACPRequest(() =>
cwd: this.config.cwd, this.connection!.newSession({
mcpServers: normalizeMcpServers(this.config.mcpServers), cwd: this.config.cwd,
}); mcpServers: this.acpMcpServers(),
}),
);
this.sessionId = response.sessionId; this.sessionId = response.sessionId;
this.bootstrapThreadEventPending = true; this.bootstrapThreadEventPending = true;
this.applySessionState(response); this.applySessionState(response);
@@ -1024,20 +1069,24 @@ export class ACPAgentSession implements AgentSession, ACPClient {
const sessionCapabilities = this.agentCapabilities?.sessionCapabilities; const sessionCapabilities = this.agentCapabilities?.sessionCapabilities;
if (this.agentCapabilities?.loadSession) { if (this.agentCapabilities?.loadSession) {
this.replayingHistory = true; this.replayingHistory = true;
const response = await this.connection.loadSession({ const response = await this.runACPRequest(() =>
sessionId: handle.sessionId, this.connection!.loadSession({
cwd: this.config.cwd, sessionId: handle.sessionId,
mcpServers: normalizeMcpServers(this.config.mcpServers), cwd: this.config.cwd,
}); mcpServers: this.acpMcpServers(),
}),
);
this.replayingHistory = false; this.replayingHistory = false;
this.historyPending = this.persistedHistory.length > 0; this.historyPending = this.persistedHistory.length > 0;
this.applySessionState(response); this.applySessionState(response);
} else if (sessionCapabilities?.resume) { } else if (sessionCapabilities?.resume) {
const response = await this.connection.unstable_resumeSession({ const response = await this.runACPRequest(() =>
sessionId: handle.sessionId, this.connection!.unstable_resumeSession({
cwd: this.config.cwd, sessionId: handle.sessionId,
mcpServers: normalizeMcpServers(this.config.mcpServers), cwd: this.config.cwd,
}); mcpServers: this.acpMcpServers(),
}),
);
this.applySessionState(response); this.applySessionState(response);
} else { } else {
throw new Error(`${this.provider} does not support ACP session resume`); 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 }, { logger: this.logger, provider: this.provider },
); );
const connection = new ClientSideConnection(() => this, stream); const connection = new ClientSideConnection(() => this, stream);
const initialize = await connection.initialize({ const initialize = await this.runACPRequest(() =>
protocolVersion: PROTOCOL_VERSION, connection.initialize({
clientCapabilities: ACP_CLIENT_CAPABILITIES, protocolVersion: PROTOCOL_VERSION,
clientInfo: { name: "Paseo", version: "dev" }, clientCapabilities: ACP_CLIENT_CAPABILITIES,
}); clientInfo: { name: "Paseo", version: "dev" },
}),
);
return { child, connection, initialize }; return { child, connection, initialize };
} }
private async runACPRequest<T>(request: () => Promise<T>): Promise<T> {
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 { private applySessionState(response: SessionStateResponse): void {
const transformed = this.sessionResponseTransformer const transformed = this.sessionResponseTransformer
? this.sessionResponseTransformer(response) ? this.sessionResponseTransformer(response)
@@ -11,6 +11,7 @@ interface CursorACPAgentClientOptions {
env?: Record<string, string>; env?: Record<string, string>;
providerId?: string; providerId?: string;
label?: string; label?: string;
providerParams?: unknown;
} }
const CURSOR_MODELS_TIMEOUT_MS = 10_000; const CURSOR_MODELS_TIMEOUT_MS = 10_000;
@@ -28,6 +29,7 @@ export class CursorACPAgentClient extends GenericACPAgentClient {
env: options.env, env: options.env,
providerId: options.providerId, providerId: options.providerId,
label: options.label, label: options.label,
providerParams: options.providerParams,
// cursor-agent publishes slash commands asynchronously via available_commands_update. // cursor-agent publishes slash commands asynchronously via available_commands_update.
waitForInitialCommands: true, waitForInitialCommands: true,
initialCommandsWaitTimeoutMs: CURSOR_INITIAL_COMMANDS_WAIT_TIMEOUT_MS, initialCommandsWaitTimeoutMs: CURSOR_INITIAL_COMMANDS_WAIT_TIMEOUT_MS,
@@ -7,6 +7,17 @@ const mockState = vi.hoisted(() => ({
})); }));
vi.mock("./acp-agent.js", () => ({ 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 { ACPAgentClient: class ACPAgentClient {
readonly provider: string; readonly provider: string;
@@ -40,7 +51,35 @@ describe("GenericACPAgentClient", () => {
}, },
}, },
defaultCommand: ["hermes", "acp"], 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,
},
});
});
}); });
@@ -1,10 +1,12 @@
import { homedir } from "node:os"; import { homedir } from "node:os";
import type { Logger } from "pino"; 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 { checkProviderLaunchAvailable, resolveProviderLaunch } from "../provider-launch-config.js";
import { import {
ACPAgentClient, ACPAgentClient,
DEFAULT_ACP_CAPABILITIES,
deriveModelDefinitionsFromACP, deriveModelDefinitionsFromACP,
deriveModesFromACP, deriveModesFromACP,
type SessionStateResponse, type SessionStateResponse,
@@ -20,12 +22,21 @@ import {
const ACP_DIAGNOSTIC_INITIALIZE_TIMEOUT_MS = 8_000; const ACP_DIAGNOSTIC_INITIALIZE_TIMEOUT_MS = 8_000;
const ACP_DIAGNOSTIC_SESSION_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<typeof GenericACPProviderParamsSchema>;
interface GenericACPAgentClientOptions { interface GenericACPAgentClientOptions {
logger: Logger; logger: Logger;
command: [string, ...string[]]; command: [string, ...string[]];
env?: Record<string, string>; env?: Record<string, string>;
providerId?: string; providerId?: string;
label?: string; label?: string;
providerParams?: unknown;
waitForInitialCommands?: boolean; waitForInitialCommands?: boolean;
initialCommandsWaitTimeoutMs?: number; initialCommandsWaitTimeoutMs?: number;
} }
@@ -43,6 +54,7 @@ export class GenericACPAgentClient extends ACPAgentClient {
env: options.env, env: options.env,
}, },
defaultCommand: options.command, defaultCommand: options.command,
capabilities: buildGenericACPCapabilities(options),
waitForInitialCommands: options.waitForInitialCommands, waitForInitialCommands: options.waitForInitialCommands,
initialCommandsWaitTimeoutMs: options.initialCommandsWaitTimeoutMs, 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 { interface ACPDiagnosticProbeResult {
status: string; status: string;
initialize: string; initialize: string;