An idiomatic C# / .NET implementation of the Agent Client Protocol (ACP), modelled after the official TypeScript SDK.
ACP is a JSON-RPC 2.0 protocol that standardises how code editors ("clients") talk to coding
agents ("agents"). This library lets you build either side in .NET 8+ (multi-targets net8.0
and net10.0).
Status: unaffiliated community port. See Related projects below for the other community .NET implementations.
dotnet add package LibAcpOr reference the source directly:
git clone https://github.com/sargeMonkey/libACP.git
cd libACP
dotnet build Acp.slnxThe NuGet package id is
LibAcpbut the assembly name and root namespace remainAcp, so yourusing Acp;directives work unchanged.
- Full coverage of the stable ACP surface (protocol version
1):initialize,authenticatesession/new,session/load,session/resume,session/list,session/closesession/prompt,session/cancel,session/updatesession/set_mode,session/set_config_option,session/request_permissionfs/read_text_file,fs/write_text_fileterminal/create,terminal/output,terminal/release,terminal/wait_for_exit,terminal/kill- Extension escape hatch via
extMethod/extNotification - The
_metafield is preserved on every type
System.Text.Jsonend-to-end (no Newtonsoft dependency)- Discriminated-union JSON converters for
ContentBlock,SessionUpdate,RequestPermissionOutcome,ToolCallContent,EmbeddedResourceResource,McpServer,RequestId - Newline-delimited JSON transport over any
System.IO.Streampair (typically stdio) - Concurrent requests are correlated by id; cancellation is local-only by design
- IL-only logging hooks via
Microsoft.Extensions.Logging.Abstractions
src/
Acp/ # The library (net8.0 / net10.0 multi-target)
tests/
Acp.Tests/ # xUnit tests (43 tests)
examples/
EchoAgent/ # Stdio agent that streams the prompt back
SampleClient/ # Spawns the EchoAgent and walks the protocol
.github/workflows/ # CI: build + test on linux/windows/mac, pack
Requires the .NET 8 SDK (or .NET 10 SDK for the second target).
dotnet build Acp.slnx
dotnet test tests/Acp.Tests/Acp.Tests.csprojusing Acp;
using Acp.Schema;
using Acp.Streaming;
var stream = NdJsonStream.FromStdio();
await using var connection = new AgentSideConnection(c => new MyAgent(c), stream);
await connection.Closed;
internal sealed class MyAgent(AgentSideConnection client) : IAgent
{
public Task<InitializeResponse> InitializeAsync(InitializeRequest req, CancellationToken ct)
=> Task.FromResult(new InitializeResponse
{
ProtocolVersion = Protocol.Version,
AgentInfo = new Implementation { Name = "my-agent", Version = "0.1.0" },
AgentCapabilities = new AgentCapabilities(),
});
public Task<NewSessionResponse> NewSessionAsync(NewSessionRequest req, CancellationToken ct)
=> Task.FromResult(new NewSessionResponse { SessionId = new SessionId(Guid.NewGuid().ToString("n")) });
public async Task<PromptResponse> PromptAsync(PromptRequest req, CancellationToken ct)
{
await client.SessionUpdateAsync(new SessionNotification
{
SessionId = req.SessionId,
Update = new AgentMessageChunk { Content = new TextContent { Text = "hello!" } },
}, ct);
return new PromptResponse { StopReason = StopReason.EndTurn };
}
public Task<AuthenticateResponse?> AuthenticateAsync(AuthenticateRequest req, CancellationToken ct)
=> Task.FromResult<AuthenticateResponse?>(new AuthenticateResponse());
public Task CancelAsync(CancelNotification n, CancellationToken ct) => Task.CompletedTask;
}Optional methods (session/load, session/list, terminal/*, etc.) are provided as default
interface methods that throw MethodNotFound until you override them.
⚠ Never write to
Console.Outin an agent — stdout is the protocol channel. Send all logs toConsole.Error(stderr) instead.
using System.Diagnostics;
using Acp;
using Acp.Schema;
using Acp.Streaming;
var psi = new ProcessStartInfo("path/to/agent")
{
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
};
using var proc = Process.Start(psi)!;
var stream = new NdJsonStream(proc.StandardOutput.BaseStream, proc.StandardInput.BaseStream);
await using var connection = new ClientSideConnection(c => new MyClient(), stream);
await connection.InitializeAsync(new InitializeRequest
{
ProtocolVersion = Protocol.Version,
ClientInfo = new Implementation { Name = "my-client", Version = "0.1.0" },
ClientCapabilities = new ClientCapabilities(),
}, CancellationToken.None);
var session = await connection.NewSessionAsync(new NewSessionRequest
{
Cwd = Environment.CurrentDirectory,
McpServers = Array.Empty<McpServer>(),
}, CancellationToken.None);
var resp = await connection.PromptAsync(new PromptRequest
{
SessionId = session.SessionId,
Prompt = new ContentBlock[] { new TextContent { Text = "Hello, agent!" } },
}, CancellationToken.None);
internal sealed class MyClient : IClient
{
public Task SessionUpdateAsync(SessionNotification n, CancellationToken ct)
{
if (n.Update is AgentMessageChunk amc && amc.Content is TextContent t)
Console.Write(t.Text);
return Task.CompletedTask;
}
public Task<RequestPermissionResponse> RequestPermissionAsync(RequestPermissionRequest r, CancellationToken ct)
=> Task.FromResult(new RequestPermissionResponse { Outcome = new CancelledPermissionOutcome() });
}dotnet build Acp.slnx
dotnet run --project examples/SampleClient -- "Hello from the .NET ACP client!"You should see the prompt echoed back, chunk by chunk, with the agent's stderr log lines and a
final stopReason=EndTurn summary.
- Transport:
IMessageStreamdefinesReadAsync/WriteAsyncover JSON-RPC envelopes. The defaultNdJsonStreamhandles newline framing (with\r\ntolerance), enforces a 16 MiB max line length, and parks parse errors asRequestErrorException(-32700)so the connection can reply to the peer rather than crashing. - Connection:
Connectionruns a single background receive loop, correlates requests by id in aConcurrentDictionary, normalises empty void responses to{}, and maps thrownRequestErrorExceptions onto the JSON-RPC error envelope. Other handler exceptions become-32603 internal errorwith the message inerror.data.details. - Schema (DTOs): immutable C#
records with[JsonPropertyName],requiredfor required fields, nullable for optional. Discriminated unions use abstract base records + a customJsonConverterthat switches on the discriminator field (type,sessionUpdate,outcome, ...) and dispatches to the right concrete record. - Routing:
AgentSideConnectionandClientSideConnectionroute inbound requests byswitch-on-method-name (matches the TS SDK), not reflection.
43 xUnit tests covering serialization round-trips, NdJson framing edge cases, JSON-RPC correlation under concurrency, error-code mapping, and a full Agent ↔ Client integration.
dotnet test tests/Acp.Tests/Acp.Tests.csprojThe unstable surface that the TS SDK marks unstable_* is intentionally not modelled in this
release: NES, elicitation, providers, document sync (document/did*), and session/set_model.
You can still reach them via the extMethod / extNotification escape hatch.
Other community .NET implementations of ACP (as of mid-2026):
- nuskey8/acp-csharp — published as
AgentClientProtocolon NuGet. Similar shape (IAcpClient/IAcpAgent,ClientSideConnection/AgentSideConnection). Used in production bynuskey8/UnityAgentClient. AgentClientProtocol.*family (e.g.AgentClientProtocol.Agent,AgentClientProtocol.Client) — date-versioned packages auto-generated from the official ACP JSON Schema.
This package (LibAcp) differs in being:
- Hand-written rather than generated, with idiomatic C#
records and discriminated-union converters chosen per the schema. - Multi-targeted at
net8.0(LTS) andnet10.0(current LTS). - Conservative about scope: only the stable protocol surface is modelled as first-class
DTOs; unstable surface (
unstable_*) is reachable viaExtMethodAsync/ExtNotificationAsync.
Pick whichever fits your project best — the protocol is the same.
MIT. This is a community implementation. The protocol itself is governed by the ACP project.
Issues and pull requests welcome. By contributing you agree your work will be released under the MIT license.
See PUBLISHING.md for how to cut a release and push to nuget.org. The
release.yml workflow builds, tests, packs and pushes on any
vX.Y.Z git tag.