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.

First Time Integrating?

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-RPC method field.
  • 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.

Functional option functions for GatewayClient
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.

ModelChatRequest fields
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.

GatewayError struct fields
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 error code catalog
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.
Synthetic Codes

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:

Required 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}"
SPIKE token helper constants
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.

Exported package constants
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
)