mcp server
go-adk-acpagent
ACP-backed Google ADK agent adapter for Go
Set up
Runs on your computer. Pick your agent and operating system.
This project does not publish an install method we can turn into a config. Follow the README instead.
README
synced Oct 26, 2026go-adk-acpagent
Run ACP-compatible coding agents as Google ADK agent.Agent
implementations.
go-adk-acpagent/v2 starts a coding-agent subprocess, talks to it with Agent
Client Protocol (ACP) over stdio, binds ACP sessions to ADK sessions, and emits
ADK events for streamed text, thoughts, tool calls, usage, plan updates, and
provider errors.
Use it when your application is built on Google ADK v2 but the coding agent you want to run is exposed as an ACP command.
What You Get
| Capability | Behavior |
|---|---|
| ADK agent | Implements the ADK agent.Agent interface. |
| ACP lifecycle | Starts one ACP subprocess per agent instance and closes it on shutdown. |
| Session binding | Creates, stores, reuses, and resumes ACP sessions through ADK session state. |
| Event mapping | Converts ACP updates into ADK events and state deltas. |
| Structured prompts | Preserves ordered ADK content and negotiates optional image/audio/embedded-resource blocks from ACP initialize capabilities; file data falls back to baseline resource links. |
| Permissions | Maps protocol callbacks into ADK-facing PermissionRequest values handled by PermissionHandler. |
| Session config | Applies ACP session-bound values such as model, mode, or thought level. |
| MCP forwarding | Sends configured MCP servers to ACP session creation and resume calls. |
| Diagnostics | Uses *slog.Logger for adapter logs and a separate writer for provider stderr. |
Try It With OpenCode
go get github.com/normahq/go-adk-acpagent/v2
package main
import (
"context"
"fmt"
"io"
"log"
"log/slog"
"os"
acpagent "github.com/normahq/go-adk-acpagent/v2"
"google.golang.org/adk/v2/agent"
"google.golang.org/adk/v2/runner"
"google.golang.org/adk/v2/session"
"google.golang.org/genai"
)
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
func run() error {
ctx := context.Background()
logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
// 1. Create the ACP-backed ADK agent.
agentRuntime, err := acpagent.NewWithContext(ctx, acpagent.Config{
Command: []string{"opencode", "acp"},
WorkingDir: "/workspace",
Logger: logger,
Stderr: io.Discard,
})
if err != nil {
return fmt.Errorf("create ACP agent: %w", err)
}
defer func() {
if err := agentRuntime.Close(); err != nil {
log.Printf("close ACP agent: %v", err)
}
}()
// 2. Wire the agent into an ADK runner.
sessionService := session.InMemoryService()
r, err := runner.New(runner.Config{
AppName: "opencode-app",
Agent: agentRuntime,
SessionService: sessionService,
})
if err != nil {
return fmt.Errorf("create runner: %w", err)
}
// 3. Create an ADK session.
sess, err := sessionService.Create(ctx, &session.CreateRequest{
AppName: "opencode-app",
UserID: "user-1",
})
if err != nil {
return fmt.Errorf("create session: %w", err)
}
// 4. Run a turn and stream events.
prompt := genai.NewContentFromText("Explain this codebase", genai.RoleUser)
for ev, err := range r.Run(ctx, "user-1", sess.Session.ID(), prompt, agent.RunConfig{}) {
if err != nil {
return fmt.Errorf("run turn: %w", err)
}
if ev.Content != nil {
for _, part := range ev.Content.Parts {
if part.Text != "" && !part.Thought {
fmt.Print(part.Text)
}
}
}
}
fmt.Println()
return nil
}
Provider Commands
| Provider | Command |
|---|---|
| OpenCode | []string{"opencode", "acp"} |
| Codex | []string{"npx", "-y", "@normahq/codex-acp-bridge@latest"} |
| Claude Code | []string{"npx", "-y", "@zed-industries/claude-code-acp@latest"} |
| Pi | []string{"npx", "-y", "pi-acp"} |
| Generic ACP | Any executable that speaks ACP on stdin/stdout. |
Runnable examples are available for OpenCode, Codex, Claude Code, and Pi.
Configuration Cheatsheet
| Field | Purpose |
|---|---|
Command | ACP subprocess argv. |
LazyInitialization | Defer process launch and initialize until invocation. |
InitializeTimeout | Bound gate wait, process launch, and initialize independently. Zero inherits SetupTimeout; negative is invalid. |
SetupTimeout | Bound session attachment; default initialize budget when InitializeTimeout is zero. |
WorkingDir | Process directory and default ACP session cwd. |
Logger | Adapter diagnostics through *slog.Logger. |
Stderr | ACP subprocess stderr forwarding. |
PermissionHandler | Application decision point for generic agent permission requests. |
SessionConfig | ACP session config values applied through session/set_config_option. |
MCPServers | MCP servers forwarded to ACP sessions. |
Instruction / GlobalInstruction | ADK-style instructions prepended to prompts. |
ReasoningEffort | Provider reasoning effort metadata when supported. |
OutputKey | ADK state key for the final visible model output. |
ACP provider error metadata helpers are available from:
import "github.com/normahq/go-adk-acpagent/v2/acperror"
Documentation By Task
| Task | Start here |
|---|---|
| Understand lifecycle | Concepts |
| Understand ACP-to-ADK event mapping | Event mapping |
| Choose a provider command | Provider recipes |
| Manage cwd, session metadata, config values, plans, and output state | Session state |
| Debug startup, JSON-RPC streams, permissions, and provider errors | Troubleshooting |
Startup And Attachment Budgets
Set independent budgets when cold initialize needs more time than session attachment:
Config{
Command: []string{"opencode", "acp"},
LazyInitialization: true,
InitializeTimeout: 60 * time.Second,
SetupTimeout: 10 * time.Second,
}
InitializeTimeout covers waiting for initialization already in progress,
process launch, and the ACP initialize handshake. It applies to eager and lazy
initialization. Zero inherits SetupTimeout for compatibility; if both are
zero, only caller and lifecycle cancellation/deadlines limit initialization.
Negative values are rejected before launch. Earlier caller deadlines and
application cancellation still win.
SetupTimeout continues to bound session/new and session/resume attachment
and related setup RPCs. Neither duration limits prompts or the lifetime of a
healthy process. A successful client survives the initialization deadline and
request cancellation, and later invocations reuse it. Lazy mode still performs
no process launch until invocation.
Call Close during shutdown. Cleanup closes stdin, allows 200ms for graceful
exit, kills the owned immediate child if needed, and waits up to 5s more. An
exceptional stalled process wait returns an explicit cleanup-timeout error;
the original reaper remains responsible until the wait finishes. Initialization
errors retain their cancellation/deadline cause, including when cleanup adds
an error. Command wrapper descendants remain outside the immediate-child
ownership contract.
Production Notes
- Call
Closeduring shutdown so the ACP subprocess exits cleanly. - Pass the application lifecycle context to
NewWithContext. The legacyConfig.Contextfield remains available for v2 compatibility. - Debug and trace logs omit prompt and update payload content. They retain
structural diagnostics such as content-block types, counts, and byte lengths.
Provider stderr remains separately controlled by
Stderr. - Keep ACP protocol messages on stdout and provider logs on stderr.
- Use
SessionConfigfor session-bound model, mode, thought-level, or provider-specific choices. UseSelectSessionConfigValuefor select options andBooleanSessionConfigValuefor boolean options. - Treat
SessionStateKeyas adapter-owned except for documented_meta,config_values, and cwd overrides.
Tests
go test -race ./...
go test ./... -coverprofile=coverage.out
go tool cover -func=coverage.out
CI requires at least 95% total statement coverage.
Provider integration tests are opt-in because they require external ACP commands and credentials:
task test:integration:codex
task test:integration:opencode