Documentation Index Fetch the complete documentation index at: https://mintlify.com/agentclientprotocol/typescript-sdk/llms.txt
Use this file to discover all available pages before exploring further.
Overview
This example demonstrates how to build a functional ACP Client using the TypeScript SDK. The client spawns an agent as a subprocess, establishes a connection, manages sessions, and handles user interactions including permission requests.
What This Example Demonstrates
Agent Process Management Spawning and communicating with an agent subprocess
Connection Lifecycle Initializing connections and negotiating capabilities
Permission Handling Prompting users for approval on sensitive operations
Session Updates Receiving and displaying real-time agent updates
Complete Code
Here’s the full implementation from src/examples/client.ts:
#!/usr/bin/env node
import { spawn } from "node:child_process" ;
import { fileURLToPath } from "node:url" ;
import { dirname , join } from "node:path" ;
import { Writable , Readable } from "node:stream" ;
import readline from "node:readline/promises" ;
import * as acp from "@agentclientprotocol/sdk" ;
class ExampleClient implements acp . Client {
async requestPermission (
params : acp . RequestPermissionRequest ,
) : Promise < acp . RequestPermissionResponse > {
console . log ( ` \n 🔐 Permission requested: ${ params . toolCall . title } ` );
console . log ( ` \n Options:` );
params . options . forEach (( option , index ) => {
console . log ( ` ${ index + 1 } . ${ option . name } ( ${ option . kind } )` );
});
while ( true ) {
const rl = readline . createInterface ({
input: process . stdin ,
output: process . stdout ,
});
const answer = await rl . question ( " \n Choose an option: " );
const trimmedAnswer = answer . trim ();
const optionIndex = parseInt ( trimmedAnswer ) - 1 ;
if ( optionIndex >= 0 && optionIndex < params . options . length ) {
return {
outcome: {
outcome: "selected" ,
optionId: params . options [ optionIndex ]. optionId ,
},
};
} else {
console . log ( "Invalid option. Please try again." );
}
}
}
async sessionUpdate ( params : acp . SessionNotification ) : Promise < void > {
const update = params . update ;
switch ( update . sessionUpdate ) {
case "agent_message_chunk" :
if ( update . content . type === "text" ) {
console . log ( update . content . text );
} else {
console . log ( `[ ${ update . content . type } ]` );
}
break ;
case "tool_call" :
console . log ( ` \n 🔧 ${ update . title } ( ${ update . status } )` );
break ;
case "tool_call_update" :
console . log (
` \n 🔧 Tool call \` ${ update . toolCallId } \` updated: ${ update . status } \n ` ,
);
break ;
case "plan" :
case "agent_thought_chunk" :
case "user_message_chunk" :
console . log ( `[ ${ update . sessionUpdate } ]` );
break ;
default :
break ;
}
}
async writeTextFile (
params : acp . WriteTextFileRequest ,
) : Promise < acp . WriteTextFileResponse > {
console . error (
"[Client] Write text file called with:" ,
JSON . stringify ( params , null , 2 ),
);
return {};
}
async readTextFile (
params : acp . ReadTextFileRequest ,
) : Promise < acp . ReadTextFileResponse > {
console . error (
"[Client] Read text file called with:" ,
JSON . stringify ( params , null , 2 ),
);
return {
content: "Mock file content" ,
};
}
}
async function main () {
// Get the current file's directory to find agent.ts
const __filename = fileURLToPath ( import . meta . url );
const __dirname = dirname ( __filename );
const agentPath = join ( __dirname , "agent.ts" );
// Spawn the agent as a subprocess via npx (npx.cmd on Windows) using tsx
const npxCmd = process . platform === "win32" ? "npx.cmd" : "npx" ;
const agentProcess = spawn ( npxCmd , [ "tsx" , agentPath ], {
stdio: [ "pipe" , "pipe" , "inherit" ],
});
// Create streams to communicate with the agent
const input = Writable . toWeb ( agentProcess . stdin ! );
const output = Readable . toWeb (
agentProcess . stdout ! ,
) as ReadableStream < Uint8Array >;
// Create the client connection
const client = new ExampleClient ();
const stream = acp . ndJsonStream ( input , output );
const connection = new acp . ClientSideConnection (( _agent ) => client , stream );
try {
// Initialize the connection
const initResult = await connection . initialize ({
protocolVersion: acp . PROTOCOL_VERSION ,
clientCapabilities: {
fs: {
readTextFile: true ,
writeTextFile: true ,
},
},
});
console . log (
`✅ Connected to agent (protocol v ${ initResult . protocolVersion } )` ,
);
// Create a new session
const sessionResult = await connection . newSession ({
cwd: process . cwd (),
mcpServers: [],
});
console . log ( `📝 Created session: ${ sessionResult . sessionId } ` );
console . log ( `💬 User: Hello, agent! \n ` );
process . stdout . write ( " " );
// Send a test prompt
const promptResult = await connection . prompt ({
sessionId: sessionResult . sessionId ,
prompt: [
{
type: "text" ,
text: "Hello, agent!" ,
},
],
});
console . log ( ` \n\n ✅ Agent completed with: ${ promptResult . stopReason } ` );
} catch ( error ) {
console . error ( "[Client] Error:" , error );
} finally {
agentProcess . kill ();
process . exit ( 0 );
}
}
main (). catch ( console . error );
Code Walkthrough
Implement the Client Interface
Create a class that implements the acp.Client interface with all required methods: class ExampleClient implements acp . Client {
async requestPermission ( params : acp . RequestPermissionRequest ) { /* ... */ }
async sessionUpdate ( params : acp . SessionNotification ) { /* ... */ }
async writeTextFile ( params : acp . WriteTextFileRequest ) { /* ... */ }
async readTextFile ( params : acp . ReadTextFileRequest ) { /* ... */ }
}
Handle Permission Requests
Implement interactive permission prompts for sensitive operations: async requestPermission (
params : acp . RequestPermissionRequest ,
): Promise < acp . RequestPermissionResponse > {
console.log( ` \n 🔐 Permission requested: ${ params . toolCall . title } ` );
// Display options to the user
params.options.forEach((option, index) => {
console.log( ` ${ index + 1 } . ${ option . name } ( ${ option . kind } )` );
});
// Get user input
const rl = readline.createInterface({
input: process . stdin ,
output: process . stdout ,
});
const answer = await rl . question ( " \n Choose an option: " );
const optionIndex = parseInt ( answer . trim ()) - 1 ;
// Return the selected option
return {
outcome: {
outcome: "selected" ,
optionId: params . options [ optionIndex ]. optionId ,
},
};
}
Handle Session Updates
Display real-time updates from the agent: async sessionUpdate ( params : acp . SessionNotification ): Promise < void > {
const update = params . update ;
switch (update.sessionUpdate) {
case "agent_message_chunk" :
// Display text responses
if ( update . content . type === "text" ) {
console . log ( update . content . text );
}
break ;
case "tool_call" :
// Show tool execution
console . log ( ` \n 🔧 ${ update . title } ( ${ update . status } )` );
break ;
case "tool_call_update" :
// Show tool status changes
console . log ( ` \n 🔧 Tool call \` ${ update . toolCallId } \` updated: ${ update . status } \n ` );
break ;
// Handle other update types...
}
}
Implement File System Operations
Provide file system capabilities to the agent: async writeTextFile (
params : acp . WriteTextFileRequest ,
): Promise < acp . WriteTextFileResponse > {
// In a real client, write the file to disk
console.error( "[Client] Write text file called with:" , params);
return {};
}
async readTextFile (
params : acp . ReadTextFileRequest ,
): Promise < acp . ReadTextFileResponse > {
// In a real client, read from disk
console.error( "[Client] Read text file called with:" , params);
return { content : "Mock file content" };
}
Spawn the Agent Process
Launch the agent as a subprocess: const agentPath = join ( __dirname , "agent.ts" );
const npxCmd = process . platform === "win32" ? "npx.cmd" : "npx" ;
const agentProcess = spawn ( npxCmd , [ "tsx" , agentPath ], {
stdio: [ "pipe" , "pipe" , "inherit" ],
});
We use npx tsx to run TypeScript files directly. Adjust this based on your agent’s runtime requirements.
Create Communication Streams
Set up bidirectional streams for client-agent communication: // Agent stdin for sending messages
const input = Writable . toWeb ( agentProcess . stdin ! );
// Agent stdout for receiving messages
const output = Readable . toWeb (
agentProcess . stdout ! ,
) as ReadableStream < Uint8Array >;
// Create the protocol stream
const stream = acp . ndJsonStream ( input , output );
Initialize the Connection
Create the connection and negotiate capabilities: const client = new ExampleClient ();
const connection = new acp . ClientSideConnection (
( _agent ) => client ,
stream
);
const initResult = await connection . initialize ({
protocolVersion: acp . PROTOCOL_VERSION ,
clientCapabilities: {
fs: {
readTextFile: true ,
writeTextFile: true ,
},
},
});
Create a Session and Send Prompts
Start a new session and interact with the agent: // Create a new session
const sessionResult = await connection . newSession ({
cwd: process . cwd (),
mcpServers: [],
});
// Send a prompt
const promptResult = await connection . prompt ({
sessionId: sessionResult . sessionId ,
prompt: [
{
type: "text" ,
text: "Hello, agent!" ,
},
],
});
console . log ( `Agent completed with: ${ promptResult . stopReason } ` );
Cleanup
Properly terminate the agent process: try {
// ... client operations
} catch ( error ) {
console . error ( "[Client] Error:" , error );
} finally {
agentProcess . kill ();
process . exit ( 0 );
}
Running the Example
Prerequisites
Ensure you have the SDK installed:
npm install @agentclientprotocol/sdk
Run the Complete Example
npx tsx src/examples/client.ts
This will:
Spawn the example agent
Initialize the connection
Create a session
Send a test prompt
Display the agent’s response
Handle any permission requests
Expected Output
✅ Connected to agent (protocol v1.0.0)
📝 Created session: a1b2c3d4e5f6...
💬 User: Hello, agent!
I'll help you with that. Let me start by reading some files...
🔧 Reading project files (pending)
🔧 Tool call `call_1` updated: completed
Now I understand the project structure...
🔧 Modifying critical configuration file (pending)
🔐 Permission requested: Modifying critical configuration file
Options:
1. Allow this change (allow_once)
2. Skip this change (reject_once)
Choose an option: 1
🔧 Tool call `call_2` updated: completed
Perfect! I've successfully updated the configuration...
✅ Agent completed with: end_turn
Key Concepts
Process Management
The client is responsible for:
Spawning the agent process
Managing stdio streams
Handling process lifecycle
Cleaning up on exit
Bidirectional Communication
The client and agent communicate through:
Client → Agent : Requests (initialize, newSession, prompt)
Agent → Client : Notifications (sessionUpdate) and requests (requestPermission)
Capability Negotiation
During initialization, the client declares its capabilities:
clientCapabilities : {
fs : {
readTextFile : true , // Client can read files
writeTextFile : true , // Client can write files
},
}
Agents can query these capabilities to determine what operations are available.
Permission Model
The client controls what the agent can do:
Auto-approved : Read operations, low-risk actions
User approval : File modifications, deletions, sensitive operations
Blocked : Operations not supported by the client
Session Lifecycle
Initialize : Establish connection and exchange capabilities
New Session : Create isolated conversation context
Prompt : Send user requests and receive responses
Updates : Handle real-time agent notifications
Cleanup : Close session and terminate agent
This example handles Windows compatibility by detecting the platform and using the appropriate npx command: const npxCmd = process . platform === "win32" ? "npx.cmd" : "npx" ;
Next Steps
Simple Agent Example Build an agent that works with this client
API Reference Explore the full ClientSideConnection API
Production Examples See how production clients integrate ACP
Client Capabilities Learn about all available client capabilities