Skip to main content

Copilot CLI ACP server

Learn about GitHub Copilot CLI's Agent Client Protocol server.

Overview

Agent Client Protocol (ACP) is a standardized protocol that enables external clients, such as IDEs, editors, and other tools, to communicate with the Copilot agent runtime. It provides a structured way to create and manage agent sessions, send prompts, receive streaming responses, and handle permission requests.

Architecture

┌─────────────────┐         ┌─────────────────────┐
│   IDE / Editor  │  ACP    │  Copilot Agent      │
│   (ACP Client)  │◄───────►│  Runtime            │
│                 │  stdio/ │  (ACP Server)       │
│                 │  TCP    │                     │
└─────────────────┘         └─────────────────────┘

The ACP server runs as part of GitHub Copilot CLI and exposes the agent's capabilities through a well-defined protocol.

Starting the ACP server

copilot --acp --stdio

Communication happens over stdin/stdout using newline-delimited JSON (NDJSON).

TCP Mode

copilot --acp --port 3000

Communication happens over a TCP socket on the specified port.

Protocol flow

1. Initialize connection

// Client sends initialize request
const response = await connection.initialize({
    clientInfo: {
        name: "MyIDE",
        version: "1.0"
    }
});

// Server responds with capabilities
// {
//     serverInfo: { name: "copilot-agent-runtime", version: "x.x.x" },
//     capabilities: { ... }
// }

2. Create a session

const session = await connection.newSession({
    cwd: "/path/to/project"
});

// Returns: { sessionId: "uuid-..." }

3. Send prompts

await connection.prompt({
    sessionId: session.sessionId,
    prompt: [
        { type: "text", text: "Fix the authentication bug in login.ts" }
    ]
});

4. Receive session updates

The client receives real-time updates via the sessionUpdate callback:

const client: acp.Client = {
    async sessionUpdate(params) {
        // params.state: "idle" | "working"
        // params.lastUpdateId: number
        // params.updates: Array of content updates

        for (const update of params.updates) {
            switch (update.type) {
                case "text":
                    console.log("Agent says:", update.text);
                    break;
                case "tool":
                    console.log("Tool execution:", update.name, update.status);
                    break;
            }
        }
    }
};

5. Handle permission requests

The agent requests permission for sensitive operations:

const client: acp.Client = {
    async requestPermission(params) {
        // params.request contains details about what permission is needed
        // e.g., tool execution, file access, URL fetch

        // Display dialog to user and return their choice
        return {
            outcome: {
                outcome: "selected",
                optionId: "allow_once"  // or "allow_always" or "reject_once"
            }
        };
    }
};

Content types

Text content

{
    type: "text",
    text: "Your message here"
}

Image content

{
    type: "image",
    data: "<base64-encoded-image>",
    mimeType: "image/png"  // or "image/jpeg", "image/gif", "image/webp"
}

Embedded resources

{
    type: "resource",
    resource: {
        uri: "file:///path/to/file.txt",
        text: "file contents",
        mimeType: "text/plain"
    }
}

Blob data

{
    type: "blob",
    data: "<base64-encoded-data>",
    mimeType: "application/octet-stream"
}

Tool execution updates

When the agent executes tools, clients receive detailed updates:

{
    type: "tool",
    id: "tool-execution-id",
    name: "edit",           // Tool name
    status: "running",      // "running" | "completed"
    kind: "edit",           // Tool category
    input: { ... },         // Tool parameters
    output: { ... },        // Tool result (when completed)
    diff: "..."             // Diff content for edit operations
}

Tool kinds

KindDescription
executeCommand execution (bash)
readFile reading operations
editFile modifications
deleteFile deletions
createFile creation
searchCode search (grep, glob)
fetchWeb fetching
agentSub-agent invocation
githubGitHub API operations
otherOther tool types

Permission types

Tool permissions

Requested when the agent wants to execute a tool:

{
    type: "tool",
    toolName: "bash",
    input: { command: "npm install" }
}

Path permissions

Requested for file system access:

{
    type: "path",
    path: "/path/to/sensitive/file",
    operation: "write"  // or "read"
}

URL permissions

Requested for network access:

{
    type: "url",
    url: "https://api.example.com/data",
    operation: "fetch"
}

Session lifecycle

┌─────────────┐
│   Created   │
└──────┬──────┘
       │ prompt()
       ▼
┌─────────────┐
│   Working   │◄───┐
└──────┬──────┘    │
       │           │ prompt()
       ▼           │
┌─────────────┐    │
│    Idle     │────┘
└──────┬──────┘
       │ destroy()
       ▼
┌─────────────┐
│  Destroyed  │
└─────────────┘

Error handling

Errors are communicated through the protocol's standard error response format:

{
    error: {
        code: -32600,
        message: "Invalid request",
        data: { ... }
    }
}

Example: Full Client Implementation

import * as acp from '@anthropic/acp-sdk';
import { spawn } from 'child_process';

async function main() {
    // Start the ACP server
    const process = spawn('copilot', ['--acp', '--stdio'], {
        stdio: ['pipe', 'pipe', 'inherit']
    });

    // Create transport stream
    const stream = acp.ndJsonStream(process.stdin, process.stdout);

    // Define client handlers
    const client: acp.Client = {
        async requestPermission(params) {
            console.log('Permission requested:', params.request);
            // Auto-approve for this example
            return {
                outcome: { outcome: "selected", optionId: "allow_once" }
            };
        },

        async sessionUpdate(params) {
            console.log(`Session ${params.sessionId} state: ${params.state}`);
            for (const update of params.updates) {
                if (update.type === 'text') {
                    process.stdout.write(update.text);
                } else if (update.type === 'tool') {
                    console.log(`Tool: ${update.name} - ${update.status}`);
                }
            }
        }
    };

    // Establish connection
    const connection = new acp.ClientSideConnection(client, stream);

    // Initialize
    await connection.initialize({
        clientInfo: { name: "example-client", version: "1.0.0" }
    });

    // Create session
    const session = await connection.newSession({
        cwd: process.cwd()
    });

    // Send prompt
    await connection.prompt({
        sessionId: session.sessionId,
        prompt: [
            { type: "text", text: "List the files in the current directory" }
        ]
    });

    // Wait for completion (in real app, handle via sessionUpdate)
    await new Promise(resolve => setTimeout(resolve, 10000));

    // Cleanup
    process.kill();
}

main().catch(console.error);

Configuration

Runtime settings

The ACP server inherits settings from the Copilot runtime:

  • Model selection: Configured via environment or runtime settings
  • Tool permissions: Managed through the permission service
  • Logging: Configurable log level and directory

Environment variables

VariableDescription
COPILOT_LOG_LEVELLog verbosity (debug, info, warn, error)
COPILOT_LOG_DIRDirectory for log files

Best practices

  1. Use Stdio Mode for IDEs: More reliable than TCP for local integrations
  2. Handle Permissions Gracefully: Always implement the requestPermission callback
  3. Process Updates Incrementally: Don't wait for completion to show progress
  4. Clean Up Sessions: Call destroy when done to free resources
  5. Handle Reconnection: Implement retry logic for network transports

Source files

FileDescription
src/cli/acp/server.tsMain ACP server implementation
src/cli/acp/mapping.tsEvent transformation layer
src/cli/acp/permissionHandlers.tsPermission request handlers
src/cli/acp/permissionMapping.tsPermission mapping utilities

Testing

ACP has end-to-end tests that can be run with:

CI=true npm run test:cli-e2e -- test/cli/e2e/acp.test.ts

Test captures are available in test/cli/e2e/captures/acp/ showing various content type handling scenarios.