LibAcp 0.1.0

dotnet add package LibAcp --version 0.1.0
                    
NuGet\Install-Package LibAcp -Version 0.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="LibAcp" Version="0.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="LibAcp" Version="0.1.0" />
                    
Directory.Packages.props
<PackageReference Include="LibAcp" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add LibAcp --version 0.1.0
                    
#r "nuget: LibAcp, 0.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package LibAcp@0.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=LibAcp&version=0.1.0
                    
Install as a Cake Addin
#tool nuget:?package=LibAcp&version=0.1.0
                    
Install as a Cake Tool

Acp — Agent Client Protocol for .NET

ci NuGet NuGet downloads License: MIT .NET

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.

Install

dotnet add package LibAcp

Or reference the source directly:

git clone https://github.com/sargeMonkey/libACP.git
cd libACP
dotnet build Acp.slnx

The NuGet package id is LibAcp but the assembly name and root namespace remain Acp, so your using Acp; directives work unchanged.

Features

  • Full coverage of the stable ACP surface (protocol version 1):
    • initialize, authenticate
    • session/new, session/load, session/resume, session/list, session/close
    • session/prompt, session/cancel, session/update
    • session/set_mode, session/set_config_option, session/request_permission
    • fs/read_text_file, fs/write_text_file
    • terminal/create, terminal/output, terminal/release, terminal/wait_for_exit, terminal/kill
    • Extension escape hatch via extMethod / extNotification
    • The _meta field is preserved on every type
  • System.Text.Json end-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.Stream pair (typically stdio)
  • Concurrent requests are correlated by id; cancellation is local-only by design
  • IL-only logging hooks via Microsoft.Extensions.Logging.Abstractions

Layout

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

Build

Requires the .NET 8 SDK (or .NET 10 SDK for the second target).

dotnet build Acp.slnx
dotnet test  tests/Acp.Tests/Acp.Tests.csproj

Quick start — writing an agent

using 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.Out in an agent — stdout is the protocol channel. Send all logs to Console.Error (stderr) instead.

Quick start — writing a client

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() });
}

Running the bundled example

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.

Architecture notes

  • Transport: IMessageStream defines ReadAsync / WriteAsync over JSON-RPC envelopes. The default NdJsonStream handles newline framing (with \r\n tolerance), enforces a 16 MiB max line length, and parks parse errors as RequestErrorException(-32700) so the connection can reply to the peer rather than crashing.
  • Connection: Connection runs a single background receive loop, correlates requests by id in a ConcurrentDictionary, normalises empty void responses to {}, and maps thrown RequestErrorExceptions onto the JSON-RPC error envelope. Other handler exceptions become -32603 internal error with the message in error.data.details.
  • Schema (DTOs): immutable C# records with [JsonPropertyName], required for required fields, nullable for optional. Discriminated unions use abstract base records + a custom JsonConverter that switches on the discriminator field (type, sessionUpdate, outcome, ...) and dispatches to the right concrete record.
  • Routing: AgentSideConnection and ClientSideConnection route inbound requests by switch-on-method-name (matches the TS SDK), not reflection.

Tests

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.csproj

Out of scope

The 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 AgentClientProtocol on NuGet. Similar shape (IAcpClient/IAcpAgent, ClientSideConnection/AgentSideConnection). Used in production by nuskey8/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) and net10.0 (current LTS).
  • Conservative about scope: only the stable protocol surface is modelled as first-class DTOs; unstable surface (unstable_*) is reachable via ExtMethodAsync / ExtNotificationAsync.

Pick whichever fits your project best — the protocol is the same.

License

MIT. This is a community implementation. The protocol itself is governed by the ACP project.

Contributing

Issues and pull requests welcome. By contributing you agree your work will be released under the MIT license.

Releasing

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.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.0 98 5/18/2026

Initial release. Full coverage of the stable ACP v1 surface, including session/*, fs/*, terminal/*, request_permission, and the _meta extensibility field. See CHANGELOG.md for details.