Chio/Docs
LOGIN · JOIN

ReferenceSDKs

Go SDK

Two Go modules: the chio-go MCP client and invariants package, and the chio-go-http middleware that fronts an http.Handler.

Source

This page reflects two Go modules in the chio repository: sdks/go/chio-go, whose packages are auth, client, invariants, nested, session, transport, and version; and sdks/go/chio-go-http, the net/http middleware. Each module path, Go directive, and version below renders from the sdks dataset at the pin. Neither module carries a specification status line and neither uses the RFC 2119 keywords; the Go source is the truth for every name on this page.

The invariants package implements the contract fixed by the Rust crate crates/sdk/chio-binding-helpers. Where the two differ, this page states what the Go package exports and cites the file. The worked application is examples/hello-chi.


Synopsis

Require the module, then build a client and initialize a session.

go
// go.mod
require github.com/backbay-labs/chio/sdks/go/chio-go v0.0.0

// main.go
import "github.com/backbay-labs/chio/sdks/go/chio-go/client"

c := client.WithStaticBearer("https://edge.example.com/mcp", token, http.DefaultClient)
sess, err := c.Initialize(ctx, client.InitializeOptions{})

Installation

Two modules, two jobs. Take chio-go-http if you are protecting an HTTP service, chio-go if you are calling a hosted Chio edge. No file under sdks/go/ imports C, so neither module needs cgo. sdks/go/chio-go/go.mod declares no dependencies; sdks/go/chio-go-http/go.mod requires github.com/google/uuid and github.com/oapi-codegen/runtime.

Module pathDirectoryGo directive
github.com/backbay-labs/chio/sdks/go/chio-gosdks/go/chio-gogo 1.23.0
github.com/backbay-labs/chio/sdks/go/chio-go-httpsdks/go/chio-go-httpgo 1.21

A consumer takes the module through a replace directive pointing at a clone. The Go modules do not resolve from a module proxy.

go
module your-service

go 1.23.0

require github.com/backbay-labs/chio/sdks/go/chio-go v0.0.0

replace github.com/backbay-labs/chio/sdks/go/chio-go => ../chio/sdks/go/chio-go

client.Client

client.Client is the top-level entry. Configure transport, authentication, and defaults once, then reuse the client across goroutines.

go
package main

import (
    "context"
    "log"
    "net/http"

    "github.com/backbay-labs/chio/sdks/go/chio-go/client"
)

func main() {
    c := client.WithStaticBearer(
        "https://edge.example.com/mcp",
        "dev-token",
        http.DefaultClient,
    )

    ctx := context.Background()
    sess, err := c.Initialize(ctx, client.InitializeOptions{})
    if err != nil {
        log.Fatal(err)
    }
    defer sess.Close(ctx)
}

client.WithStaticBearer (client/client.go:43) wraps client.New (client.go:35), which takes an explicit auth.StaticBearer value. Initialize (client.go:47) runs the MCP handshake and returns a *session.Session.

go
import (
    "github.com/backbay-labs/chio/sdks/go/chio-go/auth"
    "github.com/backbay-labs/chio/sdks/go/chio-go/client"
)

c := client.New(
    "https://edge.example.com/mcp",
    auth.StaticBearerToken("dev-token"),
    http.DefaultClient,
)

session.Session

session.Session carries the Streamable HTTP MCP methods. Every method after SetMessageHandler takes a context.Context, so deadlines and cancellation reach the transport.

go
tools, err := sess.ListTools(ctx)
if err != nil {
    return err
}
log.Printf("tools: %v", tools)

result, err := sess.CallTool(ctx, "read_file", map[string]any{
    "path": "./README.md",
})
if err != nil {
    return err
}
log.Printf("result: %v", result)

The full method set, from session/session.go. The three envelope methods return (RPCExchange, error), an alias for the transport type at transport/http.go:11. Every named MCP method returns (map[string]any, error), the result member of the terminal response.

MethodLineBehavior
SetMessageHandler(MessageHandler)60Installs the callback the session runs for a server-initiated message. Returns nothing.
SendEnvelope68Posts a caller-built JSON-RPC envelope through transport.PostRPC.
Request89Builds an envelope with the next request id and posts it.
RequestResult106Sends a request and returns the result object of the terminal response. Errors when the terminal response carries no object result.
Notification127Posts an envelope with a method and no id.
ListTools156Calls tools/list.
CallTool(ctx, name, arguments)160Calls tools/call.
ListResources167Calls resources/list.
ReadResource(ctx, uri)171Calls resources/read.
SubscribeResource(ctx, uri)175Calls resources/subscribe.
UnsubscribeResource(ctx, uri)179Calls resources/unsubscribe.
ListResourceTemplates183Calls resources/templates/list.
ListPrompts187Calls prompts/list.
GetPrompt(ctx, name, arguments)191Calls prompts/get.
Complete(ctx, params)199Calls completion/complete with the caller's params map.
SetLogLevel(ctx, level)203Calls logging/setLevel.
ListTasks207Calls tasks/list.
GetTask(ctx, taskID)211Calls tasks/get.
GetTaskResult(ctx, taskID)215Calls tasks/result.
CancelTask(ctx, taskID)219Calls tasks/cancel.
Close(ctx)223Deletes the session and returns a DeleteSessionResult.

transport.PostRPC

The transport package drives the HTTP exchange under the session. Call it directly for control over retries, headers, or connection reuse. PostRPC (transport/http.go:45) and DeleteSession (:71) each take a context.Context and an *http.Client.

go
import (
    "net/http"

    "github.com/backbay-labs/chio/sdks/go/chio-go/transport"
)

exchange, err := transport.PostRPC(
    ctx,
    http.DefaultClient,
    "https://edge.example.com/mcp",
    authToken,
    sessionID,
    protocolVersion,
    map[string]any{
        "jsonrpc": "2.0",
        "id":      1,
        "method":  "tools/list",
        "params":  map[string]any{},
    },
    nil,
)
if err != nil {
    return err
}
for _, msg := range exchange.Messages {
    log.Printf("received: %v", msg["method"])
}

_, err = transport.DeleteSession(ctx, http.DefaultClient, "https://edge.example.com/mcp", authToken, sessionID)

Authentication

The auth package ships a static-bearer value plus helpers for the OAuth 2.1 authorization-code flow with PKCE. Feed the resulting token back into client.New or client.WithStaticBearer.

  • Static bearer. auth.StaticBearerToken(token) (auth/auth.go:7) returns an auth.StaticBearer struct carrying the token.
  • OAuth helpers. From auth/oauth.go: GetJSON at line 37, PKCEChallenge at 59, AuthorizationServerMetadataURL at 64, DiscoverOAuthMetadata at 78, PerformAuthorizationCodeFlow at 122, ExchangeAccessToken at 279, and ResolveOAuthAccessToken at 329.
go
import (
    "net/http"

    "github.com/backbay-labs/chio/sdks/go/chio-go/auth"
    "github.com/backbay-labs/chio/sdks/go/chio-go/client"
)

// Dev: static bearer.
c := client.WithStaticBearer(
    "https://edge.example.com/mcp",
    "dev-token",
    http.DefaultClient,
)

// Prod: run the OAuth flow then hand the resulting token to the client.
metadata, err := auth.DiscoverOAuthMetadata(ctx, http.DefaultClient, "https://edge.example.com", nil)
if err != nil {
    return err
}
accessToken, err := auth.PerformAuthorizationCodeFlow(
    ctx, http.DefaultClient,
    "https://edge.example.com", "chio.read chio.invoke",
    metadata.AuthorizationServerMetadata, nil,
    &auth.AuthorizationCodeFlowOptions{
        ClientID:    "chio-cli",
        RedirectURI: "http://127.0.0.1:53682/callback",
    },
)
if err != nil {
    return err
}

c2 := client.WithStaticBearer(
    "https://edge.example.com/mcp",
    accessToken,
    http.DefaultClient,
)
_ = c2

invariants package

github.com/backbay-labs/chio/sdks/go/chio-go/invariants implements canonical JSON, SHA-256, Ed25519 signing and verification, receipt and capability parsing and verification, and signed manifest verification. It holds no network code.

go
package main

import (
    "fmt"

    "github.com/backbay-labs/chio/sdks/go/chio-go/invariants"
)

func main() {
    receipt, err := invariants.ParseReceiptJSON(receiptJSON)
    if err != nil {
        panic(err)
    }

    result, err := invariants.VerifyReceipt(receipt)
    if err != nil {
        panic(err)
    }

    fmt.Printf("signature valid:       %v\n", result.SignatureValid)
    fmt.Printf("parameter hash valid:  %v\n", result.ParameterHashValid)
}

Canonical JSON and parsing

From invariants/json.go and invariants/errors.go.

  • CanonicalizeJSONString(input string) (string, error), json.go:13. Canonicalizes a JSON document supplied as text.
  • CanonicalizeJSON(value any) (string, error), json.go:21. Canonicalizes an already-decoded value.
  • ParseJSONText(input string) (any, error), errors.go:24. Decodes with UseNumber, so a number keeps its source text rather than becoming a float.

Hashing

SHA256HexUTF8(input string) string (invariants/hashing.go:8) is the only hashing export.

The Rust contract at crates/sdk/chio-binding-helpers/src/hashing.rs exports two functions, sha256_hex_bytes at line 2 and sha256_hex_utf8 at line 7. Go exports no SHA256HexBytes. A caller hashing arbitrary bytes converts them with string(b), which Go passes through unchanged, or calls crypto/sha256 directly. The Bindings API page lists the Rust name.

Signing and key checks

From invariants/signing.go. The three predicates accept an optional 0x prefix and uppercase hex; the decoder normalizes both.

  • IsValidEd25519PublicKeyHex(value string) bool, line 15. True for 32 bytes of hex.
  • IsValidEd25519SignatureHex(value string) bool, line 20. True for 64 bytes of hex.
  • PublicKeyHexMatches(left, right string) bool, line 25. Compares two keys after normalization.
  • SignUTF8MessageEd25519, line 29, and VerifyUTF8MessageEd25519, line 43.
  • SignJSONStringEd25519, line 55, and VerifyJSONStringSignatureEd25519, line 68. Both canonicalize before they touch the key.

Receipts

From invariants/receipt.go. The signing body and the id preimage are different projections of the receipt, so both canonicalizers are exported.

  • ParseReceiptJSON(input string) (map[string]any, error), line 18.
  • ReceiptBodyCanonicalJSON(receipt map[string]any) (string, error), line 30. The id preimage.
  • ReceiptSigningBodyCanonicalJSON(receipt map[string]any) (string, error), line 41. The bytes the kernel signed.
  • VerifyReceipt(receipt map[string]any) (ReceiptVerification, error), line 56. Calls the next entry with a nil signer list.
  • VerifyReceiptWithTrustedSigners(receipt map[string]any, trustedSigners []string) (ReceiptVerification, error), line 60. Sets SignerTrusted from the supplied key list.
  • VerifyReceiptJSON(input string) (ReceiptVerification, error), line 154.

Capabilities

From invariants/capability.go. VerifyCapability and VerifyCapabilityJSON pass a nil depth, so the delegation chain length goes unchecked. The MaxDelegationDepth forms pass the caller's bound, and a chain longer than it fails the check (capability.go:169).

  • ParseCapabilityJSON(input string) (map[string]any, error), line 18.
  • CapabilityBodyCanonicalJSON(capability map[string]any) (string, error), line 50.
  • CapabilitySigningBody(capability map[string]any) map[string]any, line 59. Returns the projection rather than its canonical text.
  • CapabilitySigningBodyCanonicalJSON(capability map[string]any) (string, error), line 87.
  • VerifyCapability(capability map[string]any, now int64) (CapabilityVerification, error), line 91.
  • VerifyCapabilityWithMaxDelegationDepth(capability map[string]any, now int64, maxDelegationDepth int), line 95.
  • VerifyCapabilityJSON(input string, now int64) (CapabilityVerification, error), line 145.
  • VerifyCapabilityJSONWithMaxDelegationDepth(input string, now int64, maxDelegationDepth int), line 153.

Signed manifests

From invariants/manifest.go.

  • ParseSignedManifestJSON(input string) (map[string]any, error), line 82.
  • SignedManifestBodyCanonicalJSON(signedManifest map[string]any) (string, error), line 94.
  • VerifySignedManifest(signedManifest map[string]any) (ManifestVerification, error), line 102.
  • VerifySignedManifestJSON(input string) (ManifestVerification, error), line 124.

Result types

ReceiptVerification (receipt.go:3) carries Decision, Authorized, BoundaryClass, Ok, ParameterHashValid, ReceiptIDValid, ReceiptKind, Result, SignerKeyHex, SignerTrusted, SignatureValid, and TrustLevel. The other result types are CapabilityVerification with CapabilityTimeStatus (capability.go:11 and :3), ManifestVerification (manifest.go:75), and SignedMessage (signing.go:9).

Nested callback router

The nested package mounts MCP nested callbacks (elicitation, sampling, roots/list) alongside an existing HTTP server, so a client services server-initiated requests without a dedicated listener. nested.NewRouter (nested/router.go:23) takes a TranscriptHook and the package ships four response builders: RPCResult at line 30, SamplingTextResult at 38, ElicitationAcceptResult at 53, and RootsListResult at 61.

chio-go carries the invariants, the MCP transport, the client and session, the nested router, and the version constants DefaultClientName and ModuleVersion (version/version.go). It has no DPoP package and no typed receipt-query client: a Go caller queries receipts against GET /v1/receipts/query directly.

net/http middleware (chio-go-http)

github.com/backbay-labs/chio/sdks/go/chio-go-http wraps any http.Handler, evaluates the incoming request against a Chio sidecar, attaches the receipt id to the response, and short-circuits with a structured JSON error when the verdict denies. It is a separate module with its own go.mod, declaring go 1.21.

go
module your-service

go 1.21

require github.com/backbay-labs/chio/sdks/go/chio-go-http v0.0.0

replace github.com/backbay-labs/chio/sdks/go/chio-go-http => ../chio/sdks/go/chio-go-http

chio.Protect takes the handler you already have and a list of functional options. Routing does not change.

sdks/go/chio-go-http/README.md:29-48go
package main

import (
    "fmt"
    "net/http"

    chio "github.com/backbay-labs/chio/sdks/go/chio-go-http"
)

func handlePets(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, `{"pets":[]}`)
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/pets", handlePets)

    protected := chio.Protect(mux, chio.ConfigFile("chio.yaml"))
    http.ListenAndServe(":8080", protected)
}

The options, from chio-go-http/config.go.

OptionLinePurpose
ConfigFile(path)60Path to chio.yaml, which carries the routes and policies.
WithSidecarURL(url)67Override the sidecar base URL. CHIO_SIDECAR_URL is honored when this is absent.
WithTimeout(seconds)74Sidecar HTTP timeout, default 5.
WithIdentityExtractor(f)81Custom caller extraction. Defaults to Bearer, then API key, then cookie.
WithRouteResolver(f)88Map (method, path) to a route pattern, for example /pets/{petId}.

What the middleware writes

The middleware fails closed: chio.go:101 requires both an allowed verdict and an authorized receipt before the request reaches the inner handler. It sets X-Chio-Receipt-Id only after VerifyReceipt agrees with the receipt (chio.go:134). Every other path writes an ErrorResponse (types_helpers.go:212) with the fields error, message, receipt_id, and suggestion, the last two omitted when empty.

Error codeCause
chio_access_deniedThe verdict is not an allow, or the receipt is not authorized. Status comes from Verdict.HTTPStatus (types_helpers.go:103) and falls back to 403.
chio_invalid_receiptReceipt verification failed, or the returned verification does not cover the receipt. Status 502. chio.go:85, :117, :125.
chio_sidecar_unreachableThe evaluation call itself failed for any other reason. Status 502. chio.go:94.
chio_sidecar_unavailableThe verify endpoint answered 408, 404, 429, or 5xx. sidecar.go:200-212.
chio_evaluation_failedThe HTTP method is outside the allowed set (405), the request body could not be read (400), or a verify response fits no other class. chio.go:59, :75, sidecar.go:214.

The codes are constants in chio-go-http/types_helpers.go:204-209. A denied response carries the fixed suggestion provide a valid capability token in the X-Chio-Capability header or chio_capability query parameter (chio.go:110).

A worked application

examples/hello-chi puts a chi router behind the middleware and serves three routes: GET /healthz outside evaluation, GET /hello allowed, POST /echo denied without a capability.

examples/hello-chi/main.go:34-49go
func protectedHandler(sidecarURL string) http.Handler {
	return chio.Protect(
		newRouter(),
		chio.WithSidecarURL(sidecarURL),
	)
}

func newRouter() http.Handler {
	router := chi.NewRouter()

	router.Get("/healthz", healthz)
	router.Get("/hello", hello)
	router.Post("/echo", echo)

	return router
}

The example depends on the adapter and replaces it with a local path, so it builds from a clone with no module proxy:

examples/hello-chi/go.mod:5-8, 16text
require (
	github.com/backbay-labs/chio/sdks/go/chio-go-http v0.0.0
	github.com/go-chi/chi/v5 v5.2.3
)

replace github.com/backbay-labs/chio/sdks/go/chio-go-http => ../../sdks/go/chio-go-http

Run it with examples/run-hello-smokes.sh hello-chi. The script starts a trust-control plane, a sidecar, and the application, calls the read route and the write route, and writes each exchange under the example's artifacts directory.

hello-chi · allowtranscript
$ cat hello.headers hello.json
HTTP/1.1 200 OK
Content-Type: application/json
X-Chio-Receipt-Id: 25fa66e5413d596601b708653d86b3f3a7af20cf12c269b7d5bd2b3c60dba661
Date: Sat, 05 Sep 2026 11:44:09 GMT
Content-Length: 29

{"message":"hello from chi"}
exit 0
A read the sidecar allows. The middleware sets the receipt id header only after the receipt verifies, so the header's presence is the evidence the call was mediated.
sourceexamples/hello-chi/smoke.shat fe56570
hello-chi · denytranscript
$ cat deny.headers deny.json
HTTP/1.1 403 Forbidden
Content-Type: application/json
Date: Sat, 05 Sep 2026 11:44:09 GMT
Content-Length: 284

{"error":"chio_access_denied","message":"side-effect route requires a capability token","receipt_id":"04c22cdfd0b4c28d85838f715d6134ac11314294682cb6367330f0ffe0bccdaa","suggestion":"provide a valid capability token in the X-Chio-Capability header or chio_capability query parameter"}
exit 0
The same application refusing a write that carries no capability. The body names the code, the receipt the refusal was recorded under, and the header or query parameter that would satisfy it.
sourceexamples/hello-chi/smoke.shat fe56570

Errors

The invariants package returns a typed *invariants.InvariantError (invariants/errors.go:8) carrying a Code and a Message. Match it with errors.As and branch on Code.

CodeRaised by
jsonInput is not valid JSON, or a receipt, capability, or manifest is not a JSON object. errors.go:29, receipt.go:25, capability.go:25, manifest.go:89.
canonical_jsonA value has no canonical form: an unsupported type, an invalid number, or a non-finite number. json.go:84, :101, :108.
invalid_public_keyA public key is not 32 bytes of hex. signing.go:16, :44.
invalid_signatureA signature is not 64 bytes of hex. signing.go:21, :48.
invalid_hexA signing seed is not 32 bytes of hex. signing.go:30.
receiptA receipt is missing parameters, or decision is not an object. receipt.go:98, :113.
receipt_idA field the id preimage covers is absent. receipt.go:192.
go
_, err := invariants.VerifyReceiptJSON(receiptJSON)

var invErr *invariants.InvariantError
if errors.As(err, &invErr) {
    switch invErr.Code {
    case "invalid_signature":
        log.Printf("signature rejected: %s", invErr.Message)
    case "json", "canonical_json":
        log.Printf("malformed input: %s", invErr.Message)
    default:
        log.Printf("invariant %s: %s", invErr.Code, invErr.Message)
    }
}

The client, session, and transport packages return fmt.Errorf-wrapped error values with no sentinels and no typed structs. Check them with err != nil.

go
_, err := sess.CallTool(ctx, "read_file", args)
if err != nil {
    log.Printf("call failed: %v", err)
}

Conformance

invariants/vectors_test.go replays the shared binding vectors under tests/bindings/vectors/, the same files the Rust helpers and the other language bindings read, so canonical JSON, hashing, signing, and verification agree byte for byte across the SDKs.

bash
cd sdks/go/chio-go && go test ./...
  • Bindings API: the Rust contract this package implements, including the names Go does not export.
  • SDKs: the other language bindings and their package names.
  • Receipt format: the fields VerifyReceipt reads.
  • Receipt query API: the endpoint a Go caller queries directly.
  • HTTP substrate: the sidecar the middleware evaluates against.