Skip to main content
This guide walks you through building an ACP client that connects to agents, sends prompts, and handles responses.

Overview

A client is the application that:
  • Initiates connections to agents
  • Sends user prompts and requests
  • Handles permission requests from agents
  • Provides file system and terminal access
  • Displays session updates to users

Prerequisites

Add the ACP Dart SDK to your pubspec.yaml:
dependencies:
  acp_dart: ^1.0.0

Implementation Steps

1
Create your Client class
2
Implement the Client abstract class to handle agent requests:
3
import 'dart:io';
import 'package:acp_dart/acp_dart.dart';

class MyClient implements Client {
  @override
  Future<RequestPermissionResponse> requestPermission(
    RequestPermissionRequest params,
  ) async {
    // Display permission dialog to user
    print('Permission requested: ${params.toolCall.title}');
    
    for (int i = 0; i < params.options.length; i++) {
      final option = params.options[i];
      print('  ${i + 1}. ${option.name}');
    }
    
    // Get user's choice
    stdout.write('Choose an option: ');
    final choice = stdin.readLineSync();
    final index = int.tryParse(choice ?? '') ?? 0;
    
    if (index > 0 && index <= params.options.length) {
      return RequestPermissionResponse(
        outcome: SelectedOutcome(
          optionId: params.options[index - 1].optionId,
        ),
      );
    }
    
    return RequestPermissionResponse(
      outcome: CancelledOutcome(),
    );
  }

  @override
  Future<void> sessionUpdate(SessionNotification params) async {
    // Handle different types of session updates
    final update = params.update;
    
    if (update is AgentMessageChunkSessionUpdate) {
      if (update.content is TextContentBlock) {
        print((update.content as TextContentBlock).text);
      }
    } else if (update is ToolCallSessionUpdate) {
      print('🔧 ${update.title} (${update.status})');
    } else if (update is ToolCallUpdateSessionUpdate) {
      print('Tool call ${update.toolCallId}: ${update.status}');
    }
  }

  // Implement other required methods...
}
4
Implement file system operations
5
Provide file access to the agent:
6
@override
Future<ReadTextFileResponse> readTextFile(
  ReadTextFileRequest params,
) async {
  try {
    final file = File(params.path);
    final content = await file.readAsString();
    
    return ReadTextFileResponse(content: content);
  } catch (e) {
    throw RequestError.resourceNotFound(params.path);
  }
}

@override
Future<WriteTextFileResponse> writeTextFile(
  WriteTextFileRequest params,
) async {
  try {
    final file = File(params.path);
    await file.writeAsString(params.content);
    
    return WriteTextFileResponse();
  } catch (e) {
    throw RequestError.internalError('Failed to write file: $e');
  }
}
7
Implement terminal operations
8
Enable the agent to execute commands:
9
final Map<String, Process> _terminals = {};

@override
Future<CreateTerminalResponse> createTerminal(
  CreateTerminalRequest params,
) async {
  final terminalId = _generateTerminalId();
  
  final process = await Process.start(
    params.command,
    params.args ?? [],
    workingDirectory: params.cwd,
    environment: params.env?.fold<Map<String, String>>(
      {},
      (map, env) {
        map[env.name] = env.value;
        return map;
      },
    ),
  );
  
  _terminals[terminalId] = process;
  
  return CreateTerminalResponse(terminalId: terminalId);
}

@override
Future<TerminalOutputResponse> terminalOutput(
  TerminalOutputRequest params,
) async {
  final process = _terminals[params.terminalId];
  
  if (process == null) {
    throw RequestError.resourceNotFound('Terminal ${params.terminalId}');
  }
  
  // Read current output
  final output = await process.stdout.transform(utf8.decoder).join();
  final exitCode = await process.exitCode.timeout(
    Duration.zero,
    onTimeout: () => null,
  );
  
  return TerminalOutputResponse(
    output: output,
    exitStatus: exitCode != null
        ? TerminalExitStatus(exitCode: exitCode)
        : null,
    truncated: false,
  );
}

@override
Future<WaitForTerminalExitResponse> waitForTerminalExit(
  WaitForTerminalExitRequest params,
) async {
  final process = _terminals[params.terminalId];
  
  if (process == null) {
    throw RequestError.resourceNotFound('Terminal ${params.terminalId}');
  }
  
  final exitCode = await process.exitCode;
  
  return WaitForTerminalExitResponse(exitCode: exitCode);
}

@override
Future<ReleaseTerminalResponse> releaseTerminal(
  ReleaseTerminalRequest params,
) async {
  final process = _terminals.remove(params.terminalId);
  
  if (process != null) {
    process.kill();
  }
  
  return ReleaseTerminalResponse();
}

@override
Future<KillTerminalCommandResponse> killTerminal(
  KillTerminalCommandRequest params,
) async {
  final process = _terminals[params.terminalId];
  
  if (process != null) {
    process.kill();
  }
  
  return KillTerminalCommandResponse();
}
10
Connect to an agent
11
Establish the connection and initialize the protocol:
12
import 'dart:io';
import 'package:acp_dart/acp_dart.dart';

Future<void> main() async {
  // Spawn the agent as a subprocess
  final agentProcess = await Process.start('dart', [
    'run',
    'path/to/agent.dart',
  ]);

  // Create the client
  final client = MyClient();
  
  // Create NDJSON stream
  final stream = ndJsonStream(
    agentProcess.stdout,
    agentProcess.stdin,
  );
  
  // Create the client-side connection
  final connection = ClientSideConnection(
    (agent) => client,
    stream,
  );

  // Initialize the connection
  final initResult = await connection.initialize(
    InitializeRequest(
      protocolVersion: 1,
      clientCapabilities: ClientCapabilities(
        fs: FileSystemCapability(
          readTextFile: true,
          writeTextFile: true,
        ),
        terminal: true,
      ),
    ),
  );

  print('Connected to agent (protocol v${initResult.protocolVersion})');
}
13
Create a session and send prompts
14
Start a conversation with the agent:
15
// Create a new session
final sessionResult = await connection.newSession(
  NewSessionRequest(
    cwd: Directory.current.path,
    mcpServers: [
      HttpMcpServer(
        name: 'docs',
        url: 'https://example.com',
        headers: const [],
      ),
    ],
  ),
);

print('Created session: ${sessionResult.sessionId}');

// Send a prompt
final promptResult = await connection.prompt(
  PromptRequest(
    sessionId: sessionResult.sessionId,
    prompt: [
      TextContentBlock(text: 'Hello, please analyze my project'),
    ],
  ),
);

print('Agent completed with stop reason: ${promptResult.stopReason}');
16
Cancel ongoing operations
17
Cancel a prompt turn if needed:
18
// Send cancellation notification
await connection.cancel(
  CancelNotification(sessionId: sessionId),
);

Extension Methods

Implement custom protocol extensions:
@override
Future<Map<String, dynamic>>? extMethod(
  String method,
  Map<String, dynamic> params,
) async {
  if (method == '_custom_analyze') {
    // Handle custom method
    return {'result': 'analysis complete'};
  }
  
  throw RequestError.methodNotFound(method);
}

@override
Future<void>? extNotification(
  String method,
  Map<String, dynamic> params,
) async {
  if (method == '_custom_notification') {
    // Handle custom notification
    print('Custom notification: $params');
  }
}

Complete Example

See the full example in the repository:
dart run example/client.dart

Next Steps

File System Operations

Learn about file system capabilities

Terminal Operations

Execute and manage terminal commands

Build docs developers (and LLMs) love