TypeScript implementation of the Agent Client Protocol (ACP) for standardized communication between code editors and AI-powered coding agents.
The ClientSideConnection class provides the client's view of an ACP connection, allowing clients (such as code editors) to communicate with agents. It implements the Agent interface, providing type-safe methods for all agent operations including initialization, session management, and prompt processing.
Creates a new client-side connection to an agent.
/**
* Creates a new client-side connection to an agent
* @param toClient - Factory function that creates a Client handler
* @param stream - Bidirectional message stream for communication
*/
constructor(
toClient: (agent: Agent) => Client,
stream: Stream
)Parameters:
toClient: A factory function that receives an Agent interface (the connection itself) and returns a Client implementationstream: A bidirectional stream created via ndJsonStream() or custom Stream implementationUsage Example:
import { ClientSideConnection, Client, ndJsonStream } from '@agentclientprotocol/sdk';
const client: Client = {
// ... client implementation
};
const connection = new ClientSideConnection(
(agent) => client, // Factory receives agent interface for calling agent methods
ndJsonStream(outputStream, inputStream)
);Establishes the connection with an agent and negotiates protocol capabilities.
/**
* Establishes connection and negotiates capabilities
* @param params - Initialization parameters
* @returns Agent capabilities and info
*/
async initialize(
params: InitializeRequest
): Promise<InitializeResponse>Parameters:
params.protocolVersion (number): Protocol version (currently 1)params.clientCapabilities (ClientCapabilities): Client capabilitiesparams.clientInfo (Implementation): Client name, version, and optional titleClient Capabilities:
fs: File system capabilities (readTextFile, writeTextFile)terminal: Terminal support (boolean)Returns:
protocolVersion (number): Negotiated protocol versionagentCapabilities (AgentCapabilities): Agent capabilitiesagentInfo (Implementation): Agent name and versionauthMethods (AuthMethod[] | undefined): Available authentication methodsUsage Example:
const response = await connection.initialize({
protocolVersion: 1,
clientCapabilities: {
fs: {
readTextFile: true,
writeTextFile: true
},
terminal: true
},
clientInfo: {
name: 'my-editor',
version: '1.0.0',
title: 'My Code Editor'
}
});
console.log('Agent:', response.agentInfo.name);
console.log('Supports MCP HTTP:', response.agentCapabilities?.mcpCapabilities?.http);
console.log('Supports image prompts:', response.agentCapabilities?.promptCapabilities?.image);
if (response.authMethods && response.authMethods.length > 0) {
// Authentication is required
await connection.authenticate({
methodId: response.authMethods[0].id
});
}Creates a new conversation session with the agent.
/**
* Creates a new conversation session
* @param params - Session creation parameters
* @returns Session ID and available modes
*/
async newSession(
params: NewSessionRequest
): Promise<NewSessionResponse>Parameters:
params.cwd (string): Working directory (absolute path)params.mcpServers (McpServer[]): MCP servers to connect toMCP Server Types:
{ type: "http", name: string, url: string, headers: HttpHeader[] }{ type: "sse", name: string, url: string, headers: HttpHeader[] }{ name: string, command: string, args: string[], env: EnvVariable[] }Returns:
sessionId (string): Unique session identifiermodes (SessionModeState | undefined): Available modes and current modemodels (SessionModelState | undefined): UNSTABLE - Available modelsUsage Example:
const session = await connection.newSession({
cwd: '/path/to/project',
mcpServers: [{
name: 'my-mcp-server',
command: 'mcp-server',
args: ['--port', '8080'],
env: [{
name: 'API_KEY',
value: process.env.API_KEY
}]
}]
});
console.log('Session ID:', session.sessionId);
console.log('Current mode:', session.modes?.currentModeId);
console.log('Available modes:', session.modes?.availableModes.map(m => m.name));
// Store session ID for future requests
const sessionId = session.sessionId;Error Handling:
May return an auth_required error (-32000) if the agent requires authentication before allowing session creation.
Loads an existing session to resume a previous conversation.
/**
* Loads an existing session
* @param params - Session loading parameters
* @returns Session info and state
*/
async loadSession(
params: LoadSessionRequest
): Promise<LoadSessionResponse>Parameters:
params.sessionId (string): The session ID to loadparams.cwd (string): Working directory (absolute path)params.mcpServers (McpServer[]): MCP servers to connect toReturns:
modes (SessionModeState | undefined): Available modes and current modemodels (SessionModelState | undefined): UNSTABLE - Available modelssession/update notificationsAvailability: Only available if agent advertises the loadSession capability.
Usage Example:
try {
const session = await connection.loadSession({
sessionId: 'previous-session-123',
cwd: '/path/to/project',
mcpServers: []
});
console.log('Session loaded with mode:', session.modes?.currentModeId);
// Client will receive session/update notifications with conversation history
} catch (error) {
if (error.code === -32601) {
console.error('Agent does not support loading sessions');
}
}Sets the operational mode for a session.
/**
* Sets the operational mode for a session
* @param params - Mode change parameters
* @returns Empty response on success
*/
async setSessionMode(
params: SetSessionModeRequest
): Promise<SetSessionModeResponse>Parameters:
params.sessionId (string): The session IDparams.modeId (string): The mode ID to activateReturns:
{} on successAvailability: Optional method that agents may implement.
Usage Example:
// Switch from 'code' mode to 'ask' mode
await connection.setSessionMode({
sessionId: 'session-123',
modeId: 'ask'
});
// Agent may also change modes autonomously and notify via current_mode_updateNotes:
availableModes during session creationcurrent_mode_update notificationsUNSTABLE - Selects a model for a given session.
/**
* UNSTABLE - Selects a model for the session
* @param params - Model selection parameters
* @returns Empty response on success
*/
async setSessionModel(
params: SetSessionModelRequest
): Promise<SetSessionModelResponse>Parameters:
params.sessionId (string): The session IDparams.modelId (string): The model ID to activateReturns:
{} on successAvailability: This capability is not part of the spec yet and may be removed or changed at any point.
Usage Example:
await connection.setSessionModel({
sessionId: 'session-123',
modelId: 'gpt-4'
});Authenticates the client using the specified authentication method.
/**
* Authenticates the client
* @param params - Authentication parameters
* @returns Empty response on success
*/
async authenticate(
params: AuthenticateRequest
): Promise<AuthenticateResponse>Parameters:
params.methodId (string): Authentication method ID from authMethods in InitializeResponseReturns:
{} on successUsage Example:
// After initialize, if authMethods are provided
if (initResponse.authMethods && initResponse.authMethods.length > 0) {
const authMethod = initResponse.authMethods[0];
await connection.authenticate({
methodId: authMethod.id
});
// Now can create sessions
const session = await connection.newSession({
cwd: '/path/to/project',
mcpServers: []
});
}Processes a user prompt within a session.
/**
* Processes a user prompt
* @param params - Prompt parameters
* @returns Prompt completion info
*/
async prompt(
params: PromptRequest
): Promise<PromptResponse>Parameters:
params.sessionId (string): The session IDparams.prompt (ContentBlock[]): Array of content blocks representing the user's promptContent Block Types:
{ type: "text", text: string }{ type: "image", data: string, mimeType: string, uri?: string }{ type: "audio", data: string, mimeType: string }{ type: "resource_link", uri: string, name: string }{ type: "resource", resource: { uri: string, mimeType?: string, text?: string, blob?: string } }Returns:
stopReason (string): Why the turn ended - one of "end_turn", "max_tokens", "max_turn_requests", "refusal", "cancelled"Usage Example:
// Send a simple text prompt
const response = await connection.prompt({
sessionId: 'session-123',
prompt: [{
type: 'text',
text: 'Write a function to calculate fibonacci numbers'
}]
});
console.log('Stop reason:', response.stopReason);
// Send prompt with image
const imageResponse = await connection.prompt({
sessionId: 'session-123',
prompt: [{
type: 'text',
text: 'What is in this image?'
}, {
type: 'image',
data: base64ImageData,
mimeType: 'image/png'
}]
});Lifecycle:
prompt requestsession/update notifications with progress (message chunks, tool calls, plans)session/request_permission for sensitive operationsCancels ongoing operations for a session.
/**
* Cancels ongoing operations
* @param params - Cancellation parameters
*/
async cancel(
params: CancelNotification
): Promise<void>Parameters:
params.sessionId (string): The session ID to cancelUsage Example:
// User clicks cancel button
await connection.cancel({
sessionId: 'session-123'
});
// Agent should:
// - Stop language model requests
// - Abort tool call invocations
// - Send final session/update notifications
// - Respond to prompt request with stopReason: "cancelled"Notes:
session/update notifications after sending cancelprompt request with StopReason::CancelledSends an arbitrary request that is not part of the ACP spec.
/**
* Extension method for custom requests
* @param method - Custom method name (will be prefixed with underscore)
* @param params - Method parameters
* @returns Method response
*/
async extMethod(
method: string,
params: Record<string, unknown>
): Promise<Record<string, unknown>>Parameters:
method (string): Custom method name (automatically prefixed with _ in protocol)params (object): Arbitrary method parametersReturns:
Usage Example:
// Send custom request
const result = await connection.extMethod('myapp.getStatus', {
includeDetails: true
});
console.log('Custom result:', result);Sends an arbitrary notification that is not part of the ACP spec.
/**
* Extension notification for custom events
* @param method - Custom notification name (will be prefixed with underscore)
* @param params - Notification parameters
*/
async extNotification(
method: string,
params: Record<string, unknown>
): Promise<void>Parameters:
method (string): Custom notification name (automatically prefixed with _ in protocol)params (object): Arbitrary notification parametersUsage Example:
// Send custom notification
await connection.extNotification('myapp.userAction', {
action: 'click',
target: 'button'
});/**
* AbortSignal that aborts when the connection closes
*/
get signal(): AbortSignalCan be used to listen for connection closure, check status, or pass to other APIs for automatic cancellation.
Usage Example:
connection.signal.addEventListener('abort', () => {
console.log('Connection closed');
cleanup();
});
if (connection.signal.aborted) {
console.log('Already closed');
}/**
* Promise that resolves when the connection closes
*/
get closed(): Promise<void>Usage Example:
await connection.closed;
console.log('Connection closed');import { ClientSideConnection, Client, ndJsonStream } from '@agentclientprotocol/sdk';
const client: Client = {
async requestPermission(params) {
// Show dialog to user
const approved = await showPermissionDialog(
params.title,
params.description,
params.options
);
return {
outcome: approved ? 'granted' : 'denied',
selectedOption: approved ? params.options[0].id : undefined
};
},
async sessionUpdate(params) {
switch (params.update.sessionUpdate) {
case 'agent_message_chunk':
// Display streaming content
displayContent(params.update.content);
break;
case 'tool_call':
// Show tool execution
displayToolCall(params.update);
break;
case 'plan':
// Show execution plan
displayPlan(params.update.entries);
break;
case 'current_mode_update':
// Update UI to show current mode
updateModeDisplay(params.update.currentModeId);
break;
}
},
async readTextFile(params) {
const content = await fs.readFile(
params.path,
'utf-8'
);
return { content };
},
async writeTextFile(params) {
await fs.writeFile(
params.path,
params.content
);
return {};
},
async createTerminal(params) {
const terminal = spawn(params.command, params.args, {
cwd: params.cwd,
env: Object.fromEntries(
(params.env || []).map(e => [e.name, e.value])
)
});
const terminalId = registerTerminal(terminal);
return { terminalId };
},
// ... other terminal methods
};
// Create connection
const connection = new ClientSideConnection(
(agent) => client,
ndJsonStream(outputStream, inputStream)
);
// Initialize
const initResponse = await connection.initialize({
protocolVersion: 1,
clientCapabilities: {
fs: { readTextFile: true, writeTextFile: true },
terminal: true
},
clientInfo: {
name: 'example-editor',
version: '1.0.0'
}
});
// Create session
const session = await connection.newSession({
cwd: '/path/to/project',
mcpServers: []
});
// Send prompt
const response = await connection.prompt({
sessionId: session.sessionId,
prompt: [{
type: 'text',
text: 'Refactor this function to use async/await'
}]
});
console.log('Prompt completed:', response.stopReason);
// Wait for connection to close
await connection.closed;All methods can throw RequestError for protocol-level errors:
import { RequestError } from '@agentclientprotocol/sdk';
try {
await connection.newSession({
cwd: '/path/to/project',
mcpServers: []
});
} catch (error) {
if (error instanceof RequestError) {
switch (error.code) {
case -32000: // Authentication required
console.error('Please authenticate first');
await connection.authenticate({ methodId: 'some-method' });
break;
case -32601: // Method not found
console.error('Agent does not support this method');
break;
case -32603: // Internal error
console.error('Agent internal error:', error.message);
break;
default:
console.error('Request error:', error.code, error.message);
}
}
}sessionUpdate methoddomain.method)Install with Tessl CLI
npx tessl i tessl/npm-agentclientprotocol--sdk