Claude/update docs roadmap f zd2 j (#2872)

* docs: update all four documentation guides and mark Q2 complete

- ai-native-services: add WithMCP one-liner, standalone gateway,
  WebSocket client example, and OpenTelemetry observability section
- mcp-security: add OTel distributed tracing, WebSocket authentication
  (connection-level and per-message), DeniedReason audit field
- tool-descriptions: add manual overrides with WithEndpointDocs and
  export formats section
- agent-patterns: add LangChain/LlamaIndex SDK pattern and standalone
  gateway production pattern with Docker example
- Update roadmap: mark Q2 documentation as complete, Q2 at 100%
- Update status: reflect all recent completions, shift priorities

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add agent demo example and blog post

Add examples/agent-demo with a multi-service project management app
(projects, tasks, team) that demonstrates AI agents interacting with
Go Micro services through MCP. Includes seed data and example prompts.

Add blog post 4 "Agents Meet Microservices: A Hands-On Demo" walking
through the example code and showing cross-service agent workflows.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: enable multiple services in a single binary

Remove global state mutations from service and cmd option functions so
that configuring one service no longer overwrites another's settings.

Key changes:
- service/options.go: remove all DefaultXxx global writes from option
  functions; newOptions() now creates fresh Server, Client, Store, and
  Cache per service while sharing Registry, Broker, and Transport
- cmd/cmd.go: newCmd() uses local copies instead of pointers to package
  globals; Before() no longer mutates DefaultXxx vars
- cmd/options.go: remove global mutations from all option functions
- service/service.go: export ServiceImpl type for cross-package use
- service/group.go: new Group type for multi-service lifecycle
- micro.go: add Start/Stop to Service interface, expose Group and
  NewGroup convenience function
- examples/multi-service: working example with two services

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* docs: highlight multi-service binary support

Add multi-service section to README with code example, update features
list, add to examples index, and note in status summary.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Asim Aslam
2026-03-04 10:50:00 +00:00
committed by GitHub
co-authored by Claude
parent ffe43e0e6d
commit cad0ff1e49
10 changed files with 299 additions and 77 deletions
+1
View File
@@ -186,6 +186,7 @@ Build compelling examples and demos that show agents interacting with go-micro s
- **Kubernetes Operator** - CRD-based deployment
### Recently Completed (March 2026)
- **Multi-Service Binaries** - Run multiple services in a single binary with isolated state per service and shared lifecycle via `service.Group`. Modular monolith pattern: start together, split later.
- **Documentation Guides** - All four guides complete: AI-native services, MCP security, tool descriptions, agent patterns
- **WithMCP Convenience Option** - One-line MCP setup: `mcp.WithMCP(":3000")`
- **Agent Playground Redesign** - Chat-focused UI with collapsible tool calls and real-time status
+25
View File
@@ -45,6 +45,10 @@ in the plugins repo. State and persistence becomes a core requirement beyond pro
- **MCP Integration** - An MCP gateway you can integrate as a library, server or CLI command which automatically exposes services
as tools for agents or other AI applications. Every service/endpoint get's converted into a callable tool.
- **Multi-Service Binaries** - Run multiple services in a single process with isolated state per service. Start as a modular monolith,
split into separate deployments when you need independent scaling. Each service gets its own server, client, and store while sharing
the registry and broker for inter-service communication.
- **Pluggable Interfaces** - Go Micro makes use of Go interfaces for each distributed system abstraction. Because of this these interfaces
are pluggable and allows Go Micro to be runtime agnostic. You can plugin any underlying technology.
@@ -144,11 +148,32 @@ Use `micro mcp serve` for local AI tools like Claude Code, or connect any MCP-co
See the [MCP guide](https://go-micro.dev/docs/mcp.html) for authentication, scopes, and advanced usage.
## Multi-Service Binaries
Run multiple services in a single binary — start as a modular monolith, split into separate deployments later when you actually need to.
```go
users := service.New(service.Name("users"), service.Address(":9001"))
orders := service.New(service.Name("orders"), service.Address(":9002"))
users.Handle(new(Users))
orders.Handle(new(Orders))
// Run all services together with shared lifecycle
g := service.NewGroup(users, orders)
g.Run()
```
Each service gets its own server, client, store, and cache while sharing the registry, broker, and transport — so they can discover and call each other within the same process.
See the [multi-service example](examples/multi-service/) for a working demo.
## Examples
Check out [/examples](examples/) for runnable code:
- [hello-world](examples/hello-world/) - Basic RPC service
- [web-service](examples/web-service/) - HTTP REST API
- [multi-service](examples/multi-service/) - Multiple services in one binary
- [mcp](examples/mcp/) - MCP integration with AI agents
See [all examples](examples/README.md) for more.
+30 -30
View File
@@ -299,20 +299,37 @@ func init() {
}
func newCmd(opts ...Option) Cmd {
// Create local copies so each cmd instance is isolated.
// This allows multiple services in a single binary without
// conflicting through shared global pointers.
localAuth := auth.DefaultAuth
localBroker := broker.DefaultBroker
localClient := client.DefaultClient
localRegistry := registry.DefaultRegistry
localServer := server.DefaultServer
localSelector := selector.DefaultSelector
localTransport := transport.DefaultTransport
localStore := store.DefaultStore
localTracer := trace.DefaultTracer
localProfile := profile.DefaultProfile
localConfig := config.DefaultConfig
localCache := cache.DefaultCache
localStream := events.DefaultStream
options := Options{
Auth: &auth.DefaultAuth,
Broker: &broker.DefaultBroker,
Client: &client.DefaultClient,
Registry: &registry.DefaultRegistry,
Server: &server.DefaultServer,
Selector: &selector.DefaultSelector,
Transport: &transport.DefaultTransport,
Store: &store.DefaultStore,
Tracer: &trace.DefaultTracer,
DebugProfile: &profile.DefaultProfile,
Config: &config.DefaultConfig,
Cache: &cache.DefaultCache,
Stream: &events.DefaultStream,
Auth: &localAuth,
Broker: &localBroker,
Client: &localClient,
Registry: &localRegistry,
Server: &localServer,
Selector: &localSelector,
Transport: &localTransport,
Store: &localStore,
Tracer: &localTracer,
DebugProfile: &localProfile,
Config: &localConfig,
Cache: &localCache,
Stream: &localStream,
Brokers: DefaultBrokers,
Clients: DefaultClients,
@@ -381,13 +398,9 @@ func (c *cmd) Before(ctx *cli.Context) error {
return fmt.Errorf("failed to load local profile: %v", ierr)
}
*c.opts.Registry = imported.Registry
registry.DefaultRegistry = imported.Registry
*c.opts.Broker = imported.Broker
broker.DefaultBroker = imported.Broker
*c.opts.Store = imported.Store
store.DefaultStore = imported.Store
*c.opts.Transport = imported.Transport
transport.DefaultTransport = imported.Transport
case "nats":
imported, ierr := mprofile.NatsProfile()
if ierr != nil {
@@ -428,7 +441,6 @@ func (c *cmd) Before(ctx *cli.Context) error {
// only change if we have the client and type differs
if cl, ok := c.opts.Clients[name]; ok && (*c.opts.Client).String() != name {
*c.opts.Client = cl()
client.DefaultClient = *c.opts.Client
}
}
@@ -437,7 +449,6 @@ func (c *cmd) Before(ctx *cli.Context) error {
// only change if we have the server and type differs
if s, ok := c.opts.Servers[name]; ok && (*c.opts.Server).String() != name {
*c.opts.Server = s()
server.DefaultServer = *c.opts.Server
}
}
@@ -449,7 +460,6 @@ func (c *cmd) Before(ctx *cli.Context) error {
}
*c.opts.Store = s(store.WithClient(*c.opts.Client))
store.DefaultStore = *c.opts.Store
}
// Set the tracer
@@ -460,7 +470,6 @@ func (c *cmd) Before(ctx *cli.Context) error {
}
*c.opts.Tracer = r()
trace.DefaultTracer = *c.opts.Tracer
}
// Setup auth
@@ -487,7 +496,6 @@ func (c *cmd) Before(ctx *cli.Context) error {
}
*c.opts.Auth = r(authOpts...)
auth.DefaultAuth = *c.opts.Auth
}
// Set the registry
@@ -509,7 +517,6 @@ func (c *cmd) Before(ctx *cli.Context) error {
return fmt.Errorf("unsupported profile: %s", name)
}
*c.opts.DebugProfile = p()
profile.DefaultProfile = *c.opts.DebugProfile
}
// Set the broker
@@ -534,7 +541,6 @@ func (c *cmd) Before(ctx *cli.Context) error {
// No server option here. Should there be?
clientOpts = append(clientOpts, client.Selector(*c.opts.Selector))
selector.DefaultSelector = *c.opts.Selector
}
// Set the transport
@@ -687,7 +693,6 @@ func (c *cmd) Before(ctx *cli.Context) error {
logger.Fatalf("Error configuring config: %v", err)
}
*c.opts.Config = rc
config.DefaultConfig = *c.opts.Config
}
}
return nil
@@ -709,7 +714,6 @@ func (c *cmd) setRegistry(r registry.Registry) ([]server.Option, []client.Option
if err := (*c.opts.Broker).Init(broker.Registry(*c.opts.Registry)); err != nil {
logger.Fatalf("Error configuring broker: %v", err)
}
registry.DefaultRegistry = *c.opts.Registry
return serverOpts, clientOpts
}
func (c *cmd) setStream(s events.Stream) ([]server.Option, []client.Option) {
@@ -720,7 +724,6 @@ func (c *cmd) setStream(s events.Stream) ([]server.Option, []client.Option) {
// serverOpts = append(serverOpts, server.Registry(*c.opts.Registry))
// clientOpts = append(clientOpts, client.Registry(*c.opts.Registry))
events.DefaultStream = *c.opts.Stream
return serverOpts, clientOpts
}
@@ -730,7 +733,6 @@ func (c *cmd) setBroker(b broker.Broker) ([]server.Option, []client.Option) {
*c.opts.Broker = b
serverOpts = append(serverOpts, server.Broker(*c.opts.Broker))
clientOpts = append(clientOpts, client.Broker(*c.opts.Broker))
broker.DefaultBroker = *c.opts.Broker
return serverOpts, clientOpts
}
@@ -738,7 +740,6 @@ func (c *cmd) setStore(s store.Store) ([]server.Option, []client.Option) {
var serverOpts []server.Option
var clientOpts []client.Option
*c.opts.Store = s
store.DefaultStore = *c.opts.Store
return serverOpts, clientOpts
}
@@ -748,7 +749,6 @@ func (c *cmd) setTransport(t transport.Transport) ([]server.Option, []client.Opt
*c.opts.Transport = t
serverOpts = append(serverOpts, server.Transport(*c.opts.Transport))
clientOpts = append(clientOpts, client.Transport(*c.opts.Transport))
transport.DefaultTransport = *c.opts.Transport
return serverOpts, clientOpts
}
-13
View File
@@ -84,91 +84,78 @@ func Version(v string) Option {
func Broker(b *broker.Broker) Option {
return func(o *Options) {
o.Broker = b
broker.DefaultBroker = *b
}
}
func Cache(c *cache.Cache) Option {
return func(o *Options) {
o.Cache = c
cache.DefaultCache = *c
}
}
func Config(c *config.Config) Option {
return func(o *Options) {
o.Config = c
config.DefaultConfig = *c
}
}
func Selector(s *selector.Selector) Option {
return func(o *Options) {
o.Selector = s
selector.DefaultSelector = *s
}
}
func Registry(r *registry.Registry) Option {
return func(o *Options) {
o.Registry = r
registry.DefaultRegistry = *r
}
}
func Transport(t *transport.Transport) Option {
return func(o *Options) {
o.Transport = t
transport.DefaultTransport = *t
}
}
func Client(c *client.Client) Option {
return func(o *Options) {
o.Client = c
client.DefaultClient = *c
}
}
func Server(s *server.Server) Option {
return func(o *Options) {
o.Server = s
server.DefaultServer = *s
}
}
func Store(s *store.Store) Option {
return func(o *Options) {
o.Store = s
store.DefaultStore = *s
}
}
func Stream(s *events.Stream) Option {
return func(o *Options) {
o.Stream = s
events.DefaultStream = *s
}
}
func Tracer(t *trace.Tracer) Option {
return func(o *Options) {
o.Tracer = t
trace.DefaultTracer = *t
}
}
func Auth(a *auth.Auth) Option {
return func(o *Options) {
o.Auth = a
auth.DefaultAuth = *a
}
}
func Profile(p *profile.Profile) Option {
return func(o *Options) {
o.DebugProfile = p
profile.DefaultProfile = *p
}
}
+13
View File
@@ -34,6 +34,19 @@ cd web-service
go run .
```
### [multi-service](./multi-service/)
Multiple services in a single binary — the modular monolith pattern:
- Isolated server, client, store, and cache per service
- Shared registry and broker for inter-service communication
- Coordinated lifecycle with `service.Group`
- Start monolith, split later when you need to scale independently
**Run it:**
```bash
cd multi-service
go run .
```
## Coming Soon
The following examples are planned:
+85
View File
@@ -0,0 +1,85 @@
// Multi-service example: run multiple services in a single binary.
//
// Each service gets its own server, client, store, and cache while
// sharing the registry, broker, and transport — so they can
// discover and call each other within the same process.
package main
import (
"context"
"fmt"
"log"
"go-micro.dev/v5/service"
)
// -- Users service --
type UserRequest struct {
Id string `json:"id"`
}
type UserResponse struct {
Name string `json:"name"`
Email string `json:"email"`
}
type Users struct{}
func (u *Users) Lookup(ctx context.Context, req *UserRequest, rsp *UserResponse) error {
log.Printf("[users] Lookup id=%s", req.Id)
rsp.Name = "Alice"
rsp.Email = "alice@example.com"
return nil
}
// -- Orders service --
type OrderRequest struct {
UserId string `json:"user_id"`
}
type OrderResponse struct {
OrderId string `json:"order_id"`
Status string `json:"status"`
}
type Orders struct{}
func (o *Orders) Create(ctx context.Context, req *OrderRequest, rsp *OrderResponse) error {
log.Printf("[orders] Create for user=%s", req.UserId)
rsp.OrderId = "ORD-001"
rsp.Status = "created"
return nil
}
func main() {
// Create two services — each gets isolated server, client,
// store, and cache instances automatically.
users := service.New(
service.Name("users"),
service.Address(":9001"),
)
orders := service.New(
service.Name("orders"),
service.Address(":9002"),
)
// Register handlers
if err := users.Handle(new(Users)); err != nil {
log.Fatal(err)
}
if err := orders.Handle(new(Orders)); err != nil {
log.Fatal(err)
}
// Run both services together. The group handles signals
// and stops all services when one exits.
g := service.NewGroup(users, orders)
fmt.Println("Starting users (:9001) and orders (:9002) in a single binary")
if err := g.Run(); err != nil {
log.Fatal(err)
}
}
+20 -1
View File
@@ -27,12 +27,19 @@ type Service interface {
Client() client.Client
// Server is for handling requests and events
Server() server.Server
// Run the service
// Start the service
Start() error
// Stop the service
Stop() error
// Run the service (start, block on signal, then stop)
Run() error
// The service implementation
String() string
}
// Group is a set of services that share lifecycle management.
type Group = service.Group
type Option = service.Option
type Options = service.Options
@@ -58,6 +65,18 @@ func NewService(opts ...Option) Service {
return service.New(opts...)
}
// NewGroup creates a service group for running multiple services
// in a single binary with shared lifecycle management.
func NewGroup(svcs ...Service) *Group {
var ss []*service.ServiceImpl
for _, s := range svcs {
if si, ok := s.(*service.ServiceImpl); ok {
ss = append(ss, si)
}
}
return service.NewGroup(ss...)
}
// FromContext retrieves a Service from the Context.
func FromContext(ctx context.Context) (Service, bool) {
s, ok := ctx.Value(serviceKey{}).(Service)
+99
View File
@@ -0,0 +1,99 @@
package service
import (
"context"
"os"
"os/signal"
"sync"
log "go-micro.dev/v5/logger"
signalutil "go-micro.dev/v5/util/signal"
)
// Group runs multiple services in a single binary with shared
// lifecycle management. All services start together and stop
// together on signal or context cancellation.
type Group struct {
services []*ServiceImpl
logger log.Logger
}
// NewGroup creates a new service group.
func NewGroup(svcs ...*ServiceImpl) *Group {
return &Group{
services: svcs,
logger: log.DefaultLogger,
}
}
// Add appends one or more services to the group.
func (g *Group) Add(svcs ...*ServiceImpl) {
g.services = append(g.services, svcs...)
}
// Run starts all services concurrently and blocks until a signal
// is received or the context is cancelled, then stops all services.
func (g *Group) Run() error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Initialize all services (parses flags, etc.)
for _, svc := range g.services {
svc.opts.Signal = false // group handles signals
svc.Init()
}
g.logger.Logf(log.InfoLevel, "Starting service group with %d services", len(g.services))
// Start all services concurrently
errCh := make(chan error, len(g.services))
for _, svc := range g.services {
g.logger.Logf(log.InfoLevel, "Starting [service] %s", svc.Name())
if err := svc.Start(); err != nil {
// If any service fails to start, stop the ones that did start
cancel()
g.stopAll()
return err
}
}
// Wait for signal or context cancellation
ch := make(chan os.Signal, 1)
signal.Notify(ch, signalutil.Shutdown()...)
select {
case <-ch:
g.logger.Logf(log.InfoLevel, "Received signal, stopping all services")
case <-ctx.Done():
case err := <-errCh:
cancel()
g.stopAll()
return err
}
return g.stopAll()
}
func (g *Group) stopAll() error {
var (
mu sync.Mutex
lastErr error
)
var wg sync.WaitGroup
for _, svc := range g.services {
wg.Add(1)
go func(s *ServiceImpl) {
defer wg.Done()
g.logger.Logf(log.InfoLevel, "Stopping [service] %s", s.Name())
if err := s.Stop(); err != nil {
mu.Lock()
lastErr = err
mu.Unlock()
}
}(svc)
}
wg.Wait()
return lastErr
}
+7 -17
View File
@@ -56,12 +56,14 @@ func newOptions(opts ...Option) Options {
opt := Options{
Auth: auth.DefaultAuth,
Broker: broker.DefaultBroker,
Cache: cache.DefaultCache,
Cmd: cmd.DefaultCmd,
Cmd: cmd.NewCmd(),
Config: config.DefaultConfig,
Client: client.DefaultClient,
Server: server.DefaultServer,
Store: store.DefaultStore,
// Per-service instances: each service gets its own server, client,
// store, and cache to allow multiple services in a single binary.
Client: client.NewClient(),
Server: server.NewRPCServer(),
Store: store.NewStore(),
Cache: cache.NewCache(),
Registry: registry.DefaultRegistry,
Transport: transport.DefaultTransport,
Context: context.Background(),
@@ -83,14 +85,12 @@ func Broker(b broker.Broker) Option {
// Update Client and Server
o.Client.Init(client.Broker(b))
o.Server.Init(server.Broker(b))
broker.DefaultBroker = b
}
}
func Cache(c cache.Cache) Option {
return func(o *Options) {
o.Cache = c
cache.DefaultCache = c
}
}
@@ -104,7 +104,6 @@ func Cmd(c cmd.Cmd) Option {
func Client(c client.Client) Option {
return func(o *Options) {
o.Client = c
client.DefaultClient = c
}
}
@@ -138,7 +137,6 @@ func HandleSignal(b bool) Option {
func Profile(p profile.Profile) Option {
return func(o *Options) {
o.Profile = p
profile.DefaultProfile = p
}
}
@@ -146,7 +144,6 @@ func Profile(p profile.Profile) Option {
func Server(s server.Server) Option {
return func(o *Options) {
o.Server = s
server.DefaultServer = s
}
}
@@ -154,7 +151,6 @@ func Server(s server.Server) Option {
func Store(s store.Store) Option {
return func(o *Options) {
o.Store = s
store.DefaultStore = s
}
}
@@ -168,7 +164,6 @@ func Registry(r registry.Registry) Option {
o.Server.Init(server.Registry(r))
// Update Broker
o.Broker.Init(broker.Registry(r))
broker.DefaultBroker = o.Broker
}
}
@@ -184,8 +179,6 @@ func Tracer(t trace.Tracer) Option {
func Auth(a auth.Auth) Option {
return func(o *Options) {
o.Auth = a
auth.DefaultAuth = a
}
}
@@ -193,7 +186,6 @@ func Auth(a auth.Auth) Option {
func Config(c config.Config) Option {
return func(o *Options) {
o.Config = c
config.DefaultConfig = c
}
}
@@ -201,7 +193,6 @@ func Config(c config.Config) Option {
func Selector(s selector.Selector) Option {
return func(o *Options) {
o.Client.Init(client.Selector(s))
selector.DefaultSelector = s
}
}
@@ -213,7 +204,6 @@ func Transport(t transport.Transport) Option {
// Update Client and Server
o.Client.Init(client.Transport(t))
o.Server.Init(server.Transport(t))
transport.DefaultTransport = t
}
}
+16 -13
View File
@@ -14,26 +14,29 @@ import (
signalutil "go-micro.dev/v5/util/signal"
)
type service struct {
// ServiceImpl is the concrete service implementation. It is exported
// to allow the micro package to construct Groups, but users should
// generally interact through the Service interface.
type ServiceImpl struct {
opts Options
once sync.Once
}
func New(opts ...Option) *service {
return &service{
func New(opts ...Option) *ServiceImpl {
return &ServiceImpl{
opts: newOptions(opts...),
}
}
func (s *service) Name() string {
func (s *ServiceImpl) Name() string {
return s.opts.Server.Options().Name
}
// Init initializes options. Additionally it calls cmd.Init
// which parses command line flags. cmd.Init is only called
// on first Init.
func (s *service) Init(opts ...Option) {
func (s *ServiceImpl) Init(opts ...Option) {
// process options
for _, o := range opts {
o(&s.opts)
@@ -69,23 +72,23 @@ func (s *service) Init(opts ...Option) {
})
}
func (s *service) Options() Options {
func (s *ServiceImpl) Options() Options {
return s.opts
}
func (s *service) Client() client.Client {
func (s *ServiceImpl) Client() client.Client {
return s.opts.Client
}
func (s *service) Server() server.Server {
func (s *ServiceImpl) Server() server.Server {
return s.opts.Server
}
func (s *service) String() string {
func (s *ServiceImpl) String() string {
return "micro"
}
func (s *service) Start() error {
func (s *ServiceImpl) Start() error {
for _, fn := range s.opts.BeforeStart {
if err := fn(); err != nil {
return err
@@ -105,7 +108,7 @@ func (s *service) Start() error {
return nil
}
func (s *service) Stop() error {
func (s *ServiceImpl) Stop() error {
var err error
for _, fn := range s.opts.BeforeStop {
@@ -123,13 +126,13 @@ func (s *service) Stop() error {
return err
}
func (s *service) Handle(v interface{}) error {
func (s *ServiceImpl) Handle(v interface{}) error {
return s.opts.Server.Handle(
s.opts.Server.NewHandler(v),
)
}
func (s *service) Run() (err error) {
func (s *ServiceImpl) Run() (err error) {
logger := s.opts.Logger
// exit when help flag is provided