Go Micro Logo Go Micro

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:

Prerequisites

go install go-micro.dev/v5/cmd/micro@v5.16.0

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/v5"
    "go-micro.dev/v5/server"
)

func main() {
    service := micro.New("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:

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

Add MCP to your service with a single option:

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

func main() {
    service := micro.New("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/v5/gateway/mcp"
)

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

Each tool call generates a span with attributes:

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/v5/ai"
    _ "go-micro.dev/v5/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:

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