Go SDK Reference: mcpgateway
Complete API reference for the PRECINCT Go SDK. The mcpgateway
package provides an idiomatic Go client for invoking MCP tools and model
endpoints through the PRECINCT security gateway, with automatic JSON-RPC
envelope construction, SPIFFE identity injection, structured error parsing,
and exponential-backoff retry.
Start with the Integration Guide to register your agent identity and configure policies before using this SDK. For a high-level comparison of available SDKs, see the SDKs overview.
Overview
The mcpgateway package is a single-purpose Go module for
communicating with the PRECINCT gateway. It requires Go ≥1.24.6 and has
exactly one external dependency: github.com/google/uuid
(used to auto-generate session IDs).
- Module path:
github.com/RamXX/agentic_reference_architecture/POC/sdk/go - Package:
mcpgateway - Go version: ≥1.24.6
- Dependencies:
github.com/google/uuid
The package exposes three public types (GatewayClient,
GatewayError, ModelChatRequest), a constructor
(NewClient), functional option helpers (With*),
and three default-value constants.
Installation
From the Repository
go get github.com/RamXX/agentic_reference_architecture/POC/sdk/go/mcpgateway
Local Development (replace directive)
When developing alongside the PRECINCT mono-repo, use a replace
directive in your go.mod to point at the local checkout:
// go.mod
module myagent
go 1.24.6
require github.com/RamXX/agentic_reference_architecture/POC/sdk/go v0.0.0
replace github.com/RamXX/agentic_reference_architecture/POC/sdk/go => ../agentic_reference_architecture/POC/sdk/go
go mod tidy
Quick Start
package main
import (
"context"
"errors"
"fmt"
"log"
"github.com/RamXX/agentic_reference_architecture/POC/sdk/go/mcpgateway"
)
func main() {
// Create a client with gateway URL and SPIFFE identity.
client := mcpgateway.NewClient(
"http://localhost:9090",
"spiffe://poc.local/agents/mcp-client/researcher/dev",
)
ctx := context.Background()
// Call a tool. The SDK auto-wraps this as tools/call JSON-RPC.
result, err := client.Call(ctx, "tavily_search", map[string]any{
"query": "AI security best practices",
})
if err != nil {
var ge *mcpgateway.GatewayError
if errors.As(err, &ge) {
log.Fatalf("gateway denied (code=%s, middleware=%s, step=%d): %s",
ge.Code, ge.Middleware, ge.Step, ge.Message)
}
log.Fatal(err)
}
fmt.Println(result)
}
GatewayClient API
NewClient
func NewClient(url, spiffeID string, opts ...Option) *GatewayClient
Creates a new GatewayClient targeting the given gateway base URL
(e.g., "http://localhost:9090"). The spiffeID is sent
in the X-SPIFFE-ID header on every request for authentication.
If no WithSessionID option is provided, a random UUID is
generated automatically. If no WithHTTPClient option is provided,
a default *http.Client is created with the configured timeout.
Call
func (c *GatewayClient) Call(ctx context.Context, methodOrTool string, params map[string]any) (any, error)
Invokes a tool or protocol method through the gateway using MCP-spec JSON-RPC 2.0.
Auto-detection logic: Call inspects the
methodOrTool argument to decide the wire format:
-
If the string contains a
/separator (e.g.,"tools/list","resources/read") or equals"initialize"or starts with"notifications/", it is treated as a protocol method and sent verbatim as the JSON-RPCmethodfield. -
Otherwise (e.g.,
"tavily_search","calculator"), it is treated as a tool name. The SDK automatically wraps it using the MCP-spec envelope:method="tools/call",params.name=<tool>,params.arguments=<params>.
// Tool invocation (most common): auto-wrapped as tools/call
result, err := client.Call(ctx, "tavily_search", map[string]any{
"query": "SPIFFE mTLS",
})
// Protocol method: sent verbatim
result, err := client.Call(ctx, "tools/list", nil)
// Initialize handshake
result, err := client.Call(ctx, "initialize", map[string]any{
"protocolVersion": "2024-11-05",
"capabilities": map[string]any{},
"clientInfo": map[string]any{
"name": "my-agent",
"version": "1.0.0",
},
})
On success, the JSON-RPC result field is returned as
any (typically map[string]any). On gateway
denial or error, a *GatewayError is returned. Use
errors.As to inspect it.
CallModelChat
func (c *GatewayClient) CallModelChat(ctx context.Context, req ModelChatRequest) (map[string]any, error)
Sends an OpenAI-compatible chat completion request through the gateway model egress endpoint. This bypasses JSON-RPC and sends a direct HTTP POST with model-plane headers. See ModelChatRequest for the full field reference.
SessionID
func (c *GatewayClient) SessionID() string
Returns the session ID used by this client instance. Useful for logging or correlating requests across multiple tool calls within a single session.
Configuration Options
GatewayClient uses the
functional options pattern.
Each With* function returns an Option value that
customizes the client.
| Option | Signature | Default | Description |
|---|---|---|---|
WithSessionID |
func(id string) Option |
Auto-generated UUID | Sets the session ID sent in X-Session-ID. If not provided, a UUID v4 is generated at construction time. |
WithTimeout |
func(d time.Duration) Option |
30s | Sets the HTTP request timeout on the default *http.Client. Ignored if WithHTTPClient is also used. |
WithMaxRetries |
func(n int) Option |
3 | Maximum number of retry attempts for HTTP 503 responses. Set to 0 to disable retries. |
WithBackoffBase |
func(d time.Duration) Option |
1s | Base duration for exponential backoff. Retry delays are: base, base*2, base*4, etc. |
WithHTTPClient |
func(hc *http.Client) Option |
nil (auto-created) |
Injects a custom *http.Client. Required for mTLS via go-spiffe or custom TLS configuration. |
client := mcpgateway.NewClient(
"http://localhost:9090",
"spiffe://poc.local/agents/mcp-client/researcher/dev",
mcpgateway.WithSessionID("session-abc-123"),
mcpgateway.WithTimeout(60*time.Second),
mcpgateway.WithMaxRetries(5),
mcpgateway.WithBackoffBase(500*time.Millisecond),
)
ModelChatRequest
ModelChatRequest captures all options for an OpenAI-compatible
chat completion request routed through the gateway model egress plane.
| Field | Type | Default | Description |
|---|---|---|---|
Model |
string |
(required) | Model identifier (e.g., "llama-3.3-70b-versatile"). |
Messages |
[]map[string]any |
(required) | Chat messages in OpenAI format (role, content). |
Provider |
string |
"groq" |
Model provider. Sent as X-Model-Provider header. |
APIKeyRef |
string |
(empty) | API key or bearer token value. Sent via the header specified by APIKeyHeader. |
APIKeyHeader |
string |
"Authorization" |
HTTP header name for the API key. |
Endpoint |
string |
"/openai/v1/chat/completions" |
Gateway endpoint path. Can be an absolute URL or a relative path appended to the gateway base URL. |
Residency |
string |
"us" |
Data residency intent. Sent as X-Residency-Intent header. |
BudgetProfile |
string |
"standard" |
Budget profile name. Sent as X-Budget-Profile header. |
ExtraHeaders |
map[string]string |
nil |
Additional HTTP headers merged into the request. |
ExtraPayload |
map[string]any |
nil |
Additional fields merged into the JSON body (e.g., temperature, max_tokens). |
resp, err := client.CallModelChat(ctx, mcpgateway.ModelChatRequest{
Model: "llama-3.3-70b-versatile",
Messages: []map[string]any{
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "What is SPIFFE?"},
},
Provider: "groq",
APIKeyRef: "Bearer gsk_...",
ExtraPayload: map[string]any{
"temperature": 0.7,
"max_tokens": 512,
},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(resp["choices"])
GatewayError
GatewayError is a structured error type that mirrors the unified
JSON error envelope returned by every gateway middleware denial. It implements
the error interface.
| Field | Type | JSON Key | Description |
|---|---|---|---|
Code |
string |
"code" |
Machine-readable error code (e.g., "authz_policy_denied"). See Error Code Catalog. |
Message |
string |
"message" |
Human-readable error description. |
Middleware |
string |
"middleware" |
Name of the middleware layer that rejected the request (e.g., "opa_policy", "dlp_scanner"). |
Step |
int |
"middleware_step" |
Numeric position of the middleware in the chain (e.g., 6 for OPA, 7 for DLP). |
DecisionID |
string |
"decision_id" |
Audit decision ID for cross-referencing with audit logs. |
TraceID |
string |
"trace_id" |
OpenTelemetry trace ID for distributed tracing correlation. |
Details |
map[string]any |
"details" |
Optional structured data (risk scores, matched patterns, etc.). Omitted from JSON when nil. |
Remediation |
string |
"remediation" |
Optional guidance on how to resolve the error. Omitted from JSON when empty. |
DocsURL |
string |
"docs_url" |
Optional link to relevant documentation. Omitted from JSON when empty. |
HTTPStatus |
int |
(not serialized) | HTTP status code from the gateway response. Set by the SDK, not present in the JSON body. Tagged json:"-". |
Error() Method
The Error() method returns a formatted string. The format depends
on which fields are populated:
# When Message is set:
gateway error authz_policy_denied: request denied by OPA policy
# When only Code is set:
gateway error authz_policy_denied
# When neither Code nor Message is set:
gateway error (HTTP 403)
Using errors.As
result, err := client.Call(ctx, "tavily_search", params)
if err != nil {
var ge *mcpgateway.GatewayError
if errors.As(err, &ge) {
log.Printf("code=%s middleware=%s step=%d decision=%s trace=%s",
ge.Code, ge.Middleware, ge.Step, ge.DecisionID, ge.TraceID)
if ge.Remediation != "" {
log.Printf("remediation: %s", ge.Remediation)
}
if ge.Details != nil {
log.Printf("details: %v", ge.Details)
}
} else {
// Network error, context cancelled, etc.
log.Printf("non-gateway error: %v", err)
}
}
Error Code Catalog
Every gateway middleware denial returns one of the following machine-readable
error codes in the GatewayError.Code field. The full catalog is
defined in internal/gateway/middleware/error_codes.go.
| Gateway Code | Layer (Step) | HTTP Status | Description |
|---|---|---|---|
request_too_large |
Request Size (1) | 413 | Request body exceeds the configured size limit. |
auth_missing_identity |
SPIFFE Auth (3) | 401 | No X-SPIFFE-ID header present in the request. |
auth_invalid_identity |
SPIFFE Auth (3) | 401 | The provided SPIFFE ID is malformed or not recognized. |
registry_tool_unknown |
Tool Registry (5) | 403 | The requested tool is not registered in the gateway tool registry. |
registry_hash_mismatch |
Tool Registry (5) | 403 | The tool binary hash does not match the registered hash (integrity violation). |
authz_policy_denied |
OPA Policy (6) | 403 | OPA policy evaluated the request and returned a deny decision. |
authz_no_matching_grant |
OPA Policy (6) | 403 | No policy grant matches the agent-tool-action combination. |
authz_tool_not_found |
OPA Policy (6) | 403 | OPA could not find the tool in its policy data. |
dlp_credentials_detected |
DLP (7) | 403 | DLP scanner detected credentials or secrets in the request payload. |
dlp_injection_blocked |
DLP (7) | 403 | DLP scanner detected a prompt injection attempt and the policy is set to block. |
dlp_pii_blocked |
DLP (7) | 403 | DLP scanner detected PII and the policy is set to block. |
dlp_unavailable_fail_closed |
DLP (7) | 503 | DLP scanner is unavailable and the policy is fail-closed. |
exfiltration_detected |
Session Context (8) | 403 | Session context analysis detected a data exfiltration attempt. |
stepup_denied |
Step-Up Gating (9) | 403 | Step-up gating denied the request based on risk assessment. |
stepup_approval_required |
Step-Up Gating (9) | 403 | Request requires human approval before execution. |
stepup_guard_blocked |
Step-Up Gating (9) | 403 | The guard model evaluated the request and recommended blocking. |
stepup_destination_blocked |
Step-Up Gating (9) | 403 | The destination URL or endpoint is on the block list. |
stepup_unavailable_fail_closed |
Step-Up Gating (9) | 503 | Step-up gating service is unavailable and the policy is fail-closed. |
deepscan_blocked |
Deep Scan (10) | 403 | Deep scan analysis flagged the request content as unsafe. |
deepscan_unavailable_fail_closed |
Deep Scan (10) | 503 | Deep scan service is unavailable and the policy is fail-closed. |
ratelimit_exceeded |
Rate Limiting (11) | 429 | Request rate or token budget exceeded for this agent identity. |
circuit_open |
Circuit Breaker (12) | 503 | Circuit breaker is open due to repeated downstream failures. |
response_handle_store_unavailable |
Response Firewall (14) | 503 | Response handle store is unavailable. |
response_handleization_failed |
Response Firewall (14) | 500 | Response handle generation failed. |
mcp_transport_failed |
MCP Transport | 502/503 | Transport-level failure (connection refused, timeout) communicating with the MCP server. |
mcp_request_failed |
MCP Transport | 502 | The MCP server returned a JSON-RPC error in its response. |
mcp_invalid_response |
MCP Transport | 502 | The MCP server returned a malformed (non-JSON-RPC) response. |
mcp_invalid_request |
MCP Validation | 400 | The incoming request is not valid MCP JSON-RPC. |
extension_blocked |
Extension Slots | 403 | A registered extension slot denied the request. |
extension_unavailable_fail_closed |
Extension Slots | 503 | Extension slot is unavailable and the policy is fail-closed. |
contract_validation_failed |
Contract Validation | 400 | Request does not conform to the plane entry-point contract schema. |
ui_capability_denied |
UI Capability Gating | 403 | UI capability gating denied access to the requested feature. |
ui_resource_blocked |
UI Capability Gating | 403 | UI resource access is blocked by policy. |
The SDK itself may produce two additional codes that do not originate from
the gateway middleware: "invalid_response" (non-JSON response
body) and "jsonrpc_error" (JSON-RPC-level error in the response
envelope). These are generated client-side.
Wire Format
All tool calls use the JSON-RPC 2.0 wire format as specified by the Model Context Protocol (MCP).
Request
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "tavily_search",
"arguments": {
"query": "AI security"
}
},
"id": 1
}
Response (success)
{
"jsonrpc": "2.0",
"result": {
"content": [
{
"type": "text",
"text": "Search results..."
}
]
},
"id": 1
}
Response (gateway denial)
{
"code": "authz_policy_denied",
"message": "OPA policy denied the request",
"middleware": "opa_policy",
"middleware_step": 6,
"decision_id": "d-abc-123",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736"
}
Request IDs
The SDK uses an atomic.Int64 counter for JSON-RPC request IDs,
starting at 1 and incrementing monotonically per client instance. This ensures
unique, sequential IDs that are safe for concurrent use.
Required Headers
Every request sent by the SDK includes the following HTTP headers:
| Header | Value | Purpose |
|---|---|---|
Content-Type |
application/json |
JSON-RPC payload encoding. |
X-SPIFFE-ID |
The spiffeID passed to NewClient |
Agent identity for SPIFFE authentication (step 3). |
X-Session-ID |
Auto-generated UUID or user-provided value | Session correlation for audit and context tracking (step 8). |
CallModelChat sends additional model-plane headers:
X-Model-Provider, X-Residency-Intent, and
X-Budget-Profile. See ModelChatRequest.
Retry Behavior
The SDK retries only HTTP 503 Service Unavailable responses. All other error status codes (401, 403, 429, etc.) are returned immediately without retry, since they represent definitive denials.
Backoff Formula
delay = backoffBase * (1 << attempt)
attempt 0: 1s (backoffBase * 1)
attempt 1: 2s (backoffBase * 2)
attempt 2: 4s (backoffBase * 4)
With the default configuration (DefaultMaxRetries=3,
DefaultBackoffBase=1s), the SDK will attempt up to 4 total
requests (1 initial + 3 retries) over approximately 7 seconds of backoff
delay.
Context Cancellation During Backoff
During each backoff wait, the SDK monitors the context for cancellation.
If ctx.Done() fires before the backoff timer expires, the SDK
returns ctx.Err() immediately (typically
context.Canceled or context.DeadlineExceeded).
// Set a 5-second deadline. Will cancel retries if exceeded.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
result, err := client.Call(ctx, "tavily_search", params)
// If the gateway returns 503 repeatedly, err may be context.DeadlineExceeded
Non-retryable Errors
Non-GatewayError failures (network errors, DNS resolution
failures, TLS handshake failures) are never retried. These are returned
immediately as wrapped fmt.Errorf values.
Advanced Usage
mTLS with WithHTTPClient
In production, agents authenticate to the gateway using mutual TLS (mTLS)
backed by SPIFFE SVIDs. Use WithHTTPClient to inject a custom
*http.Client with the appropriate TLS configuration.
import (
"crypto/tls"
"crypto/x509"
"net/http"
"os"
"github.com/RamXX/agentic_reference_architecture/POC/sdk/go/mcpgateway"
)
// Load SVID certificate and key (provided by SPIRE agent)
cert, err := tls.LoadX509KeyPair("/run/spire/svid.pem", "/run/spire/svid-key.pem")
if err != nil {
log.Fatal(err)
}
// Load trust bundle (CA certificates)
caCert, err := os.ReadFile("/run/spire/bundle.pem")
if err != nil {
log.Fatal(err)
}
caPool := x509.NewCertPool()
caPool.AppendCertsFromPEM(caCert)
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: caPool,
}
httpClient := &http.Client{
Transport: &http.Transport{TLSClientConfig: tlsConfig},
}
client := mcpgateway.NewClient(
"https://gateway.precinct.svc:9090",
"spiffe://agentic-ref-arch.poc/agents/mcp-client/researcher/prod",
mcpgateway.WithHTTPClient(httpClient),
)
Context Cancellation
All SDK methods accept a context.Context as the first argument.
Use context cancellation to bound request duration or cancel in-flight
requests during graceful shutdown.
// Cancel all in-flight requests on SIGINT
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
result, err := client.Call(ctx, "long_running_tool", params)
Goroutine Safety
GatewayClient is safe for concurrent use from multiple goroutines.
Request IDs are managed via atomic.Int64, and the underlying
*http.Client handles connection pooling. All client state after
construction is either immutable or atomic.
client := mcpgateway.NewClient(url, spiffeID)
var wg sync.WaitGroup
for _, tool := range []string{"tool_a", "tool_b", "tool_c"} {
wg.Add(1)
go func(t string) {
defer wg.Done()
result, err := client.Call(ctx, t, params)
// handle result/err
}(tool)
}
wg.Wait()
SPIKE Token Helpers
The Go SDK provides two helper functions for constructing SPIKE token
reference strings. These functions produce opaque Bearer tokens that the
gateway resolves to real credentials at egress time (middleware layer 13).
The format matches the Python SDK's build_spike_token_ref exactly.
BuildSPIKETokenRef
func BuildSPIKETokenRef(spikeRef string, expSeconds int) string
Constructs a Bearer SPIKE token reference string suitable for use as an
Authorization header value. If spikeRef is empty,
returns an empty string. If expSeconds is <= 0, defaults
to 3600 (1 hour).
// Typical usage
ref := mcpgateway.BuildSPIKETokenRef("secrets/groq-api-key", 3600)
// Returns: "Bearer $SPIKE{ref:secrets/groq-api-key,exp:3600}"
// Custom expiration
ref := mcpgateway.BuildSPIKETokenRef("secrets/openai-key", 7200)
// Returns: "Bearer $SPIKE{ref:secrets/openai-key,exp:7200}"
BuildSPIKETokenRefWithScope
func BuildSPIKETokenRefWithScope(spikeRef string, expSeconds int, scope string) string
Like BuildSPIKETokenRef but appends an optional scope qualifier.
If scope is empty, the result is identical to BuildSPIKETokenRef.
// With scope
ref := mcpgateway.BuildSPIKETokenRefWithScope("secrets/db-cred", 1800, "read-only")
// Returns: "Bearer $SPIKE{ref:secrets/db-cred,exp:1800,scope:read-only}"
// Without scope (equivalent to BuildSPIKETokenRef)
ref := mcpgateway.BuildSPIKETokenRefWithScope("secrets/key", 3600, "")
// Returns: "Bearer $SPIKE{ref:secrets/key,exp:3600}"
| Constant | Type | Value | Description |
|---|---|---|---|
DefaultSPIKEExpSeconds |
int |
3600 |
Default token expiration in seconds (matches the Python SDK default). |
Constants
The package exports three default-value constants used by
NewClient when no corresponding option is provided.
| Constant | Type | Value | Description |
|---|---|---|---|
DefaultMaxRetries |
int |
3 |
Maximum retry attempts for HTTP 503 responses. |
DefaultBackoffBase |
time.Duration |
1 * time.Second |
Base duration for exponential backoff between retries. |
DefaultTimeout |
time.Duration |
30 * time.Second |
HTTP request timeout applied to the default *http.Client. |
const (
DefaultMaxRetries = 3
DefaultBackoffBase = 1 * time.Second
DefaultTimeout = 30 * time.Second
)