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.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 path | Directory | Go directive |
|---|---|---|
github.com/backbay-labs/chio/sdks/go/chio-go | sdks/go/chio-go | go 1.23.0 |
github.com/backbay-labs/chio/sdks/go/chio-go-http | sdks/go/chio-go-http | go 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.
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-goclient.Client
client.Client is the top-level entry. Configure transport, authentication, and defaults once, then reuse the client across goroutines.
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.
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.
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.
| Method | Line | Behavior |
|---|---|---|
SetMessageHandler(MessageHandler) | 60 | Installs the callback the session runs for a server-initiated message. Returns nothing. |
SendEnvelope | 68 | Posts a caller-built JSON-RPC envelope through transport.PostRPC. |
Request | 89 | Builds an envelope with the next request id and posts it. |
RequestResult | 106 | Sends a request and returns the result object of the terminal response. Errors when the terminal response carries no object result. |
Notification | 127 | Posts an envelope with a method and no id. |
ListTools | 156 | Calls tools/list. |
CallTool(ctx, name, arguments) | 160 | Calls tools/call. |
ListResources | 167 | Calls resources/list. |
ReadResource(ctx, uri) | 171 | Calls resources/read. |
SubscribeResource(ctx, uri) | 175 | Calls resources/subscribe. |
UnsubscribeResource(ctx, uri) | 179 | Calls resources/unsubscribe. |
ListResourceTemplates | 183 | Calls resources/templates/list. |
ListPrompts | 187 | Calls prompts/list. |
GetPrompt(ctx, name, arguments) | 191 | Calls prompts/get. |
Complete(ctx, params) | 199 | Calls completion/complete with the caller's params map. |
SetLogLevel(ctx, level) | 203 | Calls logging/setLevel. |
ListTasks | 207 | Calls tasks/list. |
GetTask(ctx, taskID) | 211 | Calls tasks/get. |
GetTaskResult(ctx, taskID) | 215 | Calls tasks/result. |
CancelTask(ctx, taskID) | 219 | Calls tasks/cancel. |
Close(ctx) | 223 | Deletes 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.
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 anauth.StaticBearerstruct carrying the token. - OAuth helpers. From
auth/oauth.go:GetJSONat line 37,PKCEChallengeat 59,AuthorizationServerMetadataURLat 64,DiscoverOAuthMetadataat 78,PerformAuthorizationCodeFlowat 122,ExchangeAccessTokenat 279, andResolveOAuthAccessTokenat 329.
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,
)
_ = c2invariants 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.
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 withUseNumber, 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, andVerifyUTF8MessageEd25519, line 43.SignJSONStringEd25519, line 55, andVerifyJSONStringSignatureEd25519, 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. SetsSignerTrustedfrom 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.
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-httpchio.Protect takes the handler you already have and a list of functional options. Routing does not change.
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.
| Option | Line | Purpose |
|---|---|---|
ConfigFile(path) | 60 | Path to chio.yaml, which carries the routes and policies. |
WithSidecarURL(url) | 67 | Override the sidecar base URL. CHIO_SIDECAR_URL is honored when this is absent. |
WithTimeout(seconds) | 74 | Sidecar HTTP timeout, default 5. |
WithIdentityExtractor(f) | 81 | Custom caller extraction. Defaults to Bearer, then API key, then cookie. |
WithRouteResolver(f) | 88 | Map (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 code | Cause |
|---|---|
chio_access_denied | The 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_receipt | Receipt verification failed, or the returned verification does not cover the receipt. Status 502. chio.go:85, :117, :125. |
chio_sidecar_unreachable | The evaluation call itself failed for any other reason. Status 502. chio.go:94. |
chio_sidecar_unavailable | The verify endpoint answered 408, 404, 429, or 5xx. sidecar.go:200-212. |
chio_evaluation_failed | The 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.
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:
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-httpRun 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.
$ cat hello.headers hello.jsonHTTP/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"}examples/hello-chi/smoke.shat fe56570$ cat deny.headers deny.jsonHTTP/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"}examples/hello-chi/smoke.shat fe56570Errors
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.
| Code | Raised by |
|---|---|
json | Input 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_json | A value has no canonical form: an unsupported type, an invalid number, or a non-finite number. json.go:84, :101, :108. |
invalid_public_key | A public key is not 32 bytes of hex. signing.go:16, :44. |
invalid_signature | A signature is not 64 bytes of hex. signing.go:21, :48. |
invalid_hex | A signing seed is not 32 bytes of hex. signing.go:30. |
receipt | A receipt is missing parameters, or decision is not an object. receipt.go:98, :113. |
receipt_id | A field the id preimage covers is absent. receipt.go:192. |
_, 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.
_, 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.
cd sdks/go/chio-go && go test ./...Related
- 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
VerifyReceiptreads. - Receipt query API: the endpoint a Go caller queries directly.
- HTTP substrate: the sidecar the middleware evaluates against.