Skip to content

About

Unofficial typed .NET SDK for the opencode HTTP API (pre-release; MCP server planned).

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

29 stars

Watchers

1 watching

Forks

Latest commit

Β 

History

1,230 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

opencode SDK for .NET

License: MIT NuGet CI Linux Tests

πŸš€ Quick Start: Install | Quick start | Guide | API coverage

Unofficial. This project is not affiliated with or endorsed by the opencode team.

πŸ“¦ New package ids and a new home (0.9.0-preview.5). The packages are now OpenCodeDotNet.Sdk and OpenCodeDotNet.Sdk.Extensions; OpenCodeAI.Sdk and OpenCodeAI.Sdk.Extensions are deprecated. Namespaces are unchanged, so only the PackageReference changes. The repository moved to the opencode-dotnet organization, and old links redirect.

A strongly typed .NET client for the opencode server β€” the HTTP API that every opencode front-end (TUI, desktop, web UI, plugins) uses. The SDK speaks the OpenCode 2.x API; the 1.x server API is not supported.

await using var server = await OpenCodeServer.StartAsync();
using var client = server.CreateClient();

var created = await client.Sessions.CreateSessionAsync(new SessionCreateRequest { Title = "hello from .NET" });
Console.WriteLine($"session {created.Session.Id}");

πŸŽ‰ Project Status

Pre-1.0, and the protocol surface is complete. All 142 operations in the pinned OpenAPI snapshot are callable β€” 140 as generated HTTP calls, and the two terminal WebSocket connections through hand-written transports. All three connection modes work: a private server the SDK starts, a server you already run, and the background service that the opencode CLI registers.

  • 🚧 Releases are 0.9.0-preview.N. A reviewed baseline locks the public surface, but it can still change before 1.0.0. See CHANGELOG.md.
  • πŸ“Œ The SDK builds against a snapshot of an upstream release tag, never a live branch. spec/SNAPSHOT.md owns the exact pin and the refresh procedure.
  • πŸ”œ An MCP server over this SDK is planned, not started. It will live in its own repository in the opencode-dotnet organization.

πŸ’‘ Why this SDK?

  • Typed all the way down. Every operation has a generated request type, a generated response envelope, and typed error models. That includes opencode's unions without a discriminator, which off-the-shelf .NET OpenAPI generators did not represent correctly.
  • One transport for every connection mode. The same pipeline owns endpoint authority, authentication, buffering, and failure mapping, whether you start the server, point at one, or discover the CLI's background service.
  • Errors you can branch on. A call throws a typed exception by default. With OpenCodeRequestOptions.NoThrow, it returns the failure as data instead β€” useful when a 404 is a normal answer.
  • Broad .NET reach. net472 is a first-class target, so the SDK works in .NET Framework hosts, not only in modern apps; netstandard2.0 is a compatibility asset for other runtimes.
  • No reflection serialization. System.Text.Json source generation throughout. Both packages declare IsAotCompatible on every modern target.
  • A pinned protocol. Each refresh to a new upstream release comes with a receipt, so a regeneration is a diff that you can review.

🧭 API Coverage

OpenCodeClient exposes one sub-client per area of the API. Every sub-client is also injectable on its own when you use dependency injection.

Area Entry point Guide
Sessions, messages, prompts, session logs client.Sessions Requests, Pagination
The global event bus client.Events Streaming
Terminals and persistent terminals client.Ptys, client.PersistentPtys Terminals
Shell commands client.Shells Requests
Files and version control client.FileSystem, client.Vcs, client.Worktrees Requests
Permissions and forms client.Permissions, client.Forms Requests
Models and providers client.Providers, client.LanguageModels, client.Credentials β€”
Agents, commands, skills, plugins client.Agents, client.Commands, client.Skills, client.Plugins β€”
MCP servers and integrations client.McpServers, client.Integrations β€”
Configuration, projects, references client.Config, client.Projects, client.References β€”
Server info and pairing client.Server Connection modes
Locations client.GetLocationAsync, client.ReloadLocationsAsync, client.Debug Requests
Web search and RPC client.Websearch, client.Rpc β€”
Operations upstream marks experimental client.Experimental Experimental APIs

The two terminal connections (pty.connect, persistentPty.connect) are WebSocket upgrades that the HTTP pipeline cannot carry. They are fully usable through the hand-written PtySession and PersistentPtySession types. Nothing is declined: src/OpenCode.Sdk/.generation-incomplete is the machine-readable map that the build reads.

πŸ“¦ Installation

dotnet add package OpenCodeDotNet.Sdk --prerelease
dotnet add package OpenCodeDotNet.Sdk.Extensions --prerelease   # dependency injection, optional

The package id is not the namespace. You install OpenCodeDotNet.Sdk, and you write using OpenCode.Sdk;. nuget.org reserves the OpenCode. id prefix for an unrelated owner, so only the PackageReference carries the OpenCodeDotNet name. The earlier ids, OpenCodeAI.Sdk and OpenCodeAI.Sdk.Extensions, receive no further versions.

You also need the opencode CLI, from the @opencode/cli npm scope. Install the release that this repository pins β€” later releases usually work, but they are not what the tests run against:

npm install -g @opencode/cli@2.0.29

You do not have to start it. OpenCodeServer.StartAsync() in the quick start starts a private server for you.

Nightly builds (GitHub Packages)

Every code push to master publishes 0.9.0-nightly.{yyyyMMdd}.g{shortSha} to GitHub Packages:

# Add the GitHub Packages source (PAT: classic token with the read:packages scope)
dotnet nuget add source https://nuget.pkg.github.com/opencode-dotnet/index.json \
  --name github-opencode-sdk \
  --username YOUR_GITHUB_USERNAME \
  --password YOUR_GITHUB_PAT \
  --store-password-in-clear-text

# Install the nightly packages
dotnet add package OpenCodeDotNet.Sdk --prerelease --source github-opencode-sdk
dotnet add package OpenCodeDotNet.Sdk.Extensions --prerelease --source github-opencode-sdk
Keep the token out of shell history with a nuget.config
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
    <add key="github-opencode-sdk" value="https://nuget.pkg.github.com/opencode-dotnet/index.json" />
  </packageSources>
  <packageSourceCredentials>
    <github-opencode-sdk>
      <add key="Username" value="%GITHUB_USERNAME%" />
      <add key="ClearTextPassword" value="%GITHUB_PAT%" />
    </github-opencode-sdk>
  </packageSourceCredentials>
</configuration>

πŸ”‘ GitHub Packages authentication: GitHub Packages requires a classic Personal Access Token with the read:packages scope, even for public packages. The NuGet registry does not accept fine-grained tokens. Linux and macOS need --store-password-in-clear-text, because NuGet cannot encrypt stored credentials there. Never commit a real token. Inside GitHub Actions you need no PAT: the workflow's own GITHUB_TOKEN works as the password.

πŸš€ Quick Start

Pick the connection mode that matches where your server lives:

You want… Use Who stops the server
A private server for this app only OpenCodeServer.StartAsync() The SDK, when you dispose it
A server you already run new OpenCodeClient(options) You
The background service the CLI shares with other clients OpenCodeServer.DiscoverAsync() or EnsureAsync() Nobody, unless you call StopAsync()

The SDK starts the server

The launcher starts a private opencode serve child, creates its password, and gives you a client bound to it. It resolves opencode from PATH as a shell does, PATHEXT included, so the npm .cmd shim works on Windows.

using OpenCode.Sdk;
using OpenCode.Sdk.Models;

await using var server = await OpenCodeServer.StartAsync();
using var client = server.CreateClient();

var info = await client.Server.GetInfoAsync();
Console.WriteLine($"opencode {info.ServerInfo.Version} (pid {info.ServerInfo.Pid})");

using var window = new CancellationTokenSource(TimeSpan.FromSeconds(30));
await foreach (var @event in client.Events.SubscribeAsync(cancellationToken: window.Token))
{
    Console.WriteLine($"{@event.GetType().Name} ({@event.Type})");
}

A server you already run

using var client = new OpenCodeClient(new OpenCodeClientOptions
{
    Endpoint = new Uri("http://127.0.0.1:4096"),
    Password = Environment.GetEnvironmentVariable("OPENCODE_PASSWORD"),
});

The client reads no environment variables of its own. The caller decides where the password comes from, as opencode's own CLI does.

The background service the CLI runs

DiscoverAsync reads the CLI's registration file with the CLI's own rules and checks the daemon's authenticated status. It returns a handle that owns nothing: disposing it never stops the service that other clients share. Null means no ready service. EnsureAsync starts one through the CLI's own election when none is running.

var server = await OpenCodeServer.DiscoverAsync();
if (server is null)
{
    return; // no ready registered service
}

using var client = server.CreateClient();

The connection guide has every option, EnsureAsync, StopAsync, launcher output capture, and pairing another client.

Dependency injection

OpenCode.Sdk.Extensions registers one OpenCodeClient singleton and every sub-client, each only when that service type is not already registered. A consumer's earlier root-client or sub-client registration wins, and repeated AddOpenCode calls add no duplicate client registrations. The default root is thread-safe and owns its transport for the container's lifetime; default sub-clients resolve from the registered root, so you can inject SessionsClient, EventsClient, or PtysClient directly. See the DI registration guide.

builder.Services.AddOpenCode(options =>
{
    options.Endpoint = new Uri("http://127.0.0.1:4096");
    options.Password = Environment.GetEnvironmentVariable("OPENCODE_PASSWORD");
});

internal sealed class SessionWorker(SessionsClient sessions) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var page = await sessions.ListSessionsAsync(
            new SessionListRequest { Limit = 10, Order = ListOrder.Descending },
            cancellationToken: stoppingToken);

        Console.WriteLine($"{page.Sessions.Count} sessions, next cursor {page.Cursor.Next ?? "<none>"}");
    }
}

AddOpenCode(IConfiguration) binds the same options from a configuration section. Configuration binding uses reflection, so that overload is annotated [RequiresDynamicCode] and [RequiresUnreferencedCode]. Under trimming or native AOT, use the configure-action overload.

πŸ“š Documentation

Guide What it covers
The guide Index of every page below, in reading order
Getting started Install, first call, and the shape of the client family
Connection modes The standalone launcher, an external server, the background service, pairing, and DI registration
Streaming The global event bus and per-session server-sent event streams
Terminals PTY and persistent-PTY sessions over the WebSocket doors
Errors and responses Throwing versus NoThrow, and the typed error model
Pagination Cursor-carrying list envelopes, Enumerate*Async, and its Pages
Requests Request records, the absent/null/set states of Optional<T>, query members, per-call location, and the permission, worktree, and shell-timeout members
Experimental APIs What upstream serves as experimental, the OPENCODE001–OPENCODE003 diagnostics, and how to opt in

Architecture, decision records, and engineering policy live under docs/. Start at AGENTS.md for the internals.

πŸš€ Platform Compatibility & Quality Status

Supported Platforms

Both packages target netstandard2.0;net472;net10.0;net11.0. The whole suite runs on net472 on Windows, real-process launcher tests included, and on net10.0 and net11.0 on Windows, Linux, and macOS. A project whose target framework falls back to the netstandard2.0 asset on a runtime the packages do not target, such as net8.0 or net9.0, gets a build warning; <SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings> silences it. A .NET Framework project needs a newer C# version than its default; the getting-started guide has the two properties to set.

Build & Test Matrix

Category Platform/Type Status Description
πŸ”§ Build Cross-Platform CI Matrix: Windows, Linux, macOS
πŸ§ͺ Tests Linux Linux Tests net8.0, net9.0, net10.0
πŸ§ͺ Tests Windows Windows Tests net472 plus every modern target
πŸ§ͺ Tests macOS macOS Tests net8.0, net9.0, net10.0
πŸ“‘ Consumer leg Linux, Windows Consumer leg Weekly and on demand: the same suite against the published @opencode/cli build for the pin

πŸ“¦ Package Status

Package NuGet.org GitHub Packages
OpenCodeDotNet.Sdk NuGet GitHub Packages
OpenCodeDotNet.Sdk.Extensions NuGet GitHub Packages

Known Issues

  • The event bus has no replay. EventsClient.SubscribeAsync is a live stream: events published while you are disconnected are lost, and a consumer slower than the producer can overflow and fail the stream. This is the server's contract, not an SDK limitation. See the streaming guide.

  • A CLI-started server's session log holds only the sync marker. The per-session log reads from the server's event store, and the distributed opencode CLI starts its server without event persistence. No flag, environment variable, or configuration key turns it on (confirmed at the pin; observed on @opencode/cli@2.0.15). SessionClient.ReadLogAsync does not fail: with or without Follow, it delivers one EventLogSynced marker and no durable events. For live session activity, subscribe to the global event bus and filter by session id. Persisted replay needs a host that embeds the opencode server library with persistence enabled.

  • On net472 and netstandard2.0, two costs are larger than on modern targets. A response body over 1 MB is copied once at wire size, because the downlevel array pool caps its buckets at 1 MB. On Unix (Mono), owner-only files that discovery and EnsureAsync write get mode 0600 through a chmod child process rather than at creation, and a failed chmod fails the write. No CI leg runs .NET Framework or Mono on Unix.

  • Each terminal connection allocates a 16 KiB receive buffer, reused across reads. The receiver also queues frames that nobody reads, so a slow or absent consumer can grow memory. See the terminal lifetime contract.

Developing

We appreciate contributions in the form of feedback, bug reports, and pull requests. Read CONTRIBUTING.md first β€” it has the full gate and the commit convention.

git clone --recurse-submodules https://github.com/opencode-dotnet/opencode-sdk-dotnet.git
cd opencode-sdk-dotnet
dotnet build --configuration Release

external/ holds read-only upstream submodules: protocol evidence, and the pinned server that the fixture-backed tests run. Those tests also need Bun, the pinned server's dependencies, and ripgrep; docs/engineering/quality-gates.md has the versions and the full completion gate.

tests/OpenCode.Sdk.Sandbox is a committed playground that drives the SDK against a real opencode serve under a debugger.

Community

Got questions or wild feature ideas?

πŸ‘‰ Open an issue β€” bug reports, questions, and proposals all land there for now.

Changelog

Please refer to CHANGELOG.md to see the complete list of changes for each release.

License

Licensed under MIT, see LICENSE for the full text. Content derived from upstream opencode carries its own notice in THIRD-PARTY-NOTICES.md.

About

Unofficial typed .NET SDK for the opencode HTTP API (pre-release; MCP server planned).

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

29 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages