Skip to main content

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(`\nOptions:`);
    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("\nChoose 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

1

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) { /* ... */ }
}
2

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("\nChoose an option: ");
  const optionIndex = parseInt(answer.trim()) - 1;

  // Return the selected option
  return {
    outcome: {
      outcome: "selected",
      optionId: params.options[optionIndex].optionId,
    },
  };
}
3

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...
  }
}
4

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" };
}
5

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.
6

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);
7

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,
    },
  },
});
8

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}`);
9

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:
  1. Spawn the example agent
  2. Initialize the connection
  3. Create a session
  4. Send a test prompt
  5. Display the agent’s response
  6. 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

  1. Initialize: Establish connection and exchange capabilities
  2. New Session: Create isolated conversation context
  3. Prompt: Send user requests and receive responses
  4. Updates: Handle real-time agent notifications
  5. Cleanup: Close session and terminate agent

Platform Considerations

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

Build docs developers (and LLMs) love