Skip to content

ACP Usage Guide

This guide covers how to use Continuum Router as an ACP (Agent Communication Protocol) agent — from configuration to connecting IDE clients.

Prerequisites

  • Continuum Router binary (see Installation)
  • At least one configured LLM backend (OpenAI, Anthropic, Ollama, etc.)
  • A configuration file with ACP enabled

Configuration

Add the acp section to your config.yaml:

backends:
  - url: http://localhost:11434
    name: ollama
    models: ["llama3.2"]

acp:
  enabled: true

  transport:
    stdio:
      enabled: true

  capabilities:
    load_session: true
    mcp: true

  default_model: "llama3.2"

  permissions:
    default_policy: ask_always
    auto_allow:
      - read
      - search
      - think
    always_ask:
      - edit
      - delete
      - execute

  sessions:
    max_concurrent: 10
    idle_timeout: "1h"
    storage: memory

For a complete configuration reference, see the ACP Architecture page.

Starting the Router in ACP Mode

Launch the router with --mode stdio to enable ACP transport:

continuum-router --config config.yaml --mode stdio

In this mode:

  • stdin/stdout are reserved for JSON-RPC 2.0 messages (NDJSON format)
  • stderr receives log output (tracing)
  • No HTTP listeners are started — the router communicates exclusively via stdio

Tip

You can still run the HTTP API gateway separately. ACP stdio mode is a different execution mode, not an add-on to HTTP mode.

Manual Testing

You can test ACP communication by piping JSON-RPC messages through stdin.

Step 1: Initialize the Protocol

Every ACP session must begin with an initialize handshake:

echo '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":1,"clientCapabilities":{"readTextFile":true,"writeTextFile":true,"terminal":false},"clientInfo":{"name":"manual-test","version":"1.0.0"}},"id":1}' \
  | continuum-router --config config.yaml --mode stdio 2>/dev/null

Expected response:

{"jsonrpc":"2.0","result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"image":false,"audio":false,"embeddedContext":false,"mcp":true,"mcpCapabilities":{"acp":true}},"agentInfo":{"name":"Continuum Router","version":"1.3.0-beta.1"},"authMethods":[]},"id":1}

Step 2: Interactive Session

For a full interactive session, use a tool like socat or a simple script:

# Start the router in background, capturing stdin/stdout
continuum-router --config config.yaml --mode stdio 2>/dev/null &
ROUTER_PID=$!

# Send messages via /proc or named pipes
# (See the scripted example below for a practical approach)

Scripted Testing

Here is a complete test session using a shell script:

#!/bin/bash
# acp-test.sh — Send a sequence of ACP messages to the router

CONFIG="config.yaml"

{
  # 1. Initialize
  echo '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":1,"clientCapabilities":{"readTextFile":true},"clientInfo":{"name":"test","version":"1.0"}},"id":1}'

  # Wait for the response to be processed
  sleep 0.5

  # 2. Create a new session
  echo '{"jsonrpc":"2.0","method":"session/new","params":{"mode":"code"},"id":2}'
  sleep 0.5

  # 3. Send a prompt (replace SESSION_ID with the actual session ID from step 2)
  # echo '{"jsonrpc":"2.0","method":"session/prompt","params":{"sessionId":"SESSION_ID","content":[{"type":"text","text":"Hello, what can you do?"}],"stream":false},"id":3}'
  # sleep 2

} | continuum-router --config "$CONFIG" --mode stdio 2>/dev/null

Using Python for Testing

For more control over the bidirectional communication:

#!/usr/bin/env python3
"""ACP client test script."""

import json
import subprocess
import sys
import threading

def read_responses(proc):
    """Read and print responses from the router."""
    for line in proc.stdout:
        resp = json.loads(line)
        print(json.dumps(resp, indent=2))

def send(proc, method, params=None, msg_id=None):
    """Send a JSON-RPC message."""
    msg = {"jsonrpc": "2.0", "method": method}
    if params:
        msg["params"] = params
    if msg_id is not None:
        msg["id"] = msg_id
    proc.stdin.write(json.dumps(msg) + "\n")
    proc.stdin.flush()

proc = subprocess.Popen(
    ["continuum-router", "--config", "config.yaml", "--mode", "stdio"],
    stdin=subprocess.PIPE,
    stdout=subprocess.PIPE,
    stderr=subprocess.DEVNULL,
    text=True,
)

# Read responses in a background thread
reader = threading.Thread(target=read_responses, args=(proc,), daemon=True)
reader.start()

# 1. Initialize
send(proc, "initialize", {
    "protocolVersion": 1,
    "clientCapabilities": {"readTextFile": True, "writeTextFile": True},
    "clientInfo": {"name": "python-test", "version": "1.0.0"},
}, msg_id=1)

import time; time.sleep(1)

# 2. Create session
send(proc, "session/new", {"mode": "code"}, msg_id=2)
time.sleep(1)

# 3. Close stdin to trigger graceful shutdown
proc.stdin.close()
proc.wait()

IDE Integration

ACP is designed for IDE clients to spawn the router as a child process and communicate over stdio. The general pattern:

IDE Process
    ├── spawn: continuum-router --config config.yaml --mode stdio
    │           stdin  ← JSON-RPC requests from IDE
    │           stdout → JSON-RPC responses to IDE
    │           stderr → logs (can be captured for debugging)
    └── communicate via NDJSON (one JSON object per line)

Integration Steps

  1. Spawn the process: Start continuum-router --mode stdio --config <path> as a child process with piped stdin/stdout.

  2. Send initialize: The first message must be the initialize request. All other methods are rejected until initialization completes.

  3. Create a session: Call session/new with a mode (code, architect, etc.) to get a sessionId.

  4. Send prompts: Use session/prompt with the session ID to send user messages. The router streams responses back as session/update notifications.

  5. Handle permissions: When the LLM requests tool operations (file edits, command execution), the router sends session/request_permission notifications. The IDE should display a UI prompt and respond with allow_once, allow_always, reject_once, or reject_always.

  6. Manage lifecycle: Use session/cancel to abort in-flight operations, session/set_mode to change modes, and close stdin for graceful shutdown.

Example: Spawning from Node.js

const { spawn } = require("child_process");
const readline = require("readline");

const router = spawn("continuum-router", [
  "--config", "config.yaml",
  "--mode", "stdio",
]);

// Parse NDJSON responses
const rl = readline.createInterface({ input: router.stdout });
rl.on("line", (line) => {
  const msg = JSON.parse(line);
  console.log("Received:", JSON.stringify(msg, null, 2));
});

// Log stderr for debugging
router.stderr.on("data", (data) => {
  console.error("[router log]", data.toString());
});

function send(method, params, id) {
  const msg = JSON.stringify({ jsonrpc: "2.0", method, params, id });
  router.stdin.write(msg + "\n");
}

// Initialize
send("initialize", {
  protocolVersion: 1,
  clientCapabilities: { readTextFile: true, writeTextFile: true },
  clientInfo: { name: "my-ide", version: "1.0.0" },
}, 1);

// After receiving initialize response, create session...
setTimeout(() => {
  send("session/new", { mode: "code" }, 2);
}, 1000);

MCP Server Tunneling

If you have MCP servers configured, ACP clients can access them through the router without managing separate MCP connections.

Configuration

Add MCP servers to your config:

acp:
  enabled: true
  capabilities:
    mcp: true
  mcp:
    max_connections_per_session: 5
    allowed_servers: []  # Empty = all allowed

mcp_servers:
  - id: filesystem
    command: npx
    args: ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"]
  - id: github
    command: npx
    args: ["-y", "@modelcontextprotocol/server-github"]
    env:
      GITHUB_TOKEN: "${GITHUB_TOKEN}"

Usage

After initializing, connect to an MCP server and route messages:

// Connect
{"jsonrpc":"2.0","method":"mcp/connect","params":{"id":"filesystem"},"id":10}

// List tools
{"jsonrpc":"2.0","method":"mcp/message","params":{"connectionId":"<returned-uuid>","message":{"jsonrpc":"2.0","method":"tools/list","id":1}},"id":11}

// Disconnect
{"jsonrpc":"2.0","method":"mcp/disconnect","params":{"connectionId":"<returned-uuid>"},"id":12}

Admin Endpoints

When running in HTTP mode alongside ACP configuration, the admin API provides ACP status endpoints:

Endpoint Description
GET /admin/acp/status ACP subsystem status and capabilities
GET /admin/acp/sessions List active ACP sessions
GET /admin/acp/agent.json Agent registry metadata for discovery
# Check ACP status
curl http://localhost:8080/admin/acp/status

# View active sessions
curl http://localhost:8080/admin/acp/sessions

# Get agent metadata
curl http://localhost:8080/admin/acp/agent.json

Troubleshooting

Router exits immediately

Ensure your config file exists and has valid YAML syntax. Check stderr output:

continuum-router --config config.yaml --mode stdio 2>router.log
# Then check router.log for errors

"Server not initialized" errors (-32002)

The initialize handshake must be the first message sent. All other methods are rejected until initialization completes.

No response to prompts

  • Verify that acp.enabled is true in your config
  • Verify that the configured backend is reachable
  • Check that acp.default_model matches a model available on your backend
  • Review stderr logs for backend connection errors

Permission requests not working

If tool calls are being silently rejected, check your permission configuration:

acp:
  permissions:
    default_policy: ask_always  # Not "allow_all" for production
    auto_allow:
      - read
      - search
      - think

MCP server fails to connect

  • Verify the MCP server command is installed (e.g., npx is available)
  • Check that the server ID matches the configuration
  • Review the server_spawn_timeout setting (default 10s)

Next Steps