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 yourpubspec.yaml:
Implementation Steps
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...
}
@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');
}
}
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();
}
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})');
}
// 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}');
Extension Methods
Implement custom protocol extensions:Complete Example
See the full example in the repository:Next Steps
File System Operations
Learn about file system capabilities
Terminal Operations
Execute and manage terminal commands