1 - Migration Guides

Step-by-step guides for migrating to Go Micro from other frameworks.

Migration Guides

Step-by-step guides for migrating to Go Micro from other frameworks.

Available Guides

Coming Soon

We’re working on additional migration guides:

  • From go-kit - Migrate from Go kit microservices framework
  • From Standard Library - Upgrade from net/http and net/rpc
  • From Gin/Echo - Transition from HTTP-only frameworks
  • From Micro v3 - Upgrade from older Go Micro versions

Why Migrate to Go Micro?

  • Pluggable Architecture - Swap components without changing code
  • Zero Configuration - Works out of the box with sensible defaults
  • Progressive Enhancement - Start simple, add complexity when needed
  • Unified Abstractions - Registry, transport, broker, store all integrated
  • Active Development - Regular updates and community support

Need Help?

1.1 - Add MCP to Existing Services

Add MCP to Existing Services

You have a working go-micro service and want to make it accessible to AI agents via MCP. This guide covers the three approaches, from simplest to most flexible.

Add a single option to your service constructor:

import "go-micro.dev/v6/gateway/mcp"

func main() {
    service := micro.NewService("myservice",
        mcp.WithMCP(":3001"),  // Add this line
    )
    service.Init()
    // ... register handlers as before
    service.Run()
}

That’s it. Your service now exposes all registered handlers as MCP tools at http://localhost:3001/mcp/tools.

Option 2: Standalone MCP Gateway

If you want the MCP gateway to run separately from your services (e.g., in production with multiple services):

import "go-micro.dev/v6/gateway/mcp"

// Start MCP gateway alongside your service
go mcp.ListenAndServe(":3001", mcp.Options{
    Registry: service.Options().Registry,
})

This discovers all services in the registry and exposes them as tools.

Option 3: CLI (No Code Changes)

If you don’t want to modify your service code at all:

# Start your service normally
go run .

# In another terminal, start the MCP gateway
micro mcp serve --address :3001

The CLI approach uses the same registry to discover running services.

Improving Agent Experience

Once MCP is enabled, improve how agents interact with your service by adding documentation.

Step 1: Add Doc Comments

Before:

func (s *Users) Get(ctx context.Context, req *GetRequest, rsp *GetResponse) error {

After:

// Get retrieves a user by their unique ID. Returns the full user profile
// including email, display name, and account status.
//
// @example {"id": "user-123"}
func (s *Users) Get(ctx context.Context, req *GetRequest, rsp *GetResponse) error {

The MCP gateway automatically extracts these comments and presents them to agents as tool descriptions.

Step 2: Add Struct Tag Descriptions

type GetRequest struct {
    ID string `json:"id" description:"User ID in UUID format"`
}

type GetResponse struct {
    Name   string `json:"name" description:"Display name"`
    Email  string `json:"email" description:"Primary email address"`
    Active bool   `json:"active" description:"Whether the account is active"`
}

Step 3: Add Auth Scopes (Optional)

Restrict which agents can call which endpoints:

handler := service.Server().NewHandler(
    new(Users),
    server.WithEndpointScopes("Users.Delete", "users:admin"),
    server.WithEndpointScopes("Users.Get", "users:read"),
)

Then configure the MCP gateway with auth:

mcp.ListenAndServe(":3001", mcp.Options{
    Registry: service.Options().Registry,
    Auth:     authProvider,
    Scopes: map[string][]string{
        "myservice.Users.Delete": {"users:admin"},
        "myservice.Users.Get":    {"users:read"},
    },
})

Using with Claude Code

Once your service is running with MCP, connect it to Claude Code:

# Option A: stdio transport (recommended for local dev)
micro mcp serve

# Option B: Add to Claude Code settings
{
  "mcpServers": {
    "my-services": {
      "command": "micro",
      "args": ["mcp", "serve"]
    }
  }
}

Verify It Works

# List all tools the MCP gateway exposes
curl http://localhost:3001/mcp/tools | jq

# Test a specific tool
curl -X POST http://localhost:3001/mcp/call \
  -H 'Content-Type: application/json' \
  -d '{"tool": "myservice.Users.Get", "arguments": {"id": "user-123"}}'

What Doesn’t Need to Change

  • Handler signatures - No changes needed to your RPC handlers
  • Proto definitions - Existing protos work as-is
  • Client code - Services calling each other still use the normal RPC client
  • Tests - Existing tests continue to work
  • Deployment - Add a port for MCP, everything else stays the same

Next Steps

1.2 - Migrating from gRPC

Step-by-step guide to migrating existing gRPC services to Go Micro.

Why Migrate?

Go Micro adds:

  • Built-in service discovery
  • Client-side load balancing
  • Pub/sub messaging
  • Multiple transport options
  • Unified tooling

You keep:

  • Your proto definitions
  • gRPC performance (via gRPC transport)
  • Type safety
  • Streaming support

Migration Strategy

Phase 1: Parallel Running

Run Go Micro alongside existing gRPC services

Phase 2: Gradual Migration

Migrate services one at a time

Phase 3: Complete Migration

All services on Go Micro

Step-by-Step Migration

1. Existing gRPC Service

// proto/hello.proto
syntax = "proto3";

package hello;
option go_package = "./proto;hello";

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}
// Original gRPC server
package main

import (
    "context"
    "log"
    "net"
    "google.golang.org/grpc"
    pb "myapp/proto"
)

type server struct {
    pb.UnimplementedGreeterServer
}

func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error) {
    return &pb.HelloReply{Message: "Hello " + req.Name}, nil
}

func main() {
    lis, _ := net.Listen("tcp", ":50051")
    s := grpc.NewServer()
    pb.RegisterGreeterServer(s, &server{})
    log.Fatal(s.Serve(lis))
}

2. Generate Go Micro Code

Update your proto generation:

# Install protoc-gen-micro
go install go-micro.dev/v6/cmd/protoc-gen-micro@latest

# Generate both gRPC and Go Micro code
protoc --proto_path=. \
  --go_out=. --go_opt=paths=source_relative \
  --go-grpc_out=. --go-grpc_opt=paths=source_relative \
  --micro_out=. --micro_opt=paths=source_relative \
  proto/hello.proto

This generates:

  • hello.pb.go - Protocol Buffers types
  • hello_grpc.pb.go - gRPC client/server (keep for compatibility)
  • hello.pb.micro.go - Go Micro client/server (new)

3. Migrate Server to Go Micro

// Go Micro server
package main

import (
    "context"
    "go-micro.dev/v6"
    "go-micro.dev/v6/server"
    pb "myapp/proto"
)

type Greeter struct{}

func (s *Greeter) SayHello(ctx context.Context, req *pb.HelloRequest, rsp *pb.HelloReply) error {
    rsp.Message = "Hello " + req.Name
    return nil
}

func main() {
    svc := micro.NewService("greeter",
    )
    svc.Init()

    pb.RegisterGreeterHandler(svc.Server(), new(Greeter))

    if err := svc.Run(); err != nil {
        log.Fatal(err)
    }
}

Key differences:

  • No manual port binding (Go Micro handles it)
  • Automatic service registration
  • Returns error, response via pointer parameter

4. Migrate Client

Original gRPC client:

conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())
defer conn.Close()

client := pb.NewGreeterClient(conn)
rsp, err := client.SayHello(context.Background(), &pb.HelloRequest{Name: "John"})

Go Micro client:

svc := micro.NewService("client")
svc.Init()

client := pb.NewGreeterService("greeter", svc.Client())
rsp, err := client.SayHello(context.Background(), &pb.HelloRequest{Name: "John"})

Benefits:

  • No hardcoded addresses
  • Automatic service discovery
  • Client-side load balancing
  • Automatic retries

5. Keep gRPC Transport (Optional)

Use gRPC as the underlying transport:

import (
    "go-micro.dev/v6"
    "go-micro.dev/v6/client"
    "go-micro.dev/v6/server"
    grpcclient "go-micro.dev/v6/client/grpc"
    grpcserver "go-micro.dev/v6/server/grpc"
)

svc := micro.NewService("greeter",
    micro.Client(grpcclient.NewClient()),
    micro.Server(grpcserver.NewServer()),
)

This gives you:

  • gRPC performance
  • Go Micro features (discovery, load balancing)
  • Compatible with existing gRPC clients

Streaming Migration

Original gRPC Streaming

service Greeter {
  rpc StreamHellos (stream HelloRequest) returns (stream HelloReply) {}
}
func (s *server) StreamHellos(stream pb.Greeter_StreamHellosServer) error {
    for {
        req, err := stream.Recv()
        if err == io.EOF {
            return nil
        }
        if err != nil {
            return err
        }
        
        stream.Send(&pb.HelloReply{Message: "Hello " + req.Name})
    }
}

Go Micro Streaming

func (s *Greeter) StreamHellos(ctx context.Context, stream server.Stream) error {
    for {
        var req pb.HelloRequest
        if err := stream.Recv(&req); err != nil {
            return err
        }
        
        if err := stream.Send(&pb.HelloReply{Message: "Hello " + req.Name}); err != nil {
            return err
        }
    }
}

Service Discovery Migration

Before (gRPC with Consul)

// Manually register with Consul
config := api.DefaultConfig()
config.Address = "consul:8500"
client, _ := api.NewClient(config)

reg := &api.AgentServiceRegistration{
    ID:      "greeter-1",
    Name:    "greeter",
    Address: "localhost",
    Port:    50051,
}
client.Agent().ServiceRegister(reg)

// Cleanup on shutdown
defer client.Agent().ServiceDeregister("greeter-1")

After (Go Micro)

import "go-micro.dev/v6/registry/consul"

reg := consul.NewConsulRegistry()
svc := micro.NewService("greeter",
    micro.Registry(reg),
)

// Registration automatic on Run()
// Deregistration automatic on shutdown
svc.Run()

Load Balancing Migration

Before (gRPC with custom LB)

// Need external load balancer or custom implementation
// Example: round-robin DNS, Envoy, nginx

After (Go Micro)

import "go-micro.dev/v6/selector"

// Client-side load balancing built-in
svc := micro.NewService("greeter",
    micro.Selector(selector.NewSelector(
        selector.SetStrategy(selector.RoundRobin),
    )),
)

Gradual Migration Path

1. Start with New Services

New services use Go Micro, existing services stay on gRPC.

// New Go Micro service can call gRPC services
// Configure gRPC endpoints directly
grpcConn, _ := grpc.Dial("old-service:50051", grpc.WithInsecure())
oldClient := pb.NewOldServiceClient(grpcConn)

2. Migrate Read-Heavy Services First

Services with many clients benefit most from service discovery.

3. Migrate Services with Fewest Dependencies

Leaf services are easier to migrate.

4. Add Adapters if Needed

// gRPC adapter for Go Micro service
type GRPCAdapter struct {
    microClient pb.GreeterService
}

func (a *GRPCAdapter) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error) {
    return a.microClient.SayHello(ctx, req)
}

// Register adapter as gRPC server
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &GRPCAdapter{microClient: microClient})

Checklist

  • Update proto generation to include --micro_out
  • Convert handler signatures (response via pointer)
  • Replace grpc.Dial with Go Micro client
  • Configure service discovery (Consul, Etcd, etc)
  • Update deployment (remove hardcoded ports)
  • Update monitoring (Go Micro metrics)
  • Test service-to-service communication
  • Update documentation
  • Train team on Go Micro patterns

Common Issues

Port Already in Use

gRPC: Manual port management

lis, _ := net.Listen("tcp", ":50051")

Go Micro: Automatic or explicit

// Let Go Micro choose
svc := micro.NewService("greeter")

// Or specify
svc := micro.NewService("greeter",
    micro.Address(":50051"),
)

Service Not Found

Check registry:

# Consul
curl http://localhost:8500/v1/catalog/services

# Or use micro CLI
micro services

Different Serialization

gRPC uses protobuf by default. Go Micro supports multiple codecs.

Ensure both use protobuf:

import "go-micro.dev/v6/codec/proto"

svc := micro.NewService("greeter",
    micro.Codec("application/protobuf", proto.Marshaler{}),
)

Performance Comparison

ScenariogRPCGo Micro (HTTP)Go Micro (gRPC)
Simple RPC~25k req/s~20k req/s~24k req/s
With DiscoveryN/A~18k req/s~22k req/s
Streaming~30k msg/s~15k msg/s~28k msg/s

Go Micro with gRPC transport performs similarly to pure gRPC

Next Steps

Need Help?

1.3 - Migrating from v5 to v6

v6 is a small, mechanical upgrade. The bulk of it is the Go module path; the behavioral changes are two, both with a one-line fix.

1. Module path: go-micro.dev/v6

Go puts the major version in the import path, so every import changes:

// before
import "go-micro.dev/v5"
import "go-micro.dev/v5/server"

// after
import "go-micro.dev/v6"
import "go-micro.dev/v6/server"

A repo-wide find/replace does it:

grep -rl 'go-micro.dev/v5' --include='*.go' . \
  | xargs sed -i 's|go-micro.dev/v5|go-micro.dev/v6|g'
go mod tidy

Update the CLI too:

go install go-micro.dev/v6/cmd/micro@latest

2. TLS is verified by default

In v5, TLS certificate verification was off by default (you opted in with MICRO_TLS_SECURE=true). In v6 it is on by default — the safe choice now that an agent, not just a human on a trusted network, can reach an endpoint.

  • Production: nothing to do. Verification is on.
  • MICRO_TLS_SECURE is gone — remove it; it’s the default now.
  • Self-signed certs (local/dev): opt out with MICRO_TLS_INSECURE=true, or call tls.InsecureConfig() directly.

3. NewService is the service constructor

The service constructor is now symmetric with NewAgent and NewFlow:

service := micro.NewService("greeter", micro.Address(":8080"))
agent   := micro.NewAgent("task-mgr", micro.AgentServices("task"))
flow    := micro.NewFlow("onboard", micro.FlowTrigger("events.user.created"))
  • micro.New("greeter", ...) still works as a deprecated alias — no rush, but prefer NewService.
  • The old name-less form micro.NewService(micro.Name("greeter"), ...) is removed; pass the name positionally: micro.NewService("greeter", ...).

Generated services already use NewService — re-running micro new or micro run --prompt emits the v6 form.

That’s it

No other API changed. Agents, services, flows, the registry/broker/store interfaces, MCP, A2A, and x402 all work as they did — just under go-micro.dev/v6 and secure by default.

2 - `micro loop` quickstart

micro loop scaffolds the autonomous improvement loop that Go Micro uses on this repository: GitHub Actions workflows for planning, building, evaluation feedback, coherence, security, and release. Use it when you want a repository to continuously turn a ranked queue into small PRs while CI remains the merge gate.

1. Initialize the loop

Run the default loop from the repository root:

micro loop init

For every role used by Go Micro itself, scaffold all workflows:

micro loop init --roles all

The command writes:

  • .github/loop/NORTH_STAR.md — the direction every increment should optimize.
  • .github/loop/PRIORITIES.md — the ranked queue; the builder takes the top open issue.
  • .github/loop/prompts/*.md — editable policy for planner, builder, triage, coherence, and security roles.
  • .github/workflows/loop-*.yml — generated GitHub Actions mechanics.

Edit the files under .github/loop/ to steer the loop. Re-run micro loop init --roles all --force only when you want to regenerate workflow mechanics from the installed CLI.

2. Configure the dispatch token

The scheduled builder needs a repository secret containing a token from a user account that the coding agent will answer. Go Micro names that secret CODEX_TRIGGER_TOKEN by default. If you use another secret name, pass it when you initialize the loop:

micro loop init --agent @codex --token-secret LOOP_TOKEN --roles all

The token needs enough repository permission to open issues, comment, push branches, create pull requests, and enable auto-merge. Run gh auth setup-git in the environment that will push branches so git push uses the same credentials as gh.

Choosing an agent

The loop is agent-agnostic by design. Each run opens a fresh tracking issue and summons the agent with an @mention comment; the prompt file (.github/loop/prompts/<role>.md) is the instruction. Any coding agent that (a) responds to an @mention on an issue and (b) can open a PR with gh works — you select it with --agent.

  • Codex (--agent @codex, the default). Point --token-secret at a PAT for the user account Codex follows, and make sure the Codex environment installs gh and runs gh auth setup-git. This is the path Go Micro itself runs on.
  • Claude Code (--agent @claude). Install anthropics/claude-code-action in the repo so a workflow responds to @claude comments and runs Claude with a repo-scoped token; then the loop’s dispatch triggers it like any other mention.
  • Any other mention-driven agent — pass its handle to --agent. The mechanics don’t care which agent it is.

Not supported by the mention model: agents triggered by issue assignment rather than a comment (e.g. GitHub Copilot’s coding agent, which you assign an issue to). The dispatch would need an “assign” adapter for those; it isn’t wired yet, so stick to mention-driven agents.

3. Make CI the gate

The loop should not be its own reviewer. Protect the default branch so PRs merge only after the required checks pass. At minimum, require the same commands the Go Micro loop verifies locally and in CI:

go build ./...
go test ./...
golangci-lint run ./...

If your repository has a harness or end-to-end grader, make that required too. Keep human approval requirements out of the autonomous path unless you intend the loop to pause for review.

4. Verify the wiring

After editing the North Star, queue, prompts, token secret, and branch protection, run:

micro loop verify

micro loop verify checks that the loop direction, queue, prompts, role workflows, and non-loop CI gate are present. Fix any reported missing items before relying on scheduled increments.

5. Operate the queue

Keep one ranked list in .github/loop/PRIORITIES.md. Each item should link a scoped issue and be small enough for one PR. The builder closes both the priority issue and the per-run tracker issue in the PR body, for example:

Closes #1234
Closes #5678

Use the North Star to keep the queue honest: favor small improvements that move developers through the services → agents → workflows lifecycle, and surface breaking API or brand/positioning decisions for humans instead of auto-merging them.

3 - 0→hero reference path

The 0→hero path is the maintained, no-secret reference for the Go Micro services → agents → workflows lifecycle. It ties the CLI inner loop and the runtime harness together so a contributor can prove the framework still works as one system, not as separate demos.

Use it when you want to answer: “Can I scaffold a service, run it locally, talk to an agent, inspect durable work, and reach the deployment boundary without cloud credentials?”

What the contract covers

BoundaryContractCI check
Scaffoldmicro new generates a runnable service with and without MCP support.go test ./cmd/micro/cli/new -run TestZeroToOne -count=1
First-agent wayfindingREADME, website index/quickstart, examples, and no-secret/0→hero docs keep the no-secret → first-agent → debugging → 0→hero links present and in order.go test ./internal/harness/zero-to-hero-ci -run TestFirstAgentWayfinding -count=1
First agentmicro new, micro agent preflight, micro run, micro chat, and micro inspect agent <name> stay available for the documented first-agent walkthrough.go test ./cmd/micro -run TestFirstAgentWalkthroughCLIBoundaries -count=1
Runmicro run remains the local development entry point.go test ./cmd/micro -run TestZeroToHeroCLIBoundaries -count=1
Chatmicro chat remains the interactive agent entry point.go test ./cmd/micro -run TestZeroToHeroCLIBoundaries -count=1
Inspectmicro inspect agent <name>, micro agent history <name>, micro inspect flow <flow>, and micro flow runs <flow> remain discoverable for run history; the no-secret debugging smoke seeds durable agent history and runs the documented inspect/history commands without provider keys.go test ./internal/harness/zero-to-hero-ci -run TestNoSecretFirstAgentDebuggingSmoke -count=1
Deploymicro deploy --dry-run prod resolves the documented deploy target without touching remote infrastructure.go test ./internal/harness/zero-to-hero-ci -run TestZeroToHeroDeployDryRunCommandSmoke -count=1
Smallest first agentexamples/first-agent runs one service-backed agent with a deterministic mock model and no provider key.go test ./examples/first-agent -run TestRunFirstAgent -count=1
Runtime reference appexamples/support runs typed services, an agent using those services as tools, an event-driven flow handoff, and an approval gate with only the model mocked.`go test ./examples/support -run ‘TestRunSupportMockSmoke
Ordered 0→hero transcriptThe maintained CI transcript walks scaffold → run/chat/inspect → support-agent chat → flow history → deploy dry-run without provider keys.make zero-to-hero-transcript
Runtime harnessesReal services, agents, durable flows, store-backed history, delegation, and A2A run with only the model mocked../internal/harness/zero-to-hero-ci/run.sh and make provider-conformance-mock

Find the one-command entrypoint

After installing the CLI, ask micro for the maintained no-secret lifecycle command:

micro zero-to-hero

The command prints the exact harness command below plus the smaller runnable examples, so a new developer can discover the 0→hero path from CLI help instead of translating this guide by hand.

Run the runnable example

From the repository root, start with the smallest service-backed agent when you want the fastest no-secret success path:

go run ./examples/first-agent

Then run the support-desk example when you want to see the full lifecycle in one terminal:

go run ./examples/support

It starts typed services, a support agent, an event-driven intake flow, and an approval gate with a deterministic mock model. Change one service method, agent prompt, or guardrail decision and run it again to learn the system by modifying a working path.

Run the whole no-secret path

From the repository root:

make harness

For the focused ordered transcript only, run:

make zero-to-hero-transcript

That target runs the scaffold contract, the CLI boundary smoke tests, the 0→hero runtime harnesses, the event-driven agent-flow harness, and mock provider conformance. It is intentionally deterministic: no provider key, cloud account, SSH access, or remote service is required.

Run focused checks while iterating

Use the dedicated inner-loop target when you need the provider-free CLI contract in one focused command:

make inner-loop

Use the smaller checks when you are working on one seam:

# Install script and first-run CLI boundary, with no network or provider keys.
make install-smoke

# Scaffold → run/call contract.
go test ./cmd/micro/cli/new -run TestZeroToOne -count=1

# First-agent walkthrough boundary: scaffold, preflight, run, chat, inspect.
go test ./cmd/micro -run TestFirstAgentWalkthroughCLIBoundaries -count=1

# CLI inner-loop commands: run, chat, inspect, flow runs, deploy --dry-run.
go test ./cmd/micro -run TestZeroToHeroCLIBoundaries -count=1
go test ./cmd/micro/cli/deploy -run TestDeployDryRun -count=1
go test ./internal/harness/zero-to-hero-ci -run TestZeroToHeroDeployDryRunCommandSmoke -count=1

# Smallest no-secret service-backed first agent.
go test ./examples/first-agent -run TestRunFirstAgent -count=1

# Maintained 0→hero support-desk reference app.
go test ./examples/support -run 'TestRunSupportMockSmoke|TestZeroToHeroReadmeDocumentsLifecycle|TestZeroToHeroInspectTranscript' -count=1

# Durable services → agents → workflows reference scenarios.
./internal/harness/zero-to-hero-ci/run.sh

# Event-as-prompt agent flow.
go run ./internal/harness/agent-flow

# Cross-provider semantics with the deterministic mock provider.
make provider-conformance-mock

Reference scenarios

  • examples/first-agent is the smallest no-secret service-backed agent: one notes service, one scoped assistant agent, and a deterministic mock model.
  • examples/support is the runnable support-desk story: customers, tickets, notify, a support agent, an intake flow, and an approval gate in one no-secret example.
  • examples/agent-plan-delegate is the smallest runnable planning/delegation example for multiple agents.
  • internal/harness/plan-delegate is the compact 0→hero scenario: real task and notify services, a conductor agent, a comms agent, plan persistence, delegation, and a workflow handoff.
  • internal/harness/universe boots a larger mini-world: inventory, payment, order confirmation, a concierge agent, durable checkpoint/resume, agent run history, flow run history, and A2A reachability.
  • internal/harness/agent-flow shows the event-driven path where a user.created event prompts an agent to call services and complete onboarding.

Together these scenarios keep the North Star executable: services expose typed capabilities, agents use those capabilities with memory and guardrails, and workflows compose the work over time.

Keeping the guide honest

If you change the CLI inner loop, durable flow APIs, agent run history, or the provider/tool semantics, update this guide and the harness in the same PR. The point of 0→hero is not a polished sample app that drifts from reality; it is a CI-verifiable contract that the documented lifecycle still works.

4 - Adding an AI Provider to Go Micro

This guide walks you through implementing a new AI model provider for go-micro’s ai package. After following these steps your provider will be available via ai.New("yourprovider") and automatically usable by the MCP gateway, the agent playground, and any service that calls service.Model().

Overview

The ai package uses the same plugin pattern as the rest of go-micro: define an interface, register an implementation, and let users swap providers with a single import. All providers live under ai/<name>/.

Files you will create:

ai/
└── yourprovider/
    ├── yourprovider.go       # Provider implementation
    └── yourprovider_test.go  # Unit tests

Discover registered provider capabilities

Go Micro exposes the provider interfaces registered in the current build, so runtime tooling and docs can report what is actually available after blank imports are linked in:

for _, row := range ai.CapabilityRows() {
    fmt.Printf("%s: chat=%t image=%t video=%t stream=%t tool_stream=%t\n", row.Provider, row.Model, row.Image, row.Video, row.Stream, row.ToolStream)
}

The built-in providers currently register these capability interfaces:

ProviderChat/text (ai.Model)Image (ai.ImageModel)Video (ai.VideoModel)Streaming (ai.Stream)Tool streaming
anthropicYesNoNoYesYes
atlascloudYesYesYesYesNo
geminiYesNoNoYesNo
groqYesNoNoYesYes
minimaxYesNoNoYesYes
mistralYesNoNoYesYes
ollamaYesNoNoYesYes
openaiYesYesNoYesYes
togetherYesNoNoYesYes

Step 1: Implement the ai.Model Interface

Every provider must satisfy ai.Model:

type Model interface {
    Init(...Option) error
    Options() Options
    Generate(ctx context.Context, req *Request, opts ...GenerateOption) (*Response, error)
    Stream(ctx context.Context, req *Request, opts ...GenerateOption) (Stream, error)
    String() string
}

Skeleton

Create ai/yourprovider/yourprovider.go:

package yourprovider

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "strings"

    "go-micro.dev/v6/ai"
)

func init() {
    ai.Register("yourprovider", func(opts ...ai.Option) ai.Model {
        return NewProvider(opts...)
    })
}

type Provider struct {
    opts ai.Options
}

func NewProvider(opts ...ai.Option) *Provider {
    options := ai.NewOptions(opts...)
    if options.Model == "" {
        options.Model = "your-default-model"
    }
    if options.BaseURL == "" {
        options.BaseURL = "https://api.yourprovider.com"
    }
    return &Provider{opts: options}
}

func (p *Provider) Init(opts ...ai.Option) error {
    for _, o := range opts {
        o(&p.opts)
    }
    return nil
}

func (p *Provider) Options() ai.Options { return p.opts }
func (p *Provider) String() string      { return "yourprovider" }

Generate

Generate is the core method. It must:

  1. Convert req.Tools into the provider’s native tool format.
  2. Send the request to the provider API.
  3. Parse the response into ai.Response (text in Reply, tool calls in ToolCalls).
  4. If p.opts.ToolHandler is set and there are tool calls, execute each tool and make a follow-up API call to get the final answer in Answer.
func (p *Provider) Generate(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (*ai.Response, error) {
    // 1. Build provider-specific tool definitions
    var tools []map[string]any
    for _, t := range req.Tools {
        tools = append(tools, map[string]any{
            // Map to your provider's schema
            "name":        t.Name,
            "description": t.Description,
            "parameters":  map[string]any{
                "type":       "object",
                "properties": t.Properties,
            },
        })
    }

    // 2. Build the API request body
    apiReq := map[string]any{
        "model":    p.opts.Model,
        "messages": []map[string]any{
            {"role": "system", "content": req.SystemPrompt},
            {"role": "user", "content": req.Prompt},
        },
    }
    if len(tools) > 0 {
        apiReq["tools"] = tools
    }

    // 3. Call the API
    resp, rawMsg, err := p.callAPI(ctx, apiReq)
    if err != nil {
        return nil, err
    }

    // 4. No tool calls → return immediately
    if len(resp.ToolCalls) == 0 {
        return resp, nil
    }

    // 5. Execute tools and follow up
    if p.opts.ToolHandler != nil {
        // ... build follow-up messages with tool results ...
        followUpResp, _, err := p.callAPI(ctx, followUpReq)
        if err == nil && followUpResp.Reply != "" {
            resp.Answer = followUpResp.Reply
        }
    }

    return resp, nil
}

Stream

If streaming is not supported yet, return a clear error:

func (p *Provider) Stream(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (ai.Stream, error) {
    return nil, fmt.Errorf("streaming not yet implemented for yourprovider")
}

API Helper

Use net/http directly — no external SDK needed:

func (p *Provider) callAPI(ctx context.Context, req map[string]any) (*ai.Response, map[string]any, error) {
    reqBody, err := json.Marshal(req)
    if err != nil {
        return nil, nil, fmt.Errorf("failed to marshal request: %w", err)
    }

    apiURL := strings.TrimRight(p.opts.BaseURL, "/") + "/v1/chat/completions"
    httpReq, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewReader(reqBody))
    if err != nil {
        return nil, nil, fmt.Errorf("failed to create request: %w", err)
    }

    httpReq.Header.Set("Content-Type", "application/json")
    httpReq.Header.Set("Authorization", "Bearer "+p.opts.APIKey)

    httpResp, err := http.DefaultClient.Do(httpReq)
    if err != nil {
        return nil, nil, fmt.Errorf("API request failed: %w", err)
    }
    defer httpResp.Body.Close()

    respBody, _ := io.ReadAll(httpResp.Body)
    if httpResp.StatusCode != 200 {
        return nil, nil, fmt.Errorf("API error (%s): %s", httpResp.Status, string(respBody))
    }

    // Parse your provider's response format into ai.Response
    // ...
}

Step 2: Write Tests

Create ai/yourprovider/yourprovider_test.go. At minimum test:

  • String() returns the correct name.
  • Init() applies options.
  • Default values are set when no options are provided.
  • Generate() without API key returns an error.
  • Stream() not implemented returns an error.
package yourprovider

import (
    "context"
    "testing"

    "go-micro.dev/v6/ai"
)

func TestProvider_String(t *testing.T) {
    p := NewProvider()
    if p.String() != "yourprovider" {
        t.Errorf("got %q, want %q", p.String(), "yourprovider")
    }
}

func TestProvider_Defaults(t *testing.T) {
    p := NewProvider()
    opts := p.Options()
    if opts.Model != "your-default-model" {
        t.Errorf("default model = %q, want %q", opts.Model, "your-default-model")
    }
    if opts.BaseURL != "https://api.yourprovider.com" {
        t.Errorf("default base URL = %q", opts.BaseURL)
    }
}

func TestProvider_Init(t *testing.T) {
    p := NewProvider()
    if err := p.Init(ai.WithModel("custom"), ai.WithAPIKey("key")); err != nil {
        t.Fatalf("Init: %v", err)
    }
    if p.Options().Model != "custom" {
        t.Errorf("model not updated")
    }
}

func TestProvider_Generate_NoAPIKey(t *testing.T) {
    p := NewProvider()
    _, err := p.Generate(context.Background(), &ai.Request{Prompt: "hi"})
    if err == nil {
        t.Error("expected error without API key")
    }
}

func TestProvider_Stream_NotImplemented(t *testing.T) {
    p := NewProvider()
    _, err := p.Stream(context.Background(), &ai.Request{Prompt: "hi"})
    if err == nil {
        t.Error("expected error for unimplemented streaming")
    }
}

Run:

go test ./ai/yourprovider/...

Step 3: Register the Provider

The init() function in your package calls ai.Register. Users enable your provider with a blank import:

import _ "go-micro.dev/v6/ai/yourprovider"

Then use it:

m := ai.New("yourprovider",
    ai.WithAPIKey("your-api-key"),
    ai.WithModel("your-model-name"),
)

resp, err := m.Generate(ctx, &ai.Request{
    Prompt:       "Hello!",
    SystemPrompt: "You are a helpful assistant",
})

Step 4: Update the README

Add your provider to the Supported AI Providers section in the project README.md. Follow the existing format:

### YourProvider

```go
m := ai.New("yourprovider",
    ai.WithAPIKey("your-key"),
    ai.WithModel("your-default-model"),
)

Default model: your-default-model Default base URL: https://api.yourprovider.com


Also add an entry in `ai/README.md` under "Supported Providers".

## Checklist

Before submitting your PR:

- [ ] `ai/yourprovider/yourprovider.go` implements `ai.Model`
- [ ] `init()` calls `ai.Register("yourprovider", ...)`
- [ ] `Generate()` handles tool calls via `ToolHandler` when set
- [ ] `ai/yourprovider/yourprovider_test.go` covers basics
- [ ] `go test ./ai/yourprovider/...` passes
- [ ] `go vet ./ai/yourprovider/...` is clean
- [ ] Provider added to `ai/README.md` under "Supported Providers"
- [ ] Provider added to project README.md under "Supported AI Providers"
- [ ] No new dependencies beyond `go-micro.dev/v6/ai` and stdlib (use
      `net/http` directly rather than an SDK)

## Design Notes

**Why `net/http` instead of an SDK?** Keeping providers dependency-free
means `go get go-micro.dev/v6` never pulls in heavy SDK trees. All
existing providers (Anthropic, OpenAI) use raw HTTP for the same reason.

**OpenAI-compatible APIs.** Many providers (Together, Groq, Fireworks,
Atlas Cloud, etc.) expose an OpenAI-compatible `/v1/chat/completions`
endpoint. In that case, users can often just use the `openai` provider
with `ai.WithBaseURL("https://api.yourprovider.com")`. A dedicated
provider package is only needed when the API differs or you want to set
provider-specific defaults.

**Tool call loop.** The current contract is one round of tool execution:
`Generate` calls tools via `ToolHandler`, feeds results back, and
returns the final answer. Multi-turn agentic loops are handled at a
higher level (e.g. the MCP gateway).

## Sponsorship

If you are an AI infrastructure company interested in becoming a
supported provider, we welcome both code contributions and sponsorships.
See the Supported AI Providers section in the project README for
current partners, and reach out via a GitHub issue or the Discord
community to discuss integration.

5 - Agent Guardrails

An autonomous agent decides its own actions at runtime, which is what makes it useful — and what makes it risky. The common failure modes are mundane: it loops, repeating the same call without making progress; it runs away, taking far more steps (and cost) than the task warrants; it takes an action that should have had a human or a policy in the way.

Go Micro separates orchestration (the model deciding what to do) from execution safety (whether a decided action is allowed to run). Every tool call an agent makes passes through one choke point, and that’s where the guardrails live — so they apply uniformly to service calls, custom tools, and delegate, without touching the model or your services.

The three agent guardrails

Stop on count — MaxSteps

Bounds the total number of tool executions in a single Ask. Once exceeded, further calls are refused and the model is told to stop and summarize. The blunt backstop against runaway cost.

micro.NewAgent("worker", micro.AgentMaxSteps(8))

Stop on repeat — LoopLimit

Bounds how many times the agent may call the same tool with the same arguments in one Ask. Identical repeated calls make no progress — MaxSteps only bounds them by total count, and a circuit breaker only catches failures, not a call that succeeds and is pointlessly repeated. When the limit is hit, the call is refused with a message that tells the model it’s looping, so it changes approach instead of spinning:

loop detected: you have already called “search.Search.Query” with the same arguments 3 times and the result will not change. Stop repeating it — try a different approach, or finish with what you have.

micro.NewAgent("worker", micro.AgentLoopLimit(3))

LoopLimit is on by default (a lenient 3) because identical repeated calls are never useful. Set AgentLoopLimit(0) to disable it.

Gate the action — ApproveTool

A hook called before each action runs. Return false to block it, with a reason that’s surfaced to the model. Use it for human-in-the-loop approval, spend limits, allow/deny lists, or any policy:

micro.NewAgent("worker", micro.AgentApproveTool(
    func(tool string, input map[string]any) (bool, string) {
        if strings.HasPrefix(tool, "billing_") {
            return false, "billing actions require sign-off"
        }
        return true, ""
    }))

ApproveTool is the integration seam

ApproveTool is also where an external policy engine plugs in. It sees every tool call before execution and can veto, so you can route decisions to your own rules, a budget service, or a third-party runtime-safety layer — without go-micro depending on it. Orchestration stays in the agent; execution safety stays in the hook. That separation is the whole point: you can swap the safety layer without touching the agent.

Wrap the whole execution — WrapTool

ApproveTool is a before gate. When you need the full lifecycle — timing, logging, metrics, retries, or inspecting the result — wrap the execution instead. WrapTool is the tool-side analogue of go-micro’s client.CallWrapper and server.HandlerWrapper: a wrapper takes the next handler and returns a new one, so code before the next(...) call runs before the tool, and code after runs after.

import "go-micro.dev/v6/ai"

func logging(next ai.ToolHandler) ai.ToolHandler {
    return func(ctx context.Context, call ai.ToolCall) ai.ToolResult {
        start := time.Now()
        res := next(ctx, call)
        log.Printf("id=%s tool=%s took=%s", call.ID, call.Name, time.Since(start))
        return res
    }
}

micro.NewAgent("worker", micro.AgentWrapTool(logging))

The handler signature is the same one every provider uses to execute a tool, and it mirrors a service handler — context first, the call in, a result out:

type ToolHandler func(ctx context.Context, call ToolCall) ToolResult
type ToolWrapper func(ToolHandler) ToolHandler

call.ID is a correlation ID carried through from the provider, so a wrapper can tie a tool call back to the request it came from. call.Scan(&v) decodes the arguments into a typed struct when you’d rather not work with the raw map.

Wrappers run outside the built-in guardrails, so they observe every call and its result — including a guardrail’s refusal. Multiple wrappers compose outermost-first (the first registered is the outer layer). A “before/after” hook is just the two halves of one wrapper, and retry is calling next again — so the wrapper is the single, composable seam for everything around execution, while MaxSteps, LoopLimit, and ApproveTool remain the named guardrails on top of it.

Reliability metadata

A wrapper has what it needs to build reliability tooling — loop handling, retry policies, auditing — without coupling to the agent:

  • What happened — a guardrail refusal is tagged with a structured reason on the result, so you switch on it rather than parse a message:

    res := next(ctx, call)
    switch res.Refused {
    case ai.RefusedLoop:     // the agent repeated an identical call
    case ai.RefusedMaxSteps: // the step budget was exhausted
    case ai.RefusedApproval: // ApproveTool blocked it
    }
    
  • Which runai.RunInfoFrom(ctx) returns a correlation id for the run, the agent’s name, and the parent run when the call came from a delegated sub-agent:

    if run, ok := ai.RunInfoFrom(ctx); ok {
        log.Printf("run=%s parent=%s agent=%s tool=%s", run.RunID, run.ParentID, run.Agent, call.Name)
    }
    
  • Per-call detailcall.ID (correlation), call.Name; duration is time.Since(start) around next, and step/attempt counts are naturally counted by the wrapper itself (it sees every call).

Execution safety at the gateway

When agents reach tools through the MCP gateway, the gateway adds its own per-tool policies, independent of the agent:

  • RateLimit — requests-per-second per tool.
  • CircuitBreaker — a tool that fails repeatedly is temporarily blocked, so a failing dependency doesn’t cascade.

Together with the agent-side guardrails, that’s a full set: bound the count, stop the spin, gate the action, rate-limit and circuit-break at the edge.

Why it matters for autonomous agents

These are most important when no human is in the loop. An agent triggered by an event runs unattended — there’s no one to notice it looping or to approve a risky call. The guardrails are what let it fail safely and recover on its own rather than quietly burning resources.

See also

6 - Agent Integration Patterns

This guide covers common patterns for integrating AI agents with Go Micro services, from single-agent workflows to multi-agent architectures.

Pattern 1: Single Agent with Multiple Services

The simplest and most common pattern. One AI agent has access to multiple microservices as MCP tools.

User → AI Agent → MCP Gateway → [Service A, Service B, Service C]

Setup

Run multiple services and expose them all through one MCP gateway:

users := micro.NewService("users", micro.Address(":8081"))
tasks := micro.NewService("tasks", micro.Address(":8082"))
notifications := micro.NewService("notifications", micro.Address(":8083"))

// Run all together as a modular monolith
g := micro.NewGroup(users, tasks, notifications)
g.Run()

With micro run, all services are discovered automatically via the registry, and the MCP tools endpoint at /mcp/tools exposes every endpoint from every service.

When to Use

  • Most applications start here
  • Agent needs to orchestrate across services (e.g., “create a task and notify the assignee”)
  • You want the agent to choose which service to call based on the user’s request

Pattern 2: Scoped Agents

Different agents have access to different subsets of tools via scopes.

Customer Agent  → MCP Gateway → [orders:read, support:write]
Internal Agent  → MCP Gateway → [orders:*, users:*, billing:*]
Admin Agent     → MCP Gateway → [*]

Setup

Create tokens with different scopes for each agent:

// Gateway with scope enforcement
mcp.ListenAndServe(":3000", mcp.Options{
    Registry: reg,
    Auth:     authProvider,
    Scopes: map[string][]string{
        "billing.Billing.Charge":    {"billing:admin"},
        "users.Users.Delete":        {"users:admin"},
        "orders.Orders.List":        {"orders:read"},
        "orders.Orders.Create":      {"orders:write"},
        "support.Support.CreateTicket": {"support:write"},
    },
})

Then issue different tokens:

  • Customer-facing agent token: scopes=["orders:read", "support:write"]
  • Internal agent token: scopes=["orders:read", "orders:write", "users:read"]
  • Admin agent token: scopes=["*"]

When to Use

  • Different trust levels for different agents
  • Customer-facing vs internal agents
  • Compliance requirements (e.g., PCI, HIPAA)

Pattern 3: Agent as Service Consumer

Your Go Micro service itself calls an AI model to process data, using the ai package.

User → API → Your Service → AI Model (Claude/GPT)
                          → Other Services

Setup

import (
    "go-micro.dev/v6/ai"
    _ "go-micro.dev/v6/ai/anthropic"
)

type SummaryService struct {
    ai    ai.Model
    tasks *TaskClient
}

func NewSummaryService() *SummaryService {
    return &SummaryService{
        ai: ai.New("anthropic",
            ai.WithAPIKey(os.Getenv("ANTHROPIC_API_KEY")),
            ai.WithModel("claude-sonnet-4-20250514"),
        ),
    }
}

// Summarize generates an AI summary of a project's tasks.
// Returns a natural language summary of task status, blockers, and progress.
//
// @example {"project_id": "proj-1"}
func (s *SummaryService) Summarize(ctx context.Context, req *SummarizeRequest, rsp *SummarizeResponse) error {
    // Fetch tasks from another service
    tasks, err := s.tasks.List(ctx, req.ProjectID)
    if err != nil {
        return err
    }

    // Use AI to summarize
    resp, err := s.ai.Generate(ctx, &ai.Request{
        Prompt:       fmt.Sprintf("Summarize these tasks:\n%s", formatTasks(tasks)),
        SystemPrompt: "You are a concise project manager. Summarize task status in 2-3 sentences.",
    })
    if err != nil {
        return err
    }

    rsp.Summary = resp.Reply
    return nil
}

When to Use

  • Your service needs to process natural language
  • Generating summaries, classifications, or extractions
  • Enriching data with AI before returning to the caller

Pattern 4: Agent with Tool Calling

An AI model calls your services as tools, with automatic tool execution via the ai package.

User → Your App → AI Model ←→ MCP Tools (your services)

Setup

import (
    "go-micro.dev/v6/ai"
    _ "go-micro.dev/v6/ai/anthropic"
)

// Define tools from your service endpoints
tools := []ai.Tool{
    {
        Name:        "create_task",
        Description: "Create a new task with title and assignee",
        Properties: map[string]any{
            "title":    map[string]any{"type": "string", "description": "Task title"},
            "assignee": map[string]any{"type": "string", "description": "Username"},
        },
    },
    {
        Name:        "list_tasks",
        Description: "List tasks filtered by status",
        Properties: map[string]any{
            "status": map[string]any{"type": "string", "description": "Filter: todo, in_progress, done"},
        },
    },
}

// Handle tool calls by routing to your services. The handler mirrors a
// go-micro RPC handler: context first, the call in, a result out.
toolHandler := func(ctx context.Context, call ai.ToolCall) ai.ToolResult {
    switch call.Name {
    case "create_task":
        var rsp CreateResponse
        err := client.Call(ctx, "tasks", "TaskService.Create", call.Input, &rsp)
        if err != nil {
            return ai.ToolResult{ID: call.ID, Content: fmt.Sprintf(`{"error": "%s"}`, err)}
        }
        b, _ := json.Marshal(rsp)
        return ai.ToolResult{ID: call.ID, Value: rsp, Content: string(b)}
    case "list_tasks":
        var rsp ListResponse
        err := client.Call(ctx, "tasks", "TaskService.List", call.Input, &rsp)
        if err != nil {
            return ai.ToolResult{ID: call.ID, Content: fmt.Sprintf(`{"error": "%s"}`, err)}
        }
        b, _ := json.Marshal(rsp)
        return ai.ToolResult{ID: call.ID, Value: rsp, Content: string(b)}
    }
    return ai.ToolResult{ID: call.ID, Content: `{"error": "unknown tool"}`}
}

m := ai.New("anthropic",
    ai.WithAPIKey(os.Getenv("ANTHROPIC_API_KEY")),
    ai.WithToolHandler(toolHandler),
)

// The model will automatically call tools and return the final answer
resp, err := m.Generate(ctx, &ai.Request{
    Prompt:       "Create a task for Alice to review the PR and tell me what tasks she has",
    SystemPrompt: "You are a helpful project management assistant",
    Tools:        tools,
})

fmt.Println(resp.Answer)
// "I've created a task for Alice to review the PR. She now has 3 tasks: ..."

When to Use

  • Building a chatbot or assistant that manages your services
  • The agent playground in micro run uses this pattern
  • You want the AI to decide which tools to call and in what order

Pattern 5: Event-Driven Agent Triggers

Services emit events that trigger agent actions via the broker.

Service → Broker Event → Agent Handler → AI Model → Action

Setup

// Publisher: emit events from your service
broker.Publish("tasks.created", &broker.Message{
    Body: taskJSON,
})

// Subscriber: agent handler reacts to events
broker.Subscribe("tasks.created", func(p broker.Event) error {
    var task Task
    json.Unmarshal(p.Message().Body, &task)

    // Use AI to auto-assign based on task content
    resp, err := aiModel.Generate(ctx, &ai.Request{
        Prompt: fmt.Sprintf("Who should handle this task? Title: %s, Description: %s. Team: alice (frontend), bob (backend), charlie (devops)", task.Title, task.Description),
        SystemPrompt: "Reply with just the username of the best person to handle this task.",
    })

    // Auto-assign
    client.Call(ctx, "tasks", "TaskService.Update", map[string]any{
        "id": task.ID,
        "assignee": strings.TrimSpace(resp.Reply),
    }, nil)

    return nil
})

When to Use

  • Automated workflows triggered by service events
  • AI-powered routing, classification, or triage
  • Background processing without user interaction

Pattern 6: Claude Code Integration

Developers use Claude Code with your services as MCP tools for local development workflows.

Developer → Claude Code → stdio MCP → [local services]

Setup

# Start services locally
micro run

# In another terminal, use Claude Code with your services
# Claude Code config (~/.claude/claude_desktop_config.json):
{
  "mcpServers": {
    "my-project": {
      "command": "micro",
      "args": ["mcp", "serve"]
    }
  }
}

Now in Claude Code:

"List all tasks that are blocked"
"Create a user account for the new hire"
"Check the health of all services"

When to Use

  • Developer productivity workflows
  • Managing services during development
  • Testing and debugging with natural language

Pattern 7: LangChain / LlamaIndex Integration

Use the official Python SDKs to connect agent frameworks directly to your services.

LangChain

from langchain_go_micro import GoMicroToolkit

# Connect to MCP gateway
toolkit = GoMicroToolkit(
    base_url="http://localhost:3000",
    token="Bearer <token>",
)

# Get LangChain tools automatically
tools = toolkit.get_tools()

# Use with any LangChain agent
from langchain.agents import AgentExecutor, create_tool_calling_agent
agent = create_tool_calling_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools)
executor.invoke({"input": "Create a task for Alice"})

LlamaIndex

from go_micro_llamaindex import GoMicroToolkit

toolkit = GoMicroToolkit(
    base_url="http://localhost:3000",
    token="Bearer <token>",
)

# Use as LlamaIndex tools
tools = toolkit.to_tool_list()

# Use with a LlamaIndex agent
from llama_index.core.agent import ReActAgent
agent = ReActAgent.from_tools(tools, llm=llm)
agent.chat("What tasks are assigned to Bob?")

When to Use

  • Python-based agent pipelines
  • RAG (Retrieval-Augmented Generation) workflows with LlamaIndex
  • Multi-step LangChain chains that orchestrate your services
  • Teams that prefer Python for AI/ML work

Pattern 8: Standalone Gateway for Production

Run the MCP gateway as a separate, horizontally scalable process.

                    ┌──────────────────┐
Claude/GPT/Agent ──→│ micro-mcp-gateway │──→ Service A (consul)
                    │   (standalone)    │──→ Service B (consul)
                    └──────────────────┘──→ Service C (consul)

Setup

micro-mcp-gateway \
  --registry consul \
  --registry-address consul:8500 \
  --address :3000 \
  --auth jwt \
  --rate-limit 10 \
  --rate-burst 20 \
  --audit

Or via Docker:

docker run -p 3000:3000 ghcr.io/micro/micro-mcp-gateway \
  --registry consul \
  --registry-address consul:8500

When to Use

  • Production deployments where you want the gateway to scale independently
  • Multiple teams deploying services but sharing one MCP endpoint
  • Enterprise environments needing centralized auth and audit

Pattern 9: Planning and Delegation

Built into the Agent abstraction. Every agent gets two harness tools — plan and delegate — with no extra setup. They are plain tools, not a separate graph runtime.

Conductor ──plan──→ (records ordered steps in memory)
            ──delegate──→ registered agent (RPC)  or  ephemeral sub-agent

Setup

Nothing to wire — the tools are added to every agent automatically. Guide their use with the prompt:

conductor := micro.NewAgent("conductor",
    micro.AgentServices("task"),
    micro.AgentPrompt(
        "For multi-step requests, call the plan tool first to record your steps. "+
            "For notifications, delegate to the \"comms\" agent (to: \"comms\")."),
    micro.AgentProvider("anthropic"),
)
  • plan records an ordered list of steps (task + status) in the agent’s store-backed memory, surfaced back on later turns so it stays oriented.
  • delegate hands a self-contained subtask to another agent. Delegate-first: if the target is a registered agent it’s reached over RPC; otherwise a focused, short-lived sub-agent is created with a fresh, isolated context. A sub-agent is just an agent — created with New, talked to with Ask; there’s no separate “spawn”/“fork” concept.

Full example: examples/agent-plan-delegate.

When to Use

  • Multi-step tasks where an explicit plan keeps the agent on track
  • Multi-agent systems where domain experts own their own services and you want hand-offs to stay distributed (not one agent doing everything)

Choosing a Pattern

PatternComplexityBest For
Single AgentLowMost applications, getting started
Scoped AgentsMediumMulti-tenant, compliance
Agent as ConsumerMediumAI-enhanced services
Tool CallingMediumChatbots, assistants
Event-DrivenHighAutomation, background processing
Claude CodeLowDeveloper workflows
LangChain/LlamaIndexMediumPython agent pipelines, RAG
Standalone GatewayMediumProduction, enterprise
Planning & DelegationMediumMulti-step tasks, distributed multi-agent systems

Start with Pattern 1 (single agent) and add complexity as needed. Most applications don’t need multi-agent architectures.

Anti-Patterns

Don’t: Chain Agents Without Coordination

Agent A → Agent B → Agent C  (no shared state, no trace IDs)

Instead, use a single agent with multiple tools, or share trace IDs via metadata.

Don’t: Give Agents Unrestricted Access

Customer Agent → scopes=["*"]  (dangerous!)

Always use the minimum required scopes. See the MCP Security Guide.

Don’t: Skip Error Documentation

If agents don’t know what errors are possible, they can’t handle them gracefully. Always document error cases in your handler comments.

Don’t: Build Agent Logic into Services

Keep services as pure business logic. Let the agent harness handle orchestration, retries, and decision-making. Your service should just do one thing well.

Next Steps

7 - Agent Loops

Most agent work is one-shot: a prompt goes in, an answer comes out. The next step in agentic systems is the loop — run a step over and over, letting the agent keep working until the goal is met instead of stopping after one pass. One agent improves an architecture while another removes duplicated abstractions, both opening pull requests continuously; a draft is refined until it’s good enough; a build is fixed and re-run until it’s green.

The catch is cost and runaway risk: a loop “burns through tokens a lot faster than a simple Q&A chatbot,” and a non-deterministic stop (“keep going until you’re done”) has no natural ceiling. So a usable loop needs two things:

  1. a stop condition — how it decides it’s done, and
  2. a hard cap — a guardrail that guarantees it always terminates.

Go Micro gives you both as a flow step: micro.FlowLoop.

The shape

micro.FlowLoop is a StepFunc, so it drops into a flow’s ordered, checkpointed step list like any other step. It runs a body step repeatedly, carrying the flow State from one pass to the next, until a stop condition fires or the iteration cap is hit — whichever comes first.

f := micro.NewFlow("refactor",
    micro.FlowProvider("anthropic"),
    micro.FlowSteps(
        micro.FlowStep{Name: "improve", Run: micro.FlowLoop(
            micro.FlowDispatch("coder"), // the body: an agent does one pass
            micro.FlowUntilLLM("Is the refactor complete with no duplicated abstractions left?"),
            micro.FlowLoopMax(5),        // the ceiling: never more than 5 passes
        )},
    ),
)

Stop conditions

Code-definedFlowUntil stops when your predicate returns true. Use it when “done” is something you can measure (tests pass, a score clears a threshold, a queue is empty):

micro.FlowUntil(func(_ context.Context, s micro.FlowState, iter int) (bool, error) {
    var d Draft
    _ = s.Scan(&d)
    return d.Quality >= 90, nil
})

Model-judgedFlowUntilLLM asks the flow’s model, after each pass, whether the goal is met, and stops on an affirmative answer. This is the supervised (“Ralph”) loop: the agent decides when it’s done, while the cap still guarantees it stops. It requires a flow model (FlowProvider/FlowAPIKey).

micro.FlowUntilLLM("Have all the failing tests been fixed?")

You can combine both — either firing stops the loop.

The guardrail

FlowLoopMax(n) is the ceiling. The body never runs more than n times, so the loop always terminates even if the stop condition never fires. When the cap is hit, the loop returns the latest state rather than erroring — the guardrail did its job. Always set it. For tighter budgets, keep the cap low and pair the loop with agent guardrails (e.g. token/spend limits) and paid tools (per-call metering) so a background loop can’t run up an unbounded bill.

Watching progress

FlowOnIteration runs after each pass — log it, or persist a summary so you can see how a long-running loop is doing:

micro.FlowOnIteration(func(iter int, s micro.FlowState) {
    log.Printf("pass %d: %s", iter, s.String())
})

Durability

A loop runs as a single flow step. The flow checkpoints the loop’s outcome (before and after the step) through its Checkpoint, and a resume re-enters the step — so keep loop bodies safe to repeat. For long loops, use FlowOnIteration to persist per-pass progress.

Run it

A complete, offline example (no API key — the body and stop condition are plain Go) is in examples/flow-loop:

go run ./examples/flow-loop/
# refining until quality >= 90
#   pass 1 → quality 30
#   pass 2 → quality 60
#   pass 3 → quality 90
# done: {"text":"draft refined (quality 90)","quality":90}

Swap the body for micro.FlowDispatch("agent") or micro.FlowLLM(...) and the stop check for micro.FlowUntilLLM(...) to turn it into a real agent loop.

See also

8 - Agent2Agent (A2A)

Go Micro speaks the Agent2Agent (A2A) protocol — the open standard for agents on different frameworks to discover and call each other over HTTP. The A2A gateway is the agent-side analogue of the MCP gateway: MCP exposes your services as tools, A2A exposes your agents as agents.

There is nothing to add to an agent. An agent already registers in the registry with type=agent metadata; the gateway discovers it, generates an Agent Card from that metadata, and translates incoming A2A tasks to the agent’s existing Agent.Chat RPC — the same call delegate and flows use.

Run it

micro a2a serve --address :4000 --base_url https://agents.example.com
micro a2a list     # agents and their Agent Card URLs

Or embed the gateway next to a service:

go a2a.Serve(a2a.Options{
    Registry: service.Options().Registry,
    Address:  ":4000",
    BaseURL:  "https://agents.example.com",
})

Gateway, or directly on the agent

A2A is JSON-RPC over HTTP — a different wire protocol from go-micro’s RPC — so something always translates between the two. That something doesn’t have to be a separate process. There are two ways to run it:

  • A gateway (above) fronts every agent in the registry behind one endpoint. Use it for a single front door, centralized discovery, and shared policy.

  • Directly on the agent. AgentA2A(addr) makes the agent serve its own A2A endpoint when it runs — no separate gateway, and the task is handled in-process (no extra RPC hop):

    agent := micro.NewAgent("task-mgr",
        micro.AgentServices("task"),
        micro.AgentProvider("anthropic"),
        micro.AgentA2A(":4000"),   // also reachable at http://host:4000 over A2A
    )
    agent.Run()
    

    The agent stays a normal go-micro service; this adds a second, A2A-native HTTP endpoint. Now any A2A client can curl it directly. Use it when each agent should be independently addressable without a gateway.

Both reuse the same handler; the only difference is whether the agent is reached over RPC (gateway) or in-process (embedded).

Discovery: cards from the registry

Every registered agent gets an Agent Card, generated from its registry metadata (name, the services it manages). Cards are not published by the agent — they are derived, the same way MCP tools are derived from service endpoints.

EndpointReturns
GET /agentsa directory of all Agent Cards
GET /agents/{name}one agent’s card
GET /agents/{name}/.well-known/agent.jsonone agent’s card (well-known path)
POST /agents/{name}the agent’s JSON-RPC endpoint
GET /.well-known/agent.jsonthe single agent’s card, when exactly one is registered

A card looks like:

{
  "name": "task-mgr",
  "description": "Go Micro agent managing: task,project",
  "url": "https://agents.example.com/agents/task-mgr",
  "version": "1.0.0",
  "protocolVersion": "0.3.0",
  "capabilities": { "streaming": true, "pushNotifications": true },
  "defaultInputModes": ["text/plain"],
  "defaultOutputModes": ["text/plain"],
  "skills": [
    { "id": "task", "name": "Task", "tags": ["task"] },
    { "id": "project", "name": "Project", "tags": ["project"] }
  ]
}

Each managed service is advertised as its own typed skill. Clients can call the whole agent at /agents/task-mgr, or address one skill directly at /agents/task-mgr/skills/task; the skill endpoint serves a focused card and routes the request to the same agent with that skill selected.

Calling an agent

A2A uses JSON-RPC 2.0 over HTTP. Send a message with message/send; the gateway runs the agent and returns a completed Task:

curl -s https://agents.example.com/agents/task-mgr \
  -H 'content-type: application/json' \
  -d '{
    "jsonrpc": "2.0", "id": 1, "method": "message/send",
    "params": { "message": {
      "role": "user", "kind": "message", "messageId": "m1",
      "parts": [{ "kind": "text", "text": "What tasks are overdue?" }]
    }}
  }'
{
  "jsonrpc": "2.0", "id": 1,
  "result": {
    "id": "…", "contextId": "…", "kind": "task",
    "status": { "state": "completed", "timestamp": "…" },
    "artifacts": [{ "artifactId": "…", "parts": [{ "kind": "text", "text": "Two: …" }] }]
  }
}

Retrieve a task later with tasks/get (params: { "id": "…" }). To continue the same piece of work, send another message/send with the previous taskId and contextId. The gateway preserves the task id, context id, and prior history, then appends the new user turn and agent reply. That makes a remote A2A task fit the Go Micro lifecycle: services are still invoked through the agent’s normal tools, the agent keeps task context across turns, and a workflow can poll one task id as the conversation progresses.

Push notifications

Operators can register a task callback with tasks/pushNotificationConfig/set:

curl -s https://agents.example.com/agents/task-mgr \
  -H 'content-type: application/json' \
  -d '{
    "jsonrpc": "2.0", "id": 2,
    "method": "tasks/pushNotificationConfig/set",
    "params": {
      "id": "task-id",
      "pushNotificationConfig": {
        "url": "https://workflow.example.com/a2a/tasks",
        "token": "optional-bearer-token"
      }
    }
  }'

The gateway stores one callback per retained task and POSTs the latest task snapshot to that URL whenever the task changes. Delivery is best effort: failures do not fail the agent turn, and there is no retry queue in the in-memory gateway. Use tasks/get as the source of truth after a missed callback or receiver outage. If a token is configured, it is sent as Authorization: Bearer <token>.

Calling out to other agents

The gateway makes your agents reachable from the A2A ecosystem. The client (a2a.Client) is the other direction: it lets a Go Micro agent or flow call an agent on any framework, by URL.

reply, err := a2a.NewClient("https://other.example.com/agents/research").
    Send(ctx, "Summarize the latest on X")

It’s wired into the two places that hand off work:

  • A flow stepflow.A2A(url) is the cross-framework counterpart to flow.Dispatch(name) (which dispatches to a local agent):

    flow.Step{Name: "research", Run: flow.A2A("https://other.example.com/agents/research")}
    
  • Agent delegate — when an agent’s delegate target is an http(s) URL, the subtask is sent to that external agent over A2A instead of to a locally registered one. Nothing else changes; the model just delegates to a URL.

Send handles the task lifecycle: if the remote returns a task that isn’t yet terminal, it polls tasks/get until it completes.

Scope

This is the JSON-RPC binding for task execution:

  • message/send runs the agent and returns a completed Task.
  • message/stream streams the completed Task as an SSE data: event, giving A2A clients a streaming-compatible path while the underlying agent call remains synchronous.
  • tasks/get returns a recent task by id.
  • Multi-turn continuation keeps task state when a new message includes the previous taskId.
  • tasks/pushNotificationConfig/set / get stores and reads a task callback for best-effort update delivery.
  • tasks/resubscribe reconnects to an existing task stream, immediately emits the current task snapshot, then streams subsequent updates until the task reaches a terminal state.
  • input-required task state carries human-input handoffs (for example checkpointed approval pauses) in task status, artifacts, and history; continue the task by sending a follow-up message with the same taskId and contextId.
  • Agent Card discovery, generated from the registry.

Both directions work: the gateway exposes your agents, and a2a.Client (via flow.A2A or delegate to a URL) calls external ones. The task binding is what makes a Go Micro agent both reachable from, and able to reach, the A2A ecosystem today.

AP2 mandate layer (opt-in)

AP2 sits above A2A as a verifiable-intent and audit layer. Go Micro keeps the A2A envelope separate from payment settlement: an A2A message can carry signed AP2 checkout or payment mandates, and the resulting task can retain the stable mandate reference plus verification result. Payment settlement state remains in the payment rail. For x402, use an AP2 payment mandate with an x402 rail reference to name the payment requirement; the existing x402 facilitator still performs verification and settlement.

See also

9 - Agents and Workflows

Go Micro’s AI primitives map directly onto the taxonomy in Anthropic’s Building Effective Agents. That post draws one distinction that matters:

  • Workflows — “LLMs and tools orchestrated through predefined code paths.” Deterministic.
  • Agents — “LLMs dynamically direct their own processes and tool usage.” Model-driven.

Go Micro has both, plus the harness they run inside — and expresses them as plain services and tools, with no graph DSL. That’s deliberate: the same post advises finding “the simplest solution possible” and being “cautious with frameworks… they obscure the underlying mechanics.”

The building block: the augmented LLM

Anthropic’s foundational unit is the augmented LLM — a model with tools, retrieval, and memory. In Go Micro:

Augmented LLMGo Micro
the modelai package (7 providers, one interface)
toolsevery service endpoint, discovered from the registry
memorythe store (file, Postgres, NATS KV)

Every endpoint is automatically a tool, so the augmented LLM is the default, not something you assemble.

Workflow ↔ flow

A Flow is a workflow in Anthropic’s exact sense: a predefined path — an event on a broker topic triggers a prompt with a fixed set of tools, deterministically. Use it when the task is well-defined and you want predictability.

f := micro.NewFlow("onboard-user",
    micro.FlowTrigger("events.user.created"),
    micro.FlowPrompt("New user {{.Data}} — create a workspace and send a welcome email."),
    micro.FlowProvider("anthropic"),
)

Flow triggers, Agent reasons

A flow doesn’t have to do the reasoning itself. Point it at an agent and it becomes a pure trigger — the event fires, the flow renders the prompt, and a registered agent handles it over RPC with its full capabilities (plan, delegate, memory, guardrails):

f := micro.NewFlow("onboard-user",
    micro.FlowTrigger("events.user.created"),
    micro.FlowPrompt("New user {{.Data}} — get them set up."),
    micro.FlowAgent("conductor"),   // the conductor agent reasons; the flow only triggers
)

This is the clean seam between the two halves of the taxonomy: the workflow (deterministic, event-driven) hands off to the agent (dynamic). One engine, two front doors — an event (flow) or a conversation (agent.Ask).

Ordered, durable steps

A flow can be a task made of ordered steps rather than a single turn — the predefined path made explicit. Each step is checkpointed before and after, so if the process dies mid-run the run resumes at the step it stopped on, without re-running the steps that already completed (and already had their side effects). This is durable execution, store-backed by default, with no separate workflow engine.

f := micro.NewFlow("checkout",
    micro.FlowTrigger("events.order.placed"),
    micro.FlowRetry(2),                       // retry each step; per-step override available
    micro.FlowSteps(
        micro.FlowStep{Name: "reserve", Run: micro.FlowCall("inventory", "Inventory.Reserve")},
        micro.FlowStep{Name: "charge",  Run: micro.FlowCall("payment", "Payment.Charge")},
        micro.FlowStep{Name: "welcome", Run: micro.FlowDispatch("comms")}, // hand a step to an agent
    ),
    // Durable by default; point the default store at Postgres/NATS KV to
    // survive a real restart, or plug in Temporal/Restate via Checkpoint.
)

A step’s action is an RPC (FlowCall), an agent hand-off (FlowDispatch), one model turn (FlowLLM), or any function. State carries a typed payload (Set/Scan) plus a Stage marker — the resume point. Runs are retained for success and failure (audit) unless you set FlowDeleteOnSuccess. On restart, f.Pending(ctx) lists incomplete runs and f.Resume(ctx, runID) continues one. See examples/flow-durable.

The pluggability is the usual go-micro shape: the built-in Checkpoint is store-backed (swap the store backend freely); implement the Checkpoint interface to delegate durability to an external engine. Most teams need neither — the default is durable.

Agent ↔ agent

An Agent is an agent in Anthropic’s exact sense: it directs itself — plans, calls tools, evaluates results, and decides the next step over many turns, with memory across them. Use it when you want flexibility and model-driven decisions.

a := micro.NewAgent("conductor",
    micro.AgentServices("task"),
    micro.AgentProvider("anthropic"),
)
a.Ask(ctx, "Plan the launch, create the tasks, and have comms notify the owner.")

Long-running memory

Agents use store-backed conversation memory by default, scoped under the agent’s name. That makes short restarts boring: the next Ask reloads the retained history from the same store backend you already use for services and flows. Long-running agents can also keep model context bounded without losing useful prior context. If you want retrieval without summaries, enable bounded active context plus a durable archive of every turn:

a := micro.NewAgent("conductor",
    micro.AgentServices("task"),
    micro.AgentProvider("anthropic"),
    micro.AgentRetrievalMemory(40),        // active messages kept in prompt context
    micro.AgentMemoryRecallLimit(5),       // archived turns recalled per Ask
)

AgentRetrievalMemory(activeLimit) switches the default memory to a store-backed retriever. The active conversation is capped at activeLimit, every turn is archived in the same scoped store used by the agent, and future asks inject matching archived turns ahead of active context. The built-in ranking is deterministic and credential-free for CI.

When you also want a rolling summary in active context, use compacting memory:

a := micro.NewAgent("conductor",
    micro.AgentServices("task"),
    micro.AgentProvider("anthropic"),
    micro.AgentCompactMemory(40, 12),      // max active messages, recent messages kept verbatim
    micro.AgentMemoryRecallLimit(5),       // compacted turns recalled per Ask
)

AgentCompactMemory(maxMessages, keepRecent) switches the default memory to a deterministic compactor. Once active history grows past maxMessages, older turns move into the durable archive, a provider-neutral summary is injected into active context, and the newest keepRecent messages stay verbatim. On future asks, archived turns whose text matches the current request are recalled ahead of the active context. Teams that need embeddings or a vector database can still provide their own AgentMemory implementation.

This is harness memory, not prompt-layer orchestration: services remain the capabilities, agents remain the dynamic decision makers, and flows remain the durable predefined paths. Compaction only keeps a scheduled or looping agent from turning every past turn into model context while still letting it remember facts that matter to the current service → agent → workflow run.

Checkpointed agent runs and compacted memory share the same store-backed shape. If a provider call fails after the prompt has been recorded, agent.Resume uses the checkpointed run id and does not append that same user turn a second time; completed tool results and recalled archived memory remain available for the retry.

The patterns — most are already here

Anthropic lists five workflow patterns. Go Micro implements the two richest ones natively, as services and tools, and the rest are ordinary compositions:

PatternGo Micro
Routing — classify input, dispatch to a specialistmicro chat’s router — discovers agents, classifies intent, routes over RPC
Orchestrator-workers — a central LLM breaks down a task, delegates to workers, synthesizesthe agent with plan (break down) + delegate (hand to workers) + reply (synthesize) — see Plan & Delegate
Prompt chaining — sequential stepschain flows, or steps in an agent’s plan
Parallelization — independent subtasks at onceGo concurrency + multiple services/agents; fan out with delegate
Evaluator-optimizer — one LLM generates, another critiques in a looptwo agents over RPC (generator + evaluator)

The orchestrator-workers example is worth calling out: the conductor agent that plans, creates tasks, and delegates the notification to a comms agent is orchestrator-workers — built without a graph engine. See examples/agent-plan-delegate.

Choosing

Follow Anthropic’s guidance:

  • Start with the augmented LLM (a single service call through a model). Most tasks need nothing more.
  • Reach for a workflow (flow) when the path is well-defined and you want predictability.
  • Reach for an agent (agent) when the task needs flexibility and model-driven decisions — and accept the higher cost and the need for guardrails.

Guardrails

Anthropic is emphatic that autonomous agents need stopping conditions, human checkpoints, and sandboxed testing. Go Micro’s agent has two built-in guardrails, both as plain options:

Stopping conditionMaxSteps bounds the number of actions an agent may take per Ask. Once exceeded, further tool calls are refused and the model is told to stop and summarize.

micro.NewAgent("conductor",
    micro.AgentServices("task"),
    micro.AgentMaxSteps(8),       // at most 8 tool calls per request
)

Human-in-the-loopApproveTool gates each action before it runs. Return false to block it; the reason is shown to the model so it can adapt. The internal plan tool is never gated (it’s bookkeeping, not an action).

micro.NewAgent("conductor",
    micro.AgentServices("task"),
    micro.AgentApproveTool(func(tool string, input map[string]any) (bool, string) {
        if strings.HasPrefix(tool, "billing_") {
            return false, "billing actions require human sign-off"
        }
        return true, ""
    }),
)

These are harness guardrails, not a separate policy engine — a counter and a callback on the path every tool call already takes. For anything that must be predictable, still prefer a workflow, and test agents against the integration harness.

Why no graph DSL

Anthropic: “be cautious with frameworks… understand the underlying code.” Go Micro’s answer is that there is no separate framework to understand — the harness is the service runtime. Workflows and agents are services, and tool use is RPC. plan and delegate are tools, not a graph DSL. The patterns above are code you can read, not a DSL you have to learn. That’s the direction we took going all in on AI.

See also

10 - Atlas Cloud Integration

Atlas Cloud Integration Guide

Atlas Cloud is an enterprise AI infrastructure platform offering 300+ models across text, image, and video through a unified, OpenAI-compatible API. It is an official Go Micro sponsor and a first-class provider in the ai package.

Quick Start

Install or update Go Micro:

go get go-micro.dev/v6@latest

Import the Atlas Cloud provider and use it:

package main

import (
    "context"
    "fmt"
    "log"

    "go-micro.dev/v6/ai"
    _ "go-micro.dev/v6/ai/atlascloud"
)

func main() {
    m := ai.New("atlascloud",
        ai.WithAPIKey("your-atlas-cloud-key"),
    )

    resp, err := m.Generate(context.Background(), &ai.Request{
        Prompt:       "What is Go Micro?",
        SystemPrompt: "You are a helpful assistant.",
    })
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(resp.Reply)
}

Configuration

Options

OptionDefaultDescription
ai.WithAPIKey(key)requiredYour Atlas Cloud API key
ai.WithModel(name)llama-3.3-70bModel to use (see Model Selection)
ai.WithBaseURL(url)https://api.atlascloud.aiAPI base URL

Environment Variables

The micro chat CLI and micro run / micro server read configuration from environment variables:

VariableDescription
ATLASCLOUD_API_KEYAPI key (used by micro chat --provider atlascloud)
MICRO_AI_API_KEYGeneric API key (used by all providers)
MICRO_AI_PROVIDERSet to atlascloud to select the provider
MICRO_AI_MODELOverride the default model
MICRO_AI_BASE_URLOverride the base URL

When using micro chat, the provider-specific variable takes precedence:

ATLASCLOUD_API_KEY=your-key micro chat --provider atlascloud

When using micro run or micro server, set the generic variables:

export MICRO_AI_API_KEY=your-key
export MICRO_AI_BASE_URL=https://api.atlascloud.ai
micro run

The server auto-detects Atlas Cloud from the base URL.

Model Selection

Atlas Cloud offers 300+ models. Some popular choices for the chat completions API:

ModelUse Case
llama-3.3-70bGeneral-purpose (default)
deepseek-v4Coding and reasoning
qwen-3.6Multilingual

Check atlascloud.ai for the full model catalog. New SOTA models are available on day zero of release.

m := ai.New("atlascloud",
    ai.WithAPIKey(key),
    ai.WithModel("deepseek-v4"),
)

Image Generation

Atlas Cloud supports text-to-image generation through the ai.ImageModel interface. This uses the same OpenAI-compatible /v1/images/generations endpoint.

import (
    "context"
    "fmt"

    "go-micro.dev/v6/ai"
    _ "go-micro.dev/v6/ai/atlascloud"
)

func main() {
    ig := ai.NewImage("atlascloud",
        ai.WithAPIKey("your-key"),
    )

    resp, err := ig.GenerateImage(context.Background(), &ai.ImageRequest{
        Prompt: "A Go gopher building microservices, digital art",
        Size:   "1024x1024",
    })
    if err != nil {
        log.Fatal(err)
    }

    // Image returned as URL or base64, depending on the model
    fmt.Println(resp.Images[0].URL)
}

ImageRequest Options

FieldDefaultDescription
PromptrequiredText description of the image
Modelgpt-image-1Image model to use
Sizeprovider defaultImage dimensions (e.g. "1024x1024")
N1Number of images to generate

Available Image Models

Atlas Cloud offers image models including gpt-image-1, flux-2, nano-banana-pro, and more. Check atlascloud.ai for the full catalog.

ig.GenerateImage(ctx, &ai.ImageRequest{
    Prompt: "A mountain landscape",
    Model:  "flux-2",
    Size:   "1024x1024",
    N:      2,
})

The ai.ImageModel interface is also implemented by the OpenAI provider, so switching between providers is a one-line change.

Using with Services (Tool Calling)

Atlas Cloud supports OpenAI-compatible function calling. Combined with Go Micro’s ai.Tools, your services become tools that the model can call:

package main

import (
    "context"
    "fmt"
    "log"

    "go-micro.dev/v6"
    "go-micro.dev/v6/ai"
    
    _ "go-micro.dev/v6/ai/atlascloud"
)

func main() {
    service := micro.NewService("my-agent")
    service.Init()

    // Discover all services as tools
    tools := ai.NewTools(service.Registry())
    discovered, err := tools.Discover()
    if err != nil {
        log.Fatal(err)
    }

    // Create a model with tool execution
    m := ai.New("atlascloud",
        ai.WithAPIKey("your-key"),
        ai.WithTools(tools),
    )

    // The model can now call your services
    resp, err := m.Generate(context.Background(), &ai.Request{
        Prompt:       "List all users and send each a welcome email",
        SystemPrompt: "You are a service orchestrator.",
        Tools:        discovered,
    })
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(resp.Answer)
}

How it works

  1. ai.NewTools(registry) creates a tool set bound to the service registry
  2. tools.Discover() walks the registry and returns every endpoint as an ai.Tool
  3. ai.WithTools(tools) wires execution into the model — tool calls are routed via RPC
  4. When the model decides to call a tool, it routes to the correct service

This works identically across all providers. Swap "atlascloud" for "anthropic" or "openai" and the same services, tools, and handlers work without changes.

Using with micro chat

micro chat is an interactive terminal agent. Start your services, then chat:

# Terminal 1: start services
micro run

# Terminal 2: chat with Atlas Cloud
ATLASCLOUD_API_KEY=your-key micro chat --provider atlascloud
> what services are running?
> get user alice@example.com
> create a new order for product-42

For a single prompt (non-interactive):

micro chat --provider atlascloud --prompt "list all services"

Using with micro run

The agent playground at /agent uses whatever AI provider is configured. To use Atlas Cloud:

export MICRO_AI_API_KEY=your-atlas-cloud-key
export MICRO_AI_BASE_URL=https://api.atlascloud.ai
micro run

Open http://localhost:8080/agent and chat with your services through Atlas Cloud.

Using with MCP

The MCP gateway (micro mcp serve) exposes services as tools for external AI agents. Atlas Cloud’s models can be used by any MCP-compatible agent that connects to the gateway. The gateway itself doesn’t depend on a specific AI provider — it serves tools over MCP, and the agent on the other end chooses which model to use.

Swapping Providers

All Go Micro AI providers implement the same ai.Model interface. To switch from Atlas Cloud to another provider, change the import and the provider name:

// Atlas Cloud
import _ "go-micro.dev/v6/ai/atlascloud"
m := ai.New("atlascloud", ai.WithAPIKey(key))

// Anthropic
import _ "go-micro.dev/v6/ai/anthropic"
m := ai.New("anthropic", ai.WithAPIKey(key))

// OpenAI
import _ "go-micro.dev/v6/ai/openai"
m := ai.New("openai", ai.WithAPIKey(key))

The rest of your code — tool discovery, handler wiring, request/response handling — stays the same.

API Compatibility

Atlas Cloud exposes an OpenAI-compatible /v1/chat/completions endpoint. This means:

  • Existing OpenAI SDK code works by changing the base URL
  • Tool calling uses the same tools and tool_calls format as OpenAI
  • Streaming follows the OpenAI SSE format (when implemented)

If you’re already using the openai provider, you can point it at Atlas Cloud directly:

import _ "go-micro.dev/v6/ai/openai"

m := ai.New("openai",
    ai.WithAPIKey("your-atlas-cloud-key"),
    ai.WithBaseURL("https://api.atlascloud.ai"),
    ai.WithModel("llama-3.3-70b"),
)

The dedicated atlascloud provider simply sets these defaults for you.

11 - Best Practices for Tool Descriptions

Your Go doc comments become the documentation that AI agents read when deciding how to call your service. Better descriptions lead to fewer errors, faster task completion, and a better user experience.

How Agents Use Your Docs

When an AI agent receives a user request like “create a task for Alice”, it:

  1. Queries the MCP tools endpoint for available tools
  2. Reads each tool’s description to understand what it does
  3. Reads the parameter schema and descriptions to build the input
  4. References the example to verify the format
  5. Makes the call

If any of these are missing or unclear, the agent guesses — and often guesses wrong.

The Three Essentials

Every handler method needs three things:

1. A Clear Description (Doc Comment)

// Create creates a new task with the given title and description.
// Returns the created task with a generated ID and initial status of "todo".
// The assignee field is optional; if omitted, the task is unassigned.

Rules:

  • First sentence: what the method does (imperative mood)
  • Second sentence: what it returns
  • Additional sentences: important behavior, constraints, edge cases

2. An Example Input (@example)

// @example {"title": "Fix login bug", "description": "Users can't log in with SSO", "assignee": "alice"}

Rules:

  • Use realistic values, not placeholders like "string" or "test"
  • Include all required fields
  • Include at least one optional field to show the format
  • Keep it on one line (the parser reads until end of line)

3. Field Descriptions (description tag)

type CreateRequest struct {
    Title    string `json:"title" description:"Task title (required, max 100 chars)"`
    Assignee string `json:"assignee,omitempty" description:"Username to assign (optional)"`
}

Rules:

  • State the type constraint if not obvious (e.g., “UUID format”, “ISO 8601 date”)
  • List valid values for enums (e.g., “todo, in_progress, or done”)
  • Note if optional (matches omitempty)

Good vs Bad Examples

Describing What a Method Does

Good:

// GetUser retrieves a user by their unique ID from the database.
// Returns the full profile including name, email, and preferences.
// Returns an error if the user does not exist.
//
// @example {"id": "user-123"}
func (s *UserService) GetUser(ctx context.Context, req *GetRequest, rsp *GetResponse) error {

Bad:

// Gets user
func (s *UserService) GetUser(ctx context.Context, req *GetRequest, rsp *GetResponse) error {

The bad version forces the agent to guess what “gets user” means, what parameters are needed, and what format the ID takes.

Describing Parameters

Good:

type SearchRequest struct {
    Query   string `json:"query" description:"Search query string (min 2 chars, max 200)"`
    Page    int    `json:"page,omitempty" description:"Page number, starting from 1 (default: 1)"`
    PerPage int    `json:"per_page,omitempty" description:"Results per page, 1-100 (default: 20)"`
    SortBy  string `json:"sort_by,omitempty" description:"Sort field: relevance, date, or name (default: relevance)"`
}

Bad:

type SearchRequest struct {
    Q string `json:"q"`
    P int    `json:"p"`
    N int    `json:"n"`
    S string `json:"s"`
}

Providing Examples

Good:

// @example {"query": "microservices architecture", "page": 1, "per_page": 10, "sort_by": "relevance"}

Bad:

// @example {"q": "string", "p": 0, "n": 0}

Patterns for Common Scenarios

CRUD Operations

// Create creates a new [resource].
// Returns the created [resource] with a generated ID.
//
// @example {realistic create payload}

// Get retrieves a [resource] by ID.
// Returns an error if the [resource] does not exist.
//
// @example {"id": "realistic-id"}

// List returns all [resources], optionally filtered by [criteria].
// Returns an empty list if no [resources] match.
//
// @example {"status": "active"}

// Update modifies an existing [resource].
// Only the provided fields are updated; omitted fields are unchanged.
// Returns an error if the [resource] does not exist.
//
// @example {"id": "realistic-id", "field": "new-value"}

// Delete removes a [resource] by ID. This action is irreversible.
// Returns an error if the [resource] does not exist.
//
// @example {"id": "realistic-id"}

Search Endpoints

// Search finds [resources] matching the query string.
// Supports full-text search across [fields].
// Results are paginated; use page and per_page to control pagination.
// Returns results sorted by relevance by default.
//
// @example {"query": "realistic search term", "page": 1, "per_page": 20}

Actions with Side Effects

// SendEmail sends an email notification to the specified recipient.
// This triggers an actual email delivery — use with caution.
// Returns an error if the email address is invalid or the mail server is unavailable.
//
// @example {"to": "alice@example.com", "subject": "Task assigned", "body": "You have a new task."}

Methods with Complex Inputs

// CreateReport generates a report for the specified date range and metrics.
// Processing may take up to 30 seconds for large date ranges.
// Valid metrics: cpu_usage, memory_usage, request_count, error_rate.
// Date format: YYYY-MM-DD (e.g., "2026-01-15").
//
// @example {"start_date": "2026-01-01", "end_date": "2026-01-31", "metrics": ["cpu_usage", "error_rate"]}

Impact on Agent Performance

Documentation QualityFirst-Call Success RateAvg Calls to Complete
No docs~25%3-4 calls
Basic (name only)~50%2-3 calls
Good (description + types)~80%1-2 calls
Excellent (description + types + example)~95%1 call

Testing Your Descriptions

1. Use micro mcp list

Check what agents will see:

micro mcp list

Verify each tool has a description and the schema looks correct.

2. Use micro mcp docs

Generate the full documentation:

micro mcp docs

Read through it as if you were an AI agent. Does it make sense without seeing the code?

3. Test with Claude Code

The ultimate test — add your service to Claude Code and try natural language commands:

"Create a task for Alice to fix the login bug"
"What tasks are assigned to Bob?"
"Mark task-1 as done"

If Claude gets it right on the first try, your docs are good.

4. Use micro mcp test

Test individual tools with specific inputs:

micro mcp test tasks.TaskService.Create

Manual Overrides

If you can’t modify the source code (e.g., third-party services), override descriptions at handler registration:

handler := service.Server().NewHandler(
    new(LegacyService),
    server.WithEndpointDocs("LegacyService.Process", server.EndpointDocs{
        Description: "Process a payment transaction. Charges the specified amount to the customer's payment method on file.",
        Example:     `{"customer_id": "cust-123", "amount_cents": 4999, "currency": "USD"}`,
    }),
)

Manual docs take precedence over auto-extracted comments. This is useful for:

  • Third-party or generated code where you can’t add comments
  • Overriding auto-extracted descriptions that aren’t agent-friendly
  • Adding examples to legacy endpoints

Export Formats

You can export tool descriptions in different formats for use with agent frameworks:

# Human-readable documentation
micro mcp docs

# JSON for custom tooling
micro mcp export --format json

# LangChain Python format
micro mcp export --format langchain

# OpenAPI specification
micro mcp export --format openapi

Common Mistakes

  1. Placeholder examples — Using "string" or "test" instead of realistic values
  2. Missing enum values — Not listing valid options for status/type fields
  3. Ambiguous field names — Single-letter or abbreviated field names without descriptions
  4. No error documentation — Not telling agents what can go wrong
  5. Missing optional field markers — Not using omitempty or noting “(optional)”
  6. Overly technical descriptions — Writing for Go developers instead of AI agents

Next Steps

12 - Building AI-Native Services

This guide walks you through building a Go Micro service that is AI-native from the start — meaning AI agents can discover, understand, and call your service automatically via the Model Context Protocol (MCP).

What You’ll Build

A task management service with full CRUD operations that:

  • Exposes every endpoint as an MCP tool automatically
  • Has rich documentation that agents can read
  • Includes auth scopes for write operations
  • Works with Claude Code, the agent playground, and any MCP client

Prerequisites

go install go-micro.dev/v6/cmd/micro@latest

Step 1: Create the Service

micro new tasks
cd tasks

Step 2: Define Your Types

Design your request/response types with description tags. These tags become parameter descriptions that agents read:

package main

import "context"

// Request types with description tags for AI agents
type Task struct {
    ID          string `json:"id" description:"Unique task identifier"`
    Title       string `json:"title" description:"Short task title (max 100 chars)"`
    Description string `json:"description" description:"Detailed task description"`
    Status      string `json:"status" description:"Task status: todo, in_progress, or done"`
    Assignee    string `json:"assignee,omitempty" description:"Username of assigned person"`
}

type CreateRequest struct {
    Title       string `json:"title" description:"Task title (required, max 100 chars)"`
    Description string `json:"description" description:"Detailed description of the task"`
    Assignee    string `json:"assignee,omitempty" description:"Username to assign the task to"`
}

type CreateResponse struct {
    Task *Task `json:"task" description:"The newly created task"`
}

type GetRequest struct {
    ID string `json:"id" description:"Task ID to retrieve"`
}

type GetResponse struct {
    Task *Task `json:"task" description:"The requested task"`
}

type ListRequest struct {
    Status string `json:"status,omitempty" description:"Filter by status: todo, in_progress, done (optional)"`
}

type ListResponse struct {
    Tasks []*Task `json:"tasks" description:"List of matching tasks"`
}

type UpdateRequest struct {
    ID     string `json:"id" description:"Task ID to update"`
    Status string `json:"status" description:"New status: todo, in_progress, or done"`
}

type UpdateResponse struct {
    Task *Task `json:"task" description:"The updated task"`
}

type DeleteRequest struct {
    ID string `json:"id" description:"Task ID to delete"`
}

type DeleteResponse struct {
    Deleted bool `json:"deleted" description:"True if the task was deleted"`
}

Key point: The description tags are parsed by the MCP gateway and shown to agents as parameter documentation. Be specific about formats, constraints, and valid values.

Step 3: Write the Handler with Doc Comments

Write standard Go doc comments on every handler method. The MCP gateway extracts these automatically at registration time.

type TaskService struct {
    tasks map[string]*Task
    nextID int
}

// Create creates a new task with the given title and description.
// Returns the created task with a generated ID and initial status of "todo".
//
// @example {"title": "Fix login bug", "description": "Users can't log in with SSO", "assignee": "alice"}
func (t *TaskService) Create(ctx context.Context, req *CreateRequest, rsp *CreateResponse) error {
    t.nextID++
    task := &Task{
        ID:          fmt.Sprintf("task-%d", t.nextID),
        Title:       req.Title,
        Description: req.Description,
        Status:      "todo",
        Assignee:    req.Assignee,
    }
    t.tasks[task.ID] = task
    rsp.Task = task
    return nil
}

// Get retrieves a task by its unique ID.
// Returns an error if the task does not exist.
//
// @example {"id": "task-1"}
func (t *TaskService) Get(ctx context.Context, req *GetRequest, rsp *GetResponse) error {
    task, ok := t.tasks[req.ID]
    if !ok {
        return fmt.Errorf("task %s not found", req.ID)
    }
    rsp.Task = task
    return nil
}

// List returns all tasks, optionally filtered by status.
// If no status filter is provided, returns all tasks.
// Valid status values: "todo", "in_progress", "done".
//
// @example {"status": "todo"}
func (t *TaskService) List(ctx context.Context, req *ListRequest, rsp *ListResponse) error {
    for _, task := range t.tasks {
        if req.Status == "" || task.Status == req.Status {
            rsp.Tasks = append(rsp.Tasks, task)
        }
    }
    return nil
}

// Update changes the status of an existing task.
// Valid status transitions: todo -> in_progress -> done.
// Returns an error if the task does not exist.
//
// @example {"id": "task-1", "status": "in_progress"}
func (t *TaskService) Update(ctx context.Context, req *UpdateRequest, rsp *UpdateResponse) error {
    task, ok := t.tasks[req.ID]
    if !ok {
        return fmt.Errorf("task %s not found", req.ID)
    }
    task.Status = req.Status
    rsp.Task = task
    return nil
}

// Delete removes a task by ID. This action is irreversible.
// Returns an error if the task does not exist.
//
// @example {"id": "task-1"}
func (t *TaskService) Delete(ctx context.Context, req *DeleteRequest, rsp *DeleteResponse) error {
    if _, ok := t.tasks[req.ID]; !ok {
        return fmt.Errorf("task %s not found", req.ID)
    }
    delete(t.tasks, req.ID)
    rsp.Deleted = true
    return nil
}

What agents see: Each method’s doc comment becomes the tool description. The @example tag provides a valid JSON input that agents can reference.

Step 4: Register with Scopes

Use server.WithEndpointScopes() to control which agents can call which endpoints:

package main

import (
    "context"
    "fmt"

    "go-micro.dev/v6"
    "go-micro.dev/v6/server"
)

func main() {
    service := micro.NewService("tasks", micro.Address(":8081"))
    service.Init()

    service.Handle(
        &TaskService{tasks: make(map[string]*Task)},
        // Read operations: any authenticated agent
        server.WithEndpointScopes("TaskService.Get", "tasks:read"),
        server.WithEndpointScopes("TaskService.List", "tasks:read"),
        // Write operations: agents with write scope
        server.WithEndpointScopes("TaskService.Create", "tasks:write"),
        server.WithEndpointScopes("TaskService.Update", "tasks:write"),
        // Delete: admin only
        server.WithEndpointScopes("TaskService.Delete", "tasks:admin"),
    )

    service.Run()
}

Step 5: Run with MCP

There are three ways to run your service with MCP enabled.

micro run

Your service is now available at:

  • Web Dashboard: http://localhost:8080/
  • Agent Playground: http://localhost:8080/agent
  • MCP Tools: http://localhost:8080/mcp/tools
  • WebSocket: ws://localhost:3000/mcp/ws
  • API Gateway: http://localhost:8080/api/tasks/TaskService/Create

Option B: WithMCP (One-Liner for Library Users)

Add MCP to your service with a single option:

import "go-micro.dev/v6/gateway/mcp"

func main() {
    service := micro.NewService("tasks",
        mcp.WithMCP(":3000"), // MCP gateway starts automatically
    )
    service.Init()
    // register handlers...
    service.Run()
}

This starts the MCP gateway on port 3000 alongside your service. All registered handlers are automatically exposed as MCP tools.

Option C: Standalone MCP Gateway

For production, run the MCP gateway as a separate process that discovers all services:

micro-mcp-gateway \
  --registry consul \
  --registry-address consul:8500 \
  --address :3000 \
  --auth jwt \
  --rate-limit 10

See the standalone gateway docs for more.

Use with Claude Code

# Start MCP server for Claude Code (stdio transport)
micro mcp serve

Add to your Claude Code config:

{
  "mcpServers": {
    "tasks": {
      "command": "micro",
      "args": ["mcp", "serve"]
    }
  }
}

Now Claude can manage your tasks:

You: "Create a task to fix the login bug and assign it to alice"
Claude: [calls tasks.TaskService.Create with {"title": "Fix login bug", ...}]
        Created task-1: "Fix login bug" assigned to alice.

You: "What tasks does alice have?"
Claude: [calls tasks.TaskService.List]
        Alice has 1 task: "Fix login bug" (status: todo)

You: "Mark it as in progress"
Claude: [calls tasks.TaskService.Update with {"id": "task-1", "status": "in_progress"}]
        Updated task-1 to "in_progress".

Use with WebSocket Clients

For real-time bidirectional communication (e.g., streaming agent frameworks):

const ws = new WebSocket("ws://localhost:3000/mcp/ws", {
  headers: { "Authorization": "Bearer <token>" }
});

// JSON-RPC 2.0 over WebSocket
ws.send(JSON.stringify({
  jsonrpc: "2.0",
  id: 1,
  method: "tools/list",
  params: {}
}));

Step 6: Test Your Tools

Use the CLI to verify tools work:

# List all available tools
micro mcp list

# Test a specific tool
micro mcp test tasks.TaskService.Create

# Generate documentation
micro mcp docs

# Export for LangChain
micro mcp export --format langchain

Step 7: Add Observability (Optional)

Enable OpenTelemetry tracing to see every agent tool call as a distributed trace:

import (
    "go.opentelemetry.io/otel"
    "go-micro.dev/v6/gateway/mcp"
)

go mcp.ListenAndServe(":3000", mcp.Options{
    Registry:      service.Options().Registry,
    TraceProvider: otel.GetTracerProvider(),
})

Each tool call generates a span with attributes:

  • mcp.tool.name — which tool was called
  • mcp.transport — HTTP, WebSocket, or stdio
  • mcp.account.id — who called it
  • mcp.auth.allowed — whether it was permitted

Trace context is propagated downstream via metadata headers (Mcp-Trace-Id, Mcp-Tool-Name, Mcp-Account-Id), so you get full distributed traces from agent through gateway to service.

Step 8: Use the AI Package (Optional)

If your service needs to call AI models directly:

import (
    "go-micro.dev/v6/ai"
    _ "go-micro.dev/v6/ai/anthropic"
)

m := ai.New("anthropic",
    ai.WithAPIKey(os.Getenv("ANTHROPIC_API_KEY")),
)

resp, err := m.Generate(ctx, &ai.Request{
    Prompt:       "Summarize these tasks: " + taskJSON,
    SystemPrompt: "You are a project manager assistant",
})

Checklist

Before shipping an AI-native service:

  • Every handler method has a doc comment explaining what it does
  • Every method has an @example tag with realistic JSON input
  • Request struct fields have description tags
  • Write/delete operations have auth scopes
  • You’ve tested with micro mcp test to verify tools work
  • You’ve tested with Claude Code or the agent playground

What Happens Under the Hood

1. You write Go comments on handler methods
2. micro registers the handler and extracts docs via go/ast
3. Docs are stored in the service registry as endpoint metadata
4. MCP gateway discovers services via the registry
5. Gateway generates JSON Schema tools with descriptions
6. AI agents query the tools endpoint and see rich descriptions
7. Agents call tools via JSON-RPC, gateway routes to your handler

Next Steps

13 - CLI & Gateway Guide

The Go Micro CLI provides two gateway modes for accessing your microservices: development (micro run) and production (micro server). Both use the same underlying gateway architecture, ensuring consistent behavior across environments.

Overview

                    ┌─────────────────────┐
                    │   HTTP Requests     │
                    └──────────┬──────────┘
                               │
                    ┌──────────▼──────────┐
                    │   Unified Gateway   │
                    │                     │
                    │  • Service Discovery│
                    │  • HTTP → RPC       │
                    │  • Web Dashboard    │
                    │  • Health Checks    │
                    └──────────┬──────────┘
                               │
                    ┌──────────▼──────────┐
                    │   Your Services     │
                    │  (via Registry)     │
                    └─────────────────────┘

Quick Comparison

Featuremicro runmicro server
PurposeLocal developmentProduction API gateway
AuthenticationYes (default admin/micro)Yes (default admin/micro)
Process ManagementYes (builds & runs services)No (services run separately)
Hot ReloadYes (watches file changes)No
Endpoint ScopesYes (/auth/scopes)Yes (/auth/scopes)
Best ForCoding, testing, iterationDeployed environments

Development Mode: micro run

Quick Start

# Create and run a service
micro new myservice
cd myservice
micro run

Open http://localhost:8080 - no login required!

What You Get

  • Instant Gateway: HTTP API at /api/{service}/{method}
  • Web Dashboard: Browse and test services at /
  • Hot Reload: Code changes trigger automatic rebuild
  • Authentication: JWT auth with default credentials (admin/micro)
  • Scopes: Endpoint access control via /auth/scopes

Example Usage

# Start with hot reload
micro run

# Log in at http://localhost:8080 with admin/micro
# Or use a token for API calls:
curl -X POST http://localhost:8080/api/myservice/Handler.Call \
  -H "Authorization: Bearer <token>" \
  -d '{"name": "World"}'

When to Use

  • Writing new services
  • Testing changes locally
  • Debugging service interactions
  • Testing auth and scopes before production

See micro run guide for full details.

Production Mode: micro server

Quick Start

# Start your services separately (e.g., via systemd, docker)
./myservice &

# Start the gateway
micro server --address :8080

Open http://localhost:8080 and log in with admin/micro.

What You Get

  • API Gateway: Secure HTTP endpoint for all services
  • JWT Authentication: Token-based access control
  • Web Dashboard: Service management UI with login
  • User Management: Create users and API tokens
  • Endpoint Scopes: Fine-grained access control per endpoint
  • Production Ready: Designed for deployed environments

Authentication

All API calls require an Authorization header:

# Get a token (via web UI or login endpoint)
TOKEN="eyJhbGc..."

# Call a service with auth
curl -X POST http://localhost:8080/api/myservice/Handler.Call \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"name": "World"}'

Managing Users, Tokens & Scopes

  1. Log in: Visit http://localhost:8080 → Enter admin/micro
  2. Create API Token: Go to /auth/tokens → Generate token with scopes
  3. Set Endpoint Scopes: Go to /auth/scopes → Restrict which endpoints require which scopes
  4. Use Token: Copy and use in Authorization: Bearer <token> header

When to Use

  • Production deployments
  • Staging environments
  • Multi-team access (with auth)
  • Public-facing APIs (with security)

Gateway Features (Both Modes)

Both commands provide the same core gateway capabilities:

1. HTTP to RPC Translation

The gateway automatically converts HTTP requests to RPC calls:

POST /api/{service}/{method}
Content-Type: application/json

{"field": "value"}

Becomes an RPC call to:

  • Service: {service}
  • Method: {method}
  • Payload: {"field": "value"}

2. Service Discovery

The gateway queries the registry (mdns, consul, etcd) to find services:

# List all services
curl http://localhost:8080/services

# Returns:
[
  {"name": "myservice", "endpoints": ["Handler.Call", "Handler.List"]},
  {"name": "users", "endpoints": ["Users.Create", "Users.Get"]}
]

Services register automatically when they start - no manual configuration needed!

3. Web Dashboard

Visit / in your browser to:

  • Browse all registered services
  • See available endpoints with request/response schemas
  • Test endpoints with auto-generated forms
  • View service health and status
  • Read API documentation

4. Health Checks

# Aggregate health of all services
curl http://localhost:8080/health

# Kubernetes-style probes
curl http://localhost:8080/health/live   # Is gateway alive?
curl http://localhost:8080/health/ready  # Are services ready?

5. Dynamic Updates

The gateway automatically picks up:

  • New services registering
  • Services going offline
  • Endpoint changes
  • Version updates

No gateway restart needed!

6. Endpoint Scopes

Scopes provide fine-grained access control over which tokens can call which endpoints. Both micro run and micro server support scopes.

Set up endpoint scopes:

  1. Visit /auth/scopes to see all discovered endpoints
  2. Set required scopes for endpoints (e.g., billing on payments.Payments.Charge)
  3. Use Bulk Set to apply scopes to all endpoints matching a pattern (e.g., greeter.*)

Create scoped tokens:

  1. Visit /auth/tokens and create a token with matching scopes
  2. A token with scope billing can call endpoints that require billing
  3. A token with scope * bypasses all scope checks
  4. Endpoints with no scopes set are open to any authenticated token

Scopes are enforced on all call paths:

  • Direct API calls (/api/{service}/{endpoint})
  • MCP tool calls (/mcp/call)
  • Agent playground tool invocations

The gateway uses auth.Account from the go-micro framework. The account’s Scopes field carries the same []string used by the framework’s wrapper/auth package for service-level auth.

Architecture Benefits

Why Unified?

Previously, micro run and micro server had separate gateway implementations. This caused:

  • ❌ Duplicated code (hard to maintain)
  • ❌ Feature lag (improvements didn’t benefit both)
  • ❌ Inconsistent behavior between dev and prod

The unified gateway means:

  • ✅ Single codebase for both commands
  • ✅ Identical HTTP API in dev and production
  • ✅ New features benefit both modes automatically
  • ✅ Easier testing and maintenance

What Changed for Users?

From a user perspective:

  • micro run and micro server both have auth enabled
  • Both use the same JWT authentication and scopes system
  • API endpoints are unchanged
  • Web UI is identical

The unification is internal - your code keeps working.

Common Patterns

Local Development → Production

# 1. Develop locally without auth
micro run
# Test: curl http://localhost:8080/api/...

# 2. Build for production
go build -o myservice

# 3. Deploy services
./myservice &  # or via systemd, docker, k8s

# 4. Start gateway with auth
micro server

# 5. Generate API token (via web UI)
# Use token in production API calls

Multi-Service Development

# micro.mu
service api
    path ./api
    port 8081

service worker
    path ./worker
    port 8082
    depends api

service web
    path ./web
    port 8090
    depends api worker

# Start all with gateway
micro run

See micro run guide for configuration details.

API Gateway Deployment

Deploy micro server as your API gateway in front of all services:

                Internet
                    │
            ┌───────▼────────┐
            │  micro server  │  :8080 (public)
            │   + JWT Auth   │
            └───────┬────────┘
                    │
        ┌───────────┼───────────┐
        │           │           │
    ┌───▼───┐   ┌──▼───┐   ┌──▼────┐
    │ users │   │ posts│   │comments│
    │ :8081 │   │ :8082│   │ :8083  │
    └───────┘   └──────┘   └────────┘
    (internal)  (internal)  (internal)

Only micro server needs public access - services can be internal.

Programmatic Usage

You can also use the gateway in your own Go code:

package main

import (
    "context"
    "log"
    "go-micro.dev/v6/cmd/micro/server"
    "go-micro.dev/v6/store"
)

func main() {
    // Start gateway with custom options
    gw, err := server.StartGateway(server.GatewayOptions{
        Address:     ":9000",
        AuthEnabled: true,  // Enable authentication
        Store:       store.DefaultStore,
        Context:     context.Background(),
    })
    if err != nil {
        log.Fatal(err)
    }

    log.Printf("Gateway running on %s", gw.Addr())

    // Block until context is cancelled
    gw.Wait()
}

This gives you full control over gateway configuration in custom deployments.

Troubleshooting

Gateway starts but no services show

Problem: http://localhost:8080 shows empty service list

Solution:

  1. Check services are running: ps aux | grep myservice
  2. Verify registry: services must register via mdns/consul/etcd
  3. Check logs: ~/micro/logs/ for service startup errors

API calls return 404

Problem: curl http://localhost:8080/api/myservice/Handler.Call returns 404

Solution:

  1. Visit http://localhost:8080/services to see registered endpoints
  2. Check exact endpoint name (case-sensitive): Handler.Call vs handler.call
  3. Ensure service is registered: micro services or check web UI

Authentication errors

Problem: API returns 401 Unauthorized

Solution:

  1. Generate token: Visit http://localhost:8080/auth/tokens
  2. Use header: Authorization: Bearer <token>
  3. Check token not expired (24h default)
  4. Verify user not deleted (tokens revoked on user deletion)

Scope errors

Problem: API returns 403 Forbidden with insufficient scopes

Solution:

  1. Check which scopes the endpoint requires: Visit /auth/scopes
  2. Ensure your token has a matching scope (check at /auth/tokens)
  3. Use a token with * scope for full access
  4. Clear scopes from the endpoint if it should be unrestricted

Port already in use

Problem: micro run or micro server won’t start

Solution:

# Check what's using port 8080
lsof -i :8080

# Use different port
micro run --address :9000
micro server --address :9000

Next Steps

Need Help?

14 - Contributing

This is a rendered copy of the repository CONTRIBUTING.md for convenient access via the documentation site.

Overview

Go Micro welcomes contributions of all kinds: code, documentation, examples, and plugins.

Quick Start

git clone --depth 1 https://github.com/micro/go-micro.git
cd go-micro
go mod download
go test ./...

Process

  1. Fork and create a feature branch
  2. Make focused changes with tests
  3. Run linting and full test suite
  4. Open a PR describing motivation and approach

Commit Format

Use conventional commits:

feat(registry): add consul health check
fix(broker): prevent reconnect storm

Testing

Run unit tests:

go test ./...

Run race/coverage:

go test -race -coverprofile=coverage.out ./...

Plugins

Place new plugins under the appropriate interface directory (e.g. registry/consul/). Include tests and usage examples. Document env vars and options.

Documentation

Docs live in internal/website/docs/. Add new examples under internal/website/docs/examples/.

Help & Questions

Use GitHub Discussions or the issue templates. For general usage questions open a “Question” issue.

Full Guide

For complete details see the repository copy of the guide on GitHub.

15 - Debugging your agent

Debugging your agent

Use this guide when an agent surprises you: it answered without using a service, called the wrong endpoint, looped, lost memory, refused a tool, or behaved differently when a flow handed work to it. The local inner loop is:

micro run          # start services, agents, gateway, dashboard
micro chat         # reproduce one turn
micro inspect ...  # read the recorded run or workflow history

Debug the lifecycle in the same order Go Micro runs it: first prove the service is registered and callable, then inspect the agent run that chose tools, then inspect any workflow that handed off to the agent.

Use the recovery command that matches where you are in the first-agent journey:

CheckpointWhen to use itCommand
Install troubleshootingmicro is not installed, not on PATH, or the shell cannot run it.Install troubleshooting
Quick recovery mapThe first-agent loop stalled and you want the short scaffold → run → chat → inspect checklist before reading this full guide.micro agent quickcheck (alias: micro agent debug)
Preflight before micro runYou have not started the local runtime yet and want to verify Go, CLI, provider-key, and gateway-port prerequisites.micro agent preflight
Doctor after micro runmicro run is active, but chat, the /agent gateway, agent registration, provider settings, or inspect/run history is not behaving.micro agent doctor

micro agent quickcheck is the quickest breadcrumb when you are unsure where the first-agent path failed: it prints the preflight, run, doctor, inspect, and no-secret fallback commands in one place. micro agent preflight is read-only and runs before the first local run; failed checks include Fix: and Next: lines for Go, CLI installation, provider-key setup, and the local gateway port. Once micro run is already up, switch to micro agent doctor so the recovery output follows the live gateway, chat settings, registered agents, provider configuration, and inspectable run history.

1. Reproduce one small turn

Start from the application directory and keep the prompt narrow enough that you can tell which tool should have run:

micro run
micro chat --prompt "Create a ticket for Pat, then list open tickets."

For a live provider, make the provider choice explicit so a later retry uses the same model boundary:

MICRO_AI_PROVIDER=anthropic \
ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
micro chat --prompt "Create a ticket for Pat, then list open tickets."

If the provider supports streaming, turn it on while you reproduce the issue:

micro chat --provider anthropic --stream

Streaming shows the final answer as it arrives. Tool execution still goes through the same agent run and is visible through inspection after the turn completes.

2. Prove the service side before blaming the model

Agents only call tools that the runtime can discover and describe. Check the service boundary first:

micro services
micro call ticket TicketService.List '{}'

If the service is missing, restart the service under micro run and verify it is using the same registry as the agent. If the direct micro call fails, fix the handler, request shape, or auth error there before debugging prompts.

When the agent calls the wrong tool or sends the wrong fields, improve the tool description at the service source:

// Create opens a customer support ticket and returns its stable ticket ID.
// @example {"customer":"Pat","subject":"Cannot log in"}
func (s *TicketService) Create(ctx context.Context, req *CreateRequest, rsp *CreateResponse) error {

Endpoint comments, request field names, description tags, and @example blocks are the model’s map of your service. A vague handler comment often looks like a reasoning failure from the outside.

3. Inspect agent run history

After a chat turn, list recent runs for that agent:

micro inspect agent support

The output shows the run id, status, number of recorded events, the last event, errors, and a short trace id when tracing is configured. Narrow the list while you iterate:

micro inspect agent support --limit 5
micro inspect agent support --status timeout
micro inspect agent support --trace abc123
micro inspect agent support --json

Useful statuses include done, refused, timeout, rate_limited, canceled, and error. Use --json when you want exact timestamps, trace/span ids, and error kinds for a bug report.

When a run is paused at stage=input-required, continue it from the CLI and then inspect the completed checkpoint without writing a Go helper:

micro agent resume-input support <run-id> --input "Approve deploy to us-east-1"
micro inspect agent support --limit 1

Run timelines are stored in the agent’s state store under that agent’s scoped state (agent/<name>/runs/...). The persisted timeline is recorded even without an OpenTelemetry exporter, so micro inspect agent remains useful in local no-secret development.

Provider-free quickcheck: if you want to verify the documented inspect path before involving a live model, run the same smoke check CI uses:

go test ./internal/harness/zero-to-hero-ci -run TestNoSecretFirstAgentDebuggingSmoke -count=1

That test seeds a local assistant run history and memory transcript, then runs micro inspect agent assistant --limit 1, micro inspect agent --status done --json assistant, and micro agent history assistant with provider credentials cleared.

4. See tool calls as they happen

When you are embedding an agent in Go and need live tool visibility, use the streaming API instead of waiting for the final answer:

stream, err := agent.StreamAsk(ctx, ag, "Create a ticket for Pat")
if err != nil {
    return err
}
for {
    ev, err := stream.Recv()
    if err != nil {
        break
    }
    switch ev.Type {
    case agent.StreamEventToolStart:
        log.Printf("tool start: %s %#v", ev.ToolCall.Name, ev.ToolCall.Input)
    case agent.StreamEventToolEnd:
        log.Printf("tool end: %s %#v", ev.ToolCall.Name, ev.Result)
    case agent.StreamEventToken:
        fmt.Print(ev.Token)
    }
}

For custom audit logging, wrap the tool execution boundary. Wrappers observe every call and result, including guardrail refusals:

wrapped := micro.AgentWrapTool(func(next ai.ToolHandler) ai.ToolHandler {
    return func(ctx context.Context, call ai.ToolCall) ai.ToolResult {
        if run, ok := ai.RunInfoFrom(ctx); ok {
            log.Printf("run=%s agent=%s tool=%s", run.RunID, run.Agent, call.Name)
        }
        res := next(ctx, call)
        if res.Refused != "" {
            log.Printf("tool refused: %s reason=%s", call.Name, res.Refused)
        }
        return res
    }
})

ag := micro.NewAgent("support", wrapped)

Use this when you need request/response payloads in your own logs. By default, Go Micro records safe run metadata; raw prompt input is not persisted unless the agent is configured with agent.TraceInputs(true).

5. Inspect memory and plans

Default agent memory is store-backed and scoped to the agent name. A restarted agent with the same micro.WithStore(...) and name reloads conversation history from the history key in agent/<name> state. If you pass micro.WithMemory(...), you own that backend; if you pass agent.NewInMemory(...), memory disappears on restart.

The built-in plan tool also saves the current plan to the same scoped agent state, so a later turn can pick up the saved plan. When memory does not persist, check that all of these are stable across restarts:

  • the agent name (micro.NewAgent("support", ...)),
  • the configured store backend (micro.WithStore(...) or the process default),
  • whether a custom in-memory Memory implementation replaced the default,
  • whether compaction/retrieval limits are intentionally hiding older turns from the active model context.

6. Inspect workflow handoffs

If a flow triggered the agent, inspect the flow too. The flow history tells you which durable stage dispatched to the agent and whether a run is still pending:

micro inspect flow intake
micro inspect flow intake --pending
micro inspect flow intake --stage notify
micro inspect flow intake --json

The older flow-specific command remains available for listing runs:

micro flow runs intake

Use the flow run id and the agent run id together when debugging handoffs: the flow explains why work started and where it checkpointed; the agent run explains which model/tool steps happened after the handoff.

7. Add traces when metadata is not enough

For local CLI debugging, micro inspect is the fastest path. For production or multi-service debugging, configure an OpenTelemetry tracer provider on the agent:

ag := micro.NewAgent("support",
    micro.AgentTraceProvider(tp),
)

Trace ids flow into the recorded run summaries, so you can pivot between micro inspect agent support --trace <prefix> and your trace backend. Keep agent.TraceInputs(true) off unless your observability backend is approved to store prompt content.

Troubleshooting table

SymptomWhat to inspectCommon fix
Agent answers without calling a servicemicro services, direct micro call, then micro inspect agent <name>Register the service, include it in micro.AgentServices(...), or improve endpoint comments and examples.
Agent loops or burns stepsmicro inspect agent <name> --status error and wrapper logsAdd or lower micro.AgentMaxSteps(...) / micro.AgentLoopLimit(...); move predictable work into a flow.
Tool is refused before it runsWrapper logs, ToolResult.Refused, micro inspect agent <name> --status refusedUpdate micro.AgentApproveTool(...) policy or prompt the user for explicit approval before retrying.
Memory is missing after restartAgent name, store backend, WithMemory, compaction/retrieval settingsUse the default store-backed memory with a persistent store, or persist your custom memory backend.
Flow handoff appears stuckmicro inspect flow <flow> --pending, then micro inspect agent <agent>Resume or fail the pending flow run; confirm the dispatched agent completed or timed out.
Provider failed or timed outmicro inspect agent <name> --status timeout / --status rate_limitedRetry with the same provider/model, raise deadlines where appropriate, or enable provider retries for transient errors.
Tool call appears as assistant textAgent run history and provider conformance checksKeep provider packages current; Go Micro normalizes provider-emitted text tool calls, and conformance tests guard this behavior.

What to include in a bug report

When you cannot explain the run locally, include:

micro inspect agent <agent> --limit 5 --json
micro inspect flow <flow> --limit 5 --json
micro services
micro call <service> <Handler.Method> '{}'

Redact secrets and user data. If you enabled agent.TraceInputs(true), inspect the JSON before sharing it because prompts may be present.

16 - Deployment Guide

This is a quick reference for deploying go-micro services. For the full guide, see the Deployment documentation.

Workflow

micro run      →  Develop locally with hot reload
micro build    →  Compile production binaries
micro deploy   →  Push to a remote Linux server via SSH + systemd
micro server   →  Optional: production web dashboard with auth

Quick Start

# Build binaries for Linux
micro build --os linux

# Deploy to server (builds automatically if needed)
micro deploy user@your-server

First-Time Server Setup

On your server (any Linux with systemd):

curl -fsSL https://go-micro.dev/install.sh | sh
sudo micro init --server

This creates /opt/micro/{bin,data,config} and a systemd template for managing services.

Deploy

micro deploy user@your-server

This builds for linux/amd64, copies binaries to /opt/micro/bin/, configures systemd services, and verifies they’re running.

Named Targets

Add deploy targets to micro.mu:

deploy prod
    ssh deploy@prod.example.com

deploy staging
    ssh deploy@staging.example.com

Then: micro deploy prod

Managing Services

micro status --remote user@server       # Check status
micro logs --remote user@server         # View logs
micro logs myservice --remote user@server -f  # Follow logs

Docker (Optional)

micro build --docker          # Build Docker images
micro build --docker --push   # Build and push
micro build --compose         # Generate docker-compose.yml

Full Documentation

See the Deployment documentation for complete details including SSH setup, environment variables, security best practices, and troubleshooting.

17 - Error Handling for AI Agents

Error Handling for AI Agents

When AI agents call your services through MCP, they need to understand errors well enough to recover or inform the user. This guide covers how to write services that give agents useful error information.

Use Typed Errors

Go Micro’s errors package provides structured errors that the MCP gateway forwards to agents with status codes and detail messages.

import "go-micro.dev/v6/errors"

func (s *Users) Get(ctx context.Context, req *GetRequest, rsp *GetResponse) error {
    if req.ID == "" {
        return errors.BadRequest("users.Get", "id is required")
    }

    user, err := s.db.FindUser(req.ID)
    if err != nil {
        return errors.NotFound("users.Get", "user %s not found", req.ID)
    }

    rsp.User = user
    return nil
}

Agents receive structured error responses like:

{
  "error": {
    "id": "users.Get",
    "code": 404,
    "detail": "user abc-123 not found",
    "status": "Not Found"
  }
}

This gives the agent enough context to decide: retry with a different ID, ask the user, or report the problem.

Error Types and When to Use Them

ErrorCodeUse When
errors.BadRequest400Missing or invalid input — agent should fix the request
errors.Unauthorized401Missing auth — agent needs credentials
errors.Forbidden403Insufficient permissions — agent can’t do this
errors.NotFound404Resource doesn’t exist — agent should try something else
errors.Conflict409Duplicate or version conflict — agent should retry or adjust
errors.InternalServerError500Server bug — agent should report to user, don’t retry

Write Error Messages for Agents

Error messages should tell the agent what went wrong and what to do about it.

Bad: Vague Errors

return fmt.Errorf("invalid request")
return errors.BadRequest("users", "failed")

Agents can’t recover from these — they don’t know what’s wrong.

Good: Actionable Errors

return errors.BadRequest("users.Create", "email is required — provide a valid email address")
return errors.BadRequest("users.Create", "email '%s' is already registered — use a different email", req.Email)
return errors.NotFound("users.Get", "no user with id '%s' — use users.List to find valid IDs", req.ID)

The agent now knows exactly what to fix or which tool to call next.

Validation Patterns

Validate inputs at the top of your handler before doing any work:

// CreateOrder places a new order for a user. The user must exist
// and at least one item is required.
//
// @example {"user_id": "u-1", "items": [{"product_id": "p-1", "quantity": 1}]}
func (s *Orders) CreateOrder(ctx context.Context, req *CreateRequest, rsp *CreateResponse) error {
    // Validate required fields
    if req.UserID == "" {
        return errors.BadRequest("orders.CreateOrder", "user_id is required")
    }
    if len(req.Items) == 0 {
        return errors.BadRequest("orders.CreateOrder", "at least one item is required")
    }

    // Validate each item
    for i, item := range req.Items {
        if item.ProductID == "" {
            return errors.BadRequest("orders.CreateOrder",
                "item[%d].product_id is required", i)
        }
        if item.Quantity <= 0 {
            return errors.BadRequest("orders.CreateOrder",
                "item[%d].quantity must be positive, got %d", i, item.Quantity)
        }
    }

    // All validations passed — do the work
    // ...
}

Document Error Cases

Tell agents what errors to expect in your doc comments:

// Transfer moves funds between two accounts. Both accounts must exist
// and the source account must have sufficient balance.
// Returns an error if the source balance is too low.
//
// @example {"from": "acc-1", "to": "acc-2", "amount": 100}
func (s *Accounts) Transfer(ctx context.Context, req *TransferRequest, rsp *TransferResponse) error {

The description “returns an error if the source balance is too low” helps agents anticipate failure modes and plan accordingly.

Don’t Expose Internal Details

Agents (and the users they serve) shouldn’t see stack traces, database errors, or internal paths.

// Bad — leaks internals
return fmt.Errorf("pq: duplicate key value violates unique constraint \"users_email_key\"")

// Good — clear message, no internals
return errors.Conflict("users.Create", "a user with email '%s' already exists", req.Email)

Idempotency for Retries

Agents may retry failed operations. Design critical operations to be idempotent:

// CreateOrUpdate upserts a config value. Safe to call multiple times
// with the same key — it will create on first call, update on subsequent calls.
//
// @example {"key": "theme", "value": "dark"}
func (s *Config) CreateOrUpdate(ctx context.Context, req *SetRequest, rsp *SetResponse) error {

When an operation is naturally idempotent, say so in the doc comment. Agents will learn they can safely retry.

Next Steps

18 - Framework Comparison

How Go Micro compares to other Go microservices frameworks.

Quick Comparison

FeatureGo Microgo-kitgRPCDapr
Learning CurveLowHighMediumMedium
BoilerplateLowHighMediumLow
Plugin SystemBuilt-inExternalLimitedSidecar
Service DiscoveryYes (mDNS, Consul, etc)No (BYO)NoYes
Load BalancingClient-sideNoNoSidecar
Pub/SubYesNoNoYes
TransportHTTP, gRPC, NATSBYOgRPC onlyHTTP, gRPC
Zero-config DevYes (mDNS)NoNoNo (needs sidecar)
Production ReadyYesYesYesYes
LanguageGo onlyGo onlyMulti-languageMulti-language

vs go-kit

go-kit Philosophy

  • “Just a toolkit” - minimal opinions
  • Compose your own framework
  • Maximum flexibility
  • Requires more decisions upfront

Go Micro Philosophy

  • “Batteries included” - opinionated defaults
  • Swap components as needed
  • Progressive complexity
  • Get started fast, customize later

When to Choose go-kit

  • You want complete control over architecture
  • You have strong opinions about structure
  • You’re building a custom framework
  • You prefer explicit over implicit

When to Choose Go Micro

  • You want to start coding immediately
  • You prefer conventions over decisions
  • You want built-in service discovery
  • You need pub/sub messaging

Code Comparison

go-kit (requires more setup):

// Define service interface
type MyService interface {
    DoThing(ctx context.Context, input string) (string, error)
}

// Implement service
type myService struct{}

func (s *myService) DoThing(ctx context.Context, input string) (string, error) {
    return "result", nil
}

// Create endpoints
func makeDo ThingEndpoint(svc MyService) endpoint.Endpoint {
    return func(ctx context.Context, request interface{}) (interface{}, error) {
        req := request.(doThingRequest)
        result, err := svc.DoThing(ctx, req.Input)
        if err != nil {
            return doThingResponse{Err: err}, nil
        }
        return doThingResponse{Result: result}, nil
    }
}

// Create transport (HTTP, gRPC, etc)
// ... more boilerplate ...

Go Micro (simpler):

type MyService struct{}

type Request struct {
    Input string `json:"input"`
}

type Response struct {
    Result string `json:"result"`
}

func (s *MyService) DoThing(ctx context.Context, req *Request, rsp *Response) error {
    rsp.Result = "result"
    return nil
}

func main() {
    svc := micro.NewService("myservice")
    svc.Init()
    svc.Handle(new(MyService))
    svc.Run()
}

vs gRPC

gRPC Focus

  • High-performance RPC
  • Multi-language support via protobuf
  • HTTP/2 transport
  • Streaming built-in

Go Micro Scope

  • Full microservices framework
  • Service discovery
  • Multiple transports (including gRPC)
  • Pub/sub messaging
  • Pluggable components

When to Choose gRPC

  • You need multi-language services
  • Performance is critical
  • You want industry-standard protocol
  • You’re okay managing service discovery separately

When to Choose Go Micro

  • You need more than just RPC (pub/sub, discovery, etc)
  • You want flexibility in transport
  • You’re building Go-only services
  • You want integrated tooling

Integration

You can use gRPC with Go Micro for native gRPC compatibility:

import (
    grpcServer "go-micro.dev/v6/server/grpc"
    grpcClient "go-micro.dev/v6/client/grpc"
)

svc := micro.NewService("myservice",
    micro.Server(grpcServer.NewServer()),
    micro.Client(grpcClient.NewClient()),
)

See Native gRPC Compatibility for a complete guide.

vs Dapr

Dapr is a distributed application runtime. Its building blocks cover service invocation, state, pub/sub, bindings, secrets, configuration, distributed locks, actors, jobs, and workflow, usually accessed through a sidecar from many languages. Dapr Agents adds an agent framework on top of those runtime capabilities.

Go Micro overlaps with Dapr on distributed-systems primitives, but the product shape is different: Go Micro is a Go framework where services, agents, tools, and flows are built from the same runtime. A service endpoint can become an AI-callable tool, and an agent is itself a registered service with memory, guardrails, planning, delegation, MCP, and A2A around it.

Decision table

NeedPrefer Go MicroPrefer DaprUse both
Primary languageYour core runtime is Go and you want library-native APIsYou run a polyglot estate and want one sidecar API across languagesGo services use Go Micro while non-Go services expose Dapr APIs
Agent modelAgents should be ordinary services: registered, discoverable, callable by RPC, MCP, and A2AAgents are primarily Python applications using Dapr AgentsDapr-hosted agents call Go Micro MCP tools, or Go Micro agents call Dapr-backed services
ToolsExisting service endpoints should become tools with minimal extra codeTools are modeled through Dapr components, bindings, or agent framework codeUse Dapr components behind Go Micro services that expose a stable tool surface
WorkflowsDeterministic steps should live beside Go services and agents in the same codebaseYou want Dapr Workflow’s sidecar-backed orchestration model across languagesLet Dapr own cross-language workflows and let Go Micro own Go-native agent/tool execution
State and pub/subYou want Go interfaces and pluggable packages directly in-processYou want component YAML and sidecar portability across backing servicesPut portable infrastructure behind Dapr and domain/tool logic in Go Micro
DeploymentYou want a simple Go binary/runtime first, with Kubernetes support as an explicit deployment targetYou are already standardized on Dapr sidecars in KubernetesRun Go Micro services in clusters that already have Dapr for shared infrastructure
InteropMCP and A2A are first-class requirements for exposing services and agentsDapr’s app APIs and agent framework are the integration boundaryBridge through MCP/A2A at the agent edge and Dapr APIs at the infrastructure edge

When to choose Dapr

  • You need a polyglot runtime contract for Node, Python, Java, .NET, Go, and other services.
  • Your platform team already operates sidecars and component configuration across Kubernetes clusters.
  • You want Dapr’s standard building blocks for state, pub/sub, bindings, secrets, actors, jobs, and workflow more than you want a Go-native service framework.
  • You are adopting Dapr Agents and want to stay in its Python-first agent stack.

When to choose Go Micro

  • You are building mostly in Go and want the agent harness to be the same runtime as your services.
  • You want service methods and their comments/examples to become AI-callable tools without maintaining a separate tool layer.
  • You want agents to be deployed, discovered, called, load-balanced, and inspected like ordinary services.
  • You need MCP and A2A at the agent/service boundary, not only an internal application API.
  • You prefer library-native composition and direct Go interfaces over sidecar component wiring.

Where Go Micro still needs to prove itself

Dapr has a mature platform narrative and broad deployment footprint. Go Micro’s agent-harness story is sharper for Go teams, but production adoption depends on keeping the no-secret getting-started path green, documenting durability semantics clearly, proving MCP/A2A conformance with external clients, and making Kubernetes deployment first-class.

Practical migration path

  1. Start with one Go Micro service that wraps a real domain capability.
  2. Add doc comments and examples so the endpoint is useful as an agent tool.
  3. Expose it through MCP for external agents or through A2A if the capability is itself an agent.
  4. If your platform already uses Dapr, keep Dapr components behind the service boundary and let Go Micro present the agent/tool contract.
  5. Move deterministic multi-step work into flows only after the service/tool boundary is stable.

vs Agent Frameworks (Google ADK)

ADK (Agent Development Kit) is Google’s open-source, code-first framework for building AI agents. It spans several languages (Python, TypeScript, Go, Java, Kotlin); adk-go is the Go implementation. It’s model-agnostic (optimized for Gemini), speaks MCP and A2A, and supports multi-agent systems, evaluation, and deployment to Cloud Run / GKE.

They overlap on agents but solve different problems. ADK is a library for building an agent process — you define an agent, its tools, and a model, then run and deploy it. Go Micro is the harness around agents once they operate real systems: service discovery, inter-service RPC, pub/sub, durable flows, tool execution, and deployment. Those pieces are out of scope for ADK, and you bring your own.

In Go Micro an agent is built as an ordinary service: it registers in the registry, is callable by RPC (Agent.Chat) and over A2A, and other services and agents discover and call it the same way they call anything else. Its endpoints are exposed as MCP tools automatically. So once you have more than one agent or service, Go Micro also gives you the discovery, RPC, pub/sub, config, and deployment around them.

Go MicroGoogle ADK
Primary unitA harnessed service (an agent is a service with an LLM inside)An agent
Service discovery / registryBuilt-in (mDNS, Consul, etcd)Not in scope
Inter-service RPC, load balancing, pub/subBuilt-inNot in scope
MCPEvery service endpoint is automatically an MCP tool (no extra code)MCP tools, wired explicitly
A2AAgents are A2A-reachable servicesSupported
Deterministic orchestrationFlowsGraph workflows
Multi-agentAgents discover & call each other via the registry; plan/delegate built inComposition, routing, workflow patterns
Evaluation suiteHarnesses/conformance today; first-class evaluation is a gapYes (criteria, user/env simulation, metrics)
Context engineeringStore-backed memory“Context as source code” (auto filter/summarize/token tracking)
LanguagesGoPython, TypeScript, Go, Java, Kotlin
BackingCommunityGoogle

When to choose ADK

  • You want an agent framework with first-class evaluation and context tooling
  • You’re polyglot, or invested in the Google Cloud / Gemini ecosystem
  • You want a cross-language A2A ecosystem with Google’s backing

When to choose Go Micro

  • You want an agent harness where agents and services are the same thing — registered, discoverable, load-balanced, and deployed the same way
  • You want your existing services to become agent tools with zero extra code (every endpoint is an MCP tool automatically)
  • You’re building in Go and want one set of primitives for services, agents, and flows

They interoperate

Both speak MCP and A2A, so this isn’t strictly either/or: a Go Micro agent and an ADK agent (in any language) can call each other over A2A, and either can consume the other’s MCP tools. A common pattern is to run Go Micro as the service mesh / runtime and let ADK (or any A2A agent) plug into it.

vs tRPC-Agent-Go

tRPC-Agent-Go (maintained by tRPC-Group, validated inside Tencent) is a production-grade Go framework for agent systems: LLM / Chain / Parallel / Cycle / Graph agents, function tools, MCP, A2A, AG-UI, Redis memory and RAG, evaluation, agent self-evolution, and OpenTelemetry. It’s a serious, well-resourced project.

They overlap heavily on agents but take a different approach. tRPC-Agent-Go is an agent SDK you run alongside your services — you compose agents and tools into graphs and conditional workflows, and your microservices (tRPC) live separately and are called into. Go Micro starts from the premise that an agent is a service — one runtime where every endpoint is automatically a tool, an agent registers and is discovered and load-balanced like anything else, and workflows are durable code paths rather than a graph DSL. The premise is that the line between “your services” and “your agents” is accidental complexity; remove it and there’s less to wire and keep in sync.

Go MicrotRPC-Agent-Go
Primary unitA harnessed service (an agent is a service with an LLM inside)An agent
OrchestrationDurable flow steps + Loop — plain code pathsGraph / Chain / Parallel / Cycle agents (graph DSL)
Services as toolsEvery endpoint is automatically an MCP toolFunction tools + MCP, wired explicitly
Service runtimeBuilt in — agents are services (registry, RPC, load balancing, pub/sub)Runs alongside your existing service stack (tRPC)
MCP / A2ABoth, generated from the registryBoth
Evaluation / self-evolutionVerification loop on the roadmap; not yet first-classFirst-class today
Memory / RAGStore-backed memory (Postgres, NATS KV, file); RAG on the roadmapIn-memory / Redis memory; RAG today
ObservabilityOpenTelemetry run timelines, micro runsOpenTelemetry, Langfuse examples
BackingIndependent, communitytRPC-Group / Tencent

When to choose tRPC-Agent-Go

  • You want a graph/workflow DSL for composing agents and tools
  • You’re on tRPC, or want to add agents alongside an existing service stack
  • You want first-class evaluation and self-evolution today, with a large team behind it

When to choose Go Micro

  • You want one runtime where services, agents, and flows are the same primitives — registered, discoverable, and deployed the same way
  • You want your existing services to become agent tools with zero extra code
  • You prefer durable flows and plain code paths over a graph DSL, in a small, independent framework you can hold in your head

They interoperate

Both speak MCP and A2A, so a Go Micro agent and a tRPC-Agent-Go agent can call each other over A2A, and either can consume the other’s MCP tools. You can run Go Micro as the service-and-agent runtime and still reach an agent built on tRPC-Agent-Go.

Feature Deep Dive

Service Discovery

Go Micro: Built-in with plugins

// Zero-config for dev
svc := micro.NewService("myservice")

// Consul for production
reg := consul.NewRegistry()
svc := micro.NewService("myservice", micro.Registry(reg))

go-kit: Bring your own

// You implement service discovery
// Can be 100+ lines of code

gRPC: No built-in discovery

// Use external solution like Consul
// or service mesh like Istio

Load Balancing

Go Micro: Client-side, pluggable strategies

// Built-in: random, round-robin
selector := selector.NewSelector(
    selector.SetStrategy(selector.RoundRobin),
)

go-kit: Manual implementation

// You implement load balancing
// Using loadbalancer package

gRPC: Via external load balancer

# Use external LB like Envoy, nginx

Pub/Sub

Go Micro: First-class

broker.Publish("topic", &broker.Message{Body: []byte("data")})
broker.Subscribe("topic", handler)

go-kit: Not provided

// Use external message broker directly
// NATS, Kafka, etc

gRPC: Streaming only

// Use bidirectional streams
// Not traditional pub/sub

Migration Paths

See specific migration guides:

Coming Soon:

  • From go-kit
  • From Standard Library

Decision Matrix

Choose Go Micro if:

  • ✅ Building Go microservices
  • ✅ Want fast iteration
  • ✅ Need service discovery
  • ✅ Want pub/sub built-in
  • ✅ Prefer conventions

Choose go-kit if:

  • ✅ Want maximum control
  • ✅ Have strong architectural opinions
  • ✅ Building custom framework
  • ✅ Prefer explicit composition

Choose gRPC if:

  • ✅ Need multi-language support
  • ✅ Performance is primary concern
  • ✅ Just need RPC (not full framework)
  • ✅ Have service discovery handled

Choose Dapr if:

  • ✅ Polyglot services
  • ✅ Heavy Kubernetes usage
  • ✅ Want portable cloud abstractions
  • ✅ Need state management

Performance

Rough benchmarks (requests/sec, single instance):

FrameworkSimple RPCWith DiscoveryWith Tracing
Go Micro~20k~18k~15k
gRPC~25kN/A~20k
go-kit~22kN/A~18k
HTTP std~30kN/AN/A

Benchmarks are approximate and vary by configuration

Community & Ecosystem

  • Go Micro: Active, growing plugins
  • gRPC: Huge, multi-language
  • go-kit: Mature, stable
  • Dapr: Growing, Microsoft-backed

Recommendation

Start with Go Micro if you’re building Go microservices and want to move fast. You can always:

  • Use gRPC transport: micro.Transport(grpc.NewTransport())
  • Integrate with go-kit components
  • Mix and match as needed

The pluggable architecture means you’re not locked in.

19 - Health Checks

The health package provides health check functionality for microservices, including Kubernetes-style liveness and readiness probes.

Quick Start

import "go-micro.dev/v6/health"

func main() {
    // Register health checks
    health.Register("database", health.PingCheck(db.Ping))
    health.Register("cache", health.TCPCheck("localhost:6379", time.Second))
    
    // Add health endpoints
    mux := http.NewServeMux()
    health.RegisterHandlers(mux)  // Registers /health, /health/live, /health/ready
    
    http.ListenAndServe(":8080", mux)
}

Endpoints

EndpointPurposeReturns 200 when
/healthOverall health statusAll critical checks pass
/health/liveKubernetes liveness probeService is running
/health/readyKubernetes readiness probeAll critical checks pass

Response Format

{
  "status": "up",
  "checks": [
    {
      "name": "database",
      "status": "up",
      "duration": 1234567
    },
    {
      "name": "cache",
      "status": "up", 
      "duration": 567890
    }
  ],
  "info": {
    "go_version": "go1.22.0",
    "go_os": "linux",
    "go_arch": "amd64",
    "version": "1.0.0"
  }
}

When unhealthy:

  • HTTP status: 503 Service Unavailable
  • status: "down"
  • Failed checks include an error field

Built-in Checks

PingCheck

For database connections with a Ping() method:

health.Register("postgres", health.PingCheck(db.Ping))
health.Register("mysql", health.PingContextCheck(db.PingContext))

TCPCheck

Verify TCP connectivity:

health.Register("redis", health.TCPCheck("localhost:6379", time.Second))
health.Register("kafka", health.TCPCheck("kafka:9092", 2*time.Second))

HTTPCheck

Verify an HTTP endpoint returns 200:

health.Register("api", health.HTTPCheck("http://api.internal/health", time.Second))

DNSCheck

Verify DNS resolution:

health.Register("dns", health.DNSCheck("api.example.com"))

CustomCheck

Any function returning an error:

health.Register("disk", health.CustomCheck(func() error {
    var stat syscall.Statfs_t
    if err := syscall.Statfs("/", &stat); err != nil {
        return err
    }
    freeGB := stat.Bavail * uint64(stat.Bsize) / 1e9
    if freeGB < 1 {
        return fmt.Errorf("low disk space: %dGB free", freeGB)
    }
    return nil
}))

RegistryCheck

Verifies the service registry is still reachable. A go-micro service can keep running while it has silently lost its connection to the registry (etcd, Consul, …) — the process looks healthy, but other services can no longer discover it. RegistryCheck surfaces that state so a readiness probe can take the pod out of rotation.

svc := micro.NewService("orders")

health.Register("registry", health.RegistryCheck(svc.Options().Registry))

Registered checks are critical by default, so when the registry connection is lost, /health/ready returns 503 and Kubernetes stops routing to the pod:

readinessProbe:
  httpGet:
    path: /health/ready
    port: 8080
  periodSeconds: 5

The check lists services under the configured probe timeout, so an unreachable registry is reported as down rather than hanging the probe. It works with any registry implementation — the connectivity is exercised through the standard ListServices call.

Critical vs Non-Critical Checks

By default, all checks are critical. A critical check failure marks the service as not ready.

For non-critical checks (monitoring only):

health.RegisterCheck(health.Check{
    Name:     "external-api",
    Check:    health.HTTPCheck("https://api.external.com/status", 5*time.Second),
    Critical: false,  // Won't affect readiness
    Timeout:  5 * time.Second,
})

Timeouts

Default timeout is 5 seconds. Override per-check:

health.RegisterCheck(health.Check{
    Name:    "slow-db",
    Check:   health.PingCheck(db.Ping),
    Timeout: 10 * time.Second,
})

Adding Service Info

Include metadata in health responses:

health.SetInfo("version", "1.0.0")
health.SetInfo("commit", "abc123")
health.SetInfo("service", "users")

Kubernetes Configuration

apiVersion: v1
kind: Pod
spec:
  containers:
  - name: app
    livenessProbe:
      httpGet:
        path: /health/live
        port: 8080
      initialDelaySeconds: 5
      periodSeconds: 10
    readinessProbe:
      httpGet:
        path: /health/ready
        port: 8080
      initialDelaySeconds: 5
      periodSeconds: 5

Integration with micro run

When using micro run with a micro.mu config that specifies ports, the runner waits for /health to return 200 before starting dependent services:

service database
    path ./database
    port 8081

service api
    path ./api
    port 8080
    depends database

The api service won’t start until database’s /health endpoint is ready.

Programmatic Usage

// Check readiness in code
if health.IsReady(ctx) {
    // Service is healthy
}

// Get full health status
resp := health.Run(ctx)
fmt.Printf("Status: %s\n", resp.Status)
for _, check := range resp.Checks {
    fmt.Printf("  %s: %s (%v)\n", check.Name, check.Status, check.Duration)
}

Best Practices

  1. Keep checks fast - Health endpoints are called frequently
  2. Use timeouts - Don’t let slow dependencies block health checks
  3. Non-critical for optional deps - External APIs, caches that have fallbacks
  4. Critical for required deps - Databases, message queues
  5. Include version info - Helps debugging in production

20 - Install troubleshooting

Install troubleshooting

Use this page before micro new or micro agent demo when the CLI install is unclear. The goal is to prove three boundaries in order: the micro binary is on PATH, it is the version you expected, and the no-secret first-run path works without provider keys.

1. Choose one install path

Binary installer (no Go required to install)

curl -fsSL https://go-micro.dev/install.sh | sh

Use this when you want the released micro binary without building it yourself. The generated services still need a Go toolchain when you run micro run, but the installer itself does not require Go.

Go install (build from source)

go install go-micro.dev/v6/cmd/micro@latest

Use this when Go is already installed and you want the binary in your Go bin directory. If the command succeeds but micro is not found, your Go bin directory is probably not on PATH.

2. Verify PATH and version

Check which binary your shell will run:

command -v micro
micro --version

If command -v micro prints nothing, add the install directory to PATH, then open a new terminal and retry. Common locations are:

export PATH="$HOME/.micro/bin:$PATH"      # binary installer
export PATH="$(go env GOPATH)/bin:$PATH"  # go install

If micro --version shows an older binary than expected, remove the stale copy or put the intended install directory earlier in PATH.

3. Run the no-secret smoke path

Once micro resolves, prove the local service runtime before adding LLM provider keys:

micro new helloworld
cd helloworld
micro run

In another terminal:

curl -X POST http://localhost:8080/api/helloworld/Helloworld.Call \
  -H 'Content-Type: application/json' -d '{"name":"World"}'

This checks the scaffold, local build, gateway, and service registration without calling a model provider.

4. Recover common failures

SymptomCheckFix
micro: command not foundcommand -v microAdd the installer bin directory or $(go env GOPATH)/bin to PATH, then open a new terminal.
micro run cannot find Gogo versionInstall Go 1.24 or newer from https://go.dev/doc/install.
The gateway port is busylsof -i :8080Stop the process using the port, or run with a different address.
Provider-key errors block an agent runmicro agent preflightStay on the no-secret path first: run micro agent demo, then the no-secret first-agent guide.

5. Continue the first-agent on-ramp

After install verification succeeds, continue in order:

  1. micro agent demo — print the provider-free first-agent demo command and next docs steps.
  2. No-secret first-agent transcript — prove an agent can use services without a provider key.
  3. Your First Agent — build and chat with your own service-backed agent.
  4. Debugging your agent — inspect registration, tool calls, run history, and provider failures.
  5. 0→hero Reference — walk the full services → agents → workflows lifecycle.

For repository contributors, make install-smoke runs the same installer seam against a local build without network access.

21 - MCP Security Guide

This guide covers how to secure your MCP gateway for production use, including authentication, per-tool scopes, rate limiting, and audit logging.

Overview

The MCP gateway provides four layers of security:

  1. Authentication - Verify the caller’s identity via bearer tokens
  2. Scopes - Control which tools each token can access
  3. Rate Limiting - Prevent abuse with per-tool rate limits
  4. Audit Logging - Record every tool call for compliance and debugging

Authentication

Bearer Token Auth

The MCP gateway uses bearer token authentication. Tokens are validated by the configured auth.Auth provider.

import (
    "go-micro.dev/v6/gateway/mcp"
    "go-micro.dev/v6/auth"
)

gateway := mcp.ListenAndServe(":3000", mcp.Options{
    Registry: service.Options().Registry,
    Auth:     authProvider, // auth.Auth implementation
})

Agents pass tokens in the Authorization header:

curl -X POST http://localhost:3000/mcp/call \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"tool": "tasks.TaskService.Create", "input": {"title": "New task"}}'

Using micro run / micro server

When using micro run or micro server, authentication is handled automatically:

  • Development mode (micro run): Auth is disabled by default for easy development
  • Production mode (micro server): JWT auth is enabled with user management at /auth

Create tokens with specific scopes via the dashboard at /auth/tokens.

Per-Tool Scopes

Scopes control which tools a token can access. There are two ways to set scopes.

Service-Level Scopes

Set scopes when registering your handler. These travel with the service through the registry:

handler := service.Server().NewHandler(
    new(TaskService),
    server.WithEndpointScopes("TaskService.Get", "tasks:read"),
    server.WithEndpointScopes("TaskService.List", "tasks:read"),
    server.WithEndpointScopes("TaskService.Create", "tasks:write"),
    server.WithEndpointScopes("TaskService.Update", "tasks:write"),
    server.WithEndpointScopes("TaskService.Delete", "tasks:admin"),
)

Gateway-Level Scopes

Override or add scopes at the gateway without modifying services. Gateway scopes take precedence:

mcp.ListenAndServe(":3000", mcp.Options{
    Registry: reg,
    Auth:     authProvider,
    Scopes: map[string][]string{
        "tasks.TaskService.Create": {"tasks:write"},
        "tasks.TaskService.Delete": {"tasks:admin"},
        "billing.Billing.Charge":   {"billing:admin"},
    },
})

Scope Enforcement

When a tool is called:

  1. Gateway checks if the tool has required scopes
  2. If scopes are defined, the caller’s token must include at least one matching scope
  3. A token with scope * has unrestricted access (admin)
  4. If no scopes are defined for a tool, any authenticated token can call it
  5. Denied calls return 403 Forbidden

Common Scope Patterns

PatternUse Case
service:readRead-only access to a service
service:writeCreate and update operations
service:adminDelete and destructive operations
*Full admin access (use sparingly)
internalInternal-only tools not exposed to external agents

Token Examples

Token A: scopes=["tasks:read"]
  ✅ Can call TaskService.Get, TaskService.List
  ❌ Cannot call TaskService.Create, TaskService.Delete

Token B: scopes=["tasks:read", "tasks:write"]
  ✅ Can call Get, List, Create, Update
  ❌ Cannot call TaskService.Delete (needs tasks:admin)

Token C: scopes=["*"]
  ✅ Can call everything (admin)

Rate Limiting

Prevent abuse with per-tool rate limiting using a token bucket algorithm:

mcp.ListenAndServe(":3000", mcp.Options{
    Registry: reg,
    RateLimit: &mcp.RateLimitConfig{
        RequestsPerSecond: 10,  // Sustained rate
        Burst:             20,  // Allow bursts up to 20
    },
})

When the rate limit is exceeded, calls return 429 Too Many Requests.

Choosing Rate Limits

Service TypeRequests/secBurstRationale
Read-heavy API50100High throughput, low cost
Write API1020Moderate, prevents spam
Expensive operation25Protect downstream resources
Internal tool100200Trusted callers, higher limits

Audit Logging

Record every tool call for compliance, debugging, and analytics:

mcp.ListenAndServe(":3000", mcp.Options{
    Registry: reg,
    Auth:     authProvider,
    AuditFunc: func(record mcp.AuditRecord) {
        log.Printf("[AUDIT] tool=%s account=%s allowed=%v duration=%v err=%v",
            record.Tool,
            record.AccountID,
            record.Allowed,
            record.Duration,
            record.Error,
        )
    },
})

AuditRecord Fields

FieldTypeDescription
ToolstringFull tool name (e.g., tasks.TaskService.Create)
AccountIDstringCaller’s account ID from the auth token
Scopes[]stringScopes on the caller’s token
AllowedboolWhether the call was permitted
Durationtime.DurationHow long the call took
ErrorerrorError if the call failed
TraceIDstringUUID trace ID for correlation
DeniedReasonstringWhy the call was denied (empty if allowed)

Production Audit Logging

For production, send audit records to a structured logging system:

AuditFunc: func(r mcp.AuditRecord) {
    // Structured JSON logging
    logger.Info("mcp_tool_call",
        "tool", r.Tool,
        "account", r.AccountID,
        "allowed", r.Allowed,
        "duration_ms", r.Duration.Milliseconds(),
        "trace_id", r.TraceID,
    )

    // Alert on denied calls
    if !r.Allowed {
        alerting.Notify("MCP access denied",
            "tool", r.Tool,
            "account", r.AccountID,
        )
    }
},

Tracing

Every MCP tool call gets a UUID trace ID, propagated via metadata headers:

HeaderDescription
Mcp-Trace-IdUUID for the tool call
Mcp-Tool-NameName of the tool called
Mcp-Account-IdCaller’s account ID

These are available in your handler via context metadata:

func (t *TaskService) Create(ctx context.Context, req *CreateRequest, rsp *CreateResponse) error {
    md, _ := metadata.FromContext(ctx)
    traceID := md["Mcp-Trace-Id"]
    log.Printf("Creating task, trace: %s", traceID)
    // ...
}

OpenTelemetry Integration

For full distributed tracing, plug in an OpenTelemetry trace provider:

import (
    "go.opentelemetry.io/otel"
    "go-micro.dev/v6/gateway/mcp"
)

mcp.ListenAndServe(":3000", mcp.Options{
    Registry:      reg,
    TraceProvider: otel.GetTracerProvider(),
})

Each tool call creates a span (mcp.tool.call) with these attributes:

AttributeExample
mcp.tool.nametasks.TaskService.Create
mcp.transporthttp, websocket, stdio
mcp.account.iduser-123
mcp.trace.ida1b2c3d4-...
mcp.auth.allowedtrue
mcp.auth.denied_reasoninsufficient_scope
mcp.scopes.requiredtasks:write
mcp.rate_limitedfalse

The gateway propagates W3C trace context downstream, so you get end-to-end traces from agent → gateway → service in Jaeger, Zipkin, or any OTel-compatible backend.

WebSocket Authentication

The WebSocket transport supports two authentication methods:

Pass the token in the WebSocket upgrade request:

const ws = new WebSocket("ws://localhost:3000/mcp/ws", {
  headers: { "Authorization": "Bearer <token>" }
});

The token is validated once on connection and applies to all messages on that connection.

Per-Message Auth

For stateless connections, pass a _token parameter with each tool call:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "tasks.TaskService.Create",
    "arguments": {"title": "New task"},
    "_token": "Bearer <token>"
  }
}

Connection-level auth takes precedence over per-message auth.

Production Checklist

Before deploying MCP to production:

  • Auth enabled - Configure an auth.Auth provider
  • Scopes defined - Every write/delete endpoint has required scopes
  • Rate limits set - Appropriate limits for each service type
  • Audit logging active - All calls logged to a persistent store
  • HTTPS/TLS - MCP gateway behind TLS termination
  • Token rotation - Process for rotating compromised tokens
  • Monitoring - Alerts on high error rates or denied calls
  • Testing - Verified scope enforcement with micro mcp test

Full Example

package main

import (
    "log"

    "go-micro.dev/v6"
    "go-micro.dev/v6/auth"
    "go-micro.dev/v6/gateway/mcp"
    "go-micro.dev/v6/server"
)

func main() {
    service := micro.NewService("tasks",
        micro.Address(":8081"),
    )
    service.Init()

    // Register handler with scopes
    handler := service.Server().NewHandler(
        &TaskService{tasks: make(map[string]*Task)},
        server.WithEndpointScopes("TaskService.Get", "tasks:read"),
        server.WithEndpointScopes("TaskService.Create", "tasks:write"),
        server.WithEndpointScopes("TaskService.Delete", "tasks:admin"),
    )
    service.Server().Handle(handler)

    // Start MCP gateway with full security
    go mcp.ListenAndServe(":3000", mcp.Options{
        Registry: service.Options().Registry,
        Auth:     service.Options().Auth,
        Scopes: map[string][]string{
            // Gateway-level overrides
            "billing.Billing.Charge": {"billing:admin"},
        },
        RateLimit: &mcp.RateLimitConfig{
            RequestsPerSecond: 10,
            Burst:             20,
        },
        AuditFunc: func(r mcp.AuditRecord) {
            log.Printf("[AUDIT] tool=%s account=%s allowed=%v duration=%v",
                r.Tool, r.AccountID, r.Allowed, r.Duration)
        },
    })

    service.Run()
}

Next Steps

22 - MCP Troubleshooting

MCP Troubleshooting

Common issues when using the MCP gateway and AI agents with Go Micro services.

Agent Can’t Find My Tools

Symptom: Agent says “no tools available” or doesn’t list your service endpoints.

Check 1: Is the service registered?

# List registered services
micro services

If your service isn’t listed, it hasn’t registered with the registry. Make sure your service is running and using the same registry as the MCP gateway.

Check 2: Is the MCP gateway discovering services?

# List tools the gateway sees
curl http://localhost:3001/mcp/tools | jq

If empty, the gateway can’t reach the registry. Verify both use the same registry address.

Check 3: Are you using the right port?

The MCP gateway runs on its own port (default :3001 with WithMCP), separate from the service RPC port. Make sure you’re querying the MCP port, not the service port.

Tool Calls Return Errors

Symptom: Agent calls a tool but gets an error response.

“service not found”

The MCP gateway found the tool definition but can’t reach the service. The service may have stopped since the gateway cached its tools. Restart the service and try again.

“method not found”

The handler method name doesn’t match what the gateway expects. Ensure your handler is properly registered:

// Correct - registers all methods on the handler
service.Handle(new(MyHandler))

// Or with proto-generated code
pb.RegisterMyServiceHandler(service.Server(), handler.New())

“unauthorized” or “forbidden”

Auth scopes are configured but the agent’s token doesn’t have the required scope. Check your scope configuration:

// Gateway-side scopes
mcp.Options{
    Scopes: map[string][]string{
        "myservice.Users.Delete": {"users:admin"},
    },
}

Verify the agent’s bearer token includes the required scopes.

“rate limited”

The agent is making too many requests. Adjust rate limits:

mcp.Options{
    RateLimit: &mcp.RateLimitConfig{
        RequestsPerSecond: 100,  // Increase if needed
        Burst:             200,
    },
}

Agent Makes Bad Tool Calls

Symptom: Agent calls tools with wrong parameters or misunderstands what a tool does.

This is almost always a documentation problem. Improve your handler doc comments:

// Bad - agent doesn't know what this does
func (s *Users) Get(ctx context.Context, req *GetRequest, rsp *GetResponse) error {

// Good - agent understands purpose, parameters, and format
// Get retrieves a user by their unique ID. Returns the full user profile
// including email, display name, and account status.
//
// @example {"id": "user-123"}
func (s *Users) Get(ctx context.Context, req *GetRequest, rsp *GetResponse) error {

Add description struct tags to your request/response types:

type GetRequest struct {
    ID string `json:"id" description:"User ID in UUID format"`
}

See the Tool Descriptions Guide for detailed best practices.

WebSocket Connection Drops

Symptom: WebSocket connections to ws://localhost:3001/mcp/ws disconnect unexpectedly.

Check 1: Make sure your client sends periodic pings. The WebSocket transport expects heartbeats to detect stale connections.

Check 2: If running behind a reverse proxy (nginx, Caddy), ensure WebSocket upgrade headers are forwarded:

location /mcp/ws {
    proxy_pass http://localhost:3001;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 3600s;
}

Check 3: Check for connection limits. Each WebSocket connection is persistent. If you have many agents, you may need to increase file descriptor limits.

Claude Code Can’t Connect

Symptom: Claude Code doesn’t see your MCP tools after configuring the server.

Check 1: Test stdio transport manually

# This should start and wait for JSON-RPC input
micro mcp serve

If it errors, check that your services are running and the registry is accessible.

Check 2: Verify config syntax

In your Claude Code MCP settings:

{
  "mcpServers": {
    "my-services": {
      "command": "micro",
      "args": ["mcp", "serve"]
    }
  }
}

Common mistakes:

  • Wrong path to micro binary (use absolute path if needed)
  • Missing "serve" in args
  • Service not running when Claude Code starts

Check 3: Check micro is in PATH

which micro

If not found, use the full path in your config:

{
  "mcpServers": {
    "my-services": {
      "command": "/usr/local/bin/micro",
      "args": ["mcp", "serve"]
    }
  }
}

OpenTelemetry Traces Missing

Symptom: MCP gateway calls aren’t showing up in your trace collector.

The gateway only creates real spans when a TraceProvider is configured:

mcp.Options{
    TraceProvider: otel.GetTracerProvider(),
}

Without this, noop spans are used (no traces exported). Make sure you’ve initialized the OpenTelemetry SDK before starting the gateway.

Audit Logs Not Appearing

Symptom: No audit records despite tool calls succeeding.

Audit logging requires an explicit callback:

mcp.Options{
    AuditFunc: func(r mcp.AuditRecord) {
        log.Printf("[audit] tool=%s account=%s allowed=%t duration=%s",
            r.Tool, r.AccountID, r.Allowed, r.Duration)
    },
}

If AuditFunc is nil, no audit records are generated.

Performance Issues

Symptom: MCP tool calls are slow.

Check 1: Network round-trips

Each MCP tool call makes an RPC call to the underlying service. If the service is on a different host, network latency applies. Use micro mcp test to measure raw latency.

Check 2: Service discovery caching

The gateway caches service/tool metadata. If you’re seeing stale data, it’s because of caching. The cache refreshes periodically based on registry TTL.

Check 3: Rate limiting

If rate limits are too low, requests queue up. Check your rate limit configuration.

Still Stuck?

23 - micro run - Local Development

micro run provides a complete development environment for Go microservices.

Note: This guide focuses on micro run features. For a comparison with micro server and gateway architecture details, see the CLI & Gateway Guide.

Quick Start

micro new helloworld
cd helloworld
micro run

Open http://localhost:8080 to see your service.

What You Get

When you run micro run, you get:

URLDescription
http://localhost:8080Web dashboard - browse and call services
http://localhost:8080/agentAgent playground - AI chat with MCP tools
http://localhost:8080/apiAPI explorer - browse endpoints and schemas
http://localhost:8080/api/{service}/{method}API gateway - HTTP to RPC proxy
http://localhost:8080/mcp/toolsMCP tools - list all services as AI tools
http://localhost:8080/auth/tokensToken management - create and manage API tokens
http://localhost:8080/auth/scopesScope management - restrict endpoint access
http://localhost:8080/auth/usersUser management - create and manage users
http://localhost:8080/healthHealth checks - aggregated service health
http://localhost:8080/servicesService list - JSON

Plus:

  • Authentication - JWT auth enabled with default credentials (admin/micro)
  • Hot Reload - File changes trigger automatic rebuild
  • Dependency Ordering - Services start in the right order
  • Environment Management - Dev/staging/production configs
  • MCP Gateway - Optional dedicated MCP protocol listener via --mcp-address

Features

API Gateway

The gateway converts HTTP requests to RPC calls. All API calls require authentication:

# Log in at http://localhost:8080 with admin/micro to get a session
# Or use a token for programmatic access:
curl -X POST http://localhost:8080/api/helloworld/Say.Hello \
  -H "Authorization: Bearer <token>" \
  -d '{"name": "World"}'

# Response
{"message": "Hello World"}

Create tokens at /auth/tokens. The default admin token has * scope (full access).

Agent Playground

The agent playground at /agent lets you interact with your services using AI. Your services are automatically exposed as MCP (Model Context Protocol) tools — no configuration needed.

  1. Open http://localhost:8080/agent
  2. Configure your API key in Agent Settings (supports OpenAI and Anthropic)
  3. Chat with the AI agent — it can discover and call your services as tools

The MCP tools API is available at:

  • /mcp/tools — list all services as AI-callable tools
  • /mcp/call — invoke a tool (service endpoint) by name

For a dedicated MCP protocol listener (for external AI clients), use:

micro run --mcp-address :3000

Hot Reload

By default, micro run watches for .go file changes and automatically rebuilds and restarts affected services.

micro run              # Hot reload enabled (default)
micro run --no-watch   # Disable hot reload

Changes are debounced (300ms) to handle rapid saves from editors.

Configuration File

For multi-service projects, create a micro.mu file to define services, dependencies, and environments.

# Service definitions
service users
    path ./users
    port 8081

service posts
    path ./posts
    port 8082
    depends users

service web
    path ./web
    port 8089
    depends users posts

# Environment configurations
env development
    STORE_ADDRESS file://./data
    DEBUG true

env production
    STORE_ADDRESS postgres://localhost/db
    DEBUG false

micro.json (Alternative)

{
  "services": {
    "users": {
      "path": "./users",
      "port": 8081
    },
    "posts": {
      "path": "./posts",
      "port": 8082,
      "depends": ["users"]
    }
  },
  "env": {
    "development": {
      "STORE_ADDRESS": "file://./data"
    }
  }
}

Service Properties

PropertyRequiredDescription
pathYesDirectory containing the service (with main.go)
portNoPort the service listens on (enables health check waiting)
dependsNoServices that must start first (space-separated in .mu, array in .json)

Dependency Ordering

When depends is specified, services start in topological order:

  1. Services with no dependencies start first
  2. Each service waits for its dependencies to be ready
  3. If a service has a port, we wait for /health to return 200
  4. Circular dependencies are detected and reported as errors

Environment Management

micro run                    # Uses 'development' (default)
micro run --env production   # Uses 'production'
micro run --env staging      # Uses 'staging'
MICRO_ENV=test micro run     # Environment variable override

Environment variables from the config are injected into each service’s environment.

Graceful Shutdown

On SIGINT (Ctrl+C) or SIGTERM:

  1. Services stop in reverse dependency order
  2. SIGTERM is sent first (graceful)
  3. After 5 seconds, SIGKILL if still running
  4. PID files are cleaned up

Without Configuration

If no micro.mu or micro.json exists:

  1. All main.go files are discovered recursively
  2. Each is built and run
  3. No dependency ordering
  4. Hot reload still works

Logs

Service logs are written to:

  • Terminal: Colorized with service name prefix
  • File: ~/micro/logs/{service}-{hash}.log

View logs:

micro logs          # List available logs
micro logs users    # Show logs for 'users' service

Process Management

micro status        # Show running services
micro stop users    # Stop a specific service

Example: micro/blog

The micro/blog project demonstrates a multi-service setup:

# micro.mu
service users
    path ./users
    port 8081

service posts
    path ./posts
    port 8082
    depends users

service comments
    path ./comments
    port 8083
    depends users posts

service web
    path ./web
    port 8089
    depends users posts comments

Run it:

micro run github.com/micro/blog

Options

micro run                        # Gateway on :8080, hot reload
micro run --address :3000        # Custom gateway port
micro run --no-gateway           # Services only, no HTTP gateway
micro run --no-watch             # Disable hot reload
micro run --env production       # Use production environment
micro run --mcp-address :3000    # Enable MCP protocol gateway for AI clients

Tips

  1. Browse First: Open http://localhost:8080 to explore your services
  2. Try the Agent: Open http://localhost:8080/agent to chat with your services via AI
  3. Port Configuration: Set port for services to enable health check waiting
  4. Health Endpoint: Implement /health returning 200 for reliable startup sequencing
  5. Environment Separation: Keep secrets in production env, use file:// paths for development
  6. Hot Reload Scope: Only .go files trigger rebuilds; static assets don’t

24 - Native gRPC Compatibility

This guide explains how to make your Go Micro services compatible with native gRPC clients like grpcurl, grpcui, or clients generated by the standard protoc gRPC plugin in any language.

Understanding Transport vs Server

Go Micro has two different gRPC-related concepts that are often confused:

gRPC Transport (go-micro.dev/v6/transport/grpc)

The gRPC transport uses the gRPC protocol as a communication layer, similar to how you might use NATS, RabbitMQ, or HTTP. It does not guarantee compatibility with native gRPC clients.

// This uses gRPC as transport but is NOT compatible with native gRPC clients
import "go-micro.dev/v6/transport/grpc"

t := grpc.NewTransport()
service := micro.NewService("helloworld",
    micro.Transport(t),
)

When using the gRPC transport:

  • Communication between Go Micro services works fine
  • Native gRPC clients (grpcurl, etc.) will fail with “Unimplemented” errors
  • The protocol is used like a message bus, not as a standard gRPC server

gRPC Server/Client (go-micro.dev/v6/server/grpc and go-micro.dev/v6/client/grpc)

The gRPC server and client provide native gRPC compatibility. These implement a proper gRPC server that any gRPC client can communicate with.

// This IS compatible with native gRPC clients
import (
    "go-micro.dev/v6"
    grpcServer "go-micro.dev/v6/server/grpc"
    grpcClient "go-micro.dev/v6/client/grpc"
)

service := micro.NewService("helloworld",
    micro.Server(grpcServer.NewServer()),
    micro.Client(grpcClient.NewClient()),
)

When to Use Which

Use CaseSolution
Need native gRPC client compatibilityUse gRPC server/client
Need to call service with grpcurlUse gRPC server
Need polyglot gRPC clients (Python, Java, etc.)Use gRPC server
Only Go Micro services communicatingEither works
Want gRPC as a message protocol (like NATS)Use gRPC transport

Complete Example: Native gRPC Compatible Service

Proto Definition

syntax = "proto3";

package helloworld;
option go_package = "./proto;helloworld";

service Say {
    rpc Hello(Request) returns (Response) {}
}

message Request {
    string name = 1;
}

message Response {
    string message = 1;
}

Generate Code

# Install protoc-gen-micro
go install go-micro.dev/v6/cmd/protoc-gen-micro@latest

# Generate Go code
protoc --proto_path=. \
    --go_out=. --go_opt=paths=source_relative \
    --micro_out=. --micro_opt=paths=source_relative \
    proto/helloworld.proto

Server Implementation

package main

import (
    "context"
    "log"

    "go-micro.dev/v6"
    grpcServer "go-micro.dev/v6/server/grpc"
    pb "example.com/helloworld/proto"
)

type Say struct{}

func (s *Say) Hello(ctx context.Context, req *pb.Request, rsp *pb.Response) error {
    rsp.Message = "Hello " + req.Name
    return nil
}

func main() {
    // Create service with gRPC server for native gRPC compatibility
    // Note: Server must be set before Name to ensure the name is applied to the gRPC server
    service := micro.NewService("helloworld",
        micro.Server(grpcServer.NewServer()),
        micro.Address(":8080"),
    )

    service.Init()

    // Register handler
    pb.RegisterSayHandler(service.Server(), &Say{})

    // Run service
    if err := service.Run(); err != nil {
        log.Fatal(err)
    }
}

Client Implementation (Go Micro)

package main

import (
    "context"
    "fmt"
    "log"

    "go-micro.dev/v6"
    grpcClient "go-micro.dev/v6/client/grpc"
    pb "example.com/helloworld/proto"
)

func main() {
    // Create service with gRPC client
    service := micro.NewService("helloworld.client",
        micro.Client(grpcClient.NewClient()),
    )
    service.Init()

    // Create client - use the service name "helloworld" (not the proto package name)
    // Go Micro uses this name for registry lookup, which may differ from the package name
    sayService := pb.NewSayService("helloworld", service.Client())

    // Call service
    rsp, err := sayService.Hello(context.Background(), &pb.Request{Name: "Alice"})
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(rsp.Message) // Output: Hello Alice
}

Testing with grpcurl

Once your service is running with the gRPC server, you can use grpcurl:

# List available services
grpcurl -plaintext localhost:8080 list

# Call the Hello method
grpcurl -proto ./proto/helloworld.proto \
    -plaintext \
    -d '{"name":"Alice"}' \
    localhost:8080 helloworld.Say.Hello

Using Both gRPC Server and Client Together

For full native gRPC compatibility (both inbound and outbound), use both:

package main

import (
    "go-micro.dev/v6"
    grpcClient "go-micro.dev/v6/client/grpc"
    grpcServer "go-micro.dev/v6/server/grpc"
)

func main() {
    service := micro.NewService("helloworld",
        micro.Server(grpcServer.NewServer()),
        micro.Client(grpcClient.NewClient()),
        micro.Address(":8080"),
    )

    service.Init()
    // ... register handlers
    service.Run()
}

Common Errors

“unknown service” Error with grpcurl

If you see this error:

ERROR:
  Code: Unimplemented
  Message: unknown service helloworld.Say

Cause: You’re using the gRPC transport instead of the gRPC server.

Solution: Change from:

// Wrong - uses transport
t := grpc.NewTransport()
service := micro.NewService("helloworld",
    micro.Transport(t),
)

To:

// Correct - uses server
import grpcServer "go-micro.dev/v6/server/grpc"

service := micro.NewService("helloworld",
    micro.Server(grpcServer.NewServer()),
)

Import Path Confusion

Note the different import paths:

// Transport (NOT native gRPC compatible)
import "go-micro.dev/v6/transport/grpc"

// Server (native gRPC compatible)
import "go-micro.dev/v6/server/grpc"

// Client (native gRPC compatible)
import "go-micro.dev/v6/client/grpc"

Service Name vs Package Name

When creating a client to call another service, use the service name passed to micro.NewService, not the proto package name:

// If the server was started with micro.NewService("helloworld", ...)
sayService := pb.NewSayService("helloworld", service.Client())  // Use service name

// NOT the package name from the proto file
// sayService := pb.NewSayService("helloworld.Say", service.Client())  // Wrong!

Go Micro uses the service name for registry lookup, which may differ from the proto package name.

Environment Variable Configuration

You can also configure the server and client via environment variables:

# Use gRPC server
MICRO_SERVER=grpc go run main.go

# Use gRPC client
MICRO_CLIENT=grpc go run main.go

Summary

ComponentImport PathNative gRPC Compatible
Transportgo-micro.dev/v6/transport/grpc❌ No
Servergo-micro.dev/v6/server/grpc✅ Yes
Clientgo-micro.dev/v6/client/grpc✅ Yes

For native gRPC compatibility with tools like grpcurl or polyglot clients, always use the gRPC server and client packages, not the transport.

25 - No-secret first-agent transcript

This is the fastest first-agent success path when you do not have a provider key handy. It starts from the maintained examples/support app and uses the repository harness that CI already runs: real Go Micro services, registry, broker, client, store, agent loop, flow handoff, and guardrail code with only the LLM provider mocked.

Use it before the live-provider Your First Agent walkthrough when you want to see the services → agents → workflows lifecycle run end to end with no secrets.

What this proves

  • Services expose typed customers, tickets, and notify endpoints.
  • The support agent discovers those endpoints as tools and uses them to triage a ticket.
  • The intake flow turns a ticket.created event into an agent run.
  • The approval gate intercepts the customer email action before the tool executes.

Transcript

If you installed the CLI first, ask it for the no-secret path:

micro agent demo

From a fresh clone of the repository, first run the smallest service-backed agent:

git clone https://github.com/micro/go-micro.git
cd go-micro
go run ./examples/first-agent

Then run the maintained support-agent transcript that exercises the full lifecycle:

go run ./examples/support

The default provider is mock, so the command does not need ANTHROPIC_API_KEY, OPENAI_API_KEY, or any other secret. A healthy run prints the event, service calls, guardrail decision, and final support-agent reply in one terminal:

> event: events.ticket.created {"id":"ticket-1","customer":"alice@acme.com",...}

    [customers] looked up Alice (pro plan)
    [tickets]   ticket-1 → priority=high status=in_progress
    ▣ approval gate notify_NotifyService_Send(alice@acme.com) — approved
    [notify]    📨 to=alice@acme.com: "Hi Alice — thanks for reaching out..."

support agent: Hi Alice — thanks for reaching out...

✓ ticket triaged and the customer was replied to — triggered by an event

That single run is the no-secret version of the first-agent loop: a service capability exists, an agent calls it as a tool, and workflow infrastructure can trigger and inspect the work.

CI-backed check

Run the same deterministic paths as focused tests:

go test ./examples/first-agent -run TestRunFirstAgent -count=1
go test ./examples/support -run TestRunSupportMockSmoke -count=1

For the broader no-secret contract that also checks scaffold, chat/inspect CLI boundaries, flow history, deploy dry-run, and mock provider conformance, run:

make harness

Equivalent scaffold → run → chat → inspect path

When you are ready to build the smaller live-agent version yourself, follow Your First Agent. The command shape is the same, but a live micro chat turn needs a provider key because the model is no longer mocked:

micro agent preflight
micro run
micro chat assistant
micro inspect agent assistant

CI keeps those CLI boundaries present with:

go test ./cmd/micro -run TestFirstAgentWalkthroughCLIBoundaries -count=1
go test ./internal/harness/zero-to-hero-ci -run TestNoSecretFirstAgentDebuggingSmoke -count=1

Debug transcript checkpoint

A successful first chat turn should always leave an inspectable trail. After the chat command finishes, continue the same terminal transcript with the inspection and history commands before changing prompts or provider settings:

micro chat assistant --prompt "Triage ticket-1 for Alice"
micro inspect agent assistant --limit 1
micro agent history assistant

The inspection output is the checkpoint that the runnable loop did not stop at chat: it should show a recent agent run with a status, event count, last event, and trace breadcrumb when tracing is configured. micro agent history assistant then confirms the conversation memory that future turns will reuse. If either command is empty after a successful chat turn, keep the failing transcript and use Debugging your agent to check provider failures, run history, memory, and tool-call inspection before changing application code.

If micro agent preflight reports a missing provider key, you can still use this no-secret path because it runs against the mock model; the command now prints this guide as the next step for that failure. If chat behaves unexpectedly, continue to Debugging your agent for provider checks, run history, memory, and tool-call inspection.

26 - Payments (x402)

Go Micro can require a payment before a tool runs, using x402 — the open HTTP 402 Payment Required standard for stablecoin payments, designed for AI agents and onchain APIs. It lets every Go Micro endpoint, already exposed as an AI-callable tool, become a paid tool: a service answers a call with 402 and payment requirements, the client pays and retries, and the gateway verifies the payment before serving.

Payments are opt-in and dependency-light. Go Micro carries no chain or crypto code — it speaks the protocol and delegates verification and settlement to a pluggable facilitator (Coinbase CDP, Alchemy, or self-hosted), so Base and Solana are just different facilitators behind one interface.

The wrapper

The core is HTTP middleware in go-micro.dev/v6/wrapper/x402:

import "go-micro.dev/v6/wrapper/x402"

pay := x402.Middleware(x402.Config{
    PayTo:          "0xYourAddress",            // where payments go (required)
    Network:        "base",                     // or "solana", ...
    Amount:         "10000",                    // smallest units, e.g. 0.01 USDC
    FacilitatorURL: "https://facilitator.example",
})
mux.Handle("/paid", pay(handler))

A request with no X-PAYMENT header gets a 402 with the requirements; once a payment verifies through the facilitator, the request is served (with settlement details on the X-PAYMENT-RESPONSE header).

Pluggable facilitator

Config.Facilitator is an interface; the default is an HTTPFacilitator pointed at FacilitatorURL. Implement your own to target any chain or hosted service:

type Facilitator interface {
    Verify(ctx context.Context, payment string, req Requirements) (Result, error)
}

At the MCP gateway

Because every endpoint is already an MCP tool, the gateway is where you charge. Payments are wired into both micro mcp serve and the standalone micro-mcp-gateway, gated on /mcp/call (listing tools and health stay free), and off unless you set a pay-to address.

micro mcp serve --address :3000 \
    --x402_pay_to 0xYourAddress \
    --x402_network solana \
    --x402_amount 10000 \
    --x402_facilitator https://facilitator.example

A shoppable catalog

When payments are enabled, /mcp/tools advertises each priced tool’s payment requirements, so an agent can see the cost before calling and choose by price — the catalog is shoppable, not just discoverable:

{
  "tools": [
    { "name": "weather.Weather.Forecast", "description": "...",
      "payment": { "amount": "10000", "network": "solana", "asset": "USDC", "payTo": "0x…" } },
    { "name": "time.Time.Now", "description": "..." }
  ]
}

Free tools carry no payment block. This is the foundation for a tool marketplace: offering a tool is registering a priced service; using it is list → choose → call → pay.

Per-tool amounts

Different tools can cost different amounts. Pricing is an operator concern — the payTo address is the operator’s, and amounts change without redeploying anyone’s service — so it’s configured at the gateway with a file, the same way per-tool scopes and rate limits are. Point the gateway at an x402 config:

micro mcp serve --address :3000 --x402_config x402.json
{
  "payTo": "0xYourAddress",
  "network": "solana",
  "asset": "USDC",
  "amount": "0",
  "amounts": {
    "weather.Weather.Forecast": "10000",
    "search.Search.Query": "5000"
  }
}

amount is the default (here "0" — free unless priced), and amounts sets per-tool overrides keyed by tool name. There is no “pricing” abstraction; it’s the x402 amount, resolved per tool, in the protocol’s own vocabulary. micro mcp serve accepts the file via --x402_config; the standalone gateway accepts the same file via --x402-config or the X402_CONFIG environment variable.

Paying for tools (the consumer side)

The counterpart to the server middleware is x402.Client — an HTTP client that settles 402 challenges automatically, up to a spend budget. This is the safety piece for an autonomous caller: it pays what a tool requires, but refuses (before paying) once a call would exceed the budget.

c := &x402.Client{
    Payer:  myWallet,   // constructs the payment payload (signs with a wallet)
    Budget: 1_000_000,  // max total spend in the asset's smallest unit (0 = unlimited)
}

resp, err := c.Do(req) // a 402 is paid and retried; over-budget calls error instead

Payer is an interface (Pay(ctx, Requirements) (payment string, error)) — the consumer counterpart to Facilitator. The budget accumulates across calls, so a long-running agent can be handed a fixed allowance for a task. Budget is reserved before payment is created, which means parallel paid calls cannot race past the cap; if payment creation or verification fails, the reservation is released.

Agent-level spend guardrail

For unattended agents, set the same cap at the agent tool-execution layer so paid tools are refused before their handler — and therefore before a payer — can run:

agent := micro.NewAgent("buyer",
    micro.AgentMaxSteps(8),
    micro.AgentMaxSpend(20_000), // per Ask, smallest units
    micro.AgentToolSpend("weather.Weather.Forecast", 10_000),
)

AgentMaxSpend is disabled by default (0). AgentToolSpend records the price discovered from your shoppable MCP/x402 catalog for the tools this agent may call. When a call would exceed the per-run allowance, the result is a normal structured guardrail refusal with Refused: "spend_budget" and an explanatory error in the run timeline/inspect output, distinct from provider/model failures.

Live facilitator conformance

The regular test suite uses in-process facilitators and does not need network credentials. To smoke-test a hosted facilitator, run the opt-in live conformance test with a real payment payload and matching requirements:

GO_MICRO_X402_LIVE_FACILITATOR_URL=https://facilitator.example \
GO_MICRO_X402_LIVE_PAYMENT='...' \
GO_MICRO_X402_LIVE_PAY_TO=0xYourAddress \
GO_MICRO_X402_LIVE_NETWORK=base \
GO_MICRO_X402_LIVE_AMOUNT=1 \
go test ./wrapper/x402 -run TestLiveFacilitatorConformance -count=1

Leave those variables unset in normal CI; the live test skips unless the facilitator URL, payment payload, and pay-to address are all provided.

Notes

  • Opt-in. No pay-to address (and no config), no payments — nothing changes.
  • No crypto in the framework. The facilitator does verification and settlement on-chain; Go Micro speaks HTTP.
  • A paying agent needs a budget. Use AgentMaxSpend plus AgentToolSpend next to MaxSteps and ApproveTool so a run has an explicit allowance before any paid tool can execute.

See also

AP2 payment mandates

AP2 can authorize an x402 payment without making A2A carry settlement state. A payment mandate records the buyer intent and names an x402 rail reference; the existing x402 facilitator remains responsible for payment verification and settlement. This keeps AP2 as the signed mandate/audit layer while x402 stays the pluggable payment rail.

27 - Plan & Delegate

Every Go Micro agent has two built-in capabilities, on top of the service tools it discovers:

  • plan — record an ordered plan in memory before doing multi-step work.
  • delegate — hand a self-contained subtask to another agent.

They are exposed to the model as ordinary tools. There is no separate graph runtime to configure — these harness capabilities are tools, and the agent calls them the same way it calls a service endpoint. They are added automatically to every agent, so you don’t wire anything up. micro chat exposes them too, so you get planning and delegation even when talking to your services directly.

Prerequisites

  • Go 1.24+
  • An API key for any supported provider (Anthropic, OpenAI, Gemini, Groq, Mistral, Together, Atlas Cloud)
export ANTHROPIC_API_KEY=sk-ant-...

Smallest possible agent

An agent doesn’t need any services to plan — plan and delegate are always available.

package main

import (
	"context"
	"fmt"
	"os"

	"go-micro.dev/v6"
)

func main() {
	a := micro.NewAgent("assistant",
		micro.AgentProvider("anthropic"),
		micro.AgentAPIKey(os.Getenv("ANTHROPIC_API_KEY")),
	)

	resp, err := a.Ask(context.Background(),
		"Plan how to launch a product, then carry out what you can.")
	if err != nil {
		panic(err)
	}
	fmt.Println(resp.Reply)
}

Save it in a fresh module and run:

mkdir my-agent && cd my-agent
go mod init my-agent
go get go-micro.dev/v6
# save the code above as main.go
export ANTHROPIC_API_KEY=sk-ant-...
go run main.go

The agent records its plan with the plan tool, then works through it. The plan is saved to the agent’s store-backed memory and shown back to it on later turns, so it stays oriented across a long task.

plan

The model calls plan with an ordered list of steps, each with a task and a status (pending, in_progress, done):

{
  "steps": [
    {"task": "draft the announcement", "status": "in_progress"},
    {"task": "schedule the email",     "status": "pending"},
    {"task": "publish the blog post",  "status": "pending"}
  ]
}

The plan is persisted under agent/{name}/plan in the store — file-backed by default, Postgres or NATS KV in production — and re-injected into the system prompt on subsequent turns. Memory survives restarts.

You don’t have to do anything to enable this. Nudge the agent to use it from the prompt when you want disciplined multi-step behaviour:

micro.AgentPrompt("For multi-step requests, call the plan tool first to record your steps, then carry them out.")

delegate

delegate hands a self-contained subtask to another agent. It resolves delegate-first:

  1. If to names a registered agent that owns the relevant services, the subtask is sent to it over RPC (Agent.Chat). The domain expert handles its own services.
  2. Otherwise a focused, short-lived sub-agent is created for the subtask with a fresh, isolated context, asked the task, and torn down.

A sub-agent is just an agent — created with New, talked to with Ask. There is no separate “spawn” or “fork” concept to learn. Ephemeral sub-agents load and persist no history and have no built-in tools, so they can’t plan or re-delegate — which keeps delegation from recursing.

{
  "task": "Notify owner@acme.com that the launch plan is ready",
  "to": "comms"
}

This is how intelligence stays distributed: an agent doesn’t need to know how to do everything, only who does. It mirrors how Go Micro already works — agents are services, and services call each other over RPC.

A multi-agent example

Two services (task, notify) and two agents. The conductor owns task; comms owns notify. Asked to create tasks and notify someone, the conductor plans the work, creates the tasks with its own tools, then delegates the notification to comms — which, being a registered agent, receives the hand-off over RPC.

comms := micro.NewAgent("comms",
	micro.AgentServices("notify"),
	micro.AgentPrompt("You handle outbound notifications."),
	micro.AgentProvider("anthropic"),
	micro.AgentAPIKey(key),
)
go comms.Run()

conductor := micro.NewAgent("conductor",
	micro.AgentServices("task"),
	micro.AgentPrompt(
		"For multi-step requests, call the plan tool first. "+
			"For notifications, delegate to the \"comms\" agent (to: \"comms\")."),
	micro.AgentProvider("anthropic"),
	micro.AgentAPIKey(key),
)

resp, _ := conductor.Ask(ctx,
	"Create three launch tasks: Design, Build, and Ship. "+
		"Then make sure owner@acme.com is notified that the launch plan is ready.")

A typical run:

→ plan({"steps":[{"task":"create Design task","status":"pending"}, ...]})
→ task_TaskService_Add({"title":"Design"})
→ task_TaskService_Add({"title":"Build"})
→ task_TaskService_Add({"title":"Ship"})
→ delegate({"task":"Notify owner@acme.com that the launch plan is ready","to":"comms"})
  📨 notify: to=owner@acme.com message="The launch plan is ready"

The full, runnable code is in examples/agent-plan-delegate.

When to use what

You want…Use
The agent to stay on track over a long, multi-step taskplan
One domain expert to handle its own servicesdelegate with to set to that agent
A focused helper for a one-off subtask, with its own clean contextdelegate with no matching agent (ephemeral sub-agent)

How it fits

plan and delegate don’t add a new layer to the framework — they’re tools, the same primitive everything else uses. That’s deliberate: services are the only abstraction, the LLM calls them as tools, and an agent’s own capabilities are no exception.

28 - Provider Conformance Matrix

Go Micro treats model providers as interchangeable pieces of the same agent harness: services expose tools, agents reason over them, and workflows stitch the work together. The conformance harness keeps that promise honest by running the same deterministic services → agents → workflows scenarios against every configured provider.

The live harness is in internal/harness/provider-conformance. It skips providers without API keys by default, so it is safe to run locally, and it fails when any configured provider breaks the shared contract.

go run ./internal/harness/provider-conformance

For a no-key smoke test of the same harness wiring, run the mock provider:

go run ./internal/harness/provider-conformance -providers mock

Status legend

StatusMeaning
✅ VerifiedCovered by the provider-conformance harness for configured live providers.
⚠️ UnverifiedImplemented in the public API, but not yet exercised by provider conformance.
— UnsupportedNot exposed by that provider integration today.

Harness coverage by capability

These rows describe what the conformance harness verifies today. A provider is considered conformant when the configured-key run passes all selected harnesses.

CapabilityHarness coverageNotes
Simple generation✅ VerifiedEach harness asks the provider to produce an agent response through ai.Model.
Service tool calls✅ VerifiedHarness services are discovered and invoked as model-selected tools.
Multi-step tool use✅ VerifiedThe universe and plan-delegate harnesses require more than one service/tool action.
plan✅ Verifiedplan-delegate verifies that the conductor agent stores a plan in scoped state.
delegate✅ Verifiedplan-delegate verifies agent-to-agent delegation over real RPC.
Guardrail/stop behavior✅ Verifieduniverse runs with guardrails enabled and asserts the guarded path completes.
Streaming⚠️ Unverifiedai.Model.Stream exists on the interface, but end-to-end streaming conformance is a roadmap item.
Structured errors⚠️ UnverifiedError handling is covered by normal test suites, but provider conformance does not yet compare structured provider errors.

Provider capability matrix

This matrix combines the registered provider interfaces with the conformance coverage above. The chat/text column is the harness path: when the provider has a configured key, the conformance command exercises the verified rows in the previous section.

ProviderChat/text agent harnessImageVideoStreamingStructured errors
anthropic✅ Verified when configured— Unsupported— Unsupported✅ Verified when configured⚠️ Unverified
openai✅ Verified when configured✅ Registered— Unsupported⚠️ Unverified⚠️ Unverified
gemini✅ Verified when configured— Unsupported— Unsupported✅ Verified when configured⚠️ Unverified
groq✅ Verified when configured— Unsupported— Unsupported⚠️ Unverified⚠️ Unverified
mistral✅ Verified when configured— Unsupported— Unsupported⚠️ Unverified⚠️ Unverified
together✅ Verified when configured— Unsupported— Unsupported⚠️ Unverified⚠️ Unverified
atlascloud✅ Verified when configured✅ Registered✅ Registered⚠️ Unverified⚠️ Unverified

Running a focused check

Use -providers to select a provider and -harnesses to narrow the scenario:

go run ./internal/harness/provider-conformance \
  -providers openai,anthropic \
  -harnesses agent-flow,plan-delegate

By default missing live-provider keys are reported as skips. Add -require-configured in CI when a selected provider must be present:

go run ./internal/harness/provider-conformance \
  -providers openai \
  -require-configured

The command also prints the registered model, image, and video provider capabilities before running conformance. Disable that with -capabilities=false when you only want pass/fail output.

For automation, add -summary-json to capture the selected providers, harnesses, registered capability rows, and pass/skip/fail results in a stable machine-readable file. Add -capabilities-markdown when you also want a ready-to-publish Markdown support table for release notes, docs, or issue updates:

go run ./internal/harness/provider-conformance \
  -providers mock \
  -summary-json provider-conformance-summary.json \
  -capabilities-markdown provider-capabilities.md

29 - Quick Start

Get up and running with go-micro in under 5 minutes.

Install

go install go-micro.dev/v5/cmd/micro@v5.16.0

Note: Use a specific version instead of @latest to avoid module path conflicts. See releases for the latest version.

Create Your First Service

# Create a new service
micro new helloworld
cd helloworld

# Review the generated code
ls -la

# Run locally with hot reload
micro run

# Test it
curl -X POST http://localhost:8080/api/helloworld/Helloworld.Call \
  -H "Content-Type: application/json" \
  -d '{"name": "World"}'

Next Steps

Common Patterns

RPC Service

package main

import "go-micro.dev/v5"

type Greeter struct{}

func (g *Greeter) Hello(ctx context.Context, req *Request, rsp *Response) error {
    rsp.Message = "Hello " + req.Name
    return nil
}

func main() {
    service := micro.New("greeter")
    service.Handle(new(Greeter))
    service.Run()
}

Pub/Sub Event Handler

import "go-micro.dev/v5"

func main() {
    service := micro.New("subscriber")
    
    // Subscribe to events
    micro.RegisterSubscriber("user.created", service.Server(), 
        func(ctx context.Context, event *UserCreatedEvent) error {
            log.Infof("User created: %s", event.Email)
            return nil
        },
    )
    
    service.Run()
}

Publishing Events

publisher := micro.NewEvent("user.created", client)
publisher.Publish(ctx, &UserCreatedEvent{
    Email: "user@example.com",
})

Get Help

30 - Testing Micro Services

The testing package provides utilities for testing micro services in isolation.

Quick Start

import (
    "testing"
    "go-micro.dev/v6/test"
)

func TestGreeter(t *testing.T) {
    h := test.NewHarness(t)
    defer h.Stop()

    h.Name("greeter").Register(new(GreeterHandler))
    h.Start()

    var rsp HelloResponse
    err := h.Call("GreeterHandler.Hello", &HelloRequest{Name: "World"}, &rsp)
    if err != nil {
        t.Fatal(err)
    }

    if rsp.Message != "Hello World" {
        t.Errorf("expected 'Hello World', got '%s'", rsp.Message)
    }
}

How It Works

The harness creates isolated instances of:

  • Registry - In-memory registry for service discovery
  • Transport - HTTP transport for RPC
  • Broker - In-memory broker for events

This allows your service to run without affecting or being affected by other services.

API

Creating a Harness

h := test.NewHarness(t)
defer h.Stop()  // Always stop to clean up

Configuring

h.Name("myservice")      // Set service name (default: "test")
h.Register(handler)      // Set the handler
h.Start()                // Start the service

Making Calls

// Simple call
err := h.Call("Handler.Method", &request, &response)

// With context
err := h.CallContext(ctx, "Handler.Method", &request, &response)

Assertions

// Check service is running
h.AssertServiceRunning()

// Check call succeeds
h.AssertCallSucceeds("Handler.Method", &req, &rsp)

// Check call fails
h.AssertCallFails("Handler.Method", &req, &rsp)

Advanced Access

// Get the client for custom calls
client := h.Client()

// Get the server
server := h.Server()

// Get the registry
reg := h.Registry()

Example: Testing a User Service

package users

import (
    "context"
    "testing"
    "go-micro.dev/v6/test"
)

type UsersHandler struct {
    users map[string]*User
}

type User struct {
    ID   string
    Name string
}

type CreateRequest struct {
    Name string
}

type CreateResponse struct {
    User *User
}

func (h *UsersHandler) Create(ctx context.Context, req *CreateRequest, rsp *CreateResponse) error {
    user := &User{ID: "123", Name: req.Name}
    h.users[user.ID] = user
    rsp.User = user
    return nil
}

func TestUsersCreate(t *testing.T) {
    h := test.NewHarness(t)
    defer h.Stop()

    handler := &UsersHandler{users: make(map[string]*User)}
    h.Name("users").Register(handler)
    h.Start()

    var rsp CreateResponse
    h.AssertCallSucceeds("UsersHandler.Create", &CreateRequest{Name: "Alice"}, &rsp)

    if rsp.User == nil {
        t.Fatal("user is nil")
    }
    if rsp.User.Name != "Alice" {
        t.Errorf("expected Alice, got %s", rsp.User.Name)
    }

    // Verify the user was stored
    if _, ok := handler.users["123"]; !ok {
        t.Error("user not stored in handler")
    }
}

Limitations

Due to go-micro’s global defaults, each harness should test one service. If you need to test service-to-service communication, consider:

  1. Integration tests - Run services as separate processes
  2. Mock clients - Mock the client calls to dependent services
  3. Contract tests - Test service interfaces separately

Tips

  1. Always defer Stop() - Ensures cleanup even if test fails
  2. Use meaningful names - h.Name("users") makes logs clearer
  3. Test edge cases - Use AssertCallFails for error paths
  4. Keep handlers simple - Complex handlers are harder to test

31 - The Agent Harness

The first wave of agent frameworks solved one problem: put a model in a loop with some tools. The harder problem is operating that loop — and that’s what a harness is.

A harness is the runtime around an agent:

  • the tools it can call,
  • the memory it keeps,
  • the guardrails that bound it,
  • the workflows that trigger and structure it,
  • the state that survives a restart,
  • the observability to see what it did,
  • the services it depends on,
  • and the protocols other agents use to reach it.

Go Micro’s bet is that this runtime is the one you already deploy. An agent is a service with a model inside; the harness is the distributed-systems machinery services already have. So you don’t bolt a separate orchestration product onto your stack — the harness is the stack.

The pieces, and what they map to

Harness concernIn Go MicroStatus
ToolsEvery service endpoint is an MCP-callable tool from registry metadata — no extra codeShipped
MemoryStore-backed agent memory (AgentMemory), durable across restartsShipped
GuardrailsMaxSteps, LoopLimit, ApproveTool, tool wrappers — enforced at the call siteShipped
WorkflowsDurable flows; micro.FlowLoop for run-until-doneShipped
Planning / delegationBuilt-in plan and delegate tools on every agentShipped
Discovery & RPCRegistry + client; agents and services find and call each otherShipped
InteropMCP (tools), A2A (agents), x402 (paid tools)Shipped
ResiliencePer-call timeout with context propagation; opt-in retry/backoff (ModelRetry) across the loopShipped
Durable runsCheckpoint and resume an agent run with the same checkpoint backend flows useShipped
ObservabilityRunInfo → OpenTelemetry spans for runs, model calls, tools, delegation, and failures; persisted run historyShipped
Streamingai.Stream through chat, agent, and A2AIn progress

The “in progress” rows are exactly the roadmap’s Now and Next, and the work is happening in the open.

Durable agent runs

Agents can persist their execution history to the same Checkpoint backend as flows. A checkpointed Ask records the run id, original prompt, model result, and completed tool calls. If the process restarts after a tool succeeds but before the model finishes, AgentResume continues the same run and returns the recorded tool result instead of re-running the side effect. If a run already completed, resume returns the persisted response without calling the model.

agent := micro.NewAgent("conductor",
    micro.AgentProvider("anthropic"),
    micro.AgentWithCheckpoint(checkpoint),
)

resp, err := agent.Ask(ctx, "charge order 42 and send a receipt")
if err != nil {
    // On startup, or after a transient failure, discover unfinished work:
    pending, _ := micro.AgentPending(ctx, agent)
    for _, run := range pending {
        _, _ = micro.AgentResume(ctx, agent, run.ID)
    }
}
_ = resp

Choose the boundary deliberately: use a durable flow when the steps are known (reserve, charge, confirm) and each step has deterministic retry/resume semantics. Use a checkpointed agent run when the model is deciding which tools to call or how many turns it needs, but the side effects of completed tool calls still need crash-safe resume. Flows and agents share the same Checkpoint interface, so a flow can safely dispatch to a checkpointed agent for the open-ended part.

For human-in-the-loop runs that pause through the built-in request_input tool, resume with the operator’s response:

_, err := micro.AgentResumeInput(ctx, agent, runID, "Deploy to us-east-1")

Observing agent runs

Pass an OpenTelemetry tracer provider when you construct an agent to turn the agent’s RunInfo into spans:

agent := micro.NewAgent("conductor",
    micro.AgentProvider("anthropic"),
    micro.AgentTraceProvider(otel.GetTracerProvider()),
)

A traced Ask emits a parent agent.run span plus child spans for agent.model.call and agent.tool.call. Delegate tool calls are marked with agent.delegate=true; ephemeral sub-agents start their own agent.run span with agent.run.parent_id set to the delegating run, so a trace shows the hand-off from service-like agent to sub-agent. Failure and refusal outcomes set error status on the relevant span and are also recorded in the persisted run timeline.

Important span attributes include:

AttributeMeaning
agent.run.idStable run correlation ID surfaced as ai.RunInfo.RunID
agent.run.parent_idParent run for delegated sub-agent work
agent.nameAgent that owns the run or call
agent.model.provider / agent.model.nameProvider and configured model for model calls
agent.tool.nameTool invoked by the model
agent.delegateWhether the tool call is a delegation boundary
agent.latency_msElapsed time for the run/call
agent.tokens.*Token usage when the provider reports it

Why services are the right substrate

An agent that does real work needs typed, discoverable, callable capabilities — which is what a service is. The harness is credible because of the service layer, not in spite of it:

  • Tools are services — endpoint metadata becomes the tool schema; an RPC executes the call.
  • Agents are services — they register, load-balance, expose Agent.Chat, and are reachable by other agents.
  • Workflows are code paths — use a flow when the path is known; hand off to an agent when it isn’t.
  • Safety lives at execution — guardrails run on the one path every tool call takes.

When to reach for it

Use Go Micro when the agent has to operate a system, not just answer a prompt — when it needs real tools, state that survives, limits you can enforce, and a way to be seen and called. If you only need a model in a loop, you don’t need a harness. When that loop has to touch production, you do.

See also

32 - Your First Agent

This walkthrough builds the smallest useful Go Micro agent path: one service with typed endpoints, one agent scoped to that service, and one CLI conversation that proves the agent can use the service as a tool. It is the 0→1 version of the services → agents → workflows lifecycle: build capability first, add intelligence on top, then keep a clear path toward flows when the work needs to run on events or schedules.

Runnable reference first

If you want to run the lifecycle before copying code, start with the no-secret first-agent transcript or run the maintained support-desk example from the repository root:

go run ./examples/support

It uses a deterministic mock model by default, so it needs no provider key, and it exercises the same shape this guide teaches: services become tools, an agent uses them, and a flow can trigger the work. Use the transcript for expected output, then use this guide when you are ready to build the smaller 0→1 version yourself.

What you’ll build

A tiny task assistant:

  1. A task service exposes Create and List endpoints.
  2. An assistant agent is scoped to the task service.
  3. micro run starts both in the local harness.
  4. micro chat asks the agent to create and list tasks.

The same service endpoints are normal RPC methods, dashboard/API actions, MCP tools, and agent tools. You do not write a second integration layer for the agent.

Prerequisites

  • Go 1.24 or newer.
  • The micro CLI installed.
  • An LLM provider key for live agent calls. For example:
export ANTHROPIC_API_KEY=sk-ant-...

Plain service calls work without a model key; the key is only needed when the agent reasons over tools.

Run the read-only first-agent preflight before starting the walkthrough. The same CLI boundary is covered by CI with go test ./cmd/micro -run TestFirstAgentWalkthroughCLIBoundaries -count=1, and the copy/paste tutorial code is built from a clean temporary workspace with go test ./internal/harness/zero-to-hero-ci -run TestYourFirstAgentTutorialSmoke -count=1, so the documented scaffold → run → chat → inspect path stays visible in the local harness:

micro agent preflight

It checks Go 1.24+, the micro binary, provider-key setup, and the default local gateway port without contacting a provider. Failed checks include a Fix: line and a Next: line that points back to this guide, the no-secret walkthrough, or the debugging guide. Use it before micro run; if micro run is already active but micro chat, the /agent gateway, registration, provider settings, or inspect history is failing, run the after-run recovery check instead:

micro agent doctor

1. Create a workspace

mkdir first-agent
cd first-agent
go mod init example.com/first-agent
go get go-micro.dev/v6@v6

Add main.go:

package main

import (
	"context"
	"fmt"
	"os"
	"sync"

	micro "go-micro.dev/v6"
)

type CreateRequest struct {
	Title string `json:"title"`
}

type CreateResponse struct {
	ID    string `json:"id"`
	Title string `json:"title"`
}

type ListRequest struct{}

type ListResponse struct {
	Tasks []CreateResponse `json:"tasks"`
}

type TaskService struct {
	mu    sync.Mutex
	next  int
	tasks []CreateResponse
}

// Create adds a task to the list.
// @example {"title":"Write first agent guide"}
func (t *TaskService) Create(ctx context.Context, req *CreateRequest, rsp *CreateResponse) error {
	t.mu.Lock()
	defer t.mu.Unlock()

	t.next++
	*rsp = CreateResponse{ID: fmt.Sprintf("task-%d", t.next), Title: req.Title}
	t.tasks = append(t.tasks, *rsp)
	return nil
}

// List returns all known tasks.
// @example {}
func (t *TaskService) List(ctx context.Context, req *ListRequest, rsp *ListResponse) error {
	t.mu.Lock()
	defer t.mu.Unlock()

	rsp.Tasks = append([]CreateResponse(nil), t.tasks...)
	return nil
}

func main() {
	service := micro.NewService("task")
	service.Handle(new(TaskService))

	agent := micro.NewAgent("assistant",
		micro.AgentServices("task"),
		micro.AgentPrompt("You help manage tasks. Use the task service before answering."),
		micro.AgentProvider("anthropic"),
		micro.AgentAPIKey(os.Getenv("ANTHROPIC_API_KEY")),
	)

	go agent.Run()
	service.Run()
}

Why the comments matter: endpoint comments and @example tags become tool descriptions, so the agent has enough context to choose task.Create and task.List correctly.

2. Run the service and agent

From the same directory:

micro run

The local harness starts the service, gateway, dashboard, MCP tool surface, and agent playground. You can also verify the service directly before involving the agent:

micro call task TaskService.Create '{"title":"Ship the walkthrough"}'
micro call task TaskService.List '{}'

3. Chat with the agent

In another terminal, ask the agent to use the service:

micro chat assistant

Try:

Create a task called "Review the first-agent walkthrough", then show me all tasks.

A healthy run shows the agent calling the task service and then summarizing the result. Inspect the recorded run when you want to see the tool calls, memory, and timing behind the answer:

micro inspect agent assistant

If inspect shows stage=input-required, provide the missing value and inspect the completed run from the same local store:

micro agent resume-input assistant <run-id> --input "Approve the next step"
micro inspect agent assistant --limit 1

If the model refuses to call tools, tighten the prompt so it explicitly uses the task service before answering.

4. Know what just happened

  • The service registered typed RPC endpoints.
  • Go Micro derived tool descriptions from the endpoint names, comments, request fields, and examples.
  • The agent registered as another service with an Agent.Chat endpoint.
  • micro chat sent your message to the agent.
  • The agent selected the scoped task tools, called them over the same runtime, and stored conversation history in memory.

That is the core lifecycle: services provide capability, agents use the capability, and the same runtime can later put the interaction behind a flow.

5. Make it a workflow when the path is event-driven

Once the prompt should run because something happened rather than because a human typed a message, move the handoff into a flow:

flow := micro.NewFlow("task-triage",
	micro.FlowTrigger("tasks.created"),
	micro.FlowPrompt("Review this new task and decide the next action: {{.Data}}"),
	micro.FlowProvider("anthropic"),
	micro.FlowAPIKey(os.Getenv("ANTHROPIC_API_KEY")),
)

Use flows for deterministic triggers and long-running orchestration; keep the agent for judgment, tool use, and handoffs when the path is not known up front.

Troubleshooting

SymptomCheck
The agent says it cannot access tasks.Confirm the agent was created with micro.AgentServices("task") and that micro agent list shows assistant.
Tool calls use the wrong fields.Add or improve doc comments and @example tags on the service methods.
Plain service calls work but chat fails.Check that your provider key is exported in the shell that runs micro run.
You need a no-secret reference path.Run make harness from the Go Micro repository; it exercises the services → agents → workflows lifecycle with a mock provider.

Next steps