ACP 사용 가이드¶
이 가이드는 Continuum Router를 ACP(Agent Communication Protocol) 에이전트로 사용하는 방법을 설정부터 IDE 클라이언트 연결까지 다룹니다.
사전 요구 사항¶
- Continuum Router 바이너리 (설치 참조)
- 하나 이상의 설정된 LLM 백엔드 (OpenAI, Anthropic, Ollama 등)
- ACP가 활성화된 설정 파일
설정¶
config.yaml에 acp 섹션을 추가합니다:
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
전체 설정 레퍼런스는 ACP 아키텍처 페이지를 참조하세요.
ACP 모드로 라우터 시작하기¶
--mode stdio로 라우터를 실행하면 ACP 전송이 활성화됩니다:
이 모드에서는 다음과 같이 동작합니다:
- stdin/stdout은 JSON-RPC 2.0 메시지(NDJSON 형식) 전용으로 예약됩니다
- stderr로 로그가 출력됩니다 (tracing)
- HTTP 리스너를 시작하지 않으며, 라우터는 오직 stdio로만 통신합니다
Tip
HTTP API 게이트웨이는 여전히 별도로 실행할 수 있습니다. ACP stdio 모드는 HTTP 모드에 덧붙는 기능이 아니라 별개의 실행 모드입니다.
수동 테스트¶
stdin으로 JSON-RPC 메시지를 파이프하여 ACP 통신을 테스트할 수 있습니다.
1단계: 프로토콜 초기화¶
모든 ACP 세션은 initialize 핸드셰이크로 시작해야 합니다:
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
예상 응답:
{"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}
2단계: 대화형 세션¶
완전한 대화형 세션에는 socat 같은 도구나 간단한 스크립트를 사용합니다:
# 라우터를 백그라운드로 시작하고 stdin/stdout을 캡처
continuum-router --config config.yaml --mode stdio 2>/dev/null &
ROUTER_PID=$!
# /proc 또는 명명된 파이프로 메시지 전송
# (실용적인 접근 방법은 아래 스크립트 예제 참조)
스크립트로 테스트하기¶
다음은 셸 스크립트를 사용한 전체 테스트 세션입니다:
#!/bin/bash
# acp-test.sh: ACP 메시지 시퀀스를 라우터로 전송
CONFIG="config.yaml"
{
# 1. 초기화
echo '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":1,"clientCapabilities":{"readTextFile":true},"clientInfo":{"name":"test","version":"1.0"}},"id":1}'
# 응답이 처리될 때까지 대기
sleep 0.5
# 2. 새 세션 생성
echo '{"jsonrpc":"2.0","method":"session/new","params":{"mode":"code"},"id":2}'
sleep 0.5
# 3. 프롬프트 전송 (SESSION_ID를 2단계에서 받은 실제 세션 ID로 교체)
# 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
Python으로 테스트하기¶
양방향 통신을 더 세밀하게 제어하려면 다음과 같이 합니다:
#!/usr/bin/env python3
"""ACP 클라이언트 테스트 스크립트."""
import json
import subprocess
import sys
import threading
def read_responses(proc):
"""라우터의 응답을 읽어 출력합니다."""
for line in proc.stdout:
resp = json.loads(line)
print(json.dumps(resp, indent=2))
def send(proc, method, params=None, msg_id=None):
"""JSON-RPC 메시지를 전송합니다."""
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,
)
# 백그라운드 스레드에서 응답 읽기
reader = threading.Thread(target=read_responses, args=(proc,), daemon=True)
reader.start()
# 1. 초기화
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. 세션 생성
send(proc, "session/new", {"mode": "code"}, msg_id=2)
time.sleep(1)
# 3. stdin을 닫아 정상 종료 유도
proc.stdin.close()
proc.wait()
IDE 통합¶
ACP는 IDE 클라이언트가 라우터를 자식 프로세스로 스폰하고 stdio로 통신하도록 설계되었습니다. 일반적인 패턴은 다음과 같습니다:
IDE 프로세스
│
├── 스폰: continuum-router --config config.yaml --mode stdio
│ stdin ← IDE가 보내는 JSON-RPC 요청
│ stdout → IDE로 전달되는 JSON-RPC 응답
│ stderr → 로그 (디버깅용으로 캡처 가능)
│
└── NDJSON으로 통신 (한 줄에 JSON 객체 하나)
통합 단계¶
-
프로세스 스폰:
continuum-router --mode stdio --config <path>를 stdin/stdout이 파이프로 연결된 자식 프로세스로 시작합니다. -
initialize전송: 첫 메시지는 반드시initialize요청이어야 합니다. 초기화가 완료될 때까지 다른 모든 메서드는 거부됩니다. -
세션 생성:
session/new를 모드(code,architect등)와 함께 호출하여sessionId를 받습니다. -
프롬프트 전송: 세션 ID와 함께
session/prompt를 호출해 사용자 메시지를 보냅니다. 라우터는session/update알림으로 응답을 스트리밍해 돌려줍니다. -
권한 처리: LLM이 도구 작업(파일 편집, 명령 실행)을 요청하면 라우터가
session/request_permission알림을 보냅니다. IDE는 UI 프롬프트를 표시한 뒤allow_once,allow_always,reject_once,reject_always중 하나로 응답해야 합니다. -
라이프사이클 관리: 진행 중인 작업을 중단하려면
session/cancel을, 모드를 바꾸려면session/set_mode를 사용하고, 정상 종료하려면 stdin을 닫습니다.
예제: Node.js에서 스폰하기¶
const { spawn } = require("child_process");
const readline = require("readline");
const router = spawn("continuum-router", [
"--config", "config.yaml",
"--mode", "stdio",
]);
// NDJSON 응답 파싱
const rl = readline.createInterface({ input: router.stdout });
rl.on("line", (line) => {
const msg = JSON.parse(line);
console.log("Received:", JSON.stringify(msg, null, 2));
});
// 디버깅을 위해 stderr 로그 출력
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");
}
// 초기화
send("initialize", {
protocolVersion: 1,
clientCapabilities: { readTextFile: true, writeTextFile: true },
clientInfo: { name: "my-ide", version: "1.0.0" },
}, 1);
// initialize 응답을 받은 후 세션 생성...
setTimeout(() => {
send("session/new", { mode: "code" }, 2);
}, 1000);
MCP 서버 터널링¶
MCP 서버가 설정되어 있다면, ACP 클라이언트는 별도의 MCP 연결을 관리하지 않고도 라우터를 통해 MCP 서버에 접근할 수 있습니다.
설정¶
설정에 MCP 서버를 추가합니다:
acp:
enabled: true
capabilities:
mcp: true
mcp:
max_connections_per_session: 5
allowed_servers: [] # 비어 있으면 모두 허용
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}"
사용법¶
초기화한 뒤 MCP 서버에 연결하고 메시지를 라우팅합니다:
// 연결
{"jsonrpc":"2.0","method":"mcp/connect","params":{"id":"filesystem"},"id":10}
// 도구 목록 조회
{"jsonrpc":"2.0","method":"mcp/message","params":{"connectionId":"<returned-uuid>","message":{"jsonrpc":"2.0","method":"tools/list","id":1}},"id":11}
// 연결 해제
{"jsonrpc":"2.0","method":"mcp/disconnect","params":{"connectionId":"<returned-uuid>"},"id":12}
관리자 엔드포인트¶
ACP 설정과 함께 HTTP 모드로 실행하면 Admin API가 ACP 상태 엔드포인트를 제공합니다:
| 엔드포인트 | 설명 |
|---|---|
GET /admin/acp/status |
ACP 서브시스템 상태와 기능 |
GET /admin/acp/sessions |
활성 ACP 세션 목록 |
GET /admin/acp/agent.json |
디스커버리용 에이전트 레지스트리 메타데이터 |
# ACP 상태 확인
curl http://localhost:8080/admin/acp/status
# 활성 세션 보기
curl http://localhost:8080/admin/acp/sessions
# 에이전트 메타데이터 가져오기
curl http://localhost:8080/admin/acp/agent.json
문제 해결¶
라우터가 즉시 종료되는 경우¶
설정 파일이 존재하고 YAML 문법이 올바른지 확인하세요. stderr 출력을 확인합니다:
"Server not initialized" 오류 (-32002)¶
initialize 핸드셰이크가 가장 먼저 전송되는 메시지여야 합니다. 초기화가 완료될 때까지 다른 모든 메서드는 거부됩니다.
프롬프트에 응답이 없는 경우¶
- 설정에서
acp.enabled가true인지 확인합니다 - 설정한 백엔드에 연결할 수 있는지 확인합니다
acp.default_model이 백엔드에서 사용 가능한 모델과 일치하는지 확인합니다- stderr 로그에서 백엔드 연결 오류를 살펴봅니다
권한 요청이 동작하지 않는 경우¶
도구 호출이 아무 안내 없이 거부된다면 권한 설정을 확인하세요:
acp:
permissions:
default_policy: ask_always # 프로덕션에서는 "allow_all"을 사용하지 마세요
auto_allow:
- read
- search
- think
MCP 서버 연결에 실패하는 경우¶
- MCP 서버 명령이 설치되어 있는지 확인합니다 (예:
npx사용 가능 여부) - 서버 ID가 설정과 일치하는지 확인합니다
server_spawn_timeout설정을 확인합니다 (기본값 10초)