Compare commits

...

55 Commits

Author SHA1 Message Date
Asim Aslam defb2786ef update AI provider documentation (#2897)
goreleaser / goreleaser (push) Waiting to run
* feat: add prometheus monitoring wrapper

Reintroduces the Prometheus metrics wrapper previously available in the
plugins repository, updated for go-micro v5. Exposes request count and
latency histograms for handlers, subscribers, and outgoing client calls
via NewHandlerWrapper, NewSubscriberWrapper, NewCallWrapper and
NewClientWrapper, labelled with service/endpoint/status.

Options cover namespace, subsystem, const labels, histogram buckets and
a custom registerer; duplicate collectors (e.g. from multiple wrappers
sharing the same config) are reused transparently via a cached
metrics bundle.

Fixes #2893

* fix(registry/etcd): clear lease/register caches on KeepAlive channel closure

When the etcd client's long-lived KeepAlive channel closes (e.g. because
the lease expired on the server side during a network partition), the
previous cleanup only removed the channel bookkeeping. The stale entries
in `leases` and `register` caused the next registerNode() heartbeat to
hit the "unchanged hash" short-circuit and skip re-registration entirely,
so the service permanently disappeared from etcd.

Extract the cleanup into handleKeepAliveClosed and also drop the cached
lease id and hash so the next heartbeat performs a full Grant+Put and
the service recovers within one RegisterInterval.

Regression introduced by #2822; fix is symmetric with the existing
synchronous KeepAliveOnce recovery path that propagates
rpctypes.ErrLeaseNotFound.

* docs: add AI provider integration guide and Supported AI Providers section

Add a step-by-step guide for AI infrastructure companies to implement
ai.Model and contribute a provider to go-micro. Covers the full
lifecycle: skeleton, tool call handling, tests, registration, and PR
checklist.

Add a "Supported AI Providers" section to the project README that lists
current providers (Anthropic, OpenAI) in a table and links to the
integration guide with a call-to-action for new providers and sponsors.

Streamline the "Adding a New Provider" section in ai/README.md to point
to the new guide instead of duplicating a full code listing.

* Update contribution guidelines in README.md

Removed Discord contact information for platform contributions.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-10 17:14:04 +01:00
Asim Aslam 15cc876848 Emphasize 'Micro' in README link 2026-05-10 17:01:53 +01:00
Asim Aslam d3cfb3a99a Update app platform name from 'Mu' to 'Micro' 2026-05-10 17:01:23 +01:00
Asim Aslam b1680ca2da Update app platform link from Mu.xyz to Micro 2026-05-10 16:21:41 +01:00
Asim Aslam 90f7617fac Claude/fix issue 2893 x3rpd (#2895)
* feat: add prometheus monitoring wrapper

Reintroduces the Prometheus metrics wrapper previously available in the
plugins repository, updated for go-micro v5. Exposes request count and
latency histograms for handlers, subscribers, and outgoing client calls
via NewHandlerWrapper, NewSubscriberWrapper, NewCallWrapper and
NewClientWrapper, labelled with service/endpoint/status.

Options cover namespace, subsystem, const labels, histogram buckets and
a custom registerer; duplicate collectors (e.g. from multiple wrappers
sharing the same config) are reused transparently via a cached
metrics bundle.

Fixes #2893

* fix(registry/etcd): clear lease/register caches on KeepAlive channel closure

When the etcd client's long-lived KeepAlive channel closes (e.g. because
the lease expired on the server side during a network partition), the
previous cleanup only removed the channel bookkeeping. The stale entries
in `leases` and `register` caused the next registerNode() heartbeat to
hit the "unchanged hash" short-circuit and skip re-registration entirely,
so the service permanently disappeared from etcd.

Extract the cleanup into handleKeepAliveClosed and also drop the cached
lease id and hash so the next heartbeat performs a full Grant+Put and
the service recovers within one RegisterInterval.

Regression introduced by #2822; fix is symmetric with the existing
synchronous KeepAliveOnce recovery path that propagates
rpctypes.ErrLeaseNotFound.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-20 14:55:01 +01:00
Asim Aslam 79722e0c27 feat: add prometheus monitoring wrapper (#2894)
Reintroduces the Prometheus metrics wrapper previously available in the
plugins repository, updated for go-micro v5. Exposes request count and
latency histograms for handlers, subscribers, and outgoing client calls
via NewHandlerWrapper, NewSubscriberWrapper, NewCallWrapper and
NewClientWrapper, labelled with service/endpoint/status.

Options cover namespace, subsystem, const labels, histogram buckets and
a custom registerer; duplicate collectors (e.g. from multiple wrappers
sharing the same config) are reused transparently via a cached
metrics bundle.

Fixes #2893

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-15 09:24:43 +01:00
jejefferson 49070afa8c server/grpc: improve graceful stop behavior (#2892)
* server/grpc: improve graceful stop behavior

* server/grpc: add graceful stop example and test

* examples/graceful-stop: document verification steps
2026-04-10 08:10:10 +01:00
Asim Aslam 03f4759474 Fix inline style attribute in index.html 2026-04-08 07:42:12 +01:00
Asim Aslam f6fb541ace Enhance Mu.xyz promotion with styled div
Updated the promotional text for Mu.xyz with enhanced styling.
2026-04-08 07:40:51 +01:00
Asim Aslam 1426c8f349 Update README to use emoji for Mu.xyz link 2026-04-08 07:34:54 +01:00
Asim Aslam 5aedb601ba Add Mu.xyz promotional link to README
Added a promotional link for Mu.xyz app platform.
2026-04-08 07:34:25 +01:00
Asim Aslam bd13975acf Update container width and add promotional text
Increased the maximum width of the container and added a promotional paragraph for Mu.xyz.
2026-04-08 07:33:47 +01:00
Asim Aslam 213a09276e Update default.html 2026-03-26 10:11:00 +00:00
Asim Aslam f4291957e8 Remove Blog link from index.html
Removed the Blog link from the website index.
2026-03-26 10:09:12 +00:00
Asim Aslam 07411b1ef6 Remove article on chat app from blog index
Removed an article about building a chat app from the blog.
2026-03-26 10:08:54 +00:00
Asim Aslam 544496eec6 Delete internal/website/blog/8.md 2026-03-26 10:08:26 +00:00
Asim Aslam 2e95c4c610 Remove showcase section from index.html
Removed the showcase section for Go Micro projects.
2026-03-26 10:08:01 +00:00
BombartSimon 7b785df302 feat: add connection timeout call option (#2891) 2026-03-16 13:34:24 +00:00
Asim Aslam d96f12f57b Claude/update docs roadmap f zd2 j (#2889)
* feat: add agent platform showcase and blog post

Add a complete platform example (Users, Posts, Comments, Mail) that
mirrors micro/blog, demonstrating how existing microservices become
AI-accessible through MCP with zero code changes.

Includes blog post "Your Microservices Are Already an AI Platform"
walking through real agent workflows: signup, content creation,
commenting, tagging, and cross-service messaging.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* refactor: rename handler types to drop redundant Service suffix

UserService → Users, PostService → Posts, CommentService → Comments,
MailService → Mail. Matches micro/blog naming convention.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* refactor: consolidate top-level directories, reduce framework bloat

Move internal/non-public packages behind internal/ or into their
parent packages where they belong:

- deploy/ → gateway/mcp/deploy/ (Helm charts belong with the gateway)
- profile/ → service/profile/ (preset plugin profiles are a service concern)
- scripts/ → internal/scripts/ (install script is not public API)
- test/ → internal/test/ (test harness is not public API)
- util/ → internal/util/ (internal helpers shouldn't be imported externally)

Also fixes CLAUDE.md merge conflict markers and updates project
structure documentation.

All import paths updated. Build and tests pass.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* refactor: redesign model package to match framework conventions

Rename model.Database interface to model.Model (consistent with
client.Client, server.Server, store.Store). Remove generics in
favor of interface{}-based API with reflection.

Key changes:
- model.Model interface: Register once, CRUD infers table from type
- DefaultModel + NewModel() + package-level convenience functions
- Schema registered via Register(&User{}), no per-call schema passing
- Memory implementation as default (in model package, like store)
- memory/sqlite/postgres backends updated for new interface
- protoc-gen-micro generates RegisterXModel() instead of generic factory
- All docs, blog, and README updated

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* docs: clarify blog post 7 uses modular monolith, not multi-service

Blog post 7 demonstrated all handlers in a single process but framed
it as microservices without acknowledging the architectural difference.

- Add "A Note on Architecture" section explaining this is a modular
  monolith demo and pointing to micro/blog for multi-service
- Clarify that handlers can be broken out into separate services later
- Fix "service registry" language to match single-process reality
- Restructure "Adding MCP to Existing Services" to distinguish the
  in-process approach from registry-based gateway options
- Update closing to acknowledge both paradigms
- Fix README type names (&CommentService{} -> &Comments{}, etc.)

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* docs: add Micro Chat to website showcase

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: github artifact release CI (#2886)

* 👷feat(ci): add artifact and docker releases

* 💚fix(ci): build issues

* 💚fix(ci): add permissions

* 💚fix(ci): multiple artifacts

* 💚fix(ci): split archives

* 💚fix(ci): cross platform list

* 🚧chore(ci): package name

* 🐛fix(script): install script extract arch

* 👷fix(ci): docker origin go-micro

* Update image reference in goreleaser configuration (#2887)

Fix wrong order `user/repo`

* Add blog post on building a chat app with Go Micro

Added a blog post detailing the development of a full chat app using Go Micro, outlining features, architecture, and lessons learned.

* docs: add blog post 8 to index, put Blog before Docs on homepage

- Add "We Built a Full Chat App in a Day" (blog/8) to blog index
- Reorder homepage links: Blog first (primary), Docs second
- Rename "Documentation" to "Docs"

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Alexander Serheyev <74361701+alex-dna-tech@users.noreply.github.com>
2026-03-07 13:00:22 +00:00
Asim Aslam 8608c15fbb Add blog post on building a chat app with Go Micro
Added a blog post detailing the development of a full chat app using Go Micro, outlining features, architecture, and lessons learned.
2026-03-07 12:49:39 +00:00
Asim Aslam 9823f6df5c Add platform showcase, blog post, and refactor project structure (#2888)
* feat: add agent platform showcase and blog post

Add a complete platform example (Users, Posts, Comments, Mail) that
mirrors micro/blog, demonstrating how existing microservices become
AI-accessible through MCP with zero code changes.

Includes blog post "Your Microservices Are Already an AI Platform"
walking through real agent workflows: signup, content creation,
commenting, tagging, and cross-service messaging.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* refactor: rename handler types to drop redundant Service suffix

UserService → Users, PostService → Posts, CommentService → Comments,
MailService → Mail. Matches micro/blog naming convention.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* refactor: consolidate top-level directories, reduce framework bloat

Move internal/non-public packages behind internal/ or into their
parent packages where they belong:

- deploy/ → gateway/mcp/deploy/ (Helm charts belong with the gateway)
- profile/ → service/profile/ (preset plugin profiles are a service concern)
- scripts/ → internal/scripts/ (install script is not public API)
- test/ → internal/test/ (test harness is not public API)
- util/ → internal/util/ (internal helpers shouldn't be imported externally)

Also fixes CLAUDE.md merge conflict markers and updates project
structure documentation.

All import paths updated. Build and tests pass.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* refactor: redesign model package to match framework conventions

Rename model.Database interface to model.Model (consistent with
client.Client, server.Server, store.Store). Remove generics in
favor of interface{}-based API with reflection.

Key changes:
- model.Model interface: Register once, CRUD infers table from type
- DefaultModel + NewModel() + package-level convenience functions
- Schema registered via Register(&User{}), no per-call schema passing
- Memory implementation as default (in model package, like store)
- memory/sqlite/postgres backends updated for new interface
- protoc-gen-micro generates RegisterXModel() instead of generic factory
- All docs, blog, and README updated

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* docs: clarify blog post 7 uses modular monolith, not multi-service

Blog post 7 demonstrated all handlers in a single process but framed
it as microservices without acknowledging the architectural difference.

- Add "A Note on Architecture" section explaining this is a modular
  monolith demo and pointing to micro/blog for multi-service
- Clarify that handlers can be broken out into separate services later
- Fix "service registry" language to match single-process reality
- Restructure "Adding MCP to Existing Services" to distinguish the
  in-process approach from registry-based gateway options
- Update closing to acknowledge both paradigms
- Fix README type names (&CommentService{} -> &Comments{}, etc.)

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* docs: add Micro Chat to website showcase

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-07 09:33:18 +00:00
Alexander Serheyev 28f7f53143 Update image reference in goreleaser configuration (#2887)
goreleaser / goreleaser (push) Waiting to run
Fix wrong order `user/repo`
2026-03-06 15:30:53 +00:00
Alexander Serheyev 0842431050 feat: github artifact release CI (#2886)
goreleaser / goreleaser (push) Waiting to run
* 👷feat(ci): add artifact and docker releases

* 💚fix(ci): build issues

* 💚fix(ci): add permissions

* 💚fix(ci): multiple artifacts

* 💚fix(ci): split archives

* 💚fix(ci): cross platform list

* 🚧chore(ci): package name

* 🐛fix(script): install script extract arch

* 👷fix(ci): docker origin go-micro
2026-03-06 14:49:18 +00:00
Asim Aslam 7a5d86a2a4 Claude/update docs roadmap f zd2 j (#2885)
* feat: add agent platform showcase and blog post

Add a complete platform example (Users, Posts, Comments, Mail) that
mirrors micro/blog, demonstrating how existing microservices become
AI-accessible through MCP with zero code changes.

Includes blog post "Your Microservices Are Already an AI Platform"
walking through real agent workflows: signup, content creation,
commenting, tagging, and cross-service messaging.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* refactor: rename handler types to drop redundant Service suffix

UserService → Users, PostService → Posts, CommentService → Comments,
MailService → Mail. Matches micro/blog naming convention.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* refactor: consolidate top-level directories, reduce framework bloat

Move internal/non-public packages behind internal/ or into their
parent packages where they belong:

- deploy/ → gateway/mcp/deploy/ (Helm charts belong with the gateway)
- profile/ → service/profile/ (preset plugin profiles are a service concern)
- scripts/ → internal/scripts/ (install script is not public API)
- test/ → internal/test/ (test harness is not public API)
- util/ → internal/util/ (internal helpers shouldn't be imported externally)

Also fixes CLAUDE.md merge conflict markers and updates project
structure documentation.

All import paths updated. Build and tests pass.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* refactor: redesign model package to match framework conventions

Rename model.Database interface to model.Model (consistent with
client.Client, server.Server, store.Store). Remove generics in
favor of interface{}-based API with reflection.

Key changes:
- model.Model interface: Register once, CRUD infers table from type
- DefaultModel + NewModel() + package-level convenience functions
- Schema registered via Register(&User{}), no per-call schema passing
- Memory implementation as default (in model package, like store)
- memory/sqlite/postgres backends updated for new interface
- protoc-gen-micro generates RegisterXModel() instead of generic factory
- All docs, blog, and README updated

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* docs: clarify blog post 7 uses modular monolith, not multi-service

Blog post 7 demonstrated all handlers in a single process but framed
it as microservices without acknowledging the architectural difference.

- Add "A Note on Architecture" section explaining this is a modular
  monolith demo and pointing to micro/blog for multi-service
- Clarify that handlers can be broken out into separate services later
- Fix "service registry" language to match single-process reality
- Restructure "Adding MCP to Existing Services" to distinguish the
  in-process approach from registry-based gateway options
- Update closing to acknowledge both paradigms
- Fix README type names (&CommentService{} -> &Comments{}, etc.)

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-05 13:13:31 +00:00
Asim Aslam 8ecdf07e6b Update description of the blogging platform example 2026-03-05 12:03:26 +00:00
Asim Aslam 1bb25d6e7f Add agent platform showcase and refactor project structure (#2884)
* feat: add agent platform showcase and blog post

Add a complete platform example (Users, Posts, Comments, Mail) that
mirrors micro/blog, demonstrating how existing microservices become
AI-accessible through MCP with zero code changes.

Includes blog post "Your Microservices Are Already an AI Platform"
walking through real agent workflows: signup, content creation,
commenting, tagging, and cross-service messaging.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* refactor: rename handler types to drop redundant Service suffix

UserService → Users, PostService → Posts, CommentService → Comments,
MailService → Mail. Matches micro/blog naming convention.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* refactor: consolidate top-level directories, reduce framework bloat

Move internal/non-public packages behind internal/ or into their
parent packages where they belong:

- deploy/ → gateway/mcp/deploy/ (Helm charts belong with the gateway)
- profile/ → service/profile/ (preset plugin profiles are a service concern)
- scripts/ → internal/scripts/ (install script is not public API)
- test/ → internal/test/ (test harness is not public API)
- util/ → internal/util/ (internal helpers shouldn't be imported externally)

Also fixes CLAUDE.md merge conflict markers and updates project
structure documentation.

All import paths updated. Build and tests pass.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* refactor: redesign model package to match framework conventions

Rename model.Database interface to model.Model (consistent with
client.Client, server.Server, store.Store). Remove generics in
favor of interface{}-based API with reflection.

Key changes:
- model.Model interface: Register once, CRUD infers table from type
- DefaultModel + NewModel() + package-level convenience functions
- Schema registered via Register(&User{}), no per-call schema passing
- Memory implementation as default (in model package, like store)
- memory/sqlite/postgres backends updated for new interface
- protoc-gen-micro generates RegisterXModel() instead of generic factory
- All docs, blog, and README updated

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-05 11:21:41 +00:00
Asim Aslam b8fc0902d7 Claude/update docs roadmap f zd2 j (#2883)
* feat: add agent platform showcase and blog post

Add a complete platform example (Users, Posts, Comments, Mail) that
mirrors micro/blog, demonstrating how existing microservices become
AI-accessible through MCP with zero code changes.

Includes blog post "Your Microservices Are Already an AI Platform"
walking through real agent workflows: signup, content creation,
commenting, tagging, and cross-service messaging.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* refactor: rename handler types to drop redundant Service suffix

UserService → Users, PostService → Posts, CommentService → Comments,
MailService → Mail. Matches micro/blog naming convention.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-05 08:56:24 +00:00
Asim Aslam 524e16296b Update documentation, add agent demo, and enhance service API (#2882)
* 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

* feat: unify service API and clean up developer experience

- Unified service creation: micro.New("name", opts...) as canonical API
- Clean handler registration: service.Handle(handler, opts...) accepts
  server.HandlerOption args directly, no need to reach through Server()
- Unexported serviceImpl: users interact through Service interface only
- Service groups use Service interface (not concrete type)
- Fixed Stop() to properly propagate BeforeStop/AfterStop errors
- Fixed store init: error-level log instead of fatal on init failure
- Updated all examples to use consistent patterns
- Updated README, getting-started, MCP docs, and guides
- Added blog post about the DX cleanup

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* fix: add blog post 5 to blog index

Blog post 5 (Developer Experience Cleanup) existed as a file but was
missing from the blog index page.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: make micro new generate MCP-enabled services by default

- main.go template includes mcp.WithMCP(":3001") by default
- Handler template has agent-friendly doc comments with @example tags
- Proto template has descriptive field comments
- README includes MCP usage, Claude Code config, and tool description tips
- Makefile adds mcp-tools, mcp-test, mcp-serve targets
- go.mod updated to Go 1.22
- Added --no-mcp flag to opt out of MCP integration
- Post-create output shows MCP endpoint URLs

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* docs: add MCP migration guide and troubleshooting guide

- Migration guide: 3 approaches to add MCP to existing services
  (WithMCP one-liner, standalone gateway, CLI)
- Troubleshooting guide: common issues with agents, WebSocket,
  Claude Code, auth, rate limiting, and performance

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* refactor: rename model/ package to ai/ for AI model providers

The model/ package name conflicted with the conventional use of "model"
for data models. Renamed to ai/ which better describes the package's
purpose (AI provider abstraction for Anthropic, OpenAI, etc.) and frees
up model/ for future data model layer use.

- Rename model/ → ai/ with package name change
- Update all Go imports from go-micro.dev/v5/model to go-micro.dev/v5/ai
- Update cmd/micro/server/server.go references (model.X → ai.X)
- Update all documentation and roadmap references
- All tests pass, CLI builds successfully

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add model package for typed data access with CRUD and queries

New model/ package provides a typed data model layer using Go generics.
Supports structured CRUD operations, WHERE filters, ordering, pagination,
and automatic schema creation from struct tags.

Three backends:
- memory: in-memory for development and testing
- sqlite: embedded SQL for dev and single-node production
- postgres: full PostgreSQL for production deployments

Key features:
- Generic Model[T] with Create/Read/Update/Delete/List/Count
- Query builder: Where(), WhereOp(), OrderAsc/Desc(), Limit(), Offset()
- Struct tags: model:"key" for primary key, model:"index" for indexes
- Auto table creation from struct schema
- 19 tests passing across memory and sqlite backends

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add model code generation to protoc-gen-micro

Extend the micro plugin to generate model structs from proto messages
annotated with // @model. Generated alongside client/server code in
the same .pb.micro.go file.

For a proto message like:
  // @model
  message User { string id = 1; string name = 2; }

Generates:
- UserModel struct with model:"key" and json tags
- NewUserModel(db) factory returning *model.Model[UserModel]
- UserModelFromProto(*User) *UserModel converter
- (*UserModel).ToProto() *User converter

Supports @model(table=custom_table, key=custom_field) options.
Adds GetComments() to generator for plugin comment inspection.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add Model() to Service interface for Client/Server/Model trifecta

Every service now exposes Client(), Server(), and Model() — call services,
handle requests, and save/query data from the same interface. Includes
README docs, blog post, and a full model guide on the docs site.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add Helm chart for MCP gateway Kubernetes deployment

Adds official Helm chart at deploy/helm/mcp-gateway/ with:
- Deployment, Service, ServiceAccount templates
- HPA for auto-scaling based on CPU/memory
- Ingress with TLS support
- Configurable registry (consul, etcd, mdns), rate limiting,
  JWT auth, audit logging, and per-tool scopes
- Security context (non-root, read-only rootfs, drop all caps)
- NOTES.txt with post-install connection instructions

Updates roadmap and status docs to reflect Helm Charts as delivered.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* docs: add Helm chart entry to changelog

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add per-tool circuit breakers to MCP gateway

Protects downstream services from cascading failures. When a tool's
RPC calls fail repeatedly, the circuit opens and rejects requests
immediately until the service recovers (half-open probe pattern).

- CircuitBreakerConfig with MaxFailures, Timeout, MaxHalfOpen
- Per-tool breakers created during service discovery
- Integrated into HTTP call path with 503 response when open
- Records success/failure after each RPC call
- --circuit-breaker and --circuit-breaker-timeout CLI flags
- 8 unit tests covering all state transitions

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-05 08:32:59 +00:00
Asim Aslam d91812b476 Update documentation and add multi-service support with examples (#2881)
* 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

* feat: unify service API and clean up developer experience

- Unified service creation: micro.New("name", opts...) as canonical API
- Clean handler registration: service.Handle(handler, opts...) accepts
  server.HandlerOption args directly, no need to reach through Server()
- Unexported serviceImpl: users interact through Service interface only
- Service groups use Service interface (not concrete type)
- Fixed Stop() to properly propagate BeforeStop/AfterStop errors
- Fixed store init: error-level log instead of fatal on init failure
- Updated all examples to use consistent patterns
- Updated README, getting-started, MCP docs, and guides
- Added blog post about the DX cleanup

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* fix: add blog post 5 to blog index

Blog post 5 (Developer Experience Cleanup) existed as a file but was
missing from the blog index page.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: make micro new generate MCP-enabled services by default

- main.go template includes mcp.WithMCP(":3001") by default
- Handler template has agent-friendly doc comments with @example tags
- Proto template has descriptive field comments
- README includes MCP usage, Claude Code config, and tool description tips
- Makefile adds mcp-tools, mcp-test, mcp-serve targets
- go.mod updated to Go 1.22
- Added --no-mcp flag to opt out of MCP integration
- Post-create output shows MCP endpoint URLs

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* docs: add MCP migration guide and troubleshooting guide

- Migration guide: 3 approaches to add MCP to existing services
  (WithMCP one-liner, standalone gateway, CLI)
- Troubleshooting guide: common issues with agents, WebSocket,
  Claude Code, auth, rate limiting, and performance

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* refactor: rename model/ package to ai/ for AI model providers

The model/ package name conflicted with the conventional use of "model"
for data models. Renamed to ai/ which better describes the package's
purpose (AI provider abstraction for Anthropic, OpenAI, etc.) and frees
up model/ for future data model layer use.

- Rename model/ → ai/ with package name change
- Update all Go imports from go-micro.dev/v5/model to go-micro.dev/v5/ai
- Update cmd/micro/server/server.go references (model.X → ai.X)
- Update all documentation and roadmap references
- All tests pass, CLI builds successfully

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add model package for typed data access with CRUD and queries

New model/ package provides a typed data model layer using Go generics.
Supports structured CRUD operations, WHERE filters, ordering, pagination,
and automatic schema creation from struct tags.

Three backends:
- memory: in-memory for development and testing
- sqlite: embedded SQL for dev and single-node production
- postgres: full PostgreSQL for production deployments

Key features:
- Generic Model[T] with Create/Read/Update/Delete/List/Count
- Query builder: Where(), WhereOp(), OrderAsc/Desc(), Limit(), Offset()
- Struct tags: model:"key" for primary key, model:"index" for indexes
- Auto table creation from struct schema
- 19 tests passing across memory and sqlite backends

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add model code generation to protoc-gen-micro

Extend the micro plugin to generate model structs from proto messages
annotated with // @model. Generated alongside client/server code in
the same .pb.micro.go file.

For a proto message like:
  // @model
  message User { string id = 1; string name = 2; }

Generates:
- UserModel struct with model:"key" and json tags
- NewUserModel(db) factory returning *model.Model[UserModel]
- UserModelFromProto(*User) *UserModel converter
- (*UserModel).ToProto() *User converter

Supports @model(table=custom_table, key=custom_field) options.
Adds GetComments() to generator for plugin comment inspection.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add Model() to Service interface for Client/Server/Model trifecta

Every service now exposes Client(), Server(), and Model() — call services,
handle requests, and save/query data from the same interface. Includes
README docs, blog post, and a full model guide on the docs site.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add Helm chart for MCP gateway Kubernetes deployment

Adds official Helm chart at deploy/helm/mcp-gateway/ with:
- Deployment, Service, ServiceAccount templates
- HPA for auto-scaling based on CPU/memory
- Ingress with TLS support
- Configurable registry (consul, etcd, mdns), rate limiting,
  JWT auth, audit logging, and per-tool scopes
- Security context (non-root, read-only rootfs, drop all caps)
- NOTES.txt with post-install connection instructions

Updates roadmap and status docs to reflect Helm Charts as delivered.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-05 07:28:30 +00:00
Asim Aslam 76bfeae456 Claude/update docs roadmap f zd2 j (#2880)
* 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

* feat: unify service API and clean up developer experience

- Unified service creation: micro.New("name", opts...) as canonical API
- Clean handler registration: service.Handle(handler, opts...) accepts
  server.HandlerOption args directly, no need to reach through Server()
- Unexported serviceImpl: users interact through Service interface only
- Service groups use Service interface (not concrete type)
- Fixed Stop() to properly propagate BeforeStop/AfterStop errors
- Fixed store init: error-level log instead of fatal on init failure
- Updated all examples to use consistent patterns
- Updated README, getting-started, MCP docs, and guides
- Added blog post about the DX cleanup

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* fix: add blog post 5 to blog index

Blog post 5 (Developer Experience Cleanup) existed as a file but was
missing from the blog index page.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: make micro new generate MCP-enabled services by default

- main.go template includes mcp.WithMCP(":3001") by default
- Handler template has agent-friendly doc comments with @example tags
- Proto template has descriptive field comments
- README includes MCP usage, Claude Code config, and tool description tips
- Makefile adds mcp-tools, mcp-test, mcp-serve targets
- go.mod updated to Go 1.22
- Added --no-mcp flag to opt out of MCP integration
- Post-create output shows MCP endpoint URLs

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* docs: add MCP migration guide and troubleshooting guide

- Migration guide: 3 approaches to add MCP to existing services
  (WithMCP one-liner, standalone gateway, CLI)
- Troubleshooting guide: common issues with agents, WebSocket,
  Claude Code, auth, rate limiting, and performance

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* refactor: rename model/ package to ai/ for AI model providers

The model/ package name conflicted with the conventional use of "model"
for data models. Renamed to ai/ which better describes the package's
purpose (AI provider abstraction for Anthropic, OpenAI, etc.) and frees
up model/ for future data model layer use.

- Rename model/ → ai/ with package name change
- Update all Go imports from go-micro.dev/v5/model to go-micro.dev/v5/ai
- Update cmd/micro/server/server.go references (model.X → ai.X)
- Update all documentation and roadmap references
- All tests pass, CLI builds successfully

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add model package for typed data access with CRUD and queries

New model/ package provides a typed data model layer using Go generics.
Supports structured CRUD operations, WHERE filters, ordering, pagination,
and automatic schema creation from struct tags.

Three backends:
- memory: in-memory for development and testing
- sqlite: embedded SQL for dev and single-node production
- postgres: full PostgreSQL for production deployments

Key features:
- Generic Model[T] with Create/Read/Update/Delete/List/Count
- Query builder: Where(), WhereOp(), OrderAsc/Desc(), Limit(), Offset()
- Struct tags: model:"key" for primary key, model:"index" for indexes
- Auto table creation from struct schema
- 19 tests passing across memory and sqlite backends

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add model code generation to protoc-gen-micro

Extend the micro plugin to generate model structs from proto messages
annotated with // @model. Generated alongside client/server code in
the same .pb.micro.go file.

For a proto message like:
  // @model
  message User { string id = 1; string name = 2; }

Generates:
- UserModel struct with model:"key" and json tags
- NewUserModel(db) factory returning *model.Model[UserModel]
- UserModelFromProto(*User) *UserModel converter
- (*UserModel).ToProto() *User converter

Supports @model(table=custom_table, key=custom_field) options.
Adds GetComments() to generator for plugin comment inspection.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add Model() to Service interface for Client/Server/Model trifecta

Every service now exposes Client(), Server(), and Model() — call services,
handle requests, and save/query data from the same interface. Includes
README docs, blog post, and a full model guide on the docs site.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-04 13:13:34 +00:00
Asim Aslam f07a49e0d9 docs: add CHANGELOG.md with structured release history (#2878)
Keep a Changelog format covering 2026.01 through current unreleased
work. Includes all MCP gateway features, CLI commands, agent SDKs,
developer experience improvements, and documentation additions.

This replaces ad-hoc blog posts as the canonical "what changed" reference.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-04 12:05:06 +00:00
Asim Aslam 6247b1d065 chore: move internal status docs out of top level (#2879)
Move Claude-context working documents to internal/docs/:
- CURRENT_STATUS_SUMMARY.md
- PROJECT_STATUS_2026.md
- ROADMAP_2026.md
- IMPLEMENTATION_SUMMARY.md

These are session-tracking docs, not useful to most contributors.
Top level now only has standard files: README, CHANGELOG,
CONTRIBUTING, SECURITY, CLAUDE.md, and ROADMAP.

Updated all cross-references in CLAUDE.md, ROADMAP.md, and
website docs.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-04 11:59:52 +00:00
Asim Aslam c800dd3729 feat: add deployment example, workflow example, and MCP benchmarks (#2877)
Docker Compose deployment example (examples/deployment/):
- docker-compose.yml with Consul, MCP gateway, Jaeger tracing
- Dockerfile and Dockerfile.gateway for multi-stage builds
- README with architecture diagram and customization guide

Cross-service workflow example (examples/mcp/workflow/):
- Inventory, Orders, Notifications services
- Shows agents orchestrating multi-step workflows from natural language
- Stock check → reserve → order → notify in a single agent conversation

MCP gateway benchmark suite (gateway/mcp/benchmark_test.go):
- ListTools: ~20μs (10 tools), ~48μs (100 tools)
- Tool lookup: ~19ns (zero-alloc, scales to 500+ tools)
- Auth inspect: ~7ns, scope check: ~16ns
- Rate limiter: ~111ns per check
- JSON encode/decode: ~1.5-2μs per tool

Updated examples README with new examples index.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-04 11:40:10 +00:00
Asim Aslam 870b540922 Claude/mcp dx improvements f zd2 j (#2876)
* docs: add MCP migration guide and troubleshooting guide

- Migration guide: 3 approaches to add MCP to existing services
  (WithMCP one-liner, standalone gateway, CLI)
- Troubleshooting guide: common issues with agents, WebSocket,
  Claude Code, auth, rate limiting, and performance

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add CRUD example, error handling guide, and status updates

- CRUD contact book example (examples/mcp/crud/) with 6 operations,
  rich doc comments, @example tags, and description struct tags
- Error handling guide for writing agent-friendly error responses
  using typed errors, actionable messages, and idempotency patterns
- Updated MCP examples index, CLAUDE.md, and status summary

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-04 11:32:27 +00:00
Asim Aslam fad2fd7af4 Claude/update docs roadmap f zd2 j (#2875)
* 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

* feat: unify service API and clean up developer experience

- Unified service creation: micro.New("name", opts...) as canonical API
- Clean handler registration: service.Handle(handler, opts...) accepts
  server.HandlerOption args directly, no need to reach through Server()
- Unexported serviceImpl: users interact through Service interface only
- Service groups use Service interface (not concrete type)
- Fixed Stop() to properly propagate BeforeStop/AfterStop errors
- Fixed store init: error-level log instead of fatal on init failure
- Updated all examples to use consistent patterns
- Updated README, getting-started, MCP docs, and guides
- Added blog post about the DX cleanup

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* fix: add blog post 5 to blog index

Blog post 5 (Developer Experience Cleanup) existed as a file but was
missing from the blog index page.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: make micro new generate MCP-enabled services by default

- main.go template includes mcp.WithMCP(":3001") by default
- Handler template has agent-friendly doc comments with @example tags
- Proto template has descriptive field comments
- README includes MCP usage, Claude Code config, and tool description tips
- Makefile adds mcp-tools, mcp-test, mcp-serve targets
- go.mod updated to Go 1.22
- Added --no-mcp flag to opt out of MCP integration
- Post-create output shows MCP endpoint URLs

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-04 11:24:33 +00:00
Asim Aslam 19892f2c67 Claude/update docs roadmap f zd2 j (#2874)
* 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

* feat: unify service API and clean up developer experience

- Unified service creation: micro.New("name", opts...) as canonical API
- Clean handler registration: service.Handle(handler, opts...) accepts
  server.HandlerOption args directly, no need to reach through Server()
- Unexported serviceImpl: users interact through Service interface only
- Service groups use Service interface (not concrete type)
- Fixed Stop() to properly propagate BeforeStop/AfterStop errors
- Fixed store init: error-level log instead of fatal on init failure
- Updated all examples to use consistent patterns
- Updated README, getting-started, MCP docs, and guides
- Added blog post about the DX cleanup

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* fix: add blog post 5 to blog index

Blog post 5 (Developer Experience Cleanup) existed as a file but was
missing from the blog index page.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-04 11:19:43 +00:00
Asim Aslam d2036b880d Claude/update docs roadmap f zd2 j (#2873)
* 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

* feat: unify service API and clean up developer experience

- Unified service creation: micro.New("name", opts...) as canonical API
- Clean handler registration: service.Handle(handler, opts...) accepts
  server.HandlerOption args directly, no need to reach through Server()
- Unexported serviceImpl: users interact through Service interface only
- Service groups use Service interface (not concrete type)
- Fixed Stop() to properly propagate BeforeStop/AfterStop errors
- Fixed store init: error-level log instead of fatal on init failure
- Updated all examples to use consistent patterns
- Updated README, getting-started, MCP docs, and guides
- Added blog post about the DX cleanup

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-04 11:13:27 +00:00
Asim Aslam cad0ff1e49 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>
2026-03-04 10:50:00 +00:00
Asim Aslam ffe43e0e6d Claude/update docs roadmap f zd2 j (#2871)
* 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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-04 10:16:17 +00:00
Asim Aslam ec9473f86f Update sponsorship information in README.md 2026-03-04 10:10:46 +00:00
Asim Aslam 4c673f61b1 Fix formatting in README.md links section 2026-03-04 10:10:18 +00:00
Asim Aslam bed94bfa95 Update sponsorship information in README
Updated sponsorship link and removed sponsor text.
2026-03-04 10:10:04 +00:00
Asim Aslam d02ff2ecfa feat: add standalone MCP gateway binary (#2870)
Add cmd/micro-mcp-gateway for production MCP gateway deployment
independent of micro run. Supports:

- Registry selection: mdns, consul, etcd (via --registry flag)
- Rate limiting per tool (--rate-limit, --rate-burst)
- JWT authentication (--auth)
- Per-tool scope requirements (--scope tool=scope1,scope2)
- Audit logging to stdout (--audit)
- Environment variable configuration for all flags
- Dockerfile for containerized deployment

Usage:
  micro-mcp-gateway --address :3000 --registry consul --registry-address consul:8500

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-04 10:08:09 +00:00
Asim Aslam 6d9645adce feat: polish agent playground UI (#2869)
Redesign the /agent playground with improved UX:
- Chat-focused layout with full-height message area and sticky input
- Collapsible tool call cards showing name, input, result, and timing
- Thinking indicator while waiting for agent response
- Settings panel collapsed by default (auto-opens if no API key)
- Empty state with available tools preview
- Clear chat button
- Better visual hierarchy with distinct message styles

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-04 09:38:04 +00:00
Asim Aslam beeaad748e Claude/update docs roadmap f zd2 j (#2868)
* feat: add LlamaIndex SDK for Go Micro services

Add LlamaIndex integration package that enables LlamaIndex agents to
discover and call Go Micro microservices through the MCP gateway.
Follows the same pattern as the existing LangChain SDK.

- GoMicroToolkit with from_gateway() factory and tool filtering
- FunctionTool integration via llama_index.core.tools
- Auth support, error handling, and retry configuration
- Examples for basic agent and RAG + microservices workflows
- Unit tests with mocked gateway responses

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* docs: update status for OTel, WebSocket, and LlamaIndex SDK completion

Reflect recently completed work in roadmap and status documents:
- Q2 progress: 85% -> 95% (WebSocket, LlamaIndex SDK done)
- Q3 progress: 40% -> 50% (OpenTelemetry integration done)
- Transports: 2 -> 3 (added WebSocket)
- Agent SDKs: 1 -> 2 (added LlamaIndex)
- Test coverage: 568 -> 1,000+ lines

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add WithMCP convenience option, improve startup banner, and blog post

- Add mcp.WithMCP(":3000") service option for one-line MCP setup
- Improve `micro run` startup banner to show Agent playground, MCP
  tools, and WebSocket endpoints prominently
- Add blog post: "Building the AI-Native Future of Go Micro with Claude"
  covering WebSocket transport, OTel integration, LlamaIndex SDK, and
  Anthropic's Claude Max sponsorship
- Update blog index and navigation links

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-04 09:28:07 +00:00
Asim Aslam 076b7c37be feat: add WebSocket transport for MCP gateway (#2867)
Add bidirectional WebSocket transport at /mcp/ws using JSON-RPC 2.0
protocol (same as stdio). This enables persistent connections for
real-time AI agents that need streaming tool interactions.

New files:
- gateway/mcp/websocket.go: WebSocketTransport with connection-level
  auth, per-message auth fallback, write serialization, OTel tracing
- gateway/mcp/websocket_test.go: 14 tests covering initialize,
  tools/list, tool calls, auth (header + param), scopes, rate
  limiting, audit, concurrent requests, multiple connections,
  error handling, and connection persistence

Changes:
- gateway/mcp/mcp.go: Register /mcp/ws handler in serveHTTP
- go.mod: Added github.com/gorilla/websocket v1.5.3

Auth supports two modes:
- Connection-level: Bearer token in WebSocket upgrade request headers
- Per-message: _token field in JSON-RPC params (same as stdio)

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-04 08:26:09 +00:00
Asim Aslam ab6f027741 Claude/update docs roadmap f zd2 j (#2866)
* Update docs and roadmap to March 2026 with focus priorities

- ROADMAP.md: Updated from Nov 2025 to reflect Q1 completions and current state
- ROADMAP_2026.md: Updated status to March 2026, added model package as delivered
- CURRENT_STATUS_SUMMARY.md: Rewrote with March 2026 status and clear next priorities
- PROJECT_STATUS_2026.md: Added model package section, updated recommendations
- Website roadmap: Updated Q3 security status and timestamps

Key focus areas identified: documentation guides, multi-protocol MCP,
LlamaIndex SDK, and OpenTelemetry integration.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* Add CLAUDE.md and four documentation guides to fill doc gaps

- CLAUDE.md: Project guide with structure, build commands, and priorities
- ai-native-services.md: End-to-end tutorial building an MCP-enabled task service
- mcp-security.md: Production security guide (auth, scopes, rate limiting, audit)
- tool-descriptions.md: Best practices for writing Go comments that help agents
- agent-patterns.md: Six integration patterns from single-agent to event-driven
- Updated docs index with new "AI & Agents" section linking all four guides

These were the highest priority gaps identified in the roadmap analysis:
the framework has solid features that were under-documented.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* feat: add OpenTelemetry tracing to MCP gateway

Integrate OpenTelemetry spans into the MCP gateway for both HTTP and
stdio transports. Each tool call now creates a server span with rich
attributes (tool name, account ID, auth outcome, transport type).
Trace context is propagated to downstream RPC calls via metadata,
enabling end-to-end distributed tracing through Jaeger, Grafana, etc.

New files:
- gateway/mcp/otel.go: Span creation, attribute constants, metadata carrier
- gateway/mcp/otel_test.go: 8 tests covering span creation, auth denied/allowed,
  rate limiting, trace propagation, noop provider, and missing token

Changes:
- Options.TraceProvider: Optional trace.TracerProvider field
- handleCallTool (HTTP): Creates OTel spans with auth/rate-limit attributes
- handleToolsCall (stdio): Same instrumentation for stdio transport
- go.mod: Added go.opentelemetry.io/otel/sdk v1.35.0 (test dependency)

The existing MCP trace ID (UUID) is preserved for backward compatibility
and recorded as a span attribute alongside the W3C trace context.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-04 08:16:06 +00:00
Asim Aslam 14ec9b955f Update status docs: March 2026 progress and roadmap refinement (#2865)
* Update docs and roadmap to March 2026 with focus priorities

- ROADMAP.md: Updated from Nov 2025 to reflect Q1 completions and current state
- ROADMAP_2026.md: Updated status to March 2026, added model package as delivered
- CURRENT_STATUS_SUMMARY.md: Rewrote with March 2026 status and clear next priorities
- PROJECT_STATUS_2026.md: Added model package section, updated recommendations
- Website roadmap: Updated Q3 security status and timestamps

Key focus areas identified: documentation guides, multi-protocol MCP,
LlamaIndex SDK, and OpenTelemetry integration.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

* Add CLAUDE.md and four documentation guides to fill doc gaps

- CLAUDE.md: Project guide with structure, build commands, and priorities
- ai-native-services.md: End-to-end tutorial building an MCP-enabled task service
- mcp-security.md: Production security guide (auth, scopes, rate limiting, audit)
- tool-descriptions.md: Best practices for writing Go comments that help agents
- agent-patterns.md: Six integration patterns from single-agent to event-driven
- Updated docs index with new "AI & Agents" section linking all four guides

These were the highest priority gaps identified in the roadmap analysis:
the framework has solid features that were under-documented.

https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-04 07:33:19 +00:00
Asim Aslam a789c9e94b Update Go Micro version in README 2026-02-21 05:22:24 +00:00
Copilot e110ccb5ff Refactor model interface to high-level idiomatic Go API (#2863)
* Initial plan

* Add model package with provider abstraction interface

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Add unit tests for model providers

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Refactor server to use model package abstraction

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Use strings.Contains instead of custom substring search

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Add comprehensive documentation for model package

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Refactor model interface to be more idiomatic Go

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Simplify server code to use new high-level Generate API

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Update documentation for new high-level model API

Co-authored-by: asim <17530+asim@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: asim <17530+asim@users.noreply.github.com>
2026-02-14 18:15:07 +00:00
Copilot e3337efd81 Update 2026 roadmap to reflect Q2 completions (85% → docs only) (#2862)
* Initial plan

* Update 2026 roadmap to reflect Q2 completions (CLI exports and LangChain SDK)

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Update PROJECT_STATUS and roadmap docs to reflect Q2 85% completion

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Fix CLI integration status table to show all commands as complete

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Address code review feedback: fix duplicates and add missing metrics

Co-authored-by: asim <17530+asim@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: asim <17530+asim@users.noreply.github.com>
2026-02-13 17:36:25 +00:00
Copilot 13d1116dee Implement Q2 2026 roadmap: MCP CLI export commands and LangChain SDK (#2861)
* Initial plan

* Implement micro mcp docs and export commands (Q2 2026 roadmap)

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Add comprehensive CLI examples and documentation for new MCP commands

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Add LangChain Python SDK for Go Micro (Q2 2026 roadmap)

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Update PROJECT_STATUS to reflect LangChain SDK completion

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Add implementation summary for Roadmap 2026 session

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: asim <17530+asim@users.noreply.github.com>
2026-02-13 14:39:48 +00:00
Copilot 1db7903010 [WIP] Implement missing features from documentation (#2859)
* Initial plan

* Add --header and --metadata flags to micro call command

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Apply code formatting with gofmt

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Add clarifying comments for dual metadata handling paths

Co-authored-by: asim <17530+asim@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: asim <17530+asim@users.noreply.github.com>
2026-02-13 14:20:34 +00:00
Copilot 5e1042e5ae Implement micro mcp test command (#2857)
* Initial plan

* Implement micro mcp test command

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Add test for parseTool function

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Address code review feedback - simplify parseTool

Co-authored-by: asim <17530+asim@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: asim <17530+asim@users.noreply.github.com>
2026-02-13 14:19:41 +00:00
Copilot 0f6453488e Implement missing --service flag for micro deploy command (#2858)
* Initial plan

* Implement --service flag for micro deploy command

Co-authored-by: asim <17530+asim@users.noreply.github.com>

* Address code review feedback - optimize validation and add comments

Co-authored-by: asim <17530+asim@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: asim <17530+asim@users.noreply.github.com>
2026-02-13 14:19:03 +00:00
227 changed files with 20080 additions and 1254 deletions
+51
View File
@@ -0,0 +1,51 @@
name: goreleaser
on:
push:
tags:
- 'v*.*.*'
permissions:
contents: write
id-token: write
packages: write
attestations: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
-
name: Set up Go
uses: actions/setup-go@v5
with:
go-version: stable
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
-
name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
-
name: Run GoReleaser
uses: goreleaser/goreleaser-action@v7
with:
distribution: goreleaser
version: '~> v2'
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+136
View File
@@ -0,0 +1,136 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
version: 2
before:
hooks:
- go mod tidy
builds:
- main: ./cmd/micro
id: micro
binary: micro
env:
- CGO_ENABLED=0
- >-
{{- if eq .Os "darwin" }}
{{- if eq .Arch "amd64"}}CC=o64-clang{{- end }}
{{- if eq .Arch "arm64"}}CC=aarch64-apple-darwin20.2-clang{{- end }}
{{- end }}
{{- if eq .Os "windows" }}
{{- if eq .Arch "amd64" }}CC=x86_64-w64-mingw32-gcc{{- end }}
{{- end }}
goos:
- linux
- windows
- darwin
goarch:
- amd64
- arm
- arm64
goarm:
- 7
ignore:
- goos: windows
goarch: arm
- main: ./cmd/protoc-gen-micro
id: protoc-gen-micro
binary: protoc-gen-micro
env:
- CGO_ENABLED=0
- >-
{{- if eq .Os "darwin" }}
{{- if eq .Arch "amd64"}}CC=o64-clang{{- end }}
{{- if eq .Arch "arm64"}}CC=aarch64-apple-darwin20.2-clang{{- end }}
{{- end }}
{{- if eq .Os "windows" }}
{{- if eq .Arch "amd64" }}CC=x86_64-w64-mingw32-gcc{{- end }}
{{- end }}
goos:
- linux
- windows
- darwin
goarch:
- amd64
- arm
- arm64
goarm:
- 7
ignore:
- goos: windows
goarch: arm
archives:
- id: micro
ids:
- micro
formats: [tar.gz]
name_template: >-
{{ .Binary }}_
{{- .Os }}_
{{- .Arch }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
files:
- none*
format_overrides:
- goos: windows
formats: [zip]
- id: protoc-gen-micro
ids:
- protoc-gen-micro
formats: [tar.gz]
name_template: >-
{{ .Binary }}_
{{- .Os }}_
{{- .Arch }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
files:
- none*
format_overrides:
- goos: windows
formats: [zip]
report_sizes: true
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
dockers_v2:
-
ids:
- micro
- protoc-gen-micro
images:
- "micro/micro"
- "ghcr.io/micro/go-micro"
tags:
- "v{{ .Version }}"
- "{{ if .IsNightly }}nightly{{ end }}"
- "{{ if not .IsNightly }}latest{{ end }}"
labels:
"io.artifacthub.package.readme-url": "https://raw.githubusercontent.com/micro/go-micro/refs/heads/master/README.md"
"io.artifacthub.package.logo-url": "https://www.gravatar.com/avatar/09d1da3ea9ee61753219a19016d6a672?s=120&r=g&d=404"
"org.opencontainers.image.description": "A Go Platform built for Developers"
"org.opencontainers.image.created": "{{.Date}}"
"org.opencontainers.image.title": "{{.ProjectName}}"
"org.opencontainers.image.revision": "{{.FullCommit}}"
"org.opencontainers.image.version": "{{.Version}}"
"org.opencontainers.image.source": "{{.GitURL}}"
"org.opencontainers.image.url": "{{.GitURL}}"
"org.opencontainers.image.licenses": "MIT"
platforms:
- linux/amd64
- linux/arm64
retry:
attempts: 5
delay: 5s
max_delay: 2m
+105
View File
@@ -0,0 +1,105 @@
# Changelog
All notable changes to Go Micro are documented here.
Format follows [Keep a Changelog](https://keepachangelog.com/). Go Micro uses
calendar-based versions (YYYY.MM) for the AI-native era.
---
## [Unreleased]
### Added
- **Agent platform showcase** — full platform example (Users, Posts, Comments, Mail) mirroring [micro/blog](https://github.com/micro/blog), demonstrating how existing microservices become agent-accessible with zero code changes (`examples/mcp/platform/`).
- **Blog post: "Your Microservices Are Already an AI Platform"** — walkthrough of agent-service interaction patterns using real-world services (`internal/website/blog/7.md`).
- **Circuit breakers for MCP gateway** — per-tool circuit breakers protect downstream services from cascading failures. Configurable max failures, open-state timeout, and half-open probing. Available via `Options.CircuitBreaker` and `--circuit-breaker` CLI flag (`gateway/mcp/circuitbreaker.go`).
- **Helm chart for MCP gateway** — official Helm chart at `deploy/helm/mcp-gateway/` with Deployment, Service, ServiceAccount, HPA, and Ingress templates. Supports Consul/etcd/mDNS registries, JWT auth, rate limiting, audit logging, per-tool scopes, TLS ingress, and auto-scaling.
- **MCP gateway benchmarks** — comprehensive benchmark suite for tool listing, lookup, auth, rate limiting, and JSON serialization (`gateway/mcp/benchmark_test.go`)
- **Workflow example** — cross-service orchestration demo with Inventory, Orders, and Notifications services showing agents chaining multi-step workflows from natural language (`examples/mcp/workflow/`)
- **Docker Compose deployment** — production-like setup with Consul registry, standalone MCP gateway, and Jaeger tracing in one `docker-compose up` (`examples/deployment/`)
---
## [2026.03] - March 2026
### Added
#### Developer Experience
- **`micro new` MCP templates** — `micro new myservice` generates MCP-enabled services with doc comments, `@example` tags, and `WithMCP()` wired in. Use `--no-mcp` to opt out.
- **`micro.New("name")` unified API** — single way to create services: `micro.New("greeter")` or `micro.New("greeter", micro.Address(":8080"))`. Replaces `micro.NewService()` + `service.New()` dual API.
- **`service.Handle()` simplified registration** — register handlers with `service.Handle(new(Greeter))` instead of manual `server.NewHandler` + `server.Handle`.
- **`micro.NewGroup()` modular monoliths** — run multiple services in one binary with shared lifecycle: `micro.NewGroup(users, orders).Run()`.
- **`mcp.WithMCP()` one-liner** — add MCP to any service with a single option: `micro.New("name", mcp.WithMCP(":3001"))`.
- **CRUD example** — contact book service with 6 operations, rich agent docs, and validation patterns (`examples/mcp/crud/`).
#### MCP Gateway
- **WebSocket transport** — bidirectional JSON-RPC 2.0 streaming over WebSocket for real-time agent communication (`gateway/mcp/websocket.go`).
- **OpenTelemetry integration** — full span instrumentation across HTTP, stdio, and WebSocket transports with W3C trace context propagation (`gateway/mcp/otel.go`).
- **Standalone gateway binary** — `micro-mcp-gateway` with Docker support for running the MCP gateway independently of services.
- **Per-tool auth scopes** — service-level (`server.WithEndpointScopes()`) and gateway-level (`Options.Scopes`) scope enforcement with bearer token auth.
- **Rate limiting** — per-tool token bucket rate limiting (`Options.RateLimit`).
- **Audit logging** — immutable audit records per tool call with trace ID, account, scopes, duration, and errors (`Options.AuditFunc`).
#### AI Model Package
- **`model.Model` interface** — unified AI provider abstraction with `Generate()` and `Stream()` methods.
- **Anthropic Claude provider** — `model/anthropic` with tool execution and auto-calling.
- **OpenAI GPT provider** — `model/openai` with provider auto-detection from base URL.
#### Agent SDKs
- **LangChain SDK** — `contrib/langchain-go-micro/` Python package with auto-discovery, tool generation, and multi-agent workflow examples.
- **LlamaIndex SDK** — `contrib/go-micro-llamaindex/` Python package with RAG integration examples.
#### Documentation
- **AI-native services guide** — building services for AI agents from scratch
- **MCP security guide** — auth, scopes, and audit logging
- **Tool descriptions guide** — writing doc comments that improve agent performance
- **Agent patterns guide** — architecture patterns for agent integration
- **Error handling guide** — writing agent-friendly error responses with typed errors
- **Troubleshooting guide** — common MCP issues and solutions
- **Migration guide** — add MCP to existing services in 5 minutes
#### CLI
- **`micro mcp serve`** — start MCP server (stdio for Claude Code, HTTP for web agents)
- **`micro mcp list`** — list available tools (human-readable or JSON)
- **`micro mcp test`** — test tools with JSON input
- **`micro mcp docs`** — generate tool documentation
- **`micro mcp export`** — export to LangChain, OpenAPI, or JSON formats
#### Agent Playground
- **Chat-focused UI** — redesigned playground with collapsible tool calls, real-time status, and thinking indicators
- **Provider settings** — configurable OpenAI/Anthropic provider, model, and API key
### Changed
- Service interface moved to `service.Service` with `micro.Service` as a type alias for backward compatibility.
- `service.New()` returns `service.Service` interface (was `*ServiceImpl`).
- `service.NewGroup()` accepts `service.Service` interface (was `*ServiceImpl`).
- `go.mod` template in `micro new` updated to Go 1.22.
### Fixed
- Handler `Handle()` method accepts variadic `server.HandlerOption` for scopes and metadata.
- Store initialization uses service name as table automatically.
- Service `Stop()` properly aggregates errors from lifecycle hooks.
---
## [2026.02] - February 2026
### Added
- **MCP gateway library** — `gateway/mcp/` with HTTP/SSE and stdio transports, service discovery, tool generation, and JSON schema generation from Go types (2,500+ lines).
- **CLI integration** — `micro run --mcp-address` flag to start MCP alongside services.
- **Documentation extraction** — auto-extract tool descriptions from Go doc comments with `@example` tag and struct tag parsing.
- **Blog post** — "Making Microservices AI-Native with MCP"
- **MCP examples** — `examples/mcp/hello/` and `examples/mcp/documented/`
---
## [2026.01] - January 2026
### Added
- **`micro deploy`** — deploy services to any Linux server via SSH + systemd with `micro deploy user@server`.
- **`micro build`** — build Go binaries and Docker images with `micro build --docker`.
- **Blog post** — "Introducing micro deploy"
---
_For earlier changes, see the [git log](https://github.com/micro/go-micro/commits/master)._
+145
View File
@@ -0,0 +1,145 @@
# CLAUDE.md - Go Micro Project Guide
## Project Overview
Go Micro is a framework for distributed systems development in Go. It provides pluggable abstractions for service discovery, RPC, pub/sub, config, auth, storage, and more.
The framework is evolving into an **AI-native platform** where every microservice is automatically accessible to AI agents via the Model Context Protocol (MCP).
## Build & Test
```bash
# Run all tests
make test
# Run tests for a specific package
go test ./gateway/mcp/...
go test ./ai/...
go test ./model/...
# Lint
make lint
# Format
make fmt
# Build CLI
go build -o micro ./cmd/micro
# Run locally with hot reload
micro run
```
## Project Structure
```
go-micro/
├── ai/ # AI model providers (Anthropic, OpenAI)
├── auth/ # Authentication (JWT, no-op)
├── broker/ # Message broker (NATS, RabbitMQ)
├── cache/ # Caching (Redis)
├── client/ # RPC client (gRPC)
├── cmd/micro/ # CLI tool (run, deploy, mcp, build, server)
├── codec/ # Message codecs (JSON, Proto)
├── config/ # Dynamic config (env, file, etcd, NATS)
├── errors/ # Error handling
├── events/ # Event system (NATS JetStream)
├── gateway/
│ ├── api/ # REST API gateway
│ └── mcp/ # MCP gateway (core AI integration)
│ └── deploy/ # Helm charts for MCP gateway
├── health/ # Health checking
├── logger/ # Logging
├── metadata/ # Context metadata
├── model/ # Typed data models (CRUD, queries, schemas)
├── registry/ # Service discovery (mDNS, Consul, etcd)
├── selector/ # Client-side load balancing
├── server/ # RPC server
├── service/ # Service interface + profiles
├── store/ # Data persistence (Postgres, NATS KV)
├── transport/ # Network transport
├── wrapper/ # Middleware (auth, trace, metrics)
├── examples/ # Working examples
└── internal/ # Non-public: docs, utils, test harness
```
## Key Architectural Decisions
- **Plugin architecture**: All abstractions use Go interfaces. Defaults work out of the box, everything is swappable.
- **Progressive complexity**: Zero-config for development, full control for production.
- **AI-native by default**: Every service is automatically an MCP tool. No extra code needed.
- **In-repo plugins**: Plugins live in the main repo to avoid version compatibility issues.
- **Reflection-based registration**: Handlers are registered via reflection for minimal boilerplate.
## Code Conventions
- Standard Go conventions (gofmt, golint)
- Functional options pattern for configuration (`WithX()` functions)
- Interface-first design: define the interface, then implement
- Tests alongside code (not in separate test directories)
- Commit messages: imperative mood, concise summary line
## Current Focus & Priorities (March 2026)
### Status
- **Q1 2026 (MCP Foundation):** COMPLETE
- **Q2 2026 (Agent DX):** COMPLETE (100%)
- **Q3 2026 (Production):** 50% complete (ahead of schedule)
### Priority 1: Agent Showcase & Examples
Build compelling demos showing agents interacting with go-micro services in realistic scenarios.
### Priority 2: Additional Protocol Support
- gRPC reflection-based MCP
- HTTP/3 support
### Priority 3: Kubernetes & Deployment
- Helm Charts for MCP gateway
- Kubernetes Operator with CRDs
### Recently Completed
- **`micro new` MCP Templates** - Scaffolds MCP-enabled services with doc comments, `@example` tags, `WithMCP()`. `--no-mcp` to opt out.
- **CRUD Example** - Contact book service with 6 operations, rich agent docs (`examples/mcp/crud/`)
- **Migration Guide** - "Add MCP to Existing Services" guide with 3 approaches
- **Troubleshooting Guide** - Common MCP issues and solutions
- **Error Handling Guide** - Patterns for agent-friendly error responses
- **Documentation Guides** - Six guides: AI-native services, MCP security, tool descriptions, agent patterns, error handling, troubleshooting
- **WithMCP Option** - One-line MCP setup (`gateway/mcp/option.go`)
- **Agent Playground Redesign** - Chat-focused UI with collapsible tool calls
- **Standalone Gateway Binary** - `micro-mcp-gateway` with Docker support
- **WebSocket Transport** - Bidirectional JSON-RPC 2.0 streaming (`gateway/mcp/websocket.go`)
- **OpenTelemetry Integration** - Full span instrumentation with W3C trace context (`gateway/mcp/otel.go`)
- **LlamaIndex SDK** - Python package with RAG examples (`contrib/go-micro-llamaindex/`)
## Key Files
| Purpose | File |
|---------|------|
| MCP Gateway | `gateway/mcp/mcp.go` |
| MCP Docs | `gateway/mcp/DOCUMENTATION.md` |
| AI Interface | `ai/model.go` |
| Model Layer | `model/model.go` |
| CLI Entry | `cmd/micro/main.go` |
| MCP CLI | `cmd/micro/mcp/` |
| Server (run/server) | `cmd/micro/server/server.go` |
| Roadmap | `internal/docs/ROADMAP_2026.md` |
| Status | `internal/docs/CURRENT_STATUS_SUMMARY.md` |
| Changelog | `CHANGELOG.md` |
| Docs Site | `internal/website/docs/` |
## Roadmap & Status Documents
- **[ROADMAP.md](ROADMAP.md)** - General framework roadmap
- **[internal/docs/ROADMAP_2026.md](internal/docs/ROADMAP_2026.md)** - AI-native era roadmap with business model
- **[internal/docs/CURRENT_STATUS_SUMMARY.md](internal/docs/CURRENT_STATUS_SUMMARY.md)** - Quick status overview
- **[internal/docs/PROJECT_STATUS_2026.md](internal/docs/PROJECT_STATUS_2026.md)** - Detailed technical status
- **[internal/docs/IMPLEMENTATION_SUMMARY.md](internal/docs/IMPLEMENTATION_SUMMARY.md)** - Implementation notes
- **[CHANGELOG.md](CHANGELOG.md)** - What changed and when
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) for full guidelines. Key points:
- Open an issue before large changes
- Include tests for new features
- Run `make test` and `make lint` before submitting
- Follow commit message format: `type: description` (e.g., `feat: add WebSocket transport`)
-282
View File
@@ -1,282 +0,0 @@
# Go Micro - Current Status Summary
**Updated:** February 11, 2026
## 🎯 Executive Summary
**Go Micro's MCP integration is 3-4 months ahead of schedule**, with Q1 2026 goals complete and significant Q2/Q3 2026 features already delivered.
### Quick Status
-**Q1 2026 (MCP Foundation):** 100% COMPLETE
- 🟢 **Q2 2026 (Agent DX):** 60% COMPLETE (ahead of schedule)
- 🟢 **Q3 2026 (Production):** 40% COMPLETE (ahead of schedule)
- 🟡 **Q4 2026 (Ecosystem):** 0% COMPLETE (on track)
---
## 📊 What's Been Built
### ✅ Core MCP Integration (Q1 - COMPLETE)
- **MCP Gateway Library** (`gateway/mcp/`) - 2,083 lines
- HTTP/SSE transport
- Stdio JSON-RPC 2.0 transport
- Service discovery & tool generation
- Schema generation from Go types
- **CLI Commands** (`micro mcp`)
- `micro mcp serve` - Start MCP server (stdio or HTTP)
- `micro mcp list` - List available tools
- `micro mcp test` - Test tools (placeholder)
- **Documentation**
- Complete API documentation
- 2 working examples (hello, documented)
- Blog post: "Making Microservices AI-Native with MCP"
### ✅ Advanced Features (Q2/Q3 - DELIVERED EARLY)
#### 🔒 Security & Auth
- **Per-Tool Scopes**
- Service-level: `server.WithEndpointScopes("Blog.Create", "blog:write")`
- Gateway-level: `Options.Scopes` map for overrides
- Bearer token authentication
- Scope enforcement before RPC execution
#### 📊 Observability
- **Tracing**
- UUID trace IDs per tool call
- Metadata propagation (`Mcp-Trace-Id`, `Mcp-Tool-Name`, `Mcp-Account-Id`)
- Full call chain tracking
- **Audit Logging**
- Immutable audit records per tool call
- Captures: tool, account, scopes, allowed/denied, duration, errors
- Callback function: `Options.AuditFunc`
#### 🚦 Rate Limiting
- Per-tool rate limiters
- Configurable requests/second and burst
- Token bucket algorithm
#### 📝 Documentation Extraction
- Auto-extract from Go doc comments
- `@example` tag support for JSON examples
- Struct tag parsing for parameter descriptions
- Manual override via `WithEndpointDocs()`
---
## 🚀 What Works Today
### For Claude Code Users
```bash
# Start MCP server for Claude Code
micro mcp serve
# Add to ~/.claude/claude_desktop_config.json:
{
"mcpServers": {
"my-services": {
"command": "micro",
"args": ["mcp", "serve"]
}
}
}
```
### For Library Users
```go
package main
import (
"go-micro.dev/v5"
"go-micro.dev/v5/gateway/mcp"
)
func main() {
service := micro.NewService(micro.Name("myservice"))
service.Init()
// Add MCP gateway (3 lines!)
go mcp.ListenAndServe(":3000", mcp.Options{
Registry: service.Options().Registry,
Auth: authProvider, // Optional: auth.Auth
Scopes: map[string][]string{ // Optional: per-tool scopes
"myservice.Handler.Create": {"write"},
},
RateLimit: &mcp.RateLimitConfig{ // Optional
RequestsPerSecond: 10,
Burst: 20,
},
AuditFunc: func(r mcp.AuditRecord) { // Optional
log.Printf("[audit] %+v", r)
},
})
service.Run()
}
```
### For Service Developers
```go
// Just add Go comments - docs extracted automatically!
// GetUser retrieves a user by ID. Returns full profile with email and preferences.
//
// @example {"id": "user-123"}
func (s *UserService) GetUser(ctx context.Context, req *GetUserRequest, rsp *GetUserResponse) error {
// implementation
}
// Register with scopes
handler := service.Server().NewHandler(
new(UserService),
server.WithEndpointScopes("UserService.Delete", "users:admin"),
)
```
---
## 📈 Test Coverage
**568 lines** of comprehensive tests covering:
- ✅ Scope validation & enforcement
- ✅ Auth provider integration
- ✅ Trace ID generation & propagation
- ✅ Audit record creation
- ✅ Rate limiting
- ✅ HTTP & Stdio transports
- ✅ Tool discovery & schema generation
---
## 🎯 What's Next (Recommended Priorities)
### Immediate (Next 2 Weeks)
1. **Complete `micro mcp test` command** (~1 day)
- Implement actual tool testing with JSON input/output
2. **LangChain SDK** (~1 week)
- Python package: `go-micro-langchain`
- Auto-generate LangChain tools from registry
- Example multi-agent workflow
- **Impact:** Largest agent framework integration
3. **Interactive Playground** (~1 week)
- Web UI for testing services with AI
- Real-time tool call visualization
- **Impact:** Critical for demos and sales
### Short-Term (Next Month)
4. **WebSocket Transport** (~3 days)
- Bidirectional streaming for long-running operations
5. **LlamaIndex SDK** (~1 week)
- Python package for RAG integration
6. **Case Studies** (ongoing)
- Document real-world usage
---
## 📊 By The Numbers
| Metric | Value |
|--------|-------|
| **Production Code** | 2,083 lines |
| **Test Code** | 568 lines |
| **Documentation Files** | 4+ |
| **Working Examples** | 2 |
| **CLI Commands** | 3 |
| **Transports** | 2 (HTTP/SSE, Stdio) |
| **Q1 Completion** | 100% |
| **Ahead of Schedule** | 3-4 months |
---
## 🔍 Where We Are on the Roadmap
### Q1 2026: MCP Foundation
**Status:** ✅ COMPLETE (100%)
- All 6 planned deliverables complete
- Production-ready implementation
- Comprehensive documentation
### Q2 2026: Agent Developer Experience
**Status:** 🟢 IN PROGRESS (60% complete)
**COMPLETED (ahead of schedule):**
- ✅ Stdio transport for Claude Code
-`micro mcp serve` and `list` commands
- ✅ Tool descriptions from comments
-`@example` tag support
- ✅ Schema generation from struct tags
- ✅ HTTP/SSE with auth
**NOT YET STARTED:**
-`micro mcp test` (full implementation)
-`micro mcp docs` and `export` commands
- ❌ Agent SDKs (LangChain, LlamaIndex, AutoGPT)
- ❌ Interactive Agent Playground
- ❌ Multi-protocol (WebSocket, gRPC, HTTP/3)
### Q3 2026: Production & Scale
**Status:** 🟢 IN PROGRESS (40% complete)
**COMPLETED (ahead of schedule):**
- ✅ Per-tool authentication & scopes
- ✅ Agent call tracing
- ✅ Rate limiting
- ✅ Audit logging
- ✅ Bearer token auth
**NOT YET STARTED:**
- ❌ Standalone MCP Gateway binary
- ❌ Kubernetes Operator
- ❌ Helm Charts
- ❌ OpenTelemetry integration
- ❌ Full observability dashboards
### Q4 2026: Ecosystem & Monetization
**Status:** 🟡 PLANNING (0% complete)
- All features planned for Q4 2026
- On track to start in Q4
---
## 📖 Key Documents
1. **[PROJECT_STATUS_2026.md](./PROJECT_STATUS_2026.md)** - Comprehensive 20-page status report
2. **[ROADMAP_2026.md](./ROADMAP_2026.md)** - Updated roadmap with completion markers
3. **[/gateway/mcp/DOCUMENTATION.md](./gateway/mcp/DOCUMENTATION.md)** - Complete MCP documentation
4. **[/examples/mcp/README.md](./examples/mcp/README.md)** - Examples and usage guide
5. **[/internal/website/blog/2.md](./internal/website/blog/2.md)** - Launch blog post
---
## 🎉 Key Achievements
1. **✅ Production-Ready in Q1** - Ahead of schedule
2. **✅ Security-First** - Auth, scopes, audit from day one
3. **✅ Developer-Friendly** - 3 lines of code to enable MCP
4. **✅ Claude Code Ready** - Works with Anthropic's flagship IDE
5. **✅ Comprehensive Testing** - 90%+ test coverage
6. **✅ Well-Documented** - Multiple docs + examples + blog post
---
## 💡 Bottom Line
**Go Micro is production-ready for AI agent integration TODAY.**
The Q1 2026 foundation is solid, with advanced Q2/Q3 features already delivered. The framework is:
- ✅ Ready for production use
- ✅ Secure by default
- ✅ Easy to use (3 lines of code)
- ✅ Well-tested and documented
- ✅ Compatible with Claude Code and other AI tools
**Next focus:** Agent SDKs and developer tools to drive adoption.
---
**For detailed technical analysis, see [PROJECT_STATUS_2026.md](./PROJECT_STATUS_2026.md)**
+26
View File
@@ -0,0 +1,26 @@
FROM alpine:latest
ARG TARGETPLATFORM
ENV USER=micro
ENV GROUPNAME=$USER
ARG UID=1001
ARG GID=1001
RUN addgroup --gid "$GID" "$GROUPNAME" \
&& adduser \
--disabled-password \
--gecos "" \
--home "/micro" \
--ingroup "$GROUPNAME" \
--no-create-home \
--uid "$UID" "$USER"
ENV PATH=/usr/local/go/bin:$PATH
RUN apk --no-cache add git make curl
COPY --from=golang:1.26.0-alpine /usr/local/go /usr/local/go
COPY $TARGETPLATFORM/micro /usr/local/go/bin/
COPY $TARGETPLATFORM/protoc-gen-micro /usr/local/go/bin/
WORKDIR /micro
EXPOSE 8080
ENTRYPOINT ["/usr/local/go/bin/micro"]
CMD ["server"]
+25 -1
View File
@@ -1,4 +1,14 @@
.PHONY: test test-race test-coverage lint fmt install-tools proto clean help
NAME = micro
GIT_COMMIT = $(shell git rev-parse --short HEAD)
GIT_TAG = $(shell git describe --abbrev=0 --tags --always --match "v*")
GIT_IMPORT = go-micro.dev/v5/cmd/micro
BUILD_DATE = $(shell date +%s)
LDFLAGS = -X $(GIT_IMPORT).BuildDate=$(BUILD_DATE) -X $(GIT_IMPORT).GitCommit=$(GIT_COMMIT) -X $(GIT_IMPORT).GitTag=$(GIT_TAG)
# GORELEASER_DOCKER_IMAGE = ghcr.io/goreleaser/goreleaser-cross:v1.25.7
GORELEASER_DOCKER_IMAGE = ghcr.io/goreleaser/goreleaser:latest
.PHONY: test test-race test-coverage lint fmt install-tools proto clean help gorelease-dry-run gorelease-dry-run-docker
# Default target
help:
@@ -13,6 +23,9 @@ help:
@echo " make proto - Generate protobuf code"
@echo " make clean - Clean build artifacts"
$(NAME):
CGO_ENABLED=0 go build -ldflags "-s -w ${LDFLAGS}" -o $(NAME) cmd/micro/main.go
# Run tests
test:
go test -v ./...
@@ -56,3 +69,14 @@ clean:
find . -name "*.test" -type f -delete
go clean -cache -testcache
# Try binary release
gorelease-dry-run:
docker run \
--rm \
-e CGO_ENABLED=0 \
-v $(CURDIR):/$(NAME) \
-v /var/run/docker.sock:/var/run/docker.sock \
-w /$(NAME) \
$(GORELEASER_DOCKER_IMAGE) \
--clean --verbose --skip=publish,validate --snapshot
+128 -6
View File
@@ -2,7 +2,9 @@
Go Micro is a framework for distributed systems development.
**[📖 Documentation](https://go-micro.dev/docs/)** | [Sponsor the project](https://github.com/sponsors/micro) | [Discord](https://discord.gg/jwTYuUVAGh)
**[📖 Documentation](https://go-micro.dev/docs/)** | [Sponsored by Anthropic](https://go-micro.dev/blog/3)
*️⃣ Try [**Micro**](https://micro.mu) - a new app platform by the creator of Go Micro
## Overview
@@ -24,6 +26,10 @@ Go Micro abstracts away the details of distributed systems. Here are the main fe
- **Data Storage** - A simple data store interface to read, write and delete records. It includes support for many storage backends
in the plugins repo. State and persistence becomes a core requirement beyond prototyping and Micro looks to build that into the framework.
- **Data Model** - A typed data model layer with CRUD operations, queries, and multiple backends (memory, SQLite, Postgres). Define Go
structs with tags and get type-safe Create/Read/Update/Delete/List/Count operations. Accessible via `service.Model()` alongside
`service.Client()` and `service.Server()` for a complete service experience: call services, handle requests, save and query data.
- **Service Discovery** - Automatic service registration and name resolution. Service discovery is at the core of micro service
development. When service A needs to speak to service B it needs the location of that service. The default discovery mechanism is
multicast DNS (mdns), a zeroconf system.
@@ -45,6 +51,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.
@@ -53,7 +63,7 @@ in the plugins repo. State and persistence becomes a core requirement beyond pro
To make use of Go Micro
```bash
go get go-micro.dev/v5@latest
go get go-micro.dev/v5@v5.16.0
```
Create a service and register a handler
@@ -95,10 +105,7 @@ func main() {
Set a fixed address
```go
service := micro.NewService(
micro.Name("helloworld"),
micro.Address(":8080"),
)
service := micro.New("helloworld", micro.Address(":8080"))
```
Call it via curl
@@ -144,11 +151,104 @@ 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 := micro.New("users", micro.Address(":9001"))
orders := micro.New("orders", micro.Address(":9002"))
users.Handle(new(Users))
orders.Handle(new(Orders))
// Run all services together with shared lifecycle
g := micro.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.
## Data Model
Go Micro includes a typed data model layer for persistence. Define a struct, tag a key field, and get type-safe CRUD and query operations backed by memory, SQLite, or Postgres.
```go
import (
"go-micro.dev/v5/model"
"go-micro.dev/v5/model/sqlite"
)
// Define your data type
type User struct {
ID string `json:"id" model:"key"`
Name string `json:"name"`
Email string `json:"email" model:"index"`
Age int `json:"age"`
}
```
Register your types and use the model:
```go
service := micro.New("users")
// Register and use the service's model backend
db := service.Model()
db.Register(&User{})
// CRUD operations
db.Create(ctx, &User{ID: "1", Name: "Alice", Email: "alice@example.com", Age: 30})
user := &User{}
db.Read(ctx, "1", user)
user.Name = "Alice Smith"
db.Update(ctx, user)
db.Delete(ctx, "1", &User{})
```
Query with filters, ordering, and pagination:
```go
var results []*User
// Find users by field
db.List(ctx, &results, model.Where("email", "alice@example.com"))
// Complex queries
db.List(ctx, &results,
model.WhereOp("age", ">=", 18),
model.OrderDesc("name"),
model.Limit(10),
model.Offset(20),
)
count, _ := users.Count(ctx, model.Where("age", 30))
```
Swap backends with an option:
```go
// Development: in-memory (default)
service := micro.New("users")
// Production: SQLite or Postgres
db, _ := sqlite.New(model.WithDSN("file:app.db"))
service := micro.New("users", micro.Model(db))
```
Every service gets `Client()`, `Server()`, and `Model()` — call services, handle requests, and save data all from the same interface.
## 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.
@@ -274,7 +374,9 @@ Package reference: https://pkg.go.dev/go-micro.dev/v5
**User Guides:**
- [Getting Started](internal/website/docs/getting-started.md)
- [Data Model](internal/website/docs/model.md)
- [MCP & AI Agents](internal/website/docs/mcp.md)
- [AI Provider Integration](internal/website/docs/guides/ai-provider-guide.md)
- [Plugins Overview](internal/website/docs/plugins.md)
- [Learn by Example](internal/website/docs/examples/index.md)
- [Deployment Guide](internal/website/docs/deployment.md)
@@ -287,6 +389,26 @@ Package reference: https://pkg.go.dev/go-micro.dev/v5
- [TLS Security Migration](internal/website/docs/TLS_SECURITY_UPDATE.md)
- [Security Migration Guide](internal/website/docs/SECURITY_MIGRATION.md)
## Supported AI Providers
Go Micros `ai` package gives every provider the same interface: `Init`, `Generate`, `Stream`, and functional options. Swap providers with a single import.
| Provider | Import | Default Model |
|----------|--------|---------------|
| **Anthropic** | `go-micro.dev/v5/ai/anthropic` | `claude-sonnet-4-20250514` |
| **OpenAI** | `go-micro.dev/v5/ai/openai` | `gpt-4o` |
Any provider that exposes an OpenAI-compatible API can also be used directly:
```go
m := ai.New("openai",
ai.WithAPIKey("your-key"),
ai.WithBaseURL("https://api.yourprovider.com"),
)
```
**Want to add your platform?** See the [AI Provider Integration Guide](internal/website/docs/guides/ai-provider-guide.md) for how to implement `ai.Model` and submit a PR. We welcome both code contributions and sponsorships from AI infrastructure companies — reach out via a [GitHub issue](https://github.com/micro/go-micro/issues).
## Adopters
- [Sourse](https://sourse.eu) - Work in the field of earth observation, including embedded Kubernetes running onboard aircraft, and weve built a mission management SaaS platform using Go Micro.
+31 -14
View File
@@ -2,18 +2,27 @@
This roadmap outlines the planned features and improvements for Go Micro. Community feedback and contributions are welcome!
> **🚀 NEW:** See [ROADMAP_2026.md](ROADMAP_2026.md) for the **AI-Native Era roadmap** focused on MCP integration, agent-first development, and business sustainability. This document covers general framework improvements.
> **See [internal/docs/ROADMAP_2026.md](internal/docs/ROADMAP_2026.md) for the AI-Native Era roadmap** focused on MCP integration, agent-first development, and business sustainability. This document covers general framework improvements.
## Current Focus (Q1 2026)
## Current Focus (Q1 2026) - COMPLETE
### Documentation & Developer Experience
- [x] Modernize documentation structure
- [x] Add learn-by-example guides
- [x] Update issue templates
- [x] MCP integration documentation
- [x] Agent playground and MCP tools registry
- [ ] Create video tutorials
- [ ] Interactive documentation site
- [ ] Plugin discovery dashboard
### AI & Model Integration
- [x] AI package with provider abstraction (`ai.Model` interface)
- [x] Anthropic Claude provider (`ai/anthropic`)
- [x] OpenAI GPT provider (`ai/openai`)
- [x] Tool execution with auto-calling support
- [x] Streaming support via `ai.Stream`
### Observability
- [ ] OpenTelemetry native support
- [ ] Auto-instrumentation for handlers
@@ -22,7 +31,10 @@ This roadmap outlines the planned features and improvements for Go Micro. Commun
- [ ] Integration with popular observability platforms
### Developer Tools
- [ ] `micro dev` with hot reload
- [x] `micro run` with hot reload and unified gateway
- [x] `micro deploy` with SSH + systemd deployment
- [x] `micro mcp` command suite (serve, list, test, docs, export)
- [ ] `micro dev` with enhanced hot reload
- [ ] Service templates (`micro new --template`)
- [ ] Better error messages with suggestions
- [ ] Debug tooling improvements
@@ -31,8 +43,8 @@ This roadmap outlines the planned features and improvements for Go Micro. Commun
## Q2 2026
### Production Readiness
- [ ] Health check standardization
- [ ] Graceful shutdown improvements
- [x] Health check standardization
- [x] Graceful shutdown improvements
- [ ] Resource cleanup best practices
- [ ] Load testing framework integration
- [ ] Performance benchmarking suite
@@ -45,6 +57,10 @@ This roadmap outlines the planned features and improvements for Go Micro. Commun
- [ ] Multi-cluster patterns
### Security
- [x] Bearer token authentication for MCP
- [x] Per-tool scope enforcement
- [x] Audit logging
- [x] Rate limiting
- [ ] mTLS by default option
- [ ] Secret management integration (Vault, AWS Secrets Manager)
- [ ] RBAC improvements
@@ -62,7 +78,7 @@ This roadmap outlines the planned features and improvements for Go Micro. Commun
### Streaming & Async
- [ ] Improved streaming support
- [ ] Server-sent events (SSE) support
- [x] Server-sent events (SSE) support (via MCP gateway)
- [ ] WebSocket plugin
- [ ] Event sourcing patterns
- [ ] CQRS examples
@@ -116,6 +132,7 @@ This roadmap outlines the planned features and improvements for Go Micro. Commun
### Differentiation
- **Batteries included, fully swappable** - Start simple, scale complex
- **Zero-config local development** - No infrastructure required to start
- **AI-native by default** - Every service is an MCP tool automatically
- **Plugin ecosystem in-repo** - No version compatibility hell
- **Progressive complexity** - Learn as you grow
- **Cloud-native first** - Built for Kubernetes and containers
@@ -125,11 +142,11 @@ This roadmap outlines the planned features and improvements for Go Micro. Commun
We welcome contributions to any roadmap items! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
### High Priority Areas
1. Documentation improvements
2. Real-world examples
3. Plugin development
4. Performance optimizations
5. Testing infrastructure
1. Documentation improvements (guides, tutorials)
2. Multi-protocol MCP support (WebSocket, gRPC)
3. Agent SDK integrations (LlamaIndex, AutoGPT)
4. OpenTelemetry integration
5. Kubernetes operator and Helm charts
### How to Contribute
- Pick an item from the roadmap
@@ -139,7 +156,7 @@ We welcome contributions to any roadmap items! See [CONTRIBUTING.md](CONTRIBUTIN
## Feedback
Have suggestions for the roadmap?
Have suggestions for the roadmap?
- Open a [feature request](.github/ISSUE_TEMPLATE/feature_request.md)
- Start a discussion in GitHub Discussions
@@ -160,6 +177,6 @@ We follow semantic versioning:
---
Last updated: November 2025
Last updated: March 2026
This roadmap is subject to change based on community needs and priorities. Star the repo to stay updated! ⭐
This roadmap is subject to change based on community needs and priorities.
+219
View File
@@ -0,0 +1,219 @@
# AI Package
The `ai` package provides a simple, high-level interface for AI model providers like Anthropic Claude and OpenAI GPT.
## Interface
The Model interface follows the same patterns as other go-micro packages (Registry, Client, Broker):
```go
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
}
```
## Quick Start
```go
import (
"context"
"go-micro.dev/v5/ai"
_ "go-micro.dev/v5/ai/anthropic"
_ "go-micro.dev/v5/ai/openai"
)
// Create a model
m := ai.New("openai",
ai.WithAPIKey("your-api-key"),
ai.WithModel("gpt-4o"),
)
// Generate a response
req := &ai.Request{
Prompt: "What is Go?",
SystemPrompt: "You are a helpful programming assistant",
}
resp, err := m.Generate(context.Background(), req)
if err != nil {
log.Fatal(err)
}
fmt.Println(resp.Reply)
```
## Options
Configure the model using functional options:
```go
m := ai.New("anthropic",
ai.WithAPIKey("your-key"), // Required
ai.WithModel("claude-sonnet-4-20250514"), // Optional, uses provider default
ai.WithBaseURL("https://api.anthropic.com"), // Optional, uses provider default
)
```
You can also update options after creation:
```go
m.Init(
ai.WithModel("gpt-4o-mini"),
ai.WithAPIKey("new-key"),
)
```
## Using Tools
The model can automatically execute tool calls when provided with a tool handler:
```go
// Define a tool handler
toolHandler := func(name string, input map[string]any) (result any, content string) {
// Execute the tool and return results
switch name {
case "get_weather":
return map[string]string{"temp": "72F"}, `{"temp": "72F"}`
default:
return nil, `{"error": "unknown tool"}`
}
}
// Create model with tool handler
m := ai.New("openai",
ai.WithAPIKey("your-key"),
ai.WithToolHandler(toolHandler),
)
// Provide tools in the request
req := &ai.Request{
Prompt: "What's the weather?",
SystemPrompt: "You are a helpful assistant",
Tools: []ai.Tool{
{
Name: "get_weather",
Description: "Get current weather",
Properties: map[string]any{
"location": map[string]any{
"type": "string",
"description": "City name",
},
},
},
},
}
// Generate will automatically call tools and return final answer
resp, err := m.Generate(context.Background(), req)
fmt.Println(resp.Answer) // Final answer after tool execution
```
## Response Structure
```go
type Response struct {
Reply string // Initial reply from model
ToolCalls []ToolCall // Tools the model wants to call
Answer string // Final answer (after tool execution if handler provided)
}
```
- `Reply`: The model's first response
- `ToolCalls`: List of tools the model requested (if any)
- `Answer`: The final answer after tools are executed (only set if ToolHandler is provided)
## Supported Providers
### Anthropic Claude
```go
m := ai.New("anthropic",
ai.WithAPIKey("sk-ant-..."),
ai.WithModel("claude-sonnet-4-20250514"), // default
)
```
Default model: `claude-sonnet-4-20250514`
Default base URL: `https://api.anthropic.com`
### OpenAI GPT
```go
m := ai.New("openai",
ai.WithAPIKey("sk-..."),
ai.WithModel("gpt-4o"), // default
)
```
Default model: `gpt-4o`
Default base URL: `https://api.openai.com`
## Auto-Detection
Use `AutoDetectProvider()` to detect the provider from a base URL:
```go
provider := ai.AutoDetectProvider("https://api.anthropic.com")
// Returns "anthropic"
m := ai.New(provider, ai.WithAPIKey("..."))
```
## Adding a New Provider
See the full **[AI Provider Integration Guide](../internal/website/docs/guides/ai-provider-guide.md)** for a step-by-step walkthrough, checklist, and design notes.
Quick summary:
1. Create `ai/yourprovider/yourprovider.go` implementing `ai.Model`.
2. Call `ai.Register("yourprovider", ...)` in `init()`.
3. Add tests in `ai/yourprovider/yourprovider_test.go`.
4. Users enable the provider with a blank import:
```go
import _ "go-micro.dev/v5/ai/yourprovider"
```
We welcome contributions and sponsorships from AI infrastructure companies — see the guide for details.
## Comparison with Other Packages
The ai package follows the same patterns as other go-micro packages:
**Registry:**
```go
r := registry.NewRegistry(registry.Addrs("..."))
r.Register(service)
```
**Client:**
```go
c := client.NewClient(client.Retries(3))
c.Call(ctx, req, rsp)
```
**AI:**
```go
m := ai.New("openai", ai.WithAPIKey("..."))
m.Generate(ctx, req)
```
All use:
- `Init()` to update options
- `Options()` to get current options
- `String()` to get the implementation name
- Functional options pattern
## Testing
```bash
go test ./ai/...
```
## Examples
See the [server implementation](../cmd/micro/server/server.go) for a complete example of using the ai package with tool execution.
+227
View File
@@ -0,0 +1,227 @@
// Package anthropic implements the Anthropic Claude model provider
package anthropic
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"go-micro.dev/v5/ai"
)
func init() {
ai.Register("anthropic", func(opts ...ai.Option) ai.Model {
return NewProvider(opts...)
})
}
// Provider implements the ai.Model interface for Anthropic Claude
type Provider struct {
opts ai.Options
}
// NewProvider creates a new Anthropic provider
func NewProvider(opts ...ai.Option) *Provider {
options := ai.NewOptions(opts...)
// Set defaults if not provided
if options.Model == "" {
options.Model = "claude-sonnet-4-20250514"
}
if options.BaseURL == "" {
options.BaseURL = "https://api.anthropic.com"
}
return &Provider{
opts: options,
}
}
// Init initializes the provider with options
func (p *Provider) Init(opts ...ai.Option) error {
for _, o := range opts {
o(&p.opts)
}
return nil
}
// Options returns the provider options
func (p *Provider) Options() ai.Options {
return p.opts
}
// String returns the provider name
func (p *Provider) String() string {
return "anthropic"
}
// Generate generates a response from the model
func (p *Provider) Generate(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (*ai.Response, error) {
// Build tools for Anthropic format
var anthropicTools []map[string]any
for _, t := range req.Tools {
anthropicTools = append(anthropicTools, map[string]any{
"name": t.Name,
"description": t.Description,
"input_schema": map[string]any{
"type": "object",
"properties": t.Properties,
},
})
}
// Build initial request
apiReq := map[string]any{
"model": p.opts.Model,
"max_tokens": 4096,
"system": req.SystemPrompt,
"messages": []map[string]any{
{"role": "user", "content": req.Prompt},
},
}
if len(anthropicTools) > 0 {
apiReq["tools"] = anthropicTools
}
// Make API call
resp, rawContent, err := p.callAPI(ctx, apiReq)
if err != nil {
return nil, err
}
// If no tool calls, return response
if len(resp.ToolCalls) == 0 {
return resp, nil
}
// If tool handler is provided, execute tools and get final answer
if p.opts.ToolHandler != nil {
var toolResults []ai.ToolResult
for _, tc := range resp.ToolCalls {
_, content := p.opts.ToolHandler(tc.Name, tc.Input)
toolResults = append(toolResults, ai.ToolResult{
ID: tc.ID,
Content: content,
})
}
// Build follow-up request with tool results
var toolResultBlocks []map[string]any
for _, tr := range toolResults {
toolResultBlocks = append(toolResultBlocks, map[string]any{
"type": "tool_result",
"tool_use_id": tr.ID,
"content": tr.Content,
})
}
followUpReq := map[string]any{
"model": p.opts.Model,
"max_tokens": 4096,
"system": req.SystemPrompt,
"messages": []map[string]any{
{"role": "user", "content": req.Prompt},
{"role": "assistant", "content": rawContent},
{"role": "user", "content": toolResultBlocks},
},
}
// Make follow-up API call
followUpResp, _, err := p.callAPI(ctx, followUpReq)
if err == nil && followUpResp.Reply != "" {
resp.Answer = followUpResp.Reply
}
}
return resp, nil
}
// Stream generates a streaming response (not yet implemented)
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 anthropic provider")
}
// callAPI makes an HTTP request to the Anthropic API
func (p *Provider) callAPI(ctx context.Context, req map[string]any) (*ai.Response, any, error) {
// Marshal request
reqBody, err := json.Marshal(req)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal request: %w", err)
}
// Build HTTP request
apiURL := strings.TrimRight(p.opts.BaseURL, "/") + "/v1/messages"
httpReq, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewReader(reqBody))
if err != nil {
return nil, nil, fmt.Errorf("failed to create request: %w", err)
}
// Set headers
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("x-api-key", p.opts.APIKey)
httpReq.Header.Set("anthropic-version", "2023-06-01")
// Make request
httpResp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, nil, fmt.Errorf("API request failed: %w", err)
}
defer httpResp.Body.Close()
// Read response
respBody, _ := io.ReadAll(httpResp.Body)
if httpResp.StatusCode != 200 {
return nil, nil, fmt.Errorf("API error (%s): %s", httpResp.Status, string(respBody))
}
// Parse response
var anthropicResp struct {
Content []struct {
Type string `json:"type"`
Text string `json:"text"`
ID string `json:"id"`
Name string `json:"name"`
Input json.RawMessage `json:"input"`
} `json:"content"`
StopReason string `json:"stop_reason"`
}
if err := json.Unmarshal(respBody, &anthropicResp); err != nil {
return nil, nil, fmt.Errorf("failed to parse response: %w", err)
}
response := &ai.Response{}
// Extract text reply
var replyParts []string
for _, block := range anthropicResp.Content {
if block.Type == "text" && block.Text != "" {
replyParts = append(replyParts, block.Text)
}
}
if len(replyParts) > 0 {
response.Reply = strings.Join(replyParts, "\n")
}
// Extract tool calls
for _, block := range anthropicResp.Content {
if block.Type == "tool_use" {
var input map[string]any
if err := json.Unmarshal(block.Input, &input); err != nil {
input = map[string]any{}
}
response.ToolCalls = append(response.ToolCalls, ai.ToolCall{
ID: block.ID,
Name: block.Name,
Input: input,
})
}
}
return response, anthropicResp.Content, nil
}
+94
View File
@@ -0,0 +1,94 @@
package anthropic
import (
"context"
"testing"
"go-micro.dev/v5/ai"
)
func TestProvider_String(t *testing.T) {
p := NewProvider()
if p.String() != "anthropic" {
t.Errorf("Expected provider name 'anthropic', got '%s'", p.String())
}
}
func TestProvider_Init(t *testing.T) {
p := NewProvider()
err := p.Init(
ai.WithModel("test-model"),
ai.WithAPIKey("test-key"),
ai.WithBaseURL("https://test.com"),
)
if err != nil {
t.Fatalf("Init failed: %v", err)
}
opts := p.Options()
if opts.Model != "test-model" {
t.Errorf("Expected model 'test-model', got '%s'", opts.Model)
}
if opts.APIKey != "test-key" {
t.Errorf("Expected API key 'test-key', got '%s'", opts.APIKey)
}
if opts.BaseURL != "https://test.com" {
t.Errorf("Expected base URL 'https://test.com', got '%s'", opts.BaseURL)
}
}
func TestProvider_Options(t *testing.T) {
p := NewProvider(
ai.WithModel("custom-model"),
ai.WithAPIKey("my-key"),
)
opts := p.Options()
if opts.Model != "custom-model" {
t.Errorf("Expected model 'custom-model', got '%s'", opts.Model)
}
if opts.APIKey != "my-key" {
t.Errorf("Expected API key 'my-key', got '%s'", opts.APIKey)
}
}
func TestProvider_Defaults(t *testing.T) {
p := NewProvider()
opts := p.Options()
if opts.Model != "claude-sonnet-4-20250514" {
t.Errorf("Expected default model 'claude-sonnet-4-20250514', got '%s'", opts.Model)
}
if opts.BaseURL != "https://api.anthropic.com" {
t.Errorf("Expected default base URL 'https://api.anthropic.com', got '%s'", opts.BaseURL)
}
}
func TestProvider_Generate_NoAPIKey(t *testing.T) {
p := NewProvider()
req := &ai.Request{
Prompt: "Hello",
SystemPrompt: "You are helpful",
}
_, err := p.Generate(context.Background(), req)
if err == nil {
t.Error("Expected error when API key is missing, got nil")
}
}
func TestProvider_Stream_NotImplemented(t *testing.T) {
p := NewProvider()
req := &ai.Request{
Prompt: "Hello",
}
_, err := p.Stream(context.Background(), req)
if err == nil {
t.Error("Expected error for unimplemented streaming, got nil")
}
}
+130
View File
@@ -0,0 +1,130 @@
// Package ai provides abstraction for AI model providers
package ai
import (
"context"
"strings"
)
// Model provides an interface for interacting with AI model providers
type Model interface {
// Init initializes the model with options
Init(...Option) error
// Options returns the model options
Options() Options
// Generate generates a response from the model
Generate(ctx context.Context, req *Request, opts ...GenerateOption) (*Response, error)
// Stream generates a streaming response (for future implementation)
Stream(ctx context.Context, req *Request, opts ...GenerateOption) (Stream, error)
// String returns the name of the provider
String() string
}
// Tool represents a tool/function that can be called by the model
type Tool struct {
Name string // LLM-safe name (e.g., "greeter_Greeter_Hello")
OriginalName string // Original name (e.g., "greeter.Greeter.Hello")
Description string
Properties map[string]any // JSON schema for tool parameters
}
// Request represents a request to generate content from a model
type Request struct {
// Prompt is the user's message/prompt
Prompt string
// SystemPrompt is the system instruction for the model
SystemPrompt string
// Tools available for the model to use
Tools []Tool
// Messages for continuing a conversation (optional)
Messages []Message
}
// Message represents a conversation message
type Message struct {
Role string // "user", "assistant", "system", "tool"
Content any // Can be string or structured content
}
// Response represents the response from a model
type Response struct {
// Reply is the text response from the model
Reply string
// ToolCalls are tool calls requested by the model
ToolCalls []ToolCall
// Answer is the final answer after tool execution (if tools were used)
Answer string
}
// ToolCall represents a request to call a tool
type ToolCall struct {
ID string // Tool call ID (for correlation)
Name string // Tool name
Input map[string]any // Tool input arguments
}
// ToolResult represents the result of a tool execution
type ToolResult struct {
ID string // Tool call ID (for correlation)
Content string // Tool execution result (JSON string)
}
// Stream is the interface for streaming responses (future implementation)
type Stream interface {
// Recv receives the next chunk of the response
Recv() (*Response, error)
// Close closes the stream
Close() error
}
// ToolHandler is a function that handles tool calls
type ToolHandler func(name string, input map[string]any) (result any, content string)
// NewFunc creates a new Model instance
type NewFunc func(...Option) Model
var providers = make(map[string]NewFunc)
// Register registers a model provider
func Register(name string, fn NewFunc) {
providers[name] = fn
}
// New creates a new Model instance based on the provider name
func New(provider string, opts ...Option) Model {
if fn, ok := providers[provider]; ok {
return fn(opts...)
}
// Default to first registered provider
if len(providers) > 0 {
for _, fn := range providers {
return fn(opts...)
}
}
return nil
}
// AutoDetectProvider attempts to detect the provider from the base URL
func AutoDetectProvider(baseURL string) string {
if baseURL == "" {
return "openai"
}
// Simple detection based on URL
if strings.Contains(baseURL, "anthropic") {
return "anthropic"
}
return "openai"
}
// DefaultModel is a default model instance
var DefaultModel Model
// Generate generates a response using the default model
func Generate(ctx context.Context, req *Request, opts ...GenerateOption) (*Response, error) {
if DefaultModel == nil {
return nil, nil
}
return DefaultModel.Generate(ctx, req, opts...)
}
+226
View File
@@ -0,0 +1,226 @@
// Package openai implements the OpenAI model provider
package openai
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"go-micro.dev/v5/ai"
)
func init() {
ai.Register("openai", func(opts ...ai.Option) ai.Model {
return NewProvider(opts...)
})
}
// Provider implements the ai.Model interface for OpenAI
type Provider struct {
opts ai.Options
}
// NewProvider creates a new OpenAI provider
func NewProvider(opts ...ai.Option) *Provider {
options := ai.NewOptions(opts...)
// Set defaults if not provided
if options.Model == "" {
options.Model = "gpt-4o"
}
if options.BaseURL == "" {
options.BaseURL = "https://api.openai.com"
}
return &Provider{
opts: options,
}
}
// Init initializes the provider with options
func (p *Provider) Init(opts ...ai.Option) error {
for _, o := range opts {
o(&p.opts)
}
return nil
}
// Options returns the provider options
func (p *Provider) Options() ai.Options {
return p.opts
}
// String returns the provider name
func (p *Provider) String() string {
return "openai"
}
// Generate generates a response from the model
func (p *Provider) Generate(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (*ai.Response, error) {
// Build tools for OpenAI format
var openaiTools []map[string]any
for _, t := range req.Tools {
openaiTools = append(openaiTools, map[string]any{
"type": "function",
"function": map[string]any{
"name": t.Name,
"description": t.Description,
"parameters": map[string]any{
"type": "object",
"properties": t.Properties,
},
},
})
}
// Build messages
messages := []map[string]any{
{"role": "system", "content": req.SystemPrompt},
{"role": "user", "content": req.Prompt},
}
// Build initial request
apiReq := map[string]any{
"model": p.opts.Model,
"messages": messages,
}
if len(openaiTools) > 0 {
apiReq["tools"] = openaiTools
}
// Make API call
resp, rawMessage, err := p.callAPI(ctx, apiReq)
if err != nil {
return nil, err
}
// If no tool calls, return response
if len(resp.ToolCalls) == 0 {
return resp, nil
}
// If tool handler is provided, execute tools and get final answer
if p.opts.ToolHandler != nil {
// Build follow-up messages
followUpMessages := append(messages, map[string]any{
"role": "assistant",
"content": rawMessage["content"],
"tool_calls": rawMessage["tool_calls"],
})
for _, tc := range resp.ToolCalls {
_, content := p.opts.ToolHandler(tc.Name, tc.Input)
followUpMessages = append(followUpMessages, map[string]any{
"role": "tool",
"tool_call_id": tc.ID,
"content": content,
})
}
followUpReq := map[string]any{
"model": p.opts.Model,
"messages": followUpMessages,
}
// Make follow-up API call
followUpResp, _, err := p.callAPI(ctx, followUpReq)
if err == nil && followUpResp.Reply != "" {
resp.Answer = followUpResp.Reply
}
}
return resp, nil
}
// Stream generates a streaming response (not yet implemented)
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 openai provider")
}
// callAPI makes an HTTP request to the OpenAI API
func (p *Provider) callAPI(ctx context.Context, req map[string]any) (*ai.Response, map[string]any, error) {
// Marshal request
reqBody, err := json.Marshal(req)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal request: %w", err)
}
// Build HTTP request
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)
}
// Set headers
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+p.opts.APIKey)
// Make request
httpResp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, nil, fmt.Errorf("API request failed: %w", err)
}
defer httpResp.Body.Close()
// Read response
respBody, _ := io.ReadAll(httpResp.Body)
if httpResp.StatusCode != 200 {
return nil, nil, fmt.Errorf("API error (%s): %s", httpResp.Status, string(respBody))
}
// Parse response
var chatResp struct {
Choices []struct {
Message struct {
Content string `json:"content"`
ToolCalls []struct {
ID string `json:"id"`
Function struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
} `json:"function"`
} `json:"tool_calls"`
} `json:"message"`
} `json:"choices"`
}
if err := json.Unmarshal(respBody, &chatResp); err != nil {
return nil, nil, fmt.Errorf("failed to parse response: %w", err)
}
if len(chatResp.Choices) == 0 {
return nil, nil, fmt.Errorf("no response from API")
}
choice := chatResp.Choices[0]
response := &ai.Response{
Reply: choice.Message.Content,
}
// Extract tool calls
for _, tc := range choice.Message.ToolCalls {
var input map[string]any
if err := json.Unmarshal([]byte(tc.Function.Arguments), &input); err != nil {
input = map[string]any{}
}
response.ToolCalls = append(response.ToolCalls, ai.ToolCall{
ID: tc.ID,
Name: tc.Function.Name,
Input: input,
})
}
// Return raw message for potential follow-up
rawMessage := map[string]any{
"content": choice.Message.Content,
"tool_calls": choice.Message.ToolCalls,
}
return response, rawMessage, nil
}
+94
View File
@@ -0,0 +1,94 @@
package openai
import (
"context"
"testing"
"go-micro.dev/v5/ai"
)
func TestProvider_String(t *testing.T) {
p := NewProvider()
if p.String() != "openai" {
t.Errorf("Expected provider name 'openai', got '%s'", p.String())
}
}
func TestProvider_Init(t *testing.T) {
p := NewProvider()
err := p.Init(
ai.WithModel("test-model"),
ai.WithAPIKey("test-key"),
ai.WithBaseURL("https://test.com"),
)
if err != nil {
t.Fatalf("Init failed: %v", err)
}
opts := p.Options()
if opts.Model != "test-model" {
t.Errorf("Expected model 'test-model', got '%s'", opts.Model)
}
if opts.APIKey != "test-key" {
t.Errorf("Expected API key 'test-key', got '%s'", opts.APIKey)
}
if opts.BaseURL != "https://test.com" {
t.Errorf("Expected base URL 'https://test.com', got '%s'", opts.BaseURL)
}
}
func TestProvider_Options(t *testing.T) {
p := NewProvider(
ai.WithModel("custom-model"),
ai.WithAPIKey("my-key"),
)
opts := p.Options()
if opts.Model != "custom-model" {
t.Errorf("Expected model 'custom-model', got '%s'", opts.Model)
}
if opts.APIKey != "my-key" {
t.Errorf("Expected API key 'my-key', got '%s'", opts.APIKey)
}
}
func TestProvider_Defaults(t *testing.T) {
p := NewProvider()
opts := p.Options()
if opts.Model != "gpt-4o" {
t.Errorf("Expected default model 'gpt-4o', got '%s'", opts.Model)
}
if opts.BaseURL != "https://api.openai.com" {
t.Errorf("Expected default base URL 'https://api.openai.com', got '%s'", opts.BaseURL)
}
}
func TestProvider_Generate_NoAPIKey(t *testing.T) {
p := NewProvider()
req := &ai.Request{
Prompt: "Hello",
SystemPrompt: "You are helpful",
}
_, err := p.Generate(context.Background(), req)
if err == nil {
t.Error("Expected error when API key is missing, got nil")
}
}
func TestProvider_Stream_NotImplemented(t *testing.T) {
p := NewProvider()
req := &ai.Request{
Prompt: "Hello",
}
_, err := p.Stream(context.Background(), req)
if err == nil {
t.Error("Expected error for unimplemented streaming, got nil")
}
}
+77
View File
@@ -0,0 +1,77 @@
package ai
import (
"context"
)
// Options for model configuration
type Options struct {
// Context for the model
Context context.Context
// Model name (e.g., "gpt-4o", "claude-sonnet-4-20250514")
Model string
// APIKey for authentication
APIKey string
// BaseURL for the API endpoint
BaseURL string
// ToolHandler handles tool calls (optional, for automatic tool execution)
ToolHandler ToolHandler
}
// GenerateOptions for generate call
type GenerateOptions struct {
// Context for this specific generate call
Context context.Context
}
// Option is a function that modifies Options
type Option func(*Options)
// GenerateOption is a function that modifies GenerateOptions
type GenerateOption func(*GenerateOptions)
// NewOptions creates new Options with defaults
func NewOptions(opts ...Option) Options {
options := Options{
Context: context.Background(),
}
for _, o := range opts {
o(&options)
}
return options
}
// WithModel sets the model name
func WithModel(m string) Option {
return func(o *Options) {
o.Model = m
}
}
// WithAPIKey sets the API key
func WithAPIKey(key string) Option {
return func(o *Options) {
o.APIKey = key
}
}
// WithBaseURL sets the base URL
func WithBaseURL(url string) Option {
return func(o *Options) {
o.BaseURL = url
}
}
// WithContext sets the context
func WithContext(ctx context.Context) Option {
return func(o *Options) {
o.Context = ctx
}
}
// WithToolHandler sets the tool handler
func WithToolHandler(handler ToolHandler) Option {
return func(o *Options) {
o.ToolHandler = handler
}
}
+3 -3
View File
@@ -20,9 +20,9 @@ import (
"go-micro.dev/v5/registry"
"go-micro.dev/v5/registry/cache"
"go-micro.dev/v5/transport/headers"
maddr "go-micro.dev/v5/util/addr"
mnet "go-micro.dev/v5/util/net"
mls "go-micro.dev/v5/util/tls"
maddr "go-micro.dev/v5/internal/util/addr"
mnet "go-micro.dev/v5/internal/util/net"
mls "go-micro.dev/v5/internal/util/tls"
"golang.org/x/net/http2"
)
+2 -2
View File
@@ -8,8 +8,8 @@ import (
"github.com/google/uuid"
log "go-micro.dev/v5/logger"
maddr "go-micro.dev/v5/util/addr"
mnet "go-micro.dev/v5/util/net"
maddr "go-micro.dev/v5/internal/util/addr"
mnet "go-micro.dev/v5/internal/util/net"
)
type memoryBroker struct {
+1 -1
View File
@@ -12,7 +12,7 @@ import (
amqp "github.com/rabbitmq/amqp091-go"
"go-micro.dev/v5/logger"
mtls "go-micro.dev/v5/util/tls"
mtls "go-micro.dev/v5/internal/util/tls"
)
type MQExchangeType string
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"context"
"time"
"go-micro.dev/v5/util/backoff"
"go-micro.dev/v5/internal/util/backoff"
)
type BackoffFunc func(ctx context.Context, req Request, attempts int) (time.Duration, error)
+1 -1
View File
@@ -19,7 +19,7 @@ import (
"go-micro.dev/v5/metadata"
"go-micro.dev/v5/registry"
"go-micro.dev/v5/selector"
pnet "go-micro.dev/v5/util/net"
pnet "go-micro.dev/v5/internal/util/net"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/encoding"
+6
View File
@@ -401,6 +401,12 @@ func WithMessageContentType(ct string) MessageOption {
}
}
func WithConnectionTimeout(d time.Duration) CallOption {
return func(o *CallOptions) {
o.ConnectionTimeout = d
}
}
// Request Options
func WithContentType(ct string) RequestOption {
+3 -3
View File
@@ -20,9 +20,9 @@ import (
"go-micro.dev/v5/selector"
"go-micro.dev/v5/transport"
"go-micro.dev/v5/transport/headers"
"go-micro.dev/v5/util/buf"
"go-micro.dev/v5/util/net"
"go-micro.dev/v5/util/pool"
"go-micro.dev/v5/internal/util/buf"
"go-micro.dev/v5/internal/util/net"
"go-micro.dev/v5/internal/util/pool"
)
const (
+31 -31
View File
@@ -23,7 +23,7 @@ import (
"go-micro.dev/v5/debug/trace"
"go-micro.dev/v5/events"
"go-micro.dev/v5/logger"
mprofile "go-micro.dev/v5/profile"
mprofile "go-micro.dev/v5/service/profile"
"go-micro.dev/v5/registry"
"go-micro.dev/v5/registry/consul"
"go-micro.dev/v5/registry/etcd"
@@ -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
}
+19
View File
@@ -0,0 +1,19 @@
FROM golang:1.23-alpine AS builder
RUN apk add --no-cache git
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /micro-mcp-gateway ./cmd/micro-mcp-gateway
FROM alpine:3.20
RUN apk add --no-cache ca-certificates
COPY --from=builder /micro-mcp-gateway /usr/local/bin/micro-mcp-gateway
EXPOSE 3000
ENTRYPOINT ["micro-mcp-gateway"]
CMD ["--address", ":3000"]
+242
View File
@@ -0,0 +1,242 @@
// Command micro-mcp-gateway runs a standalone MCP gateway that discovers
// go-micro services via a registry and exposes them as AI-accessible tools
// through the Model Context Protocol.
//
// This is the production deployment binary for the MCP gateway, intended
// to run independently of your services.
//
// Usage:
//
// # mDNS (development default)
// micro-mcp-gateway --address :3000
//
// # Consul
// micro-mcp-gateway --address :3000 --registry consul --registry-address consul:8500
//
// # etcd
// micro-mcp-gateway --address :3000 --registry etcd --registry-address etcd:2379
//
// # With auth and rate limiting
// micro-mcp-gateway --address :3000 --registry consul \
// --rate-limit 100 --rate-burst 200 --audit
package main
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"strings"
"syscall"
"time"
"go-micro.dev/v5/auth"
"go-micro.dev/v5/auth/jwt"
"go-micro.dev/v5/gateway/mcp"
"go-micro.dev/v5/registry"
"go-micro.dev/v5/registry/consul"
"go-micro.dev/v5/registry/etcd"
"github.com/urfave/cli/v2"
)
var version = "0.1.0"
func main() {
app := &cli.App{
Name: "micro-mcp-gateway",
Usage: "Standalone MCP gateway for go-micro services",
Version: version,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "address",
Usage: "Address to listen on",
Value: ":3000",
EnvVars: []string{"MCP_ADDRESS"},
},
&cli.StringFlag{
Name: "registry",
Usage: "Service registry (mdns, consul, etcd)",
Value: "mdns",
EnvVars: []string{"MICRO_REGISTRY"},
},
&cli.StringFlag{
Name: "registry-address",
Usage: "Registry address (e.g., consul:8500, etcd:2379)",
EnvVars: []string{"MICRO_REGISTRY_ADDRESS"},
},
&cli.Float64Flag{
Name: "rate-limit",
Usage: "Requests per second per tool (0 = unlimited)",
EnvVars: []string{"MCP_RATE_LIMIT"},
},
&cli.IntFlag{
Name: "rate-burst",
Usage: "Rate limit burst size",
Value: 20,
EnvVars: []string{"MCP_RATE_BURST"},
},
&cli.BoolFlag{
Name: "auth",
Usage: "Enable JWT authentication",
EnvVars: []string{"MCP_AUTH"},
},
&cli.BoolFlag{
Name: "audit",
Usage: "Enable audit logging to stdout",
EnvVars: []string{"MCP_AUDIT"},
},
&cli.StringSliceFlag{
Name: "scope",
Usage: "Tool scope requirement (format: tool=scope1,scope2)",
},
&cli.IntFlag{
Name: "circuit-breaker",
Usage: "Circuit breaker max failures before opening (0 = disabled)",
EnvVars: []string{"MCP_CIRCUIT_BREAKER"},
},
&cli.DurationFlag{
Name: "circuit-breaker-timeout",
Usage: "Circuit breaker open-state timeout before half-open probe",
Value: 30 * time.Second,
EnvVars: []string{"MCP_CIRCUIT_BREAKER_TIMEOUT"},
},
},
Action: run,
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}
func run(c *cli.Context) error {
logger := log.New(os.Stdout, "[mcp-gateway] ", log.LstdFlags)
// Configure registry
reg, err := newRegistry(c.String("registry"), c.String("registry-address"))
if err != nil {
return fmt.Errorf("registry: %w", err)
}
// Build MCP options
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
opts := mcp.Options{
Registry: reg,
Address: c.String("address"),
Context: ctx,
Logger: logger,
}
// Rate limiting
if rps := c.Float64("rate-limit"); rps > 0 {
opts.RateLimit = &mcp.RateLimitConfig{
RequestsPerSecond: rps,
Burst: c.Int("rate-burst"),
}
logger.Printf("Rate limit: %.0f req/s, burst %d", rps, c.Int("rate-burst"))
}
// Auth
if c.Bool("auth") {
opts.Auth = jwt.NewAuth()
logger.Printf("JWT authentication enabled")
}
// Scopes
if scopes := c.StringSlice("scope"); len(scopes) > 0 {
opts.Scopes = parseScopes(scopes)
for tool, s := range opts.Scopes {
logger.Printf("Scope: %s requires [%s]", tool, strings.Join(s, ", "))
}
}
// Circuit breaker
if maxFail := c.Int("circuit-breaker"); maxFail > 0 {
opts.CircuitBreaker = &mcp.CircuitBreakerConfig{
MaxFailures: maxFail,
Timeout: c.Duration("circuit-breaker-timeout"),
}
logger.Printf("Circuit breaker: max %d failures, timeout %s", maxFail, c.Duration("circuit-breaker-timeout"))
}
// Audit
if c.Bool("audit") {
opts.AuditFunc = func(r mcp.AuditRecord) {
status := "ALLOWED"
if !r.Allowed {
status = "DENIED:" + r.DeniedReason
}
logger.Printf("[audit] %s tool=%s account=%s status=%s duration=%s",
r.TraceID, r.Tool, r.AccountID, status, r.Duration)
}
logger.Printf("Audit logging enabled")
}
// Print startup info
logger.Printf("Starting MCP gateway on %s", c.String("address"))
logger.Printf("Registry: %s", c.String("registry"))
if addr := c.String("registry-address"); addr != "" {
logger.Printf("Registry address: %s", addr)
}
// Start gateway in background
errCh := make(chan error, 1)
go func() {
errCh <- mcp.ListenAndServe(opts.Address, opts)
}()
// Wait for signal or error
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
select {
case sig := <-sigCh:
logger.Printf("Received %s, shutting down...", sig)
cancel()
return nil
case err := <-errCh:
return fmt.Errorf("gateway error: %w", err)
}
}
func newRegistry(name, address string) (registry.Registry, error) {
var opts []registry.Option
if address != "" {
opts = append(opts, registry.Addrs(strings.Split(address, ",")...))
}
switch name {
case "mdns", "":
return registry.NewMDNSRegistry(opts...), nil
case "consul":
return consul.NewConsulRegistry(opts...), nil
case "etcd":
return etcd.NewEtcdRegistry(opts...), nil
default:
return nil, fmt.Errorf("unknown registry %q (supported: mdns, consul, etcd)", name)
}
}
func parseScopes(raw []string) map[string][]string {
scopes := make(map[string][]string)
for _, s := range raw {
parts := strings.SplitN(s, "=", 2)
if len(parts) != 2 {
continue
}
tool := strings.TrimSpace(parts[0])
scopeList := strings.Split(parts[1], ",")
for i := range scopeList {
scopeList[i] = strings.TrimSpace(scopeList[i])
}
scopes[tool] = scopeList
}
return scopes
}
// Ensure auth.Auth interface is satisfied at compile time.
var _ auth.Auth = jwt.NewAuth()
+30 -4
View File
@@ -39,9 +39,16 @@ func genProtoHandler(c *cli.Context) error {
func init() {
cmd.Register([]*cli.Command{
{
Name: "new",
Usage: "Create a new service",
Action: new.Run,
Name: "new",
Usage: "Create a new service",
ArgsUsage: "[name]",
Action: new.Run,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "no-mcp",
Usage: "Disable MCP gateway integration in generated code",
},
},
},
{
Name: "gen",
@@ -71,6 +78,18 @@ func init() {
{
Name: "call",
Usage: "Call a service",
Flags: []cli.Flag{
&cli.StringSliceFlag{
Name: "header",
Aliases: []string{"H"},
Usage: "Set request headers (can be used multiple times): --header 'Key:Value'",
},
&cli.StringSliceFlag{
Name: "metadata",
Aliases: []string{"m"},
Usage: "Set request metadata (can be used multiple times): --metadata 'Key:Value'",
},
},
Action: func(ctx *cli.Context) error {
args := ctx.Args()
@@ -86,9 +105,16 @@ func init() {
request = args.Get(2)
}
// Create context with metadata if provided
// Note: This is for the direct 'micro call' command.
// Dynamic service calls (e.g., 'micro helloworld call') are handled in CallService.
callCtx := context.TODO()
callCtx = util.AddMetadataToContext(callCtx, ctx.StringSlice("metadata"))
callCtx = util.AddMetadataToContext(callCtx, ctx.StringSlice("header"))
req := client.NewRequest(service, endpoint, &bytes.Frame{Data: []byte(request)})
var rsp bytes.Frame
err := client.Call(context.TODO(), req, &rsp)
err := client.Call(callCtx, req, &rsp)
if err != nil {
return err
}
+46 -3
View File
@@ -105,6 +105,21 @@ func deploySSH(c *cli.Context, target string, cfg *config.Config) error {
fmt.Printf("Deploying to %s...\n\n", target)
// Early validation: Check if the requested service exists before SSH checks
filterService := c.String("service")
if filterService != "" && cfg != nil {
found := false
for _, svc := range cfg.Services {
if svc.Name == filterService {
found = true
break
}
}
if !found && len(cfg.Services) > 0 {
return fmt.Errorf("service '%s' not found in configuration", filterService)
}
}
// Step 1: Check SSH connectivity
fmt.Print(" Checking SSH connection... ")
if err := checkSSH(target); err != nil {
@@ -129,14 +144,23 @@ func deploySSH(c *cli.Context, target string, cfg *config.Config) error {
return err
}
for _, svc := range sorted {
services = append(services, svc.Name)
// If --service flag is provided, only include that service
if filterService == "" || svc.Name == filterService {
services = append(services, svc.Name)
}
}
} else {
// Single service project
services = []string{filepath.Base(absDir)}
// If --service flag was provided for a single-service project, validate it matches
if filterService != "" && filterService != services[0] {
return fmt.Errorf("service '%s' not found (only '%s' available)", filterService, services[0])
}
}
fmt.Printf(" Building binaries... ")
if err := buildBinaries(absDir, cfg, c.Bool("build")); err != nil {
if err := buildBinaries(absDir, cfg, c.Bool("build"), services); err != nil {
fmt.Println("\u2717")
return err
}
@@ -241,7 +265,7 @@ func checkServerInit(host, remotePath string) error {
return nil
}
func buildBinaries(absDir string, cfg *config.Config, forceBuild bool) error {
func buildBinaries(absDir string, cfg *config.Config, forceBuild bool, servicesToBuild []string) error {
binDir := filepath.Join(absDir, "bin")
// Check if we already have binaries and don't need to rebuild
@@ -266,7 +290,19 @@ func buildBinaries(absDir string, cfg *config.Config, forceBuild bool) error {
return err
}
// Create a map for quick lookup of services to build
// This provides O(1) lookup time and makes the code more maintainable
shouldBuild := make(map[string]bool)
for _, svcName := range servicesToBuild {
shouldBuild[svcName] = true
}
for _, svc := range sorted {
// Only build services in the servicesToBuild list
if !shouldBuild[svc.Name] {
continue
}
svcDir := filepath.Join(absDir, svc.Path)
outPath := filepath.Join(binDir, svc.Name)
@@ -408,6 +444,9 @@ Before deploying, initialize the server:
Then deploy:
micro deploy user@server
Deploy a specific service (multi-service projects):
micro deploy user@server --service users
With a micro.mu config, you can define named targets:
deploy prod
ssh user@prod.example.com
@@ -442,6 +481,10 @@ The deploy process:
Name: "build",
Usage: "Force rebuild of binaries",
},
&cli.StringFlag{
Name: "service",
Usage: "Deploy only a specific service (for multi-service projects)",
},
},
})
}
+22 -3
View File
@@ -174,15 +174,23 @@ func Run(ctx *cli.Context) error {
}
goDir = filepath.Join(goPath, "src", path.Clean(dir))
noMCP := ctx.Bool("no-mcp")
// Select main.go template based on MCP flag
mainTmpl := tmpl.MainSRV
if noMCP {
mainTmpl = tmpl.MainSRVNoMCP
}
c := config{
Alias: dir,
Comments: nil, // Remove redundant protoComments
Comments: nil,
Dir: dir,
GoDir: goDir,
GoPath: goPath,
UseGoPath: false,
Files: []file{
{"main.go", tmpl.MainSRV},
{"main.go", mainTmpl},
{"handler/" + dir + ".go", tmpl.HandlerSRV},
{"proto/" + dir + ".proto", tmpl.ProtoSRV},
{"Makefile", tmpl.Makefile},
@@ -214,7 +222,18 @@ func Run(ctx *cli.Context) error {
fmt.Println("\nProject structure after 'make proto':")
printTree(dir)
fmt.Println("\nService created successfully! Start coding in your new service directory.")
fmt.Println()
fmt.Printf("Service %s created successfully!\n\n", dir)
fmt.Println("Next steps:")
fmt.Printf(" cd %s\n", dir)
fmt.Println(" go run .")
if !noMCP {
fmt.Println()
fmt.Println("Your service is MCP-enabled. Once running:")
fmt.Println(" MCP tools: http://localhost:3001/mcp/tools")
fmt.Println(" Claude Code: micro mcp serve")
}
fmt.Println()
return nil
}
+8 -3
View File
@@ -13,19 +13,24 @@ import (
type {{title .Alias}} struct{}
// Return a new handler
// Return a new handler.
func New() *{{title .Alias}} {
return &{{title .Alias}}{}
}
// Call is a single request handler called via client.Call or the generated client code
// Call greets a person by name and returns a welcome message.
//
// @example {"name": "Alice"}
func (e *{{title .Alias}}) Call(ctx context.Context, req *pb.Request, rsp *pb.Response) error {
log.Info("Received {{title .Alias}}.Call request")
rsp.Msg = "Hello " + req.Name
return nil
}
// Stream is a server side stream handler called via client.Stream or the generated client code
// Stream sends a sequence of numbered responses back to the caller.
// Use this for streaming large result sets or real-time updates.
//
// @example {"count": 5}
func (e *{{title .Alias}}) Stream(ctx context.Context, req *pb.StreamingRequest, stream pb.{{title .Alias}}_StreamStream) error {
log.Infof("Received {{title .Alias}}.Stream request with count: %d", req.Count)
+27
View File
@@ -3,6 +3,33 @@ package template
var (
MainSRV = `package main
import (
"{{.Dir}}/handler"
pb "{{.Dir}}/proto"
"go-micro.dev/v5"
"go-micro.dev/v5/gateway/mcp"
)
func main() {
// Create service
service := micro.New("{{lower .Alias}}",
mcp.WithMCP(":3001"),
)
// Initialize service
service.Init()
// Register handler
pb.Register{{title .Alias}}Handler(service.Server(), handler.New())
// Run service
service.Run()
}
`
MainSRVNoMCP = `package main
import (
"{{.Dir}}/handler"
pb "{{.Dir}}/proto"
+12 -8
View File
@@ -27,6 +27,18 @@ test-coverage:
go test -v -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
# List MCP tools exposed by this service
mcp-tools:
micro mcp list
# Test an MCP tool interactively
mcp-test:
micro mcp test
# Start MCP server for Claude Code
mcp-serve:
micro mcp serve
# Clean build artifacts
clean:
rm -rf bin/ coverage.out coverage.html
@@ -35,14 +47,6 @@ clean:
docker:
docker build -t {{.Alias}}:latest .
# Run with Docker Compose
docker-up:
docker-compose up -d
# Stop Docker Compose
docker-down:
docker-compose down
# Lint code
lint:
golangci-lint run ./...
+1 -1
View File
@@ -3,7 +3,7 @@ package template
var (
Module = `module {{.Dir}}
go 1.18
go 1.22
require (
go-micro.dev/v5 latest
+4
View File
@@ -17,18 +17,22 @@ message Message {
}
message Request {
// Name of the person to greet
string name = 1;
}
message Response {
// Greeting message
string msg = 1;
}
message StreamingRequest {
// Number of responses to stream back
int64 count = 1;
}
message StreamingResponse {
// Current sequence number in the stream
int64 count = 1;
}
`
+73 -13
View File
@@ -3,28 +3,88 @@ package template
var (
Readme = `# {{title .Alias}} Service
This is the {{title .Alias}} service
Generated with
` + "```" +
`
` + "```" + `
micro new {{.Alias}}
` + "```" + `
## Usage
## Getting Started
Generate the proto code
Generate the proto code:
` + "```" +
`
` + "```bash" + `
make proto
` + "```" + `
Run the service
Run the service:
` + "```" +
`
micro run .
` + "```"
` + "```bash" + `
go run .
` + "```" + `
## MCP & AI Agents
This service is MCP-enabled by default. When running, AI agents can discover
and call your service endpoints automatically.
**MCP tools endpoint:** http://localhost:3001/mcp/tools
### Test with curl
` + "```bash" + `
# List available tools
curl http://localhost:3001/mcp/tools | jq
# Call the service via MCP
curl -X POST http://localhost:3001/mcp/call \
-H 'Content-Type: application/json' \
-d '{"tool": "{{lower .Alias}}.{{title .Alias}}.Call", "arguments": {"name": "Alice"}}'
` + "```" + `
### Use with Claude Code
` + "```bash" + `
# Start MCP server for Claude Code
micro mcp serve
` + "```" + `
Or add to your Claude Code config:
` + "```json" + `
{
"mcpServers": {
"{{lower .Alias}}": {
"command": "micro",
"args": ["mcp", "serve"]
}
}
}
` + "```" + `
### Writing Good Tool Descriptions
AI agents work best when your handler methods have clear doc comments:
` + "```go" + `
// CreateUser registers a new user account with the given email and name.
// Returns the created user with their assigned ID.
//
// @example {"email": "alice@example.com", "name": "Alice Smith"}
func (s *Users) CreateUser(ctx context.Context, req *CreateRequest, rsp *CreateResponse) error {
// ...
}
` + "```" + `
See the [tool descriptions guide](https://go-micro.dev/docs/guides/tool-descriptions) for more tips.
## Development
` + "```bash" + `
make proto # Regenerate proto code
make build # Build binary
make test # Run tests
make dev # Run with hot reload (requires air)
` + "```" + `
`
)
+38 -7
View File
@@ -15,9 +15,30 @@ import (
"github.com/stretchr/objx"
"github.com/urfave/cli/v2"
"go-micro.dev/v5/client"
"go-micro.dev/v5/metadata"
"go-micro.dev/v5/registry"
)
// AddMetadataToContext parses metadata strings in the format "Key:Value" and adds them to the context
func AddMetadataToContext(ctx context.Context, metadataStrings []string) context.Context {
if len(metadataStrings) == 0 {
return ctx
}
md := make(metadata.Metadata)
for _, m := range metadataStrings {
parts := strings.SplitN(m, ":", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
md[key] = value
}
return metadata.MergeContext(ctx, md, true)
}
// LookupService queries the service for a service with the given alias. If
// no services are found for a given alias, the registry will return nil and
// the error will also be nil. An error is only returned if there was an issue
@@ -132,17 +153,27 @@ func CallService(srv *registry.Service, args []string) error {
return fmt.Errorf("Endpoint %v not found for service %v", endpoint, srv.Name)
}
// parse the flags
// create a context for the call
callCtx := context.TODO()
// parse out --header or --metadata flags before parsing request body
// Note: This is for dynamic service calls (e.g., 'micro helloworld call --header X:Y').
// Direct 'micro call' commands are handled in cli.go.
if headerFlags, ok := flags["header"]; ok {
callCtx = AddMetadataToContext(callCtx, headerFlags)
delete(flags, "header")
}
if metadataFlags, ok := flags["metadata"]; ok {
callCtx = AddMetadataToContext(callCtx, metadataFlags)
delete(flags, "metadata")
}
// parse the flags into request body
body, err := FlagsToRequest(flags, ep.Request)
if err != nil {
return err
}
// create a context for the call based on the cli context
callCtx := context.TODO()
// TODO: parse out --header or --metadata
// construct and execute the request using the json content type
req := client.DefaultClient.NewRequest(srv.Name, endpoint, body, client.WithContentType("application/json"))
var rsp json.RawMessage
@@ -387,7 +418,7 @@ func FlagsToRequest(flags map[string][]string, req *registry.Value) (map[string]
// so we do that here
if strings.Contains(key, "-") {
parts := strings.Split(key, "-")
for i, _ := range parts {
for i := range parts {
pToCreate := strings.Join(parts[0:i], ".")
if i > 0 && i < len(parts) && !result.Has(pToCreate) {
result.Set(pToCreate, map[string]interface{}{})
+74
View File
@@ -1,11 +1,13 @@
package util
import (
"context"
"reflect"
"strings"
"testing"
"github.com/davecgh/go-spew/spew"
"go-micro.dev/v5/metadata"
goregistry "go-micro.dev/v5/registry"
)
@@ -377,3 +379,75 @@ func TestDynamicFlagParsing(t *testing.T) {
}
}
func TestAddMetadataToContext(t *testing.T) {
tests := []struct {
name string
metadataStrs []string
expectedKeys []string
expectedValues []string
}{
{
name: "Single metadata",
metadataStrs: []string{"Key1:Value1"},
expectedKeys: []string{"Key1"},
expectedValues: []string{"Value1"},
},
{
name: "Multiple metadata",
metadataStrs: []string{"Key1:Value1", "Key2:Value2"},
expectedKeys: []string{"Key1", "Key2"},
expectedValues: []string{"Value1", "Value2"},
},
{
name: "Metadata with spaces",
metadataStrs: []string{"Key1: Value1 ", " Key2 : Value2"},
expectedKeys: []string{"Key1", "Key2"},
expectedValues: []string{"Value1", "Value2"},
},
{
name: "Metadata with colon in value",
metadataStrs: []string{"Authorization:Bearer token:123"},
expectedKeys: []string{"Authorization"},
expectedValues: []string{"Bearer token:123"},
},
{
name: "Empty metadata",
metadataStrs: []string{},
expectedKeys: []string{},
expectedValues: []string{},
},
{
name: "Invalid metadata format",
metadataStrs: []string{"InvalidFormat"},
expectedKeys: []string{},
expectedValues: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
ctx = AddMetadataToContext(ctx, tt.metadataStrs)
md, ok := metadata.FromContext(ctx)
if len(tt.expectedKeys) == 0 && !ok {
return // Expected no metadata
}
if !ok && len(tt.expectedKeys) > 0 {
t.Fatal("Expected metadata in context but got none")
}
for i, key := range tt.expectedKeys {
value, found := md.Get(key)
if !found {
t.Fatalf("Expected key %s not found in metadata", key)
}
if value != tt.expectedValues[i] {
t.Fatalf("Expected value %s for key %s, got %s", tt.expectedValues[i], key, value)
}
}
})
}
}
+453
View File
@@ -0,0 +1,453 @@
# MCP CLI Command Examples
This document provides examples of using the `micro mcp` commands for AI agent integration.
## Table of Contents
- [List Available Tools](#list-available-tools)
- [Test a Tool](#test-a-tool)
- [Generate Documentation](#generate-documentation)
- [Export to Different Formats](#export-to-different-formats)
## Prerequisites
You need at least one microservice running with the go-micro framework. The service will automatically be discovered via the registry (mdns by default).
Example service:
```bash
cd examples/mcp/hello
go run main.go
```
## List Available Tools
### Human-readable list
```bash
micro mcp list
```
Output:
```
Available MCP Tools:
Service: greeter
• greeter.Greeter.SayHello
Total: 1 tools
```
### JSON output
```bash
micro mcp list --json
```
Output:
```json
{
"count": 1,
"tools": [
{
"description": "Call SayHello on greeter service",
"endpoint": "Greeter.SayHello",
"name": "greeter.Greeter.SayHello",
"service": "greeter"
}
]
}
```
## Test a Tool
### Basic test
```bash
micro mcp test greeter.Greeter.SayHello '{"name": "Alice"}'
```
Output:
```
Testing tool: greeter.Greeter.SayHello
Service: greeter
Endpoint: Greeter.SayHello
Input: {"name": "Alice"}
✅ Call successful!
Response:
{
"message": "Hello Alice!"
}
```
### Test with default empty input
```bash
micro mcp test greeter.Greeter.SayHello
```
This will call the tool with an empty JSON object `{}`.
## Generate Documentation
### Markdown documentation (stdout)
```bash
micro mcp docs
```
Output:
```markdown
# MCP Tools Documentation
Generated: 2026-02-13 14:30:00
Total Tools: 1
## Service: greeter
### greeter.Greeter.SayHello
**Description:** Greets a person by name. Returns a friendly greeting message.
**Example Input:**
\`\`\`json
{"name": "Alice"}
\`\`\`
```
### Markdown documentation (save to file)
```bash
micro mcp docs --output mcp-tools.md
```
This creates a `mcp-tools.md` file with the documentation.
### JSON documentation
```bash
micro mcp docs --format json
```
Output:
```json
{
"count": 1,
"tools": [
{
"description": "Greets a person by name. Returns a friendly greeting message.",
"endpoint": "Greeter.SayHello",
"example": "{\"name\": \"Alice\"}",
"metadata": {
"description": "Greets a person by name. Returns a friendly greeting message.",
"example": "{\"name\": \"Alice\"}"
},
"name": "greeter.Greeter.SayHello",
"scopes": null,
"service": "greeter"
}
]
}
```
### JSON documentation (save to file)
```bash
micro mcp docs --format json --output tools.json
```
## Export to Different Formats
### Export to LangChain (Python)
Generate Python code with LangChain tool definitions:
```bash
micro mcp export langchain
```
Output:
```python
# LangChain Tools for Go Micro Services
# Auto-generated from MCP service discovery
from langchain.tools import Tool
import requests
import json
# Configure your MCP gateway endpoint
MCP_GATEWAY_URL = 'http://localhost:3000/mcp'
def call_mcp_tool(tool_name, arguments):
"""Call an MCP tool via HTTP gateway"""
response = requests.post(
f'{MCP_GATEWAY_URL}/call',
json={'name': tool_name, 'arguments': arguments}
)
response.raise_for_status()
return response.json()
# Define tools
tools = []
def greeter_Greeter_SayHello(arguments: str) -> str:
"""Greets a person by name. Returns a friendly greeting message."""
args = json.loads(arguments) if isinstance(arguments, str) else arguments
return json.dumps(call_mcp_tool('greeter.Greeter.SayHello', args))
tools.append(Tool(
name='greeter.Greeter.SayHello',
func=greeter_Greeter_SayHello,
description='Greets a person by name. Returns a friendly greeting message.'
))
# Example usage:
# from langchain.agents import initialize_agent, AgentType
# from langchain.llms import OpenAI
#
# llm = OpenAI(temperature=0)
# agent = initialize_agent(tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION)
# agent.run('Your query here')
```
Save to file:
```bash
micro mcp export langchain --output langchain_tools.py
```
### Export to OpenAPI 3.0
Generate an OpenAPI specification:
```bash
micro mcp export openapi
```
Output:
```json
{
"components": {
"securitySchemes": {
"bearerAuth": {
"scheme": "bearer",
"type": "http"
}
}
},
"info": {
"description": "Auto-generated OpenAPI spec from MCP service discovery",
"title": "Go Micro MCP Services",
"version": "1.0.0"
},
"openapi": "3.0.0",
"paths": {
"/mcp/call/greeter/Greeter/SayHello": {
"post": {
"description": "Greets a person by name. Returns a friendly greeting message.",
"operationId": "greeter_Greeter_SayHello",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
},
"description": "Successful response"
}
},
"summary": "greeter.Greeter.SayHello"
}
}
},
"servers": [
{
"description": "MCP Gateway",
"url": "http://localhost:3000"
}
]
}
```
Save to file:
```bash
micro mcp export openapi --output openapi.json
```
### Export to raw JSON
Export raw tool definitions:
```bash
micro mcp export json
```
This is similar to `micro mcp docs --format json` but specifically for export purposes.
Save to file:
```bash
micro mcp export json --output tools.json
```
## Using with Different Registries
By default, the commands use mdns registry. You can specify a different registry:
```bash
# Using consul
micro mcp list --registry consul --registry_address consul:8500
# Using etcd
micro mcp list --registry etcd --registry_address etcd:2379
```
## Integration Examples
### Using LangChain Export with Claude
1. Export your tools to LangChain format:
```bash
micro mcp export langchain --output my_tools.py
```
2. Use in your Python agent:
```python
from my_tools import tools
from langchain.agents import initialize_agent, AgentType
from langchain.chat_models import ChatAnthropic
llm = ChatAnthropic(model="claude-3-sonnet-20240229")
agent = initialize_agent(tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION)
result = agent.run("Greet Alice")
print(result)
```
### Using OpenAPI Export with GPT
1. Export to OpenAPI:
```bash
micro mcp export openapi --output openapi.json
```
2. Upload to ChatGPT as a custom GPT action or use with OpenAI Assistants API.
### Documentation for AI Agents
Generate documentation that AI agents can read to understand your services:
```bash
micro mcp docs --format json --output service-catalog.json
```
This JSON file can be fed to AI agents for service discovery and understanding.
## Advanced Usage
### Piping and Processing
You can pipe the output to other tools:
```bash
# Count tools per service
micro mcp list --json | jq '.tools | group_by(.service) | map({service: .[0].service, count: length})'
# Extract all tool names
micro mcp list --json | jq -r '.tools[].name'
# Filter tools by service
micro mcp list --json | jq '.tools[] | select(.service == "greeter")'
```
### Monitoring and CI/CD
Use these commands in your CI/CD pipeline:
```bash
# Validate all services are discoverable
SERVICE_COUNT=$(micro mcp list --json | jq '.count')
if [ "$SERVICE_COUNT" -lt 5 ]; then
echo "Error: Expected at least 5 services, found $SERVICE_COUNT"
exit 1
fi
# Generate documentation on each deployment
micro mcp docs --output docs/mcp-services.md
git add docs/mcp-services.md
git commit -m "Update MCP service documentation"
```
### Testing in Development
Create a script to test all your tools:
```bash
#!/bin/bash
# test-all-tools.sh
TOOLS=$(micro mcp list --json | jq -r '.tools[].name')
for tool in $TOOLS; do
echo "Testing $tool..."
micro mcp test "$tool" "{}" || echo "Failed: $tool"
done
```
## Troubleshooting
### No tools found
If `micro mcp list` shows 0 tools:
1. Verify services are running:
```bash
ps aux | grep "your-service"
```
2. Check registry (mdns might need time to discover):
```bash
# Wait a few seconds and try again
sleep 3
micro mcp list
```
3. Use a different registry if mdns is unreliable:
```bash
# Start services with consul
micro --registry consul server
# List with consul
micro mcp list --registry consul
```
### Service not responding in tests
If `micro mcp test` fails:
1. Verify the tool name is correct:
```bash
micro mcp list
```
2. Check the JSON input format:
```bash
# Invalid
micro mcp test service.Handler.Method '{invalid}'
# Valid
micro mcp test service.Handler.Method '{"key": "value"}'
```
3. Check service logs for errors.
## Next Steps
- Read the [MCP Documentation](../../gateway/mcp/DOCUMENTATION.md)
- Try the [MCP Examples](../../examples/mcp/README.md)
- Learn about [Tool Scopes and Security](../../gateway/mcp/DOCUMENTATION.md#authentication-and-scopes)
- Explore [Agent SDKs](#) (coming soon)
+554 -3
View File
@@ -8,10 +8,14 @@ import (
"log"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/urfave/cli/v2"
"go-micro.dev/v5/client"
"go-micro.dev/v5/cmd"
"go-micro.dev/v5/codec/bytes"
"go-micro.dev/v5/gateway/mcp"
"go-micro.dev/v5/registry"
)
@@ -129,6 +133,83 @@ Example:
},
Action: testAction,
},
{
Name: "docs",
Usage: "Generate MCP documentation",
Description: `Generate documentation for all available MCP tools.
The documentation includes tool names, descriptions, parameters, and examples
extracted from service metadata and Go comments.
Examples:
# Generate markdown documentation
micro mcp docs
# Generate JSON documentation
micro mcp docs --format json
# Save to file
micro mcp docs --output mcp-tools.md`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "registry",
Usage: "Registry for service discovery",
Value: "mdns",
},
&cli.StringFlag{
Name: "registry_address",
Usage: "Registry address",
},
&cli.StringFlag{
Name: "format",
Usage: "Output format (markdown, json)",
Value: "markdown",
},
&cli.StringFlag{
Name: "output",
Aliases: []string{"o"},
Usage: "Output file (default: stdout)",
},
},
Action: docsAction,
},
{
Name: "export",
Usage: "Export tools to different formats",
Description: `Export MCP tools to various agent framework formats.
Supported formats:
- langchain: LangChain tool definitions (Python)
- openapi: OpenAPI 3.0 specification
- json: Raw JSON tool definitions
Examples:
# Export to LangChain format
micro mcp export langchain
# Export to OpenAPI
micro mcp export openapi --output openapi.yaml
# Export raw JSON
micro mcp export json`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "registry",
Usage: "Registry for service discovery",
Value: "mdns",
},
&cli.StringFlag{
Name: "registry_address",
Usage: "Registry address",
},
&cli.StringFlag{
Name: "output",
Aliases: []string{"o"},
Usage: "Output file (default: stdout)",
},
},
Action: exportAction,
},
},
})
}
@@ -247,10 +328,480 @@ func testAction(ctx *cli.Context) error {
inputJSON = ctx.Args().Get(1)
}
// Validate input JSON
var inputData map[string]interface{}
if err := json.Unmarshal([]byte(inputJSON), &inputData); err != nil {
return fmt.Errorf("invalid JSON input: %w", err)
}
// Get registry
reg := registry.DefaultRegistry
if regName := ctx.String("registry"); regName != "" {
if regName != "mdns" {
return fmt.Errorf("registry %s not yet supported, use mdns", regName)
}
}
// Create MCP options
opts := mcp.Options{
Registry: reg,
Context: context.Background(),
Logger: log.New(os.Stderr, "", 0),
}
// Parse tool name (format: "service.endpoint" or "service.Handler.Method")
parts := parseTool(toolName)
if len(parts) < 2 {
return fmt.Errorf("invalid tool name format. Expected: service.endpoint or service.Handler.Method")
}
serviceName := parts[0]
endpointName := parts[1]
// If tool name has 3 parts, combine last two for endpoint (e.g., Handler.Method)
if len(parts) == 3 {
endpointName = parts[1] + "." + parts[2]
}
// Discover the tool from registry
services, err := opts.Registry.GetService(serviceName)
if err != nil || len(services) == 0 {
return fmt.Errorf("service %s not found: %w", serviceName, err)
}
// Find the endpoint
var endpoint *registry.Endpoint
for _, ep := range services[0].Endpoints {
if ep.Name == endpointName {
endpoint = ep
break
}
}
if endpoint == nil {
return fmt.Errorf("endpoint %s not found in service %s", endpointName, serviceName)
}
// Display test info
fmt.Printf("Testing tool: %s\n", toolName)
fmt.Printf("Input: %s\n", inputJSON)
fmt.Println("\nResult:")
fmt.Println("(Not yet implemented - coming soon)")
fmt.Printf("Service: %s\n", serviceName)
fmt.Printf("Endpoint: %s\n", endpointName)
fmt.Printf("Input: %s\n\n", inputJSON)
// Convert input to JSON bytes for RPC call
inputBytes, err := json.Marshal(inputData)
if err != nil {
return fmt.Errorf("failed to marshal input: %w", err)
}
// Make RPC call using bytes codec
c := opts.Client
if c == nil {
c = client.DefaultClient
}
// Create request with bytes frame
req := c.NewRequest(serviceName, endpointName, &bytes.Frame{Data: inputBytes})
// Make the call
var rsp bytes.Frame
if err := c.Call(opts.Context, req, &rsp); err != nil {
fmt.Printf("❌ Call failed: %v\n", err)
return err
}
// Parse and display response
fmt.Println("✅ Call successful!")
fmt.Println("\nResponse:")
// Try to pretty-print JSON response
var result interface{}
if err := json.Unmarshal(rsp.Data, &result); err == nil {
prettyJSON, err := json.MarshalIndent(result, "", " ")
if err == nil {
fmt.Println(string(prettyJSON))
} else {
fmt.Println(string(rsp.Data))
}
} else {
// Not JSON, print raw
fmt.Println(string(rsp.Data))
}
return nil
}
// parseTool splits a tool name into service and endpoint parts
func parseTool(toolName string) []string {
return strings.Split(toolName, ".")
}
// docsAction generates documentation for MCP tools
func docsAction(ctx *cli.Context) error {
// Get registry
reg := registry.DefaultRegistry
// Create temporary MCP server to discover tools
opts := mcp.Options{
Registry: reg,
Context: context.Background(),
Logger: log.New(os.Stderr, "", 0),
}
// Discover services
services, err := opts.Registry.ListServices()
if err != nil {
return fmt.Errorf("failed to list services: %w", err)
}
format := ctx.String("format")
outputFile := ctx.String("output")
// Prepare output writer
writer := os.Stdout
if outputFile != "" {
f, err := os.Create(outputFile)
if err != nil {
return fmt.Errorf("failed to create output file: %w", err)
}
defer f.Close()
writer = f
}
// Collect all tools with metadata
type ToolDoc struct {
Name string `json:"name"`
Service string `json:"service"`
Endpoint string `json:"endpoint"`
Description string `json:"description"`
Example string `json:"example,omitempty"`
Scopes []string `json:"scopes,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
}
var tools []ToolDoc
for _, svc := range services {
fullSvcs, err := opts.Registry.GetService(svc.Name)
if err != nil || len(fullSvcs) == 0 {
continue
}
for _, ep := range fullSvcs[0].Endpoints {
toolDoc := ToolDoc{
Name: fmt.Sprintf("%s.%s", svc.Name, ep.Name),
Service: svc.Name,
Endpoint: ep.Name,
Description: fmt.Sprintf("Call %s on %s service", ep.Name, svc.Name),
Metadata: ep.Metadata,
}
// Extract description from metadata if available
if desc, ok := ep.Metadata["description"]; ok {
toolDoc.Description = desc
}
// Extract example from metadata if available
if example, ok := ep.Metadata["example"]; ok {
toolDoc.Example = example
}
// Extract scopes from metadata if available
if scopesStr, ok := ep.Metadata["scopes"]; ok && scopesStr != "" {
toolDoc.Scopes = strings.Split(scopesStr, ",")
}
tools = append(tools, toolDoc)
}
}
// Generate output based on format
switch format {
case "json":
enc := json.NewEncoder(writer)
enc.SetIndent("", " ")
return enc.Encode(map[string]interface{}{
"tools": tools,
"count": len(tools),
})
case "markdown":
fmt.Fprintf(writer, "# MCP Tools Documentation\n\n")
fmt.Fprintf(writer, "Generated: %s\n\n", time.Now().Format("2006-01-02 15:04:05"))
fmt.Fprintf(writer, "Total Tools: %d\n\n", len(tools))
// Group by service
serviceMap := make(map[string][]ToolDoc)
for _, tool := range tools {
serviceMap[tool.Service] = append(serviceMap[tool.Service], tool)
}
for service, serviceTools := range serviceMap {
fmt.Fprintf(writer, "## Service: %s\n\n", service)
for _, tool := range serviceTools {
fmt.Fprintf(writer, "### %s\n\n", tool.Name)
fmt.Fprintf(writer, "**Description:** %s\n\n", tool.Description)
if len(tool.Scopes) > 0 {
fmt.Fprintf(writer, "**Required Scopes:** %s\n\n", strings.Join(tool.Scopes, ", "))
}
if tool.Example != "" {
fmt.Fprintf(writer, "**Example Input:**\n```json\n%s\n```\n\n", tool.Example)
}
}
}
return nil
default:
return fmt.Errorf("unsupported format: %s (supported: markdown, json)", format)
}
}
// exportAction exports tools to different formats
func exportAction(ctx *cli.Context) error {
if ctx.Args().Len() < 1 {
return fmt.Errorf("usage: micro mcp export <format>\nSupported formats: langchain, openapi, json")
}
exportFormat := ctx.Args().First()
// Get registry
reg := registry.DefaultRegistry
// Create temporary MCP server to discover tools
opts := mcp.Options{
Registry: reg,
Context: context.Background(),
Logger: log.New(os.Stderr, "", 0),
}
// Discover services
services, err := opts.Registry.ListServices()
if err != nil {
return fmt.Errorf("failed to list services: %w", err)
}
outputFile := ctx.String("output")
// Prepare output writer
writer := os.Stdout
if outputFile != "" {
f, err := os.Create(outputFile)
if err != nil {
return fmt.Errorf("failed to create output file: %w", err)
}
defer f.Close()
writer = f
}
switch exportFormat {
case "langchain":
return exportLangChain(writer, services, opts)
case "openapi":
return exportOpenAPI(writer, services, opts)
case "json":
return exportJSON(writer, services, opts)
default:
return fmt.Errorf("unsupported export format: %s\nSupported: langchain, openapi, json", exportFormat)
}
}
// exportLangChain exports tools in LangChain format (Python)
func exportLangChain(writer *os.File, services []*registry.Service, opts mcp.Options) error {
fmt.Fprintf(writer, "# LangChain Tools for Go Micro Services\n")
fmt.Fprintf(writer, "# Auto-generated from MCP service discovery\n\n")
fmt.Fprintf(writer, "from langchain.tools import Tool\n")
fmt.Fprintf(writer, "import requests\nimport json\n\n")
fmt.Fprintf(writer, "# Configure your MCP gateway endpoint\n")
fmt.Fprintf(writer, "MCP_GATEWAY_URL = 'http://localhost:3000/mcp'\n\n")
fmt.Fprintf(writer, "def call_mcp_tool(tool_name, arguments):\n")
fmt.Fprintf(writer, " \"\"\"Call an MCP tool via HTTP gateway\"\"\"\n")
fmt.Fprintf(writer, " response = requests.post(\n")
fmt.Fprintf(writer, " f'{MCP_GATEWAY_URL}/call',\n")
fmt.Fprintf(writer, " json={'name': tool_name, 'arguments': arguments}\n")
fmt.Fprintf(writer, " )\n")
fmt.Fprintf(writer, " response.raise_for_status()\n")
fmt.Fprintf(writer, " return response.json()\n\n")
fmt.Fprintf(writer, "# Define tools\n")
fmt.Fprintf(writer, "tools = []\n\n")
for _, svc := range services {
fullSvcs, err := opts.Registry.GetService(svc.Name)
if err != nil || len(fullSvcs) == 0 {
continue
}
for _, ep := range fullSvcs[0].Endpoints {
toolName := fmt.Sprintf("%s.%s", svc.Name, ep.Name)
description := fmt.Sprintf("Call %s on %s service", ep.Name, svc.Name)
if desc, ok := ep.Metadata["description"]; ok {
description = desc
}
// Generate Python function name (replace dots with underscores)
funcName := strings.ReplaceAll(toolName, ".", "_")
fmt.Fprintf(writer, "def %s(arguments: str) -> str:\n", funcName)
fmt.Fprintf(writer, " \"\"\"% s\"\"\"\n", description)
fmt.Fprintf(writer, " args = json.loads(arguments) if isinstance(arguments, str) else arguments\n")
fmt.Fprintf(writer, " return json.dumps(call_mcp_tool('%s', args))\n\n", toolName)
fmt.Fprintf(writer, "tools.append(Tool(\n")
fmt.Fprintf(writer, " name='%s',\n", toolName)
fmt.Fprintf(writer, " func=%s,\n", funcName)
fmt.Fprintf(writer, " description='%s'\n", strings.ReplaceAll(description, "'", "\\'"))
fmt.Fprintf(writer, "))\n\n")
}
}
fmt.Fprintf(writer, "# Example usage:\n")
fmt.Fprintf(writer, "# from langchain.agents import initialize_agent, AgentType\n")
fmt.Fprintf(writer, "# from langchain.llms import OpenAI\n")
fmt.Fprintf(writer, "#\n")
fmt.Fprintf(writer, "# llm = OpenAI(temperature=0)\n")
fmt.Fprintf(writer, "# agent = initialize_agent(tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION)\n")
fmt.Fprintf(writer, "# agent.run('Your query here')\n")
return nil
}
// exportOpenAPI exports tools in OpenAPI 3.0 format
func exportOpenAPI(writer *os.File, services []*registry.Service, opts mcp.Options) error {
spec := map[string]interface{}{
"openapi": "3.0.0",
"info": map[string]interface{}{
"title": "Go Micro MCP Services",
"description": "Auto-generated OpenAPI spec from MCP service discovery",
"version": "1.0.0",
},
"servers": []map[string]interface{}{
{
"url": "http://localhost:3000",
"description": "MCP Gateway",
},
},
"paths": make(map[string]interface{}),
}
paths := spec["paths"].(map[string]interface{})
for _, svc := range services {
fullSvcs, err := opts.Registry.GetService(svc.Name)
if err != nil || len(fullSvcs) == 0 {
continue
}
for _, ep := range fullSvcs[0].Endpoints {
toolName := fmt.Sprintf("%s.%s", svc.Name, ep.Name)
path := fmt.Sprintf("/mcp/call/%s", strings.ReplaceAll(toolName, ".", "/"))
description := fmt.Sprintf("Call %s on %s service", ep.Name, svc.Name)
if desc, ok := ep.Metadata["description"]; ok {
description = desc
}
operation := map[string]interface{}{
"summary": toolName,
"description": description,
"operationId": strings.ReplaceAll(toolName, ".", "_"),
"requestBody": map[string]interface{}{
"required": true,
"content": map[string]interface{}{
"application/json": map[string]interface{}{
"schema": map[string]interface{}{
"type": "object",
},
},
},
},
"responses": map[string]interface{}{
"200": map[string]interface{}{
"description": "Successful response",
"content": map[string]interface{}{
"application/json": map[string]interface{}{
"schema": map[string]interface{}{
"type": "object",
},
},
},
},
},
}
// Add scope security if available
if scopesStr, ok := ep.Metadata["scopes"]; ok && scopesStr != "" {
operation["security"] = []map[string]interface{}{
{
"bearerAuth": strings.Split(scopesStr, ","),
},
}
}
paths[path] = map[string]interface{}{
"post": operation,
}
}
}
// Add security schemes
spec["components"] = map[string]interface{}{
"securitySchemes": map[string]interface{}{
"bearerAuth": map[string]interface{}{
"type": "http",
"scheme": "bearer",
},
},
}
enc := json.NewEncoder(writer)
enc.SetIndent("", " ")
return enc.Encode(spec)
}
// exportJSON exports raw tool definitions as JSON
func exportJSON(writer *os.File, services []*registry.Service, opts mcp.Options) error {
var tools []map[string]interface{}
for _, svc := range services {
fullSvcs, err := opts.Registry.GetService(svc.Name)
if err != nil || len(fullSvcs) == 0 {
continue
}
for _, ep := range fullSvcs[0].Endpoints {
tool := map[string]interface{}{
"name": fmt.Sprintf("%s.%s", svc.Name, ep.Name),
"service": svc.Name,
"endpoint": ep.Name,
"metadata": ep.Metadata,
}
if desc, ok := ep.Metadata["description"]; ok {
tool["description"] = desc
}
if example, ok := ep.Metadata["example"]; ok {
tool["example"] = example
}
if scopesStr, ok := ep.Metadata["scopes"]; ok && scopesStr != "" {
tool["scopes"] = strings.Split(scopesStr, ",")
}
tools = append(tools, tool)
}
}
enc := json.NewEncoder(writer)
enc.SetIndent("", " ")
return enc.Encode(map[string]interface{}{
"tools": tools,
"count": len(tools),
})
}
+79
View File
@@ -0,0 +1,79 @@
package mcp
import (
"reflect"
"testing"
)
func TestParseTool(t *testing.T) {
tests := []struct {
name string
toolName string
want []string
}{
{
name: "simple two-part tool",
toolName: "service.endpoint",
want: []string{"service", "endpoint"},
},
{
name: "three-part tool (service.Handler.Method)",
toolName: "greeter.Greeter.Hello",
want: []string{"greeter", "Greeter", "Hello"},
},
{
name: "single part (invalid)",
toolName: "service",
want: []string{"service"},
},
{
name: "four-part tool",
toolName: "users.Users.Get.All",
want: []string{"users", "Users", "Get", "All"},
},
{
name: "empty string",
toolName: "",
want: []string{""},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := parseTool(tt.toolName)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseTool(%q) = %v, want %v", tt.toolName, got, tt.want)
}
})
}
}
func TestExportFormats(t *testing.T) {
// Test that export formats are recognized
formats := []string{"langchain", "openapi", "json"}
for _, format := range formats {
t.Run(format, func(t *testing.T) {
// This is a basic test to ensure the format strings are defined
// The actual export functions are tested through integration tests
if format == "" {
t.Error("export format should not be empty")
}
})
}
}
func TestDocsFormats(t *testing.T) {
// Test that docs formats are recognized
formats := []string{"markdown", "json"}
for _, format := range formats {
t.Run(format, func(t *testing.T) {
// This is a basic test to ensure the format strings are defined
// The actual docs functions are tested through integration tests
if format == "" {
t.Error("docs format should not be empty")
}
})
}
}
+21 -28
View File
@@ -358,7 +358,7 @@ func Run(c *cli.Context) error {
}
// Print startup banner
printBanner(services, gw, !c.Bool("no-watch"))
printBanner(services, gw, !c.Bool("no-watch"), c.String("mcp-address"))
// Setup signal handling
sigCh := make(chan os.Signal, 1)
@@ -427,21 +427,25 @@ func processRunning(pidStr string) bool {
return proc.Signal(syscall.Signal(0)) == nil
}
func printBanner(services []*serviceProcess, gw *server.Gateway, watching bool) {
func printBanner(services []*serviceProcess, gw *server.Gateway, watching bool, mcpAddr string) {
fmt.Println()
fmt.Println(" \033[1mMicro\033[0m")
fmt.Println()
fmt.Println(" ┌─────────────────────────────────────────────────────────────┐")
fmt.Println(" │ │")
fmt.Println(" │ \033[1mMicro\033[0m │")
fmt.Println(" │ │")
if gw != nil {
fmt.Printf(" │ Web: \033[36mhttp://localhost%s\033[0m\n", gw.Addr())
fmt.Printf(" │ API: \033[36mhttp://localhost%s/api/{service}/{method}\033[0m\n", gw.Addr())
fmt.Printf(" │ Health: \033[36mhttp://localhost%s/health\033[0m\n", gw.Addr())
fmt.Printf(" Dashboard \033[36mhttp://localhost%s\033[0m\n", gw.Addr())
fmt.Printf(" API \033[36mhttp://localhost%s/api/{service}/{method}\033[0m\n", gw.Addr())
fmt.Printf(" Agent \033[36mhttp://localhost%s/agent\033[0m\n", gw.Addr())
fmt.Printf(" Health \033[36mhttp://localhost%s/health\033[0m\n", gw.Addr())
if mcpAddr != "" {
fmt.Printf(" MCP \033[36mhttp://localhost%s\033[0m\n", mcpAddr)
fmt.Printf(" MCP Tools \033[36mhttp://localhost%s/mcp/tools\033[0m\n", mcpAddr)
fmt.Printf(" WebSocket \033[36mws://localhost%s/mcp/ws\033[0m\n", mcpAddr)
}
}
fmt.Println(" │ │")
fmt.Println(" Services:")
fmt.Println()
fmt.Println(" Services:")
for _, svc := range services {
status := "\033[32m●\033[0m" // green dot
@@ -449,30 +453,19 @@ func printBanner(services []*serviceProcess, gw *server.Gateway, watching bool)
status = "\033[31m●\033[0m" // red dot
}
name := svc.name
if len(name) > 20 {
name = name[:17] + "..."
if len(name) > 40 {
name = name[:37] + "..."
}
fmt.Printf(" %s %-20s │\n", status, name)
fmt.Printf(" %s %s\n", status, name)
}
fmt.Println(" │ │")
fmt.Println()
fmt.Println(" Auth: \033[32menabled\033[0m (admin / micro)")
if watching {
fmt.Println(" \033[33mWatching for changes...\033[0m")
fmt.Println(" │ │")
fmt.Println(" \033[33mWatching for changes...\033[0m")
}
fmt.Println(" │ Auth: \033[32menabled\033[0m (admin / micro) │")
fmt.Println(" │ │")
if gw != nil && len(services) > 0 {
svc := services[0]
fmt.Println(" │ Try: │")
fmt.Printf(" │ \033[90mcurl -X POST http://localhost%s/api/%s/...\033[0m │\n", gw.Addr(), svc.name)
fmt.Println(" │ │")
}
fmt.Println(" └─────────────────────────────────────────────────────────────┘")
fmt.Println()
}
+57 -309
View File
@@ -28,6 +28,9 @@ import (
"go-micro.dev/v5/client"
"go-micro.dev/v5/cmd"
codecBytes "go-micro.dev/v5/codec/bytes"
"go-micro.dev/v5/ai"
_ "go-micro.dev/v5/ai/anthropic"
_ "go-micro.dev/v5/ai/openai"
"go-micro.dev/v5/registry"
"go-micro.dev/v5/store"
"golang.org/x/crypto/bcrypt"
@@ -58,7 +61,7 @@ type templates struct {
authLogin *template.Template
authUsers *template.Template
playground *template.Template
scopes *template.Template
scopes *template.Template
}
type TemplateUser struct {
ID string
@@ -82,7 +85,7 @@ func parseTemplates() *templates {
authLogin: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/auth_login.html")),
authUsers: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/auth_users.html")),
playground: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/playground.html")),
scopes: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/scopes.html")),
scopes: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/scopes.html")),
}
}
@@ -606,7 +609,7 @@ func registerHandlers(mux *http.ServeMux, tmpls *templates, storeInst store.Stor
}
}
apiKey := ""
model := ""
modelName := ""
baseURL := ""
provider := ""
if settings != nil {
@@ -614,7 +617,7 @@ func registerHandlers(mux *http.ServeMux, tmpls *templates, storeInst store.Stor
apiKey = v
}
if v := settings["model"]; v != "" {
model = v
modelName = v
}
if v := settings["base_url"]; v != "" {
baseURL = v
@@ -630,39 +633,12 @@ func registerHandlers(mux *http.ServeMux, tmpls *templates, storeInst store.Stor
// Auto-detect provider if not explicitly set
if provider == "" {
if strings.Contains(baseURL, "anthropic") {
provider = "anthropic"
} else {
provider = "openai"
}
}
// Set defaults based on provider
if provider == "anthropic" {
if model == "" {
model = "claude-sonnet-4-20250514"
}
if baseURL == "" {
baseURL = "https://api.anthropic.com"
}
} else {
if model == "" {
model = "gpt-4o"
}
if baseURL == "" {
baseURL = "https://api.openai.com"
}
provider = ai.AutoDetectProvider(baseURL)
}
// Discover tools from registry
services, _ := registry.ListServices()
type toolInfo struct {
Name string // original dotted name (e.g. "greeter.Greeter.Hello")
SafeName string // LLM-safe name (dots replaced with underscores)
Description string
Properties map[string]any
}
var discoveredTools []toolInfo
var discoveredTools []ai.Tool
// safeNameMap maps LLM-safe names back to original dotted names
safeNameMap := map[string]string{}
for _, svc := range services {
@@ -689,11 +665,11 @@ func registerHandlers(mux *http.ServeMux, tmpls *templates, storeInst store.Stor
}
}
}
discoveredTools = append(discoveredTools, toolInfo{
Name: tName,
SafeName: safeName,
Description: desc,
Properties: props,
discoveredTools = append(discoveredTools, ai.Tool{
Name: safeName,
OriginalName: tName,
Description: desc,
Properties: props,
})
}
}
@@ -776,282 +752,54 @@ func registerHandlers(mux *http.ServeMux, tmpls *templates, storeInst store.Stor
return rpcResult, string(rsp.Data)
}
// callLLMAPI makes an HTTP request to the LLM provider
callLLMAPI := func(url string, body []byte) ([]byte, error) {
httpReq, err := http.NewRequestWithContext(r.Context(), "POST", url, bytes.NewReader(body))
if err != nil {
return nil, err
}
httpReq.Header.Set("Content-Type", "application/json")
if provider == "anthropic" {
httpReq.Header.Set("x-api-key", apiKey)
httpReq.Header.Set("anthropic-version", "2023-06-01")
} else {
httpReq.Header.Set("Authorization", "Bearer "+apiKey)
}
resp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("LLM API request failed: %w", err)
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
if resp.StatusCode != 200 {
return nil, fmt.Errorf("LLM API error (%s): %s", resp.Status, string(respBody))
}
return respBody, nil
// Create model with options
var modelOpts []ai.Option
modelOpts = append(modelOpts, ai.WithAPIKey(apiKey))
if modelName != "" {
modelOpts = append(modelOpts, ai.WithModel(modelName))
}
if baseURL != "" {
modelOpts = append(modelOpts, ai.WithBaseURL(baseURL))
}
modelOpts = append(modelOpts, ai.WithToolHandler(executeToolCall))
m := ai.New(provider, modelOpts...)
if m == nil {
json.NewEncoder(w).Encode(map[string]string{"error": "Failed to create model provider"})
return
}
// Build request
modelReq := &ai.Request{
Prompt: req.Prompt,
SystemPrompt: agentSystemPrompt,
Tools: discoveredTools,
}
// Generate response
response, err := m.Generate(r.Context(), modelReq)
if err != nil {
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
// Build result
result := map[string]any{}
if provider == "anthropic" {
// --- Anthropic Messages API ---
var anthropicTools []map[string]any
for _, t := range discoveredTools {
anthropicTools = append(anthropicTools, map[string]any{
"name": t.SafeName,
"description": t.Description,
"input_schema": map[string]any{
"type": "object",
"properties": t.Properties,
},
if response.Reply != "" {
result["reply"] = response.Reply
}
if len(response.ToolCalls) > 0 {
var toolCalls []map[string]any
for _, tc := range response.ToolCalls {
toolCalls = append(toolCalls, map[string]any{
"tool": tc.Name,
"input": tc.Input,
})
}
anthropicReq := map[string]any{
"model": model,
"max_tokens": 4096,
"system": agentSystemPrompt,
"messages": []map[string]any{
{"role": "user", "content": req.Prompt},
},
}
if len(anthropicTools) > 0 {
anthropicReq["tools"] = anthropicTools
}
chatBody, _ := json.Marshal(anthropicReq)
apiURL := strings.TrimRight(baseURL, "/") + "/v1/messages"
respBody, err := callLLMAPI(apiURL, chatBody)
if err != nil {
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
// Parse Anthropic response
var anthropicResp struct {
Content []struct {
Type string `json:"type"`
Text string `json:"text"`
ID string `json:"id"`
Name string `json:"name"`
Input json.RawMessage `json:"input"`
} `json:"content"`
StopReason string `json:"stop_reason"`
}
if err := json.Unmarshal(respBody, &anthropicResp); err != nil {
json.NewEncoder(w).Encode(map[string]string{"error": "Failed to parse LLM response: " + err.Error()})
return
}
// Extract text reply
var replyParts []string
for _, block := range anthropicResp.Content {
if block.Type == "text" && block.Text != "" {
replyParts = append(replyParts, block.Text)
}
}
if len(replyParts) > 0 {
result["reply"] = strings.Join(replyParts, "\n")
}
// Execute tool uses
var toolUseBlocks []struct {
ID string
Name string
Input map[string]any
}
for _, block := range anthropicResp.Content {
if block.Type == "tool_use" {
var input map[string]any
if err := json.Unmarshal(block.Input, &input); err != nil {
log.Printf("[agent] failed to parse tool input: %v", err)
input = map[string]any{}
}
toolUseBlocks = append(toolUseBlocks, struct {
ID string
Name string
Input map[string]any
}{ID: block.ID, Name: block.Name, Input: input})
}
}
if len(toolUseBlocks) > 0 {
var toolCalls []map[string]any
var toolResultBlocks []map[string]any
for _, tu := range toolUseBlocks {
rpcResult, rpcContent := executeToolCall(tu.Name, tu.Input)
toolCalls = append(toolCalls, map[string]any{
"tool": tu.Name,
"input": tu.Input,
"result": rpcResult,
})
toolResultBlocks = append(toolResultBlocks, map[string]any{
"type": "tool_result",
"tool_use_id": tu.ID,
"content": rpcContent,
})
}
result["tool_calls"] = toolCalls
// Follow-up: send tool results back to Anthropic
followUpReq := map[string]any{
"model": model,
"max_tokens": 4096,
"system": agentSystemPrompt,
"messages": []map[string]any{
{"role": "user", "content": req.Prompt},
{"role": "assistant", "content": anthropicResp.Content},
{"role": "user", "content": toolResultBlocks},
},
}
followUpBody, _ := json.Marshal(followUpReq)
if followUpRespBody, err := callLLMAPI(apiURL, followUpBody); err == nil {
var followUpResp struct {
Content []struct {
Type string `json:"type"`
Text string `json:"text"`
} `json:"content"`
}
if json.Unmarshal(followUpRespBody, &followUpResp) == nil {
var answerParts []string
for _, block := range followUpResp.Content {
if block.Type == "text" && block.Text != "" {
answerParts = append(answerParts, block.Text)
}
}
if len(answerParts) > 0 {
result["answer"] = strings.Join(answerParts, "\n")
}
}
}
}
} else {
// --- OpenAI Chat Completions API ---
var openaiTools []map[string]any
for _, t := range discoveredTools {
openaiTools = append(openaiTools, map[string]any{
"type": "function",
"function": map[string]any{
"name": t.SafeName,
"description": t.Description,
"parameters": map[string]any{
"type": "object",
"properties": t.Properties,
},
},
})
}
messages := []map[string]any{
{"role": "system", "content": agentSystemPrompt},
{"role": "user", "content": req.Prompt},
}
chatReq := map[string]any{
"model": model,
"messages": messages,
}
if len(openaiTools) > 0 {
chatReq["tools"] = openaiTools
}
chatBody, _ := json.Marshal(chatReq)
apiURL := strings.TrimRight(baseURL, "/") + "/v1/chat/completions"
respBody, err := callLLMAPI(apiURL, chatBody)
if err != nil {
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
var chatResp struct {
Choices []struct {
Message struct {
Content string `json:"content"`
ToolCalls []struct {
ID string `json:"id"`
Function struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
} `json:"function"`
} `json:"tool_calls"`
} `json:"message"`
} `json:"choices"`
}
if err := json.Unmarshal(respBody, &chatResp); err != nil {
json.NewEncoder(w).Encode(map[string]string{"error": "Failed to parse LLM response: " + err.Error()})
return
}
if len(chatResp.Choices) == 0 {
json.NewEncoder(w).Encode(map[string]string{"error": "No response from LLM"})
return
}
choice := chatResp.Choices[0]
if choice.Message.Content != "" {
result["reply"] = choice.Message.Content
}
// Execute any tool calls
if len(choice.Message.ToolCalls) > 0 {
var toolCalls []map[string]any
followUpMessages := append(messages, map[string]any{
"role": "assistant",
"content": choice.Message.Content,
"tool_calls": choice.Message.ToolCalls,
})
for _, tc := range choice.Message.ToolCalls {
var input map[string]any
if err := json.Unmarshal([]byte(tc.Function.Arguments), &input); err != nil {
log.Printf("[agent] failed to parse tool arguments: %v", err)
}
if input == nil {
input = map[string]any{}
}
rpcResult, rpcContent := executeToolCall(tc.Function.Name, input)
toolCalls = append(toolCalls, map[string]any{
"tool": tc.Function.Name,
"input": input,
"result": rpcResult,
})
followUpMessages = append(followUpMessages, map[string]any{
"role": "tool",
"tool_call_id": tc.ID,
"content": rpcContent,
})
}
result["tool_calls"] = toolCalls
// Follow-up: send tool results back to LLM for a final answer
followUpReq := map[string]any{
"model": model,
"messages": followUpMessages,
}
followUpBody, _ := json.Marshal(followUpReq)
if followUpRespBody, err := callLLMAPI(apiURL, followUpBody); err == nil {
var followUpChat struct {
Choices []struct {
Message struct {
Content string `json:"content"`
} `json:"message"`
} `json:"choices"`
}
if json.Unmarshal(followUpRespBody, &followUpChat) == nil && len(followUpChat.Choices) > 0 {
result["answer"] = followUpChat.Choices[0].Message.Content
}
}
}
result["tool_calls"] = toolCalls
}
if response.Answer != "" {
result["answer"] = response.Answer
}
json.NewEncoder(w).Encode(result)
+220 -66
View File
@@ -1,39 +1,140 @@
{{define "content"}}
<h2>Agent</h2>
<p>Chat with your microservices using AI. Configure a model API key in settings, then use the prompt to interact with your services.</p>
<style>
#agent-chat {
display: flex;
flex-direction: column;
height: calc(100vh - 160px);
max-height: 800px;
}
#agent-messages {
flex: 1;
overflow-y: auto;
padding: 1em 0;
scroll-behavior: smooth;
}
.msg { padding: 0.7em 1em; border-radius: 8px; margin-bottom: 0.6em; line-height: 1.6; }
.msg-user { background: #f0f4ff; border: 1px solid #d0d8f0; }
.msg-user b { color: #336; }
.msg-assistant { background: #fff; border: 1px solid #ddd; }
.msg-assistant b { color: #333; }
.msg-error { background: #fff5f5; border: 1px solid #e88; color: #a00; }
.msg-thinking { background: #fafafa; border: 1px solid #e5e5e5; color: #888; font-style: italic; }
.tool-call {
background: #f8f9fa; border: 1px solid #e0e0e0; border-radius: 8px;
margin-bottom: 0.6em; font-size: 0.93em; overflow: hidden;
}
.tool-header {
padding: 0.5em 1em; cursor: pointer; display: flex; align-items: center;
gap: 0.5em; user-select: none; font-weight: 600; color: #555;
}
.tool-header:hover { background: #f0f0f0; }
.tool-header .arrow { transition: transform 0.2s; display: inline-block; }
.tool-header .arrow.open { transform: rotate(90deg); }
.tool-body { padding: 0 1em 0.8em 1em; display: none; }
.tool-body.open { display: block; }
.tool-body pre {
background: #f5f5f5; padding: 0.6em; border-radius: 4px;
overflow-x: auto; font-size: 0.9em; margin: 0.4em 0;
}
.tool-body .tool-label { font-weight: 600; color: #666; font-size: 0.85em; margin-top: 0.5em; }
.tool-status { font-size: 0.8em; font-weight: normal; margin-left: auto; }
.tool-status.ok { color: #2a2; }
.tool-status.err { color: #c00; }
#prompt-bar {
display: flex; gap: 0.5em; align-items: center;
padding: 0.8em 0 0 0; border-top: 1px solid #eee;
}
#prompt-bar input {
flex: 1; margin-bottom: 0; padding: 0.6em 0.8em;
border: 1px solid #ccc; border-radius: 6px; font-size: 1em;
}
#prompt-bar button { padding: 0.6em 1.5em; border-radius: 6px; font-size: 1em; white-space: nowrap; }
#prompt-bar button:disabled { opacity: 0.5; cursor: not-allowed; }
.tools-bar {
display: flex; gap: 0.5em; align-items: center;
padding: 0.5em 0; font-size: 0.85em; color: #888;
}
.tools-bar .tool-count { font-weight: 600; color: #555; }
.settings-toggle {
background: none; border: 1px solid #ddd; border-radius: 6px;
padding: 0.3em 0.8em; font-size: 0.85em; cursor: pointer; color: #555;
}
.settings-toggle:hover { background: #f5f5f5; }
#settings-panel {
display: none; background: #fafafa; border: 1px solid #e5e5e5;
border-radius: 8px; padding: 1em 1.2em; margin-bottom: 1em;
}
#settings-panel.open { display: block; }
#settings-panel label { display: block; font-weight: 600; margin-top: 0.6em; font-size: 0.9em; }
#settings-panel input, #settings-panel select {
width: 100%; padding: 0.4em 0.6em; font-size: 0.9em;
border: 1px solid #ddd; border-radius: 4px; margin-top: 0.2em;
}
#settings-panel .settings-row { display: flex; gap: 1em; }
#settings-panel .settings-row > div { flex: 1; }
.clear-btn {
background: none; border: none; color: #999; cursor: pointer;
font-size: 0.85em; padding: 0.3em 0.5em;
}
.clear-btn:hover { color: #c00; }
.empty-state {
display: flex; flex-direction: column; align-items: center;
justify-content: center; height: 100%; color: #aaa; text-align: center;
}
.empty-state .icon { font-size: 3em; margin-bottom: 0.3em; }
.empty-state p { margin: 0.3em 0; max-width: 400px; }
</style>
<h3>Prompt</h3>
<div id="agent-messages"></div>
<form onsubmit="return false;" style="display:flex; gap:0.5em; align-items:flex-end;">
<input type="text" id="prompt-input" placeholder="e.g. List all users, Create a blog post..." style="flex:1; margin-bottom:0;">
<button id="prompt-btn" onclick="sendPrompt()">Send</button>
</form>
<div id="agent-chat">
<div class="tools-bar">
<span>Tools: <span class="tool-count" id="tool-count">...</span></span>
<button class="settings-toggle" onclick="toggleSettings()">Settings</button>
<button class="clear-btn" onclick="clearChat()">Clear chat</button>
</div>
<h3>Settings</h3>
<form id="settings-form" onsubmit="return false;">
<label style="display:block; font-weight:600;">Provider</label>
<select id="provider">
<option value="openai">OpenAI</option>
<option value="anthropic">Anthropic</option>
</select>
<label style="display:block; font-weight:600;">Model API Key</label>
<input type="password" id="api-key" placeholder="sk-... or API key for your model provider">
<label style="display:block; font-weight:600;">Model (optional)</label>
<input type="text" id="model-name" placeholder="e.g. gpt-4o or claude-sonnet-4-20250514">
<label style="display:block; font-weight:600;">Base URL (optional)</label>
<input type="text" id="base-url" placeholder="Leave blank for default">
<button onclick="saveSettings()">Save Settings</button>
<span id="settings-status" style="margin-left:0.5em; color:#888;"></span>
</form>
<div id="settings-panel">
<div class="settings-row">
<div>
<label>Provider</label>
<select id="provider">
<option value="openai">OpenAI</option>
<option value="anthropic">Anthropic</option>
</select>
</div>
<div>
<label>Model (optional)</label>
<input type="text" id="model-name" placeholder="e.g. gpt-4o, claude-sonnet-4-20250514">
</div>
</div>
<label>API Key</label>
<input type="password" id="api-key" placeholder="sk-... or API key">
<label>Base URL (optional)</label>
<input type="text" id="base-url" placeholder="Leave blank for default">
<div style="margin-top:0.8em; display:flex; gap:0.5em; align-items:center;">
<button onclick="saveSettings()" style="padding:0.4em 1em; font-size:0.9em;">Save</button>
<span id="settings-status" style="color:#888; font-size:0.85em;"></span>
</div>
</div>
<h3>Available Tools</h3>
<div id="tools-list">
<p style="color:#888;">Loading tools...</p>
<div id="agent-messages">
<div class="empty-state" id="empty-state">
<div class="icon">&#129302;</div>
<p><b>Chat with your services</b></p>
<p>Ask the agent to interact with your microservices. It will discover and call the right tools automatically.</p>
<p id="empty-tools" style="font-size:0.85em; color:#bbb; margin-top:0.8em;"></p>
</div>
</div>
<div id="prompt-bar">
<input type="text" id="prompt-input" placeholder="Ask the agent to call your services..." autofocus>
<button id="prompt-btn" onclick="sendPrompt()">Send</button>
</div>
</div>
<script>
(function() {
var tools = [];
var toolCount = document.getElementById('tool-count');
loadSettings();
loadTools();
@@ -46,10 +147,20 @@
if (data.api_key) document.getElementById('api-key').value = data.api_key;
if (data.model) document.getElementById('model-name').value = data.model;
if (data.base_url) document.getElementById('base-url').value = data.base_url;
// Auto-show settings if no API key configured
if (!data.api_key) {
document.getElementById('settings-panel').classList.add('open');
}
})
.catch(function() {});
.catch(function() {
document.getElementById('settings-panel').classList.add('open');
});
}
window.toggleSettings = function() {
document.getElementById('settings-panel').classList.toggle('open');
};
window.saveSettings = function() {
var status = document.getElementById('settings-status');
status.textContent = 'Saving...';
@@ -64,7 +175,10 @@
})
})
.then(function(r) { return r.json(); })
.then(function() { status.textContent = 'Saved'; })
.then(function() {
status.textContent = 'Saved';
setTimeout(function() { status.textContent = ''; }, 2000);
})
.catch(function(err) { status.textContent = 'Error: ' + err; });
};
@@ -73,28 +187,30 @@
.then(function(r) { return r.json(); })
.then(function(data) {
tools = data.tools || [];
renderTools();
toolCount.textContent = tools.length;
var emptyTools = document.getElementById('empty-tools');
if (tools.length > 0) {
var names = tools.slice(0, 5).map(function(t) { return t.name; });
var suffix = tools.length > 5 ? ' and ' + (tools.length - 5) + ' more' : '';
emptyTools.textContent = 'Available: ' + names.join(', ') + suffix;
} else {
emptyTools.textContent = 'No tools found. Start some services first.';
}
})
.catch(function(err) {
document.getElementById('tools-list').innerHTML = '<p style="color:#c00;">Failed to load tools: ' + err + '</p>';
.catch(function() {
toolCount.textContent = '0';
});
}
function renderTools() {
var el = document.getElementById('tools-list');
if (tools.length === 0) {
el.innerHTML = '<p style="color:#888;">No tools available. Start some services and they will appear here.</p>';
return;
}
var html = '<table><thead><tr><th>Tool</th><th>Description</th></tr></thead><tbody>';
for (var i = 0; i < tools.length; i++) {
var t = tools[i];
html += '<tr><td><code>' + escapeHtml(t.name) + '</code></td>';
html += '<td>' + escapeHtml(t.description || '') + '</td></tr>';
}
html += '</tbody></table>';
el.innerHTML = html;
}
window.clearChat = function() {
var container = document.getElementById('agent-messages');
container.innerHTML = '';
var emptyState = document.createElement('div');
emptyState.className = 'empty-state';
emptyState.id = 'empty-state';
emptyState.innerHTML = '<div class="icon">&#129302;</div><p><b>Chat with your services</b></p><p>Ask the agent to interact with your microservices.</p>';
container.appendChild(emptyState);
};
window.sendPrompt = function() {
var input = document.getElementById('prompt-input');
@@ -102,11 +218,21 @@
if (!text) return;
input.value = '';
// Remove empty state on first message
var emptyState = document.getElementById('empty-state');
if (emptyState) emptyState.remove();
addMessage('user', escapeHtml(text));
// Show thinking indicator
var thinkingId = 'thinking-' + Date.now();
addMessage('thinking', 'Agent is thinking...', thinkingId);
var btn = document.getElementById('prompt-btn');
btn.disabled = true;
btn.textContent = '...';
btn.textContent = 'Sending...';
var startTime = Date.now();
fetch('/api/agent/prompt', {
method: 'POST',
@@ -115,8 +241,15 @@
})
.then(function(r) { return r.json(); })
.then(function(data) {
var elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
btn.disabled = false;
btn.textContent = 'Send';
input.focus();
// Remove thinking indicator
var thinking = document.getElementById(thinkingId);
if (thinking) thinking.remove();
if (data.error) {
addMessage('error', escapeHtml(data.error));
return;
@@ -127,9 +260,7 @@
if (data.tool_calls && data.tool_calls.length > 0) {
for (var i = 0; i < data.tool_calls.length; i++) {
var tc = data.tool_calls[i];
addMessage('tool', '<b>Tool:</b> <code>' + escapeHtml(tc.tool) + '</code>' +
'<pre>' + escapeHtml(JSON.stringify(tc.input, null, 2)) + '</pre>' +
'<b>Result:</b><pre>' + escapeHtml(JSON.stringify(tc.result, null, 2)) + '</pre>');
addToolCall(tc, elapsed);
}
}
if (data.answer) {
@@ -139,35 +270,58 @@
.catch(function(err) {
btn.disabled = false;
btn.textContent = 'Send';
input.focus();
var thinking = document.getElementById(thinkingId);
if (thinking) thinking.remove();
addMessage('error', 'Error: ' + escapeHtml(String(err)));
});
};
function addToolCall(tc, elapsed) {
var container = document.getElementById('agent-messages');
var div = document.createElement('div');
div.className = 'tool-call';
var hasError = tc.result && tc.result.error;
var statusClass = hasError ? 'err' : 'ok';
var statusText = hasError ? 'error' : elapsed + 's';
var inputJson = JSON.stringify(tc.input, null, 2);
var resultJson = JSON.stringify(tc.result, null, 2);
div.innerHTML =
'<div class="tool-header" onclick="this.querySelector(\'.arrow\').classList.toggle(\'open\'); this.nextElementSibling.classList.toggle(\'open\');">' +
'<span class="arrow">&#9654;</span> ' +
'<code>' + escapeHtml(tc.tool) + '</code>' +
'<span class="tool-status ' + statusClass + '">' + statusText + '</span>' +
'</div>' +
'<div class="tool-body">' +
'<div class="tool-label">Input</div>' +
'<pre>' + escapeHtml(inputJson) + '</pre>' +
'<div class="tool-label">Result</div>' +
'<pre>' + escapeHtml(resultJson) + '</pre>' +
'</div>';
container.appendChild(div);
container.scrollTop = container.scrollHeight;
}
document.getElementById('prompt-input').addEventListener('keydown', function(e) {
if (e.key === 'Enter') { e.preventDefault(); window.sendPrompt(); }
});
function addMessage(type, html) {
function addMessage(type, html, id) {
var container = document.getElementById('agent-messages');
var div = document.createElement('div');
div.style.cssText = 'padding:0.8em 1em; border-radius:7px; margin-bottom:0.8em; line-height:1.6;';
div.className = 'msg msg-' + type;
if (id) div.id = id;
if (type === 'user') {
div.style.background = '#f7f7f7';
div.style.border = '1px solid #eee';
div.innerHTML = '<b>You:</b> ' + html;
div.innerHTML = '<b>You</b><br>' + html;
} else if (type === 'assistant' || type === 'answer') {
div.style.background = '#fff';
div.style.border = '1px solid #ddd';
div.innerHTML = '<b>Agent:</b> ' + html;
} else if (type === 'tool') {
div.style.background = '#fafafa';
div.style.border = '1px solid #e0e0e0';
div.style.fontSize = '0.95em';
div.innerHTML = '<b>Agent</b><br>' + html;
} else if (type === 'thinking') {
div.innerHTML = html;
} else if (type === 'error') {
div.style.background = '#fff';
div.style.border = '1px solid #c00';
div.style.color = '#c00';
div.innerHTML = html;
}
container.appendChild(div);
-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
}
}
@@ -0,0 +1,158 @@
// Code generated by protoc-gen-micro. DO NOT EDIT.
// source: user.proto
package user
import (
fmt "fmt"
proto "google.golang.org/protobuf/proto"
math "math"
)
import (
context "context"
client "go-micro.dev/v5/client"
server "go-micro.dev/v5/server"
model "go-micro.dev/v5/model"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ client.Option
var _ server.Option
var _ model.Model
// Client API for UserService service
type UserServiceService interface {
Create(ctx context.Context, in *CreateUserRequest, opts ...client.CallOption) (*CreateUserResponse, error)
Get(ctx context.Context, in *GetUserRequest, opts ...client.CallOption) (*GetUserResponse, error)
Delete(ctx context.Context, in *DeleteUserRequest, opts ...client.CallOption) (*DeleteUserResponse, error)
}
type userServiceService struct {
c client.Client
name string
}
func NewUserServiceService(name string, c client.Client) UserServiceService {
return &userServiceService{
c: c,
name: name,
}
}
func (c *userServiceService) Create(ctx context.Context, in *CreateUserRequest, opts ...client.CallOption) (*CreateUserResponse, error) {
req := c.c.NewRequest(c.name, "UserService.Create", in)
out := new(CreateUserResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userServiceService) Get(ctx context.Context, in *GetUserRequest, opts ...client.CallOption) (*GetUserResponse, error) {
req := c.c.NewRequest(c.name, "UserService.Get", in)
out := new(GetUserResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userServiceService) Delete(ctx context.Context, in *DeleteUserRequest, opts ...client.CallOption) (*DeleteUserResponse, error) {
req := c.c.NewRequest(c.name, "UserService.Delete", in)
out := new(DeleteUserResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for UserService service
type UserServiceHandler interface {
Create(context.Context, *CreateUserRequest, *CreateUserResponse) error
Get(context.Context, *GetUserRequest, *GetUserResponse) error
Delete(context.Context, *DeleteUserRequest, *DeleteUserResponse) error
}
func RegisterUserServiceHandler(s server.Server, hdlr UserServiceHandler, opts ...server.HandlerOption) error {
type userService interface {
Create(ctx context.Context, in *CreateUserRequest, out *CreateUserResponse) error
Get(ctx context.Context, in *GetUserRequest, out *GetUserResponse) error
Delete(ctx context.Context, in *DeleteUserRequest, out *DeleteUserResponse) error
}
type UserService struct {
userService
}
h := &userServiceHandler{hdlr}
return s.Handle(s.NewHandler(&UserService{h}, opts...))
}
type userServiceHandler struct {
UserServiceHandler
}
func (h *userServiceHandler) Create(ctx context.Context, in *CreateUserRequest, out *CreateUserResponse) error {
return h.UserServiceHandler.Create(ctx, in, out)
}
func (h *userServiceHandler) Get(ctx context.Context, in *GetUserRequest, out *GetUserResponse) error {
return h.UserServiceHandler.Get(ctx, in, out)
}
func (h *userServiceHandler) Delete(ctx context.Context, in *DeleteUserRequest, out *DeleteUserResponse) error {
return h.UserServiceHandler.Delete(ctx, in, out)
}
// UserModel is a model struct generated from User.
// Use NewUserModel to create a typed table backed by any model.Model.
type UserModel struct {
Id string `json:"id" model:"key"`
Name string `json:"name"`
Email string `json:"email"`
Age int32 `json:"age"`
Status string `json:"status"`
}
// RegisterUserModel registers the UserModel table with the given model backend.
func RegisterUserModel(db model.Model) error {
return db.Register(&UserModel{}, model.WithTable("users"))
}
// UserModelFromProto converts a User proto message to a UserModel.
func UserModelFromProto(p *User) *UserModel {
if p == nil {
return nil
}
return &UserModel{
Id: p.GetId(),
Name: p.GetName(),
Email: p.GetEmail(),
Age: p.GetAge(),
Status: p.GetStatus(),
}
}
// ToProto converts a UserModel to a User proto message.
func (m *UserModel) ToProto() *User {
if m == nil {
return nil
}
return &User{
Id: m.Id,
Name: m.Name,
Email: m.Email,
Age: m.Age,
Status: m.Status,
}
}
@@ -0,0 +1,41 @@
syntax = "proto3";
option go_package = "../user";
// UserService manages user accounts.
service UserService {
rpc Create(CreateUserRequest) returns (CreateUserResponse) {}
rpc Get(GetUserRequest) returns (GetUserResponse) {}
rpc Delete(DeleteUserRequest) returns (DeleteUserResponse) {}
}
// @model
message User {
string id = 1;
string name = 2;
string email = 3;
int32 age = 4;
string status = 5;
}
message CreateUserRequest {
User user = 1;
}
message CreateUserResponse {
User user = 1;
}
message GetUserRequest {
string id = 1;
}
message GetUserResponse {
User user = 1;
}
message DeleteUserRequest {
string id = 1;
}
message DeleteUserResponse {}
@@ -1193,6 +1193,15 @@ func (g *Generator) PrintComments(path string) bool {
return false
}
// GetComments returns the raw leading comment text for the given path, if any.
func (g *Generator) GetComments(path string) (string, bool) {
loc, ok := g.file.comments[path]
if !ok {
return "", false
}
return loc.GetLeadingComments(), true
}
// makeComments generates the comment string for the field, no "\n" at the end
func (g *Generator) makeComments(path string) (string, bool) {
loc, ok := g.file.comments[path]
+230 -6
View File
@@ -18,6 +18,7 @@ const (
contextPkgPath = "context"
clientPkgPath = "go-micro.dev/v5/client"
serverPkgPath = "go-micro.dev/v5/server"
modelPkgPath = "go-micro.dev/v5/model"
)
func init() {
@@ -42,6 +43,7 @@ var (
contextPkg string
clientPkg string
serverPkg string
modelPkg string
pkgImports map[generator.GoPackageName]bool
)
@@ -51,6 +53,7 @@ func (g *micro) Init(gen *generator.Generator) {
contextPkg = generator.RegisterUniquePackageName("context", nil)
clientPkg = generator.RegisterUniquePackageName("client", nil)
serverPkg = generator.RegisterUniquePackageName("server", nil)
modelPkg = generator.RegisterUniquePackageName("model", nil)
}
// Given a type name defined in a .proto, return its object.
@@ -70,29 +73,66 @@ func (g *micro) P(args ...interface{}) { g.gen.P(args...) }
// Generate generates code for the services in the given file.
func (g *micro) Generate(file *generator.FileDescriptor) {
if len(file.FileDescriptorProto.Service) == 0 {
// Check if any messages have @model annotation
hasModels := false
for i := range file.FileDescriptorProto.MessageType {
if g.isModelMessage(i) {
hasModels = true
break
}
}
if len(file.FileDescriptorProto.Service) == 0 && !hasModels {
return
}
g.P("// Reference imports to suppress errors if they are not otherwise used.")
g.P("var _ ", contextPkg, ".Context")
g.P("var _ ", clientPkg, ".Option")
g.P("var _ ", serverPkg, ".Option")
if len(file.FileDescriptorProto.Service) > 0 {
g.P("var _ ", clientPkg, ".Option")
g.P("var _ ", serverPkg, ".Option")
}
if hasModels {
g.P("var _ ", modelPkg, ".Database")
}
g.P()
for i, service := range file.FileDescriptorProto.Service {
g.generateService(file, service, i)
}
// Generate model structs for @model annotated messages
for i, msg := range file.FileDescriptorProto.MessageType {
if g.isModelMessage(i) {
g.generateModel(msg, i)
}
}
}
// GenerateImports generates the import declaration for this file.
func (g *micro) GenerateImports(file *generator.FileDescriptor, imports map[generator.GoImportPath]generator.GoPackageName) {
if len(file.FileDescriptorProto.Service) == 0 {
hasServices := len(file.FileDescriptorProto.Service) > 0
hasModels := false
for i := range file.FileDescriptorProto.MessageType {
if g.isModelMessage(i) {
hasModels = true
break
}
}
if !hasServices && !hasModels {
return
}
g.P("import (")
g.P(contextPkg, " ", strconv.Quote(path.Join(g.gen.ImportPrefix, contextPkgPath)))
g.P(clientPkg, " ", strconv.Quote(path.Join(g.gen.ImportPrefix, clientPkgPath)))
g.P(serverPkg, " ", strconv.Quote(path.Join(g.gen.ImportPrefix, serverPkgPath)))
if hasServices {
g.P(clientPkg, " ", strconv.Quote(path.Join(g.gen.ImportPrefix, clientPkgPath)))
g.P(serverPkg, " ", strconv.Quote(path.Join(g.gen.ImportPrefix, serverPkgPath)))
}
if hasModels {
g.P(modelPkg, " ", strconv.Quote(path.Join(g.gen.ImportPrefix, modelPkgPath)))
}
g.P(")")
g.P()
@@ -529,3 +569,187 @@ func (g *micro) generateServerMethod(servName string, method *pb.MethodDescripto
return hname
}
// isModelMessage checks if the message at the given index has a // @model annotation.
// Path "4,<index>" refers to message_type[index] in FileDescriptorProto.
func (g *micro) isModelMessage(msgIndex int) bool {
commentPath := fmt.Sprintf("4,%d", msgIndex)
comment, ok := g.gen.GetComments(commentPath)
if !ok {
return false
}
return strings.Contains(comment, "@model")
}
// parseModelOptions extracts options from the @model annotation comment.
// Supports: @model, @model(table=my_table), @model(key=custom_id)
func parseModelOptions(comment string) (table string, key string) {
idx := strings.Index(comment, "@model")
if idx < 0 {
return "", ""
}
rest := comment[idx+len("@model"):]
rest = strings.TrimSpace(rest)
if !strings.HasPrefix(rest, "(") {
return "", ""
}
end := strings.Index(rest, ")")
if end < 0 {
return "", ""
}
opts := rest[1:end]
for _, part := range strings.Split(opts, ",") {
kv := strings.SplitN(strings.TrimSpace(part), "=", 2)
if len(kv) != 2 {
continue
}
switch strings.TrimSpace(kv[0]) {
case "table":
table = strings.TrimSpace(kv[1])
case "key":
key = strings.TrimSpace(kv[1])
}
}
return table, key
}
// protoFieldGoType returns the Go type string for a proto field for use in model structs.
// Only supports scalar types (no nested messages or enums in model structs).
func protoFieldGoType(field *pb.FieldDescriptorProto) string {
switch field.GetType() {
case pb.FieldDescriptorProto_TYPE_DOUBLE:
return "float64"
case pb.FieldDescriptorProto_TYPE_FLOAT:
return "float32"
case pb.FieldDescriptorProto_TYPE_INT64, pb.FieldDescriptorProto_TYPE_SINT64, pb.FieldDescriptorProto_TYPE_SFIXED64:
return "int64"
case pb.FieldDescriptorProto_TYPE_UINT64, pb.FieldDescriptorProto_TYPE_FIXED64:
return "uint64"
case pb.FieldDescriptorProto_TYPE_INT32, pb.FieldDescriptorProto_TYPE_SINT32, pb.FieldDescriptorProto_TYPE_SFIXED32:
return "int32"
case pb.FieldDescriptorProto_TYPE_UINT32, pb.FieldDescriptorProto_TYPE_FIXED32:
return "uint32"
case pb.FieldDescriptorProto_TYPE_BOOL:
return "bool"
case pb.FieldDescriptorProto_TYPE_STRING:
return "string"
case pb.FieldDescriptorProto_TYPE_BYTES:
return "[]byte"
default:
return "string"
}
}
// generateModel generates the model struct, factory, and proto conversion for a message.
func (g *micro) generateModel(msg *pb.DescriptorProto, msgIndex int) {
msgName := generator.CamelCase(msg.GetName())
modelName := msgName + "Model"
// Parse options from comment
commentPath := fmt.Sprintf("4,%d", msgIndex)
comment, _ := g.gen.GetComments(commentPath)
tableName, keyField := parseModelOptions(comment)
// Default table: lowercase message name + "s"
if tableName == "" {
tableName = strings.ToLower(msg.GetName()) + "s"
}
// Default key: first field, or "id" if a field named "id" exists
if keyField == "" {
for _, field := range msg.Field {
if field.GetName() == "id" {
keyField = "id"
break
}
}
if keyField == "" && len(msg.Field) > 0 {
keyField = msg.Field[0].GetName()
}
}
// Filter to scalar fields only (skip nested messages, maps, oneofs)
type modelField struct {
goName string
jsonName string
goType string
isKey bool
proto *pb.FieldDescriptorProto
}
var fields []modelField
for _, field := range msg.Field {
ft := field.GetType()
// Skip message and enum types (not directly storable as scalars)
if ft == pb.FieldDescriptorProto_TYPE_MESSAGE || ft == pb.FieldDescriptorProto_TYPE_GROUP {
continue
}
// Skip repeated fields (slices aren't directly storable)
if field.GetLabel() == pb.FieldDescriptorProto_LABEL_REPEATED {
continue
}
goName := generator.CamelCase(field.GetName())
jsonName := field.GetJsonName()
if jsonName == "" {
jsonName = field.GetName()
}
fields = append(fields, modelField{
goName: goName,
jsonName: jsonName,
goType: protoFieldGoType(field),
isKey: field.GetName() == keyField,
proto: field,
})
}
if len(fields) == 0 {
return
}
// Generate model struct
g.P()
g.P("// ", modelName, " is a model struct generated from ", msgName, ".")
g.P("// Use New", modelName, " to create a typed table backed by any model.Model.")
g.P("type ", modelName, " struct {")
for _, f := range fields {
tags := fmt.Sprintf("`json:%q", f.jsonName)
if f.isKey {
tags += ` model:"key"`
}
tags += "`"
g.P(f.goName, " ", f.goType, " ", tags)
}
g.P("}")
g.P()
// Generate Register helper: RegisterXModel(db) registers the model with the given backend.
g.P("// Register", modelName, " registers the ", modelName, " table with the given model backend.")
g.P("func Register", modelName, "(db ", modelPkg, ".Model) error {")
g.P("return db.Register(&", modelName, "{}, ", modelPkg, `.WithTable("`, tableName, `"))`)
g.P("}")
g.P()
// Generate FromProto: XModelFromProto(*X) *XModel
g.P("// ", modelName, "FromProto converts a ", msgName, " proto message to a ", modelName, ".")
g.P("func ", modelName, "FromProto(p *", msgName, ") *", modelName, " {")
g.P("if p == nil { return nil }")
g.P("return &", modelName, "{")
for _, f := range fields {
getter := "Get" + f.goName
g.P(f.goName, ": p.", getter, "(),")
}
g.P("}")
g.P("}")
g.P()
// Generate ToProto: (*XModel).ToProto() *X
g.P("// ToProto converts a ", modelName, " to a ", msgName, " proto message.")
g.P("func (m *", modelName, ") ToProto() *", msgName, " {")
g.P("if m == nil { return nil }")
g.P("return &", msgName, "{")
for _, f := range fields {
g.P(f.goName, ": m.", f.goName, ",")
}
g.P("}")
g.P("}")
g.P()
}
@@ -0,0 +1,37 @@
package micro
import "testing"
func TestParseModelOptions(t *testing.T) {
tests := []struct {
comment string
wantTable string
wantKey string
}{
{" @model\n", "", ""},
{" @model(table=app_users)\n", "app_users", ""},
{" @model(key=user_id)\n", "", "user_id"},
{" @model(table=users, key=user_id)\n", "users", "user_id"},
{" some description\n @model(table=items)\n", "items", ""},
{" no annotation here\n", "", ""},
}
for _, tt := range tests {
table, key := parseModelOptions(tt.comment)
if table != tt.wantTable {
t.Errorf("parseModelOptions(%q): table = %q, want %q", tt.comment, table, tt.wantTable)
}
if key != tt.wantKey {
t.Errorf("parseModelOptions(%q): key = %q, want %q", tt.comment, key, tt.wantKey)
}
}
}
func TestProtoFieldGoType(t *testing.T) {
// Smoke test - just verify it doesn't panic with nil
typ := protoFieldGoType(nil)
if typ != "string" {
// nil field returns default based on zero value TYPE_DOUBLE=0
t.Logf("protoFieldGoType(nil) = %q", typ)
}
}
+65
View File
@@ -0,0 +1,65 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
*.manifest
*.spec
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# IDEs
.vscode/
.idea/
*.swp
*.swo
*~
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Ruff
.ruff_cache/
+327
View File
@@ -0,0 +1,327 @@
# LlamaIndex Go Micro Integration
[![PyPI version](https://badge.fury.io/py/go-micro-llamaindex.svg)](https://badge.fury.io/py/go-micro-llamaindex)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
Official LlamaIndex integration for Go Micro services. This package enables LlamaIndex agents to discover and call Go Micro microservices through the Model Context Protocol (MCP).
## Features
- **Automatic Service Discovery** - Discovers available services from MCP gateway
- **Dynamic Tool Generation** - Converts service endpoints into LlamaIndex tools
- **Rich Descriptions** - Uses service metadata for accurate tool descriptions
- **Authentication Support** - Bearer token auth with scope-based permissions
- **RAG Integration** - Combine service tools with LlamaIndex's RAG capabilities
- **Type-Safe** - Fully typed with Python 3.8+ type hints
## Installation
```bash
pip install go-micro-llamaindex
```
## Quick Start
### 1. Start Your Go Micro Services
```bash
# Start MCP gateway
micro mcp serve --address :3000
```
### 2. Create LlamaIndex Agent
```python
from go_micro_llamaindex import GoMicroToolkit
from llama_index.core.agent import ReActAgent
from llama_index.llms.openai import OpenAI
# Initialize toolkit from MCP gateway
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
# Create agent
llm = OpenAI(model="gpt-4")
agent = ReActAgent.from_tools(toolkit.get_tools(), llm=llm, verbose=True)
# Use the agent!
response = agent.chat("Create a user named Alice with email alice@example.com")
print(response)
```
## Usage Examples
### Basic Tool Discovery
```python
from go_micro_llamaindex import GoMicroToolkit
# Connect to MCP gateway
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
# List available tools
for tool in toolkit.get_tools():
print(f"Tool: {tool.metadata.name}")
print(f"Description: {tool.metadata.description}")
print()
```
### Authentication
```python
from go_micro_llamaindex import GoMicroToolkit
# Create toolkit with authentication
toolkit = GoMicroToolkit.from_gateway(
gateway_url="http://localhost:3000",
auth_token="your-bearer-token"
)
# Tools will automatically use the auth token
tools = toolkit.get_tools()
```
### Filter Tools by Service
```python
from go_micro_llamaindex import GoMicroToolkit
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
# Get only user service tools
user_tools = toolkit.get_tools(service_filter="users")
# Get tools matching a pattern
blog_tools = toolkit.get_tools(name_pattern="blog.*")
```
### Custom Tool Selection
```python
from go_micro_llamaindex import GoMicroToolkit
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
# Select specific tools
selected_tools = toolkit.get_tools(
include=["users.Users.Get", "users.Users.Create"]
)
# Exclude certain tools
filtered_tools = toolkit.get_tools(
exclude=["users.Users.Delete"]
)
```
### RAG + Microservices
```python
from go_micro_llamaindex import GoMicroToolkit
from llama_index.core import VectorStoreIndex, Document
from llama_index.core.agent import ReActAgent
from llama_index.core.tools import QueryEngineTool, ToolMetadata
from llama_index.llms.openai import OpenAI
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
# Combine service tools with a RAG query engine
index = VectorStoreIndex.from_documents([...])
rag_tool = QueryEngineTool(
query_engine=index.as_query_engine(),
metadata=ToolMetadata(name="docs", description="Search documentation"),
)
all_tools = [rag_tool] + toolkit.get_tools()
agent = ReActAgent.from_tools(all_tools, llm=OpenAI(model="gpt-4"))
```
### Multi-Agent Workflows
```python
from go_micro_llamaindex import GoMicroToolkit
from llama_index.core.agent import ReActAgent
from llama_index.llms.openai import OpenAI
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
llm = OpenAI(model="gpt-4")
# Agent 1: User management
user_agent = ReActAgent.from_tools(
toolkit.get_tools(service_filter="users"), llm=llm
)
# Agent 2: Blog management
blog_agent = ReActAgent.from_tools(
toolkit.get_tools(service_filter="blog"), llm=llm
)
# Coordinate between agents
user_result = user_agent.chat("Create user Alice")
blog_result = blog_agent.chat(f"Create blog post for {user_result}")
```
### Error Handling
```python
from go_micro_llamaindex import GoMicroToolkit, GoMicroError
try:
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
tools = toolkit.get_tools()
except GoMicroError as e:
print(f"Error: {e}")
```
### Advanced Configuration
```python
from go_micro_llamaindex import GoMicroToolkit, GoMicroConfig
config = GoMicroConfig(
gateway_url="http://localhost:3000",
auth_token="your-token",
timeout=30,
retry_count=3,
retry_delay=1.0,
verify_ssl=True,
)
toolkit = GoMicroToolkit(config)
tools = toolkit.get_tools()
```
## API Reference
### GoMicroToolkit
Main class for interacting with Go Micro services.
#### Methods
- `from_gateway(gateway_url, auth_token=None, **kwargs)` - Create toolkit from MCP gateway
- `get_tools(service_filter=None, name_pattern=None, include=None, exclude=None)` - Get LlamaIndex tools
- `refresh()` - Refresh tool list from gateway
- `call_tool(tool_name, arguments)` - Call a tool directly
- `list_tools()` - Get raw list of available tools
### GoMicroConfig
Configuration for the toolkit.
#### Parameters
- `gateway_url` (str) - MCP gateway URL
- `auth_token` (str, optional) - Bearer authentication token
- `timeout` (int) - Request timeout in seconds (default: 30)
- `retry_count` (int) - Number of retries (default: 3)
- `retry_delay` (float) - Delay between retries in seconds (default: 1.0)
- `verify_ssl` (bool) - Verify SSL certificates (default: True)
## Requirements
- Python 3.8+
- llama-index-core >= 0.10.0
- requests >= 2.31.0
- pydantic >= 2.0.0
## Development
### Setup
```bash
git clone https://github.com/micro/go-micro
cd go-micro/contrib/go-micro-llamaindex
# Create virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install in development mode
pip install -e ".[dev]"
```
### Running Tests
```bash
# Run all tests
pytest
# Run with coverage
pytest --cov=go_micro_llamaindex
# Run specific test
pytest tests/test_toolkit.py
```
### Code Formatting
```bash
# Format code
black go_micro_llamaindex tests
# Check types
mypy go_micro_llamaindex
# Lint
ruff check go_micro_llamaindex
```
## Examples
See the [examples](./examples) directory for complete examples:
- [basic_agent.py](./examples/basic_agent.py) - Simple ReAct agent
- [rag_with_services.py](./examples/rag_with_services.py) - RAG combined with microservices
## Troubleshooting
### Gateway Connection Issues
If you can't connect to the MCP gateway:
1. Verify the gateway is running:
```bash
curl http://localhost:3000/health
```
2. Check the gateway URL is correct
3. Verify firewall settings
### Authentication Errors
If you get authentication errors:
1. Verify your token is valid
2. Check the token has required scopes
3. Review gateway logs for details
### Tool Discovery Issues
If tools aren't being discovered:
1. List services from gateway:
```bash
curl http://localhost:3000/mcp/tools
```
2. Verify services are registered
3. Check service metadata is correct
## Contributing
Contributions are welcome! Please see [CONTRIBUTING.md](../../CONTRIBUTING.md) for details.
## License
Apache 2.0 - See [LICENSE](../../LICENSE) for details.
## Links
- [Go Micro](https://github.com/micro/go-micro)
- [MCP Documentation](../../gateway/mcp/DOCUMENTATION.md)
- [LlamaIndex](https://docs.llamaindex.ai/)
- [Issue Tracker](https://github.com/micro/go-micro/issues)
## Support
- GitHub Discussions: https://github.com/micro/go-micro/discussions
- Discord: https://discord.gg/jwTYuUVAGh
@@ -0,0 +1,44 @@
"""Basic LlamaIndex agent example using Go Micro services.
This example shows how to create a simple LlamaIndex agent that can
interact with Go Micro services through the MCP gateway.
"""
from go_micro_llamaindex import GoMicroToolkit
from llama_index.core.agent import ReActAgent
from llama_index.llms.openai import OpenAI
def main():
"""Run basic agent example."""
# Initialize toolkit from MCP gateway
print("Connecting to MCP gateway...")
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
# Get available tools
tools = toolkit.get_tools()
print(f"\nDiscovered {len(tools)} tools:")
for tool in tools:
print(f" - {tool.metadata.name}: {tool.metadata.description}")
# Create LlamaIndex ReAct agent
print("\nCreating LlamaIndex agent...")
llm = OpenAI(model="gpt-4", temperature=0)
agent = ReActAgent.from_tools(tools, llm=llm, verbose=True)
# Example queries
queries = [
"Create a user named Alice with email alice@example.com",
"Get the user we just created",
]
for query in queries:
print(f"\n{'='*60}")
print(f"Query: {query}")
print("=" * 60)
response = agent.chat(query)
print(f"\nResult: {response}")
if __name__ == "__main__":
main()
@@ -0,0 +1,72 @@
"""RAG with Go Micro services example.
This example demonstrates how to combine LlamaIndex's RAG capabilities
with Go Micro service tools, allowing an agent to both query documents
and interact with microservices.
"""
from go_micro_llamaindex import GoMicroToolkit
from llama_index.core import VectorStoreIndex, Document
from llama_index.core.agent import ReActAgent
from llama_index.core.tools import QueryEngineTool, ToolMetadata
from llama_index.llms.openai import OpenAI
def main():
"""Run RAG + services example."""
# Initialize toolkit from MCP gateway
print("Connecting to MCP gateway...")
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
# Get service tools (e.g., user management)
service_tools = toolkit.get_tools(service_filter="users")
print(f"Discovered {len(service_tools)} user service tools")
# Create a simple document index for RAG
documents = [
Document(text="Alice is the admin user with ID user-001."),
Document(text="Bob is a regular user with ID user-002."),
Document(text="The blog service supports creating, reading, and deleting posts."),
Document(text="Users need the 'blog:write' scope to create blog posts."),
]
print("Building document index...")
index = VectorStoreIndex.from_documents(documents)
query_engine = index.as_query_engine()
# Create a query engine tool for RAG
rag_tool = QueryEngineTool(
query_engine=query_engine,
metadata=ToolMetadata(
name="knowledge_base",
description="Search the knowledge base for information about users, "
"services, and permissions. Use this to look up user IDs, "
"service capabilities, and required scopes.",
),
)
# Combine RAG tool with service tools
all_tools = [rag_tool] + service_tools
# Create agent with both capabilities
print("\nCreating agent with RAG + service tools...")
llm = OpenAI(model="gpt-4", temperature=0)
agent = ReActAgent.from_tools(all_tools, llm=llm, verbose=True)
# Example: Agent uses RAG to find user ID, then calls service
queries = [
"What is Alice's user ID?",
"Look up Alice's user ID from the knowledge base, then get her full profile from the user service",
"What scope do I need to create blog posts?",
]
for query in queries:
print(f"\n{'='*60}")
print(f"Query: {query}")
print("=" * 60)
response = agent.chat(query)
print(f"\nResult: {response}")
if __name__ == "__main__":
main()
@@ -0,0 +1,17 @@
"""LlamaIndex Go Micro Integration.
This package provides LlamaIndex integration for Go Micro services through
the Model Context Protocol (MCP).
"""
from go_micro_llamaindex.toolkit import GoMicroToolkit, GoMicroConfig
from go_micro_llamaindex.exceptions import GoMicroError, GoMicroConnectionError, GoMicroAuthError
__version__ = "0.1.0"
__all__ = [
"GoMicroToolkit",
"GoMicroConfig",
"GoMicroError",
"GoMicroConnectionError",
"GoMicroAuthError",
]
@@ -0,0 +1,21 @@
"""Custom exceptions for LlamaIndex Go Micro integration."""
class GoMicroError(Exception):
"""Base exception for Go Micro integration errors."""
pass
class GoMicroConnectionError(GoMicroError):
"""Raised when unable to connect to MCP gateway."""
pass
class GoMicroAuthError(GoMicroError):
"""Raised when authentication fails."""
pass
class GoMicroToolError(GoMicroError):
"""Raised when tool execution fails."""
pass
@@ -0,0 +1,311 @@
"""LlamaIndex toolkit for Go Micro services."""
import json
import re
from typing import Any, Dict, List, Optional
from dataclasses import dataclass
import requests
from llama_index.core.tools import FunctionTool, ToolMetadata
from pydantic import BaseModel, Field
from go_micro_llamaindex.exceptions import (
GoMicroConnectionError,
GoMicroAuthError,
GoMicroToolError,
)
@dataclass
class GoMicroConfig:
"""Configuration for Go Micro MCP gateway connection.
Attributes:
gateway_url: URL of the MCP gateway (e.g., http://localhost:3000)
auth_token: Optional bearer authentication token
timeout: Request timeout in seconds
retry_count: Number of retries on failure
retry_delay: Delay between retries in seconds
verify_ssl: Whether to verify SSL certificates
"""
gateway_url: str
auth_token: Optional[str] = None
timeout: int = 30
retry_count: int = 3
retry_delay: float = 1.0
verify_ssl: bool = True
class GoMicroTool(BaseModel):
"""Represents a Go Micro service tool.
Attributes:
name: Tool name (e.g., "users.Users.Get")
service: Service name (e.g., "users")
endpoint: Endpoint name (e.g., "Users.Get")
description: Tool description
example: Example input JSON
scopes: Required auth scopes
metadata: Additional metadata from service
"""
name: str
service: str
endpoint: str
description: str
example: Optional[str] = None
scopes: Optional[List[str]] = None
metadata: Dict[str, str] = Field(default_factory=dict)
class GoMicroToolkit:
"""LlamaIndex toolkit for Go Micro services.
This class provides integration between LlamaIndex and Go Micro services
via the Model Context Protocol (MCP) gateway.
Example:
>>> toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
>>> tools = toolkit.get_tools()
>>> for tool in tools:
... print(f"Tool: {tool.metadata.name}")
"""
def __init__(self, config: GoMicroConfig):
"""Initialize the toolkit.
Args:
config: Configuration for MCP gateway connection
"""
self.config = config
self._tools: Optional[List[GoMicroTool]] = None
self._session = requests.Session()
if config.auth_token:
self._session.headers.update({
"Authorization": f"Bearer {config.auth_token}"
})
@classmethod
def from_gateway(
cls,
gateway_url: str,
auth_token: Optional[str] = None,
**kwargs: Any
) -> "GoMicroToolkit":
"""Create toolkit from MCP gateway URL.
Args:
gateway_url: URL of the MCP gateway
auth_token: Optional bearer authentication token
**kwargs: Additional configuration options
Returns:
GoMicroToolkit instance
Example:
>>> toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
"""
config = GoMicroConfig(
gateway_url=gateway_url,
auth_token=auth_token,
**kwargs
)
return cls(config)
def _make_request(
self,
method: str,
path: str,
**kwargs: Any
) -> requests.Response:
"""Make HTTP request to MCP gateway.
Args:
method: HTTP method (GET, POST, etc.)
path: API path
**kwargs: Additional request arguments
Returns:
Response object
Raises:
GoMicroConnectionError: If connection fails
GoMicroAuthError: If authentication fails
"""
url = f"{self.config.gateway_url}{path}"
kwargs.setdefault("timeout", self.config.timeout)
kwargs.setdefault("verify", self.config.verify_ssl)
try:
response = self._session.request(method, url, **kwargs)
if response.status_code == 401:
raise GoMicroAuthError("Authentication failed")
elif response.status_code == 403:
raise GoMicroAuthError("Forbidden: insufficient permissions")
response.raise_for_status()
return response
except requests.ConnectionError as e:
raise GoMicroConnectionError(
f"Failed to connect to MCP gateway at {url}: {e}"
)
except requests.Timeout as e:
raise GoMicroConnectionError(
f"Request to MCP gateway timed out: {e}"
)
except requests.RequestException as e:
if isinstance(e, (GoMicroConnectionError, GoMicroAuthError)):
raise
raise GoMicroConnectionError(f"Request failed: {e}")
def refresh(self) -> None:
"""Refresh tool list from MCP gateway.
Raises:
GoMicroConnectionError: If unable to connect to gateway
"""
response = self._make_request("GET", "/mcp/tools")
data = response.json()
tools_data = data.get("tools", [])
self._tools = [
GoMicroTool(
name=tool["name"],
service=tool["service"],
endpoint=tool["endpoint"],
description=tool.get("description", ""),
example=tool.get("example"),
scopes=tool.get("scopes"),
metadata=tool.get("metadata", {})
)
for tool in tools_data
]
def get_tools(
self,
service_filter: Optional[str] = None,
name_pattern: Optional[str] = None,
include: Optional[List[str]] = None,
exclude: Optional[List[str]] = None,
) -> List[FunctionTool]:
"""Get LlamaIndex tools from Go Micro services.
Args:
service_filter: Filter tools by service name
name_pattern: Filter tools by name pattern (regex)
include: List of tool names to include
exclude: List of tool names to exclude
Returns:
List of LlamaIndex FunctionTool objects
Example:
>>> toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
>>> all_tools = toolkit.get_tools()
>>> user_tools = toolkit.get_tools(service_filter="users")
"""
if self._tools is None:
self.refresh()
tools = self._tools or []
if service_filter:
tools = [t for t in tools if t.service == service_filter]
if name_pattern:
pattern = re.compile(name_pattern)
tools = [t for t in tools if pattern.match(t.name)]
if include:
tools = [t for t in tools if t.name in include]
if exclude:
tools = [t for t in tools if t.name not in exclude]
return [self._create_llamaindex_tool(tool) for tool in tools]
def _create_llamaindex_tool(self, tool: GoMicroTool) -> FunctionTool:
"""Create a LlamaIndex FunctionTool from a GoMicroTool.
Args:
tool: GoMicroTool to convert
Returns:
LlamaIndex FunctionTool object
"""
toolkit = self
def tool_func(arguments: str) -> str:
"""Execute the tool.
Args:
arguments: JSON string with tool arguments
Returns:
JSON string with tool result
"""
return toolkit.call_tool(tool.name, arguments)
description = tool.description
if tool.example:
description += f"\n\nExample input: {tool.example}"
return FunctionTool.from_defaults(
fn=tool_func,
name=tool.name,
description=description,
)
def call_tool(self, tool_name: str, arguments: str) -> str:
"""Call a specific tool directly.
Args:
tool_name: Name of the tool to call
arguments: JSON string with tool arguments
Returns:
JSON string with tool result
Raises:
GoMicroToolError: If tool execution fails
Example:
>>> toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
>>> result = toolkit.call_tool(
... "users.Users.Get",
... '{"id": "user-123"}'
... )
"""
try:
args = json.loads(arguments) if isinstance(arguments, str) else arguments
except json.JSONDecodeError as e:
raise GoMicroToolError(f"Invalid JSON arguments: {e}")
try:
response = self._make_request(
"POST",
"/mcp/call",
json={"name": tool_name, "arguments": args}
)
return json.dumps(response.json())
except requests.RequestException as e:
raise GoMicroToolError(f"Tool execution failed: {e}")
def list_tools(self) -> List[GoMicroTool]:
"""Get raw list of available tools.
Returns:
List of GoMicroTool objects
Example:
>>> toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
>>> for tool in toolkit.list_tools():
... print(f"{tool.name}: {tool.description}")
"""
if self._tools is None:
self.refresh()
return self._tools or []
@@ -0,0 +1,72 @@
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "go-micro-llamaindex"
version = "0.1.0"
description = "LlamaIndex integration for Go Micro services via MCP"
readme = "README.md"
requires-python = ">=3.9"
license = {text = "Apache-2.0"}
authors = [
{name = "Micro Team", email = "hello@micro.dev"}
]
keywords = ["llamaindex", "go-micro", "mcp", "microservices", "ai", "rag"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Software Development :: Libraries :: Python Modules",
]
dependencies = [
"llama-index-core>=0.10.0",
"requests>=2.31.0",
"pydantic>=2.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
"black>=23.0.0",
"mypy>=1.0.0",
"ruff>=0.1.0",
"types-requests>=2.31.0",
]
[project.urls]
Homepage = "https://github.com/micro/go-micro"
Documentation = "https://github.com/micro/go-micro/tree/master/contrib/go-micro-llamaindex"
Repository = "https://github.com/micro/go-micro"
Issues = "https://github.com/micro/go-micro/issues"
[tool.setuptools.packages.find]
where = ["."]
include = ["go_micro_llamaindex*"]
[tool.black]
line-length = 88
target-version = ['py39', 'py310', 'py311']
[tool.mypy]
python_version = "3.9"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
[tool.ruff]
line-length = 88
target-version = "py39"
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
@@ -0,0 +1,261 @@
"""Tests for GoMicroToolkit."""
import json
from unittest.mock import Mock, patch
import pytest
import requests
from go_micro_llamaindex import GoMicroToolkit, GoMicroConfig
from go_micro_llamaindex.exceptions import (
GoMicroConnectionError,
GoMicroAuthError,
)
@pytest.fixture
def mock_gateway_response():
"""Mock MCP gateway response."""
return {
"tools": [
{
"name": "users.Users.Get",
"service": "users",
"endpoint": "Users.Get",
"description": "Get a user by ID",
"example": '{"id": "user-123"}',
"scopes": ["users:read"],
"metadata": {
"description": "Get a user by ID",
"example": '{"id": "user-123"}',
"scopes": "users:read"
}
},
{
"name": "users.Users.Create",
"service": "users",
"endpoint": "Users.Create",
"description": "Create a new user",
"example": '{"name": "Alice", "email": "alice@example.com"}',
"scopes": ["users:write"],
"metadata": {}
},
{
"name": "blog.Blog.List",
"service": "blog",
"endpoint": "Blog.List",
"description": "List blog posts",
"scopes": ["blog:read"],
"metadata": {}
}
],
"count": 3
}
class TestGoMicroConfig:
"""Tests for GoMicroConfig."""
def test_config_defaults(self):
"""Test config default values."""
config = GoMicroConfig(gateway_url="http://localhost:3000")
assert config.gateway_url == "http://localhost:3000"
assert config.auth_token is None
assert config.timeout == 30
assert config.retry_count == 3
assert config.retry_delay == 1.0
assert config.verify_ssl is True
def test_config_custom_values(self):
"""Test config with custom values."""
config = GoMicroConfig(
gateway_url="http://localhost:8080",
auth_token="test-token",
timeout=60,
retry_count=5,
retry_delay=2.0,
verify_ssl=False
)
assert config.gateway_url == "http://localhost:8080"
assert config.auth_token == "test-token"
assert config.timeout == 60
assert config.retry_count == 5
assert config.retry_delay == 2.0
assert config.verify_ssl is False
class TestGoMicroToolkit:
"""Tests for GoMicroToolkit."""
def test_from_gateway(self):
"""Test creating toolkit from gateway URL."""
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
assert toolkit.config.gateway_url == "http://localhost:3000"
assert toolkit.config.auth_token is None
def test_from_gateway_with_auth(self):
"""Test creating toolkit with authentication."""
toolkit = GoMicroToolkit.from_gateway(
"http://localhost:3000",
auth_token="test-token"
)
assert toolkit.config.auth_token == "test-token"
assert "Authorization" in toolkit._session.headers
assert toolkit._session.headers["Authorization"] == "Bearer test-token"
@patch("requests.Session.request")
def test_refresh(self, mock_request, mock_gateway_response):
"""Test refreshing tool list."""
mock_response = Mock()
mock_response.json.return_value = mock_gateway_response
mock_response.status_code = 200
mock_request.return_value = mock_response
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
toolkit.refresh()
assert len(toolkit._tools) == 3
assert toolkit._tools[0].name == "users.Users.Get"
assert toolkit._tools[1].name == "users.Users.Create"
assert toolkit._tools[2].name == "blog.Blog.List"
@patch("requests.Session.request")
def test_get_tools(self, mock_request, mock_gateway_response):
"""Test getting LlamaIndex tools."""
mock_response = Mock()
mock_response.json.return_value = mock_gateway_response
mock_response.status_code = 200
mock_request.return_value = mock_response
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
tools = toolkit.get_tools()
assert len(tools) == 3
names = [t.metadata.name for t in tools]
assert "users.Users.Get" in names
assert "users.Users.Create" in names
assert "blog.Blog.List" in names
@patch("requests.Session.request")
def test_get_tools_with_service_filter(self, mock_request, mock_gateway_response):
"""Test filtering tools by service."""
mock_response = Mock()
mock_response.json.return_value = mock_gateway_response
mock_response.status_code = 200
mock_request.return_value = mock_response
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
tools = toolkit.get_tools(service_filter="users")
assert len(tools) == 2
for tool in tools:
assert "users" in tool.metadata.name
@patch("requests.Session.request")
def test_get_tools_with_include(self, mock_request, mock_gateway_response):
"""Test including specific tools."""
mock_response = Mock()
mock_response.json.return_value = mock_gateway_response
mock_response.status_code = 200
mock_request.return_value = mock_response
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
tools = toolkit.get_tools(include=["users.Users.Get"])
assert len(tools) == 1
assert tools[0].metadata.name == "users.Users.Get"
@patch("requests.Session.request")
def test_get_tools_with_exclude(self, mock_request, mock_gateway_response):
"""Test excluding specific tools."""
mock_response = Mock()
mock_response.json.return_value = mock_gateway_response
mock_response.status_code = 200
mock_request.return_value = mock_response
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
tools = toolkit.get_tools(exclude=["users.Users.Create"])
assert len(tools) == 2
names = [t.metadata.name for t in tools]
assert "users.Users.Create" not in names
@patch("requests.Session.request")
def test_get_tools_with_name_pattern(self, mock_request, mock_gateway_response):
"""Test filtering tools by name pattern."""
mock_response = Mock()
mock_response.json.return_value = mock_gateway_response
mock_response.status_code = 200
mock_request.return_value = mock_response
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
tools = toolkit.get_tools(name_pattern="blog\\..*")
assert len(tools) == 1
assert tools[0].metadata.name == "blog.Blog.List"
@patch("requests.Session.request")
def test_call_tool(self, mock_request):
"""Test calling a tool directly."""
mock_response = Mock()
mock_response.json.return_value = {"user": {"id": "user-123", "name": "Alice"}}
mock_response.status_code = 200
mock_request.return_value = mock_response
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
result = toolkit.call_tool("users.Users.Get", '{"id": "user-123"}')
result_data = json.loads(result)
assert result_data["user"]["id"] == "user-123"
@patch("requests.Session.request")
def test_list_tools(self, mock_request, mock_gateway_response):
"""Test listing raw tools."""
mock_response = Mock()
mock_response.json.return_value = mock_gateway_response
mock_response.status_code = 200
mock_request.return_value = mock_response
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
tools = toolkit.list_tools()
assert len(tools) == 3
assert tools[0].name == "users.Users.Get"
assert tools[0].service == "users"
assert tools[0].scopes == ["users:read"]
@patch("requests.Session.request")
def test_connection_error(self, mock_request):
"""Test handling connection errors."""
mock_request.side_effect = requests.ConnectionError("Connection failed")
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
with pytest.raises(GoMicroConnectionError):
toolkit.refresh()
@patch("requests.Session.request")
def test_auth_error(self, mock_request):
"""Test handling authentication errors."""
mock_response = Mock()
mock_response.status_code = 401
mock_request.return_value = mock_response
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
with pytest.raises(GoMicroAuthError):
toolkit.refresh()
@patch("requests.Session.request")
def test_timeout(self, mock_request):
"""Test handling timeouts."""
mock_request.side_effect = requests.Timeout("Request timed out")
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
with pytest.raises(GoMicroConnectionError):
toolkit.refresh()
+65
View File
@@ -0,0 +1,65 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
*.manifest
*.spec
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# IDEs
.vscode/
.idea/
*.swp
*.swo
*~
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Ruff
.ruff_cache/
+105
View File
@@ -0,0 +1,105 @@
# Contributing to LangChain Go Micro
Thank you for your interest in contributing to the LangChain Go Micro integration!
## Development Setup
1. Clone the repository:
```bash
git clone https://github.com/micro/go-micro
cd go-micro/contrib/langchain-go-micro
```
2. Create a virtual environment:
```bash
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
```
3. Install in development mode:
```bash
pip install -e ".[dev]"
```
## Running Tests
Run all tests:
```bash
pytest
```
Run with coverage:
```bash
pytest --cov=langchain_go_micro --cov-report=html
```
Run specific tests:
```bash
pytest tests/test_toolkit.py::TestGoMicroToolkit::test_get_tools
```
## Code Style
We use several tools to maintain code quality:
### Black (code formatting)
```bash
black langchain_go_micro tests examples
```
### MyPy (type checking)
```bash
mypy langchain_go_micro
```
### Ruff (linting)
```bash
ruff check langchain_go_micro tests
```
Run all checks:
```bash
black langchain_go_micro tests examples && \
mypy langchain_go_micro && \
ruff check langchain_go_micro tests
```
## Testing with Real Services
To test with real Go Micro services:
1. Start example services:
```bash
cd ../../examples/mcp/documented
go run main.go
```
2. Run integration tests:
```bash
cd contrib/langchain-go-micro
pytest tests/integration/ -v
```
## Submitting Changes
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/my-feature`)
3. Make your changes
4. Run tests and code quality checks
5. Commit your changes (`git commit -am 'Add new feature'`)
6. Push to your fork (`git push origin feature/my-feature`)
7. Create a Pull Request
## Pull Request Guidelines
- Include tests for new features
- Update documentation as needed
- Follow existing code style
- Add entry to CHANGELOG.md
- Ensure all tests pass
- Keep changes focused and atomic
## Questions?
- GitHub Discussions: https://github.com/micro/go-micro/discussions
- Discord: https://discord.gg/jwTYuUVAGh
+373
View File
@@ -0,0 +1,373 @@
# LangChain Go Micro Integration
[![PyPI version](https://badge.fury.io/py/langchain-go-micro.svg)](https://badge.fury.io/py/langchain-go-micro)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
Official LangChain integration for Go Micro services. This package enables LangChain agents to discover and call Go Micro microservices through the Model Context Protocol (MCP).
## Features
- 🔍 **Automatic Service Discovery** - Discovers available services from MCP gateway
- 🛠️ **Dynamic Tool Generation** - Converts service endpoints into LangChain tools
- 📝 **Rich Descriptions** - Uses service metadata for accurate tool descriptions
- 🔐 **Authentication Support** - Bearer token auth with scope-based permissions
-**Type-Safe** - Fully typed with Python 3.8+ type hints
- 🎯 **Easy Integration** - Works with any LangChain agent
## Installation
```bash
pip install langchain-go-micro
```
## Quick Start
### 1. Start Your Go Micro Services
```bash
# Start MCP gateway
micro mcp serve --address :3000
```
### 2. Create LangChain Agent
```python
from langchain_go_micro import GoMicroToolkit
from langchain.agents import initialize_agent, AgentType
from langchain_openai import ChatOpenAI
# Initialize toolkit from MCP gateway
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
# Create agent
llm = ChatOpenAI(model="gpt-4")
agent = initialize_agent(
toolkit.get_tools(),
llm,
agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
verbose=True
)
# Use the agent!
result = agent.run("Create a user named Alice with email alice@example.com")
print(result)
```
## Usage Examples
### Basic Tool Discovery
```python
from langchain_go_micro import GoMicroToolkit
# Connect to MCP gateway
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
# List available tools
for tool in toolkit.get_tools():
print(f"Tool: {tool.name}")
print(f"Description: {tool.description}")
print()
```
### Authentication
```python
from langchain_go_micro import GoMicroToolkit
# Create toolkit with authentication
toolkit = GoMicroToolkit.from_gateway(
gateway_url="http://localhost:3000",
auth_token="your-bearer-token"
)
# Tools will automatically use the auth token
tools = toolkit.get_tools()
```
### Filter Tools by Service
```python
from langchain_go_micro import GoMicroToolkit
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
# Get only user service tools
user_tools = toolkit.get_tools(service_filter="users")
# Get tools matching a pattern
blog_tools = toolkit.get_tools(name_pattern="blog.*")
```
### Custom Tool Selection
```python
from langchain_go_micro import GoMicroToolkit
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
# Select specific tools
selected_tools = toolkit.get_tools(
include=["users.Users.Get", "users.Users.Create"]
)
# Exclude certain tools
filtered_tools = toolkit.get_tools(
exclude=["users.Users.Delete"]
)
```
### Multi-Agent Workflows
```python
from langchain_go_micro import GoMicroToolkit
from langchain.agents import initialize_agent, AgentType
from langchain_openai import ChatOpenAI
# Create specialized agents for different services
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
# Agent 1: User management
user_agent = initialize_agent(
toolkit.get_tools(service_filter="users"),
ChatOpenAI(model="gpt-4"),
agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION
)
# Agent 2: Order processing
order_agent = initialize_agent(
toolkit.get_tools(service_filter="orders"),
ChatOpenAI(model="gpt-4"),
agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION
)
# Coordinate between agents
user = user_agent.run("Create user Alice")
order = order_agent.run(f"Create order for user {user['id']}")
```
### Error Handling
```python
from langchain_go_micro import GoMicroToolkit, GoMicroError
try:
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
tools = toolkit.get_tools()
except GoMicroError as e:
print(f"Error: {e}")
# Handle error (gateway unreachable, auth failed, etc.)
```
### Advanced Configuration
```python
from langchain_go_micro import GoMicroToolkit, GoMicroConfig
config = GoMicroConfig(
gateway_url="http://localhost:3000",
auth_token="your-token",
timeout=30, # Request timeout in seconds
retry_count=3, # Number of retries on failure
retry_delay=1.0, # Delay between retries
verify_ssl=True, # SSL certificate verification
)
toolkit = GoMicroToolkit(config)
tools = toolkit.get_tools()
```
## API Reference
### GoMicroToolkit
Main class for interacting with Go Micro services.
#### Methods
- `from_gateway(gateway_url, auth_token=None, **kwargs)` - Create toolkit from MCP gateway
- `get_tools(service_filter=None, name_pattern=None, include=None, exclude=None)` - Get LangChain tools
- `refresh()` - Refresh tool list from gateway
- `call_tool(tool_name, arguments)` - Call a tool directly
### GoMicroConfig
Configuration for the toolkit.
#### Parameters
- `gateway_url` (str) - MCP gateway URL
- `auth_token` (str, optional) - Bearer authentication token
- `timeout` (int) - Request timeout in seconds (default: 30)
- `retry_count` (int) - Number of retries (default: 3)
- `retry_delay` (float) - Delay between retries in seconds (default: 1.0)
- `verify_ssl` (bool) - Verify SSL certificates (default: True)
## Integration with LangChain Components
### With LangChain Agents
```python
from langchain_go_micro import GoMicroToolkit
from langchain.agents import initialize_agent, AgentType
from langchain_openai import ChatOpenAI
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
llm = ChatOpenAI(model="gpt-4")
agent = initialize_agent(
toolkit.get_tools(),
llm,
agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
verbose=True
)
```
### With LangChain Memory
```python
from langchain_go_micro import GoMicroToolkit
from langchain.agents import initialize_agent, AgentType
from langchain_openai import ChatOpenAI
from langchain.memory import ConversationBufferMemory
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
memory = ConversationBufferMemory(memory_key="chat_history")
agent = initialize_agent(
toolkit.get_tools(),
ChatOpenAI(model="gpt-4"),
agent=AgentType.CONVERSATIONAL_REACT_DESCRIPTION,
memory=memory,
verbose=True
)
```
### With Custom LLMs
```python
from langchain_go_micro import GoMicroToolkit
from langchain.agents import initialize_agent, AgentType
from langchain_anthropic import ChatAnthropic
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
# Use Claude instead of GPT
agent = initialize_agent(
toolkit.get_tools(),
ChatAnthropic(model="claude-3-sonnet-20240229"),
agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
verbose=True
)
```
## Requirements
- Python 3.8+
- LangChain >= 0.1.0
- requests >= 2.31.0
## Development
### Setup
```bash
git clone https://github.com/micro/go-micro
cd go-micro/contrib/langchain-go-micro
# Create virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install in development mode
pip install -e ".[dev]"
```
### Running Tests
```bash
# Run all tests
pytest
# Run with coverage
pytest --cov=langchain_go_micro
# Run specific test
pytest tests/test_toolkit.py
```
### Code Formatting
```bash
# Format code
black langchain_go_micro tests
# Check types
mypy langchain_go_micro
# Lint
ruff check langchain_go_micro
```
## Examples
See the [examples](./examples) directory for complete examples:
- [basic_agent.py](./examples/basic_agent.py) - Simple agent example
- [multi_agent.py](./examples/multi_agent.py) - Multi-agent workflow
- [with_memory.py](./examples/with_memory.py) - Agent with conversation memory
- [custom_llm.py](./examples/custom_llm.py) - Using different LLMs
## Troubleshooting
### Gateway Connection Issues
If you can't connect to the MCP gateway:
1. Verify the gateway is running:
```bash
curl http://localhost:3000/health
```
2. Check the gateway URL is correct
3. Verify firewall settings
### Authentication Errors
If you get authentication errors:
1. Verify your token is valid
2. Check the token has required scopes
3. Review gateway logs for details
### Tool Discovery Issues
If tools aren't being discovered:
1. List services from gateway:
```bash
curl http://localhost:3000/mcp/tools
```
2. Verify services are registered
3. Check service metadata is correct
## Contributing
Contributions are welcome! Please see [CONTRIBUTING.md](../../CONTRIBUTING.md) for details.
## License
Apache 2.0 - See [LICENSE](../../LICENSE) for details.
## Links
- [Go Micro](https://github.com/micro/go-micro)
- [MCP Documentation](../../gateway/mcp/DOCUMENTATION.md)
- [LangChain](https://python.langchain.com/)
- [Issue Tracker](https://github.com/micro/go-micro/issues)
## Support
- GitHub Discussions: https://github.com/micro/go-micro/discussions
- Discord: https://discord.gg/jwTYuUVAGh
@@ -0,0 +1,49 @@
"""Basic LangChain agent example using Go Micro services.
This example shows how to create a simple LangChain agent that can
interact with Go Micro services through the MCP gateway.
"""
from langchain_go_micro import GoMicroToolkit
from langchain.agents import initialize_agent, AgentType
from langchain_openai import ChatOpenAI
def main():
"""Run basic agent example."""
# Initialize toolkit from MCP gateway
print("Connecting to MCP gateway...")
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
# Get available tools
tools = toolkit.get_tools()
print(f"\nDiscovered {len(tools)} tools:")
for tool in tools:
print(f" - {tool.name}: {tool.description}")
# Create LangChain agent
print("\nCreating LangChain agent...")
llm = ChatOpenAI(model="gpt-4", temperature=0)
agent = initialize_agent(
tools,
llm,
agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
verbose=True
)
# Example queries
queries = [
"Create a user named Alice with email alice@example.com",
"Get the user we just created",
]
for query in queries:
print(f"\n{'='*60}")
print(f"Query: {query}")
print('='*60)
result = agent.run(query)
print(f"\nResult: {result}")
if __name__ == "__main__":
main()
@@ -0,0 +1,70 @@
"""Multi-agent workflow example.
This example demonstrates how to create specialized agents for different
services and coordinate between them.
"""
from langchain_go_micro import GoMicroToolkit
from langchain.agents import initialize_agent, AgentType
from langchain_openai import ChatOpenAI
def main():
"""Run multi-agent example."""
# Connect to MCP gateway
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
# Create LLM
llm = ChatOpenAI(model="gpt-4", temperature=0)
# Create specialized agents for different services
print("Creating specialized agents...")
# Agent 1: User management
user_tools = toolkit.get_tools(service_filter="users")
user_agent = initialize_agent(
user_tools,
llm,
agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
verbose=True
)
print(f"User agent: {len(user_tools)} tools")
# Agent 2: Blog management
blog_tools = toolkit.get_tools(service_filter="blog")
blog_agent = initialize_agent(
blog_tools,
llm,
agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
verbose=True
)
print(f"Blog agent: {len(blog_tools)} tools")
# Coordinate between agents
print("\n" + "="*60)
print("Multi-agent workflow")
print("="*60)
# Step 1: Create a user
print("\nStep 1: Creating user...")
user_result = user_agent.run(
"Create a user named Bob Smith with email bob@example.com"
)
print(f"User created: {user_result}")
# Step 2: Create a blog post for that user
print("\nStep 2: Creating blog post...")
blog_result = blog_agent.run(
f"Create a blog post titled 'Hello World' with content "
f"'This is my first post' by user {user_result}"
)
print(f"Blog post created: {blog_result}")
# Step 3: List user's posts
print("\nStep 3: Listing user's posts...")
posts = blog_agent.run(f"List all blog posts by {user_result}")
print(f"User's posts: {posts}")
if __name__ == "__main__":
main()
@@ -0,0 +1,17 @@
"""LangChain Go Micro Integration.
This package provides LangChain integration for Go Micro services through
the Model Context Protocol (MCP).
"""
from langchain_go_micro.toolkit import GoMicroToolkit, GoMicroConfig
from langchain_go_micro.exceptions import GoMicroError, GoMicroConnectionError, GoMicroAuthError
__version__ = "0.1.0"
__all__ = [
"GoMicroToolkit",
"GoMicroConfig",
"GoMicroError",
"GoMicroConnectionError",
"GoMicroAuthError",
]
@@ -0,0 +1,21 @@
"""Custom exceptions for LangChain Go Micro integration."""
class GoMicroError(Exception):
"""Base exception for Go Micro integration errors."""
pass
class GoMicroConnectionError(GoMicroError):
"""Raised when unable to connect to MCP gateway."""
pass
class GoMicroAuthError(GoMicroError):
"""Raised when authentication fails."""
pass
class GoMicroToolError(GoMicroError):
"""Raised when tool execution fails."""
pass
@@ -0,0 +1,319 @@
"""LangChain toolkit for Go Micro services."""
import json
import re
from typing import Any, Dict, List, Optional, Callable
from dataclasses import dataclass
import requests
from langchain.tools import Tool
from pydantic import BaseModel, Field
from langchain_go_micro.exceptions import (
GoMicroConnectionError,
GoMicroAuthError,
GoMicroToolError,
)
@dataclass
class GoMicroConfig:
"""Configuration for Go Micro MCP gateway connection.
Attributes:
gateway_url: URL of the MCP gateway (e.g., http://localhost:3000)
auth_token: Optional bearer authentication token
timeout: Request timeout in seconds
retry_count: Number of retries on failure
retry_delay: Delay between retries in seconds
verify_ssl: Whether to verify SSL certificates
"""
gateway_url: str
auth_token: Optional[str] = None
timeout: int = 30
retry_count: int = 3
retry_delay: float = 1.0
verify_ssl: bool = True
class GoMicroTool(BaseModel):
"""Represents a Go Micro service tool.
Attributes:
name: Tool name (e.g., "users.Users.Get")
service: Service name (e.g., "users")
endpoint: Endpoint name (e.g., "Users.Get")
description: Tool description
example: Example input JSON
scopes: Required auth scopes
metadata: Additional metadata from service
"""
name: str
service: str
endpoint: str
description: str
example: Optional[str] = None
scopes: Optional[List[str]] = None
metadata: Dict[str, str] = Field(default_factory=dict)
class GoMicroToolkit:
"""LangChain toolkit for Go Micro services.
This class provides integration between LangChain and Go Micro services
via the Model Context Protocol (MCP) gateway.
Example:
>>> toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
>>> tools = toolkit.get_tools()
>>> for tool in tools:
... print(f"Tool: {tool.name}")
"""
def __init__(self, config: GoMicroConfig):
"""Initialize the toolkit.
Args:
config: Configuration for MCP gateway connection
"""
self.config = config
self._tools: Optional[List[GoMicroTool]] = None
self._session = requests.Session()
# Set up authentication
if config.auth_token:
self._session.headers.update({
"Authorization": f"Bearer {config.auth_token}"
})
@classmethod
def from_gateway(
cls,
gateway_url: str,
auth_token: Optional[str] = None,
**kwargs: Any
) -> "GoMicroToolkit":
"""Create toolkit from MCP gateway URL.
Args:
gateway_url: URL of the MCP gateway
auth_token: Optional bearer authentication token
**kwargs: Additional configuration options
Returns:
GoMicroToolkit instance
Example:
>>> toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
"""
config = GoMicroConfig(
gateway_url=gateway_url,
auth_token=auth_token,
**kwargs
)
return cls(config)
def _make_request(
self,
method: str,
path: str,
**kwargs: Any
) -> requests.Response:
"""Make HTTP request to MCP gateway.
Args:
method: HTTP method (GET, POST, etc.)
path: API path
**kwargs: Additional request arguments
Returns:
Response object
Raises:
GoMicroConnectionError: If connection fails
GoMicroAuthError: If authentication fails
"""
url = f"{self.config.gateway_url}{path}"
kwargs.setdefault("timeout", self.config.timeout)
kwargs.setdefault("verify", self.config.verify_ssl)
try:
response = self._session.request(method, url, **kwargs)
if response.status_code == 401:
raise GoMicroAuthError("Authentication failed")
elif response.status_code == 403:
raise GoMicroAuthError("Forbidden: insufficient permissions")
response.raise_for_status()
return response
except requests.ConnectionError as e:
raise GoMicroConnectionError(
f"Failed to connect to MCP gateway at {url}: {e}"
)
except requests.Timeout as e:
raise GoMicroConnectionError(
f"Request to MCP gateway timed out: {e}"
)
except requests.RequestException as e:
if isinstance(e, (GoMicroConnectionError, GoMicroAuthError)):
raise
raise GoMicroConnectionError(f"Request failed: {e}")
def refresh(self) -> None:
"""Refresh tool list from MCP gateway.
Raises:
GoMicroConnectionError: If unable to connect to gateway
"""
response = self._make_request("GET", "/mcp/tools")
data = response.json()
tools_data = data.get("tools", [])
self._tools = [
GoMicroTool(
name=tool["name"],
service=tool["service"],
endpoint=tool["endpoint"],
description=tool.get("description", ""),
example=tool.get("example"),
scopes=tool.get("scopes"),
metadata=tool.get("metadata", {})
)
for tool in tools_data
]
def get_tools(
self,
service_filter: Optional[str] = None,
name_pattern: Optional[str] = None,
include: Optional[List[str]] = None,
exclude: Optional[List[str]] = None,
) -> List[Tool]:
"""Get LangChain tools from Go Micro services.
Args:
service_filter: Filter tools by service name
name_pattern: Filter tools by name pattern (regex)
include: List of tool names to include
exclude: List of tool names to exclude
Returns:
List of LangChain Tool objects
Example:
>>> toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
>>> # Get all tools
>>> all_tools = toolkit.get_tools()
>>> # Get only user service tools
>>> user_tools = toolkit.get_tools(service_filter="users")
>>> # Get specific tools
>>> selected_tools = toolkit.get_tools(include=["users.Users.Get"])
"""
if self._tools is None:
self.refresh()
tools = self._tools or []
# Apply filters
if service_filter:
tools = [t for t in tools if t.service == service_filter]
if name_pattern:
pattern = re.compile(name_pattern)
tools = [t for t in tools if pattern.match(t.name)]
if include:
tools = [t for t in tools if t.name in include]
if exclude:
tools = [t for t in tools if t.name not in exclude]
# Convert to LangChain tools
return [self._create_langchain_tool(tool) for tool in tools]
def _create_langchain_tool(self, tool: GoMicroTool) -> Tool:
"""Create a LangChain Tool from a GoMicroTool.
Args:
tool: GoMicroTool to convert
Returns:
LangChain Tool object
"""
def tool_func(arguments: str) -> str:
"""Execute the tool.
Args:
arguments: JSON string with tool arguments
Returns:
JSON string with tool result
"""
return self.call_tool(tool.name, arguments)
# Build description with example if available
description = tool.description
if tool.example:
description += f"\n\nExample input: {tool.example}"
return Tool(
name=tool.name,
func=tool_func,
description=description,
)
def call_tool(self, tool_name: str, arguments: str) -> str:
"""Call a specific tool directly.
Args:
tool_name: Name of the tool to call
arguments: JSON string with tool arguments
Returns:
JSON string with tool result
Raises:
GoMicroToolError: If tool execution fails
Example:
>>> toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
>>> result = toolkit.call_tool(
... "users.Users.Get",
... '{"id": "user-123"}'
... )
"""
# Parse arguments
try:
args = json.loads(arguments) if isinstance(arguments, str) else arguments
except json.JSONDecodeError as e:
raise GoMicroToolError(f"Invalid JSON arguments: {e}")
# Make request
try:
response = self._make_request(
"POST",
"/mcp/call",
json={"name": tool_name, "arguments": args}
)
return json.dumps(response.json())
except requests.RequestException as e:
raise GoMicroToolError(f"Tool execution failed: {e}")
def list_tools(self) -> List[GoMicroTool]:
"""Get raw list of available tools.
Returns:
List of GoMicroTool objects
Example:
>>> toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
>>> for tool in toolkit.list_tools():
... print(f"{tool.name}: {tool.description}")
"""
if self._tools is None:
self.refresh()
return self._tools or []
+73
View File
@@ -0,0 +1,73 @@
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "langchain-go-micro"
version = "0.1.0"
description = "LangChain integration for Go Micro services via MCP"
readme = "README.md"
requires-python = ">=3.8"
license = {text = "Apache-2.0"}
authors = [
{name = "Micro Team", email = "hello@micro.dev"}
]
keywords = ["langchain", "go-micro", "mcp", "microservices", "ai", "agents"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Software Development :: Libraries :: Python Modules",
]
dependencies = [
"langchain>=0.1.0",
"requests>=2.31.0",
"pydantic>=2.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
"black>=23.0.0",
"mypy>=1.0.0",
"ruff>=0.1.0",
"types-requests>=2.31.0",
]
[project.urls]
Homepage = "https://github.com/micro/go-micro"
Documentation = "https://github.com/micro/go-micro/tree/master/contrib/langchain-go-micro"
Repository = "https://github.com/micro/go-micro"
Issues = "https://github.com/micro/go-micro/issues"
[tool.setuptools.packages.find]
where = ["."]
include = ["langchain_go_micro*"]
[tool.black]
line-length = 88
target-version = ['py38', 'py39', 'py310', 'py311']
[tool.mypy]
python_version = "3.8"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
[tool.ruff]
line-length = 88
target-version = "py38"
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
@@ -0,0 +1,219 @@
"""Tests for GoMicroToolkit."""
import json
from unittest.mock import Mock, patch
import pytest
import requests
from langchain_go_micro import GoMicroToolkit, GoMicroConfig
from langchain_go_micro.exceptions import (
GoMicroConnectionError,
GoMicroAuthError,
)
@pytest.fixture
def mock_gateway_response():
"""Mock MCP gateway response."""
return {
"tools": [
{
"name": "users.Users.Get",
"service": "users",
"endpoint": "Users.Get",
"description": "Get a user by ID",
"example": '{"id": "user-123"}',
"scopes": ["users:read"],
"metadata": {
"description": "Get a user by ID",
"example": '{"id": "user-123"}',
"scopes": "users:read"
}
},
{
"name": "users.Users.Create",
"service": "users",
"endpoint": "Users.Create",
"description": "Create a new user",
"example": '{"name": "Alice", "email": "alice@example.com"}',
"scopes": ["users:write"],
"metadata": {}
}
],
"count": 2
}
class TestGoMicroConfig:
"""Tests for GoMicroConfig."""
def test_config_defaults(self):
"""Test config default values."""
config = GoMicroConfig(gateway_url="http://localhost:3000")
assert config.gateway_url == "http://localhost:3000"
assert config.auth_token is None
assert config.timeout == 30
assert config.retry_count == 3
assert config.retry_delay == 1.0
assert config.verify_ssl is True
def test_config_custom_values(self):
"""Test config with custom values."""
config = GoMicroConfig(
gateway_url="http://localhost:8080",
auth_token="test-token",
timeout=60,
retry_count=5,
retry_delay=2.0,
verify_ssl=False
)
assert config.gateway_url == "http://localhost:8080"
assert config.auth_token == "test-token"
assert config.timeout == 60
assert config.retry_count == 5
assert config.retry_delay == 2.0
assert config.verify_ssl is False
class TestGoMicroToolkit:
"""Tests for GoMicroToolkit."""
def test_from_gateway(self):
"""Test creating toolkit from gateway URL."""
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
assert toolkit.config.gateway_url == "http://localhost:3000"
assert toolkit.config.auth_token is None
def test_from_gateway_with_auth(self):
"""Test creating toolkit with authentication."""
toolkit = GoMicroToolkit.from_gateway(
"http://localhost:3000",
auth_token="test-token"
)
assert toolkit.config.auth_token == "test-token"
assert "Authorization" in toolkit._session.headers
assert toolkit._session.headers["Authorization"] == "Bearer test-token"
@patch("requests.Session.request")
def test_refresh(self, mock_request, mock_gateway_response):
"""Test refreshing tool list."""
mock_response = Mock()
mock_response.json.return_value = mock_gateway_response
mock_response.status_code = 200
mock_request.return_value = mock_response
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
toolkit.refresh()
assert len(toolkit._tools) == 2
assert toolkit._tools[0].name == "users.Users.Get"
assert toolkit._tools[1].name == "users.Users.Create"
@patch("requests.Session.request")
def test_get_tools(self, mock_request, mock_gateway_response):
"""Test getting LangChain tools."""
mock_response = Mock()
mock_response.json.return_value = mock_gateway_response
mock_response.status_code = 200
mock_request.return_value = mock_response
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
tools = toolkit.get_tools()
assert len(tools) == 2
assert tools[0].name == "users.Users.Get"
assert tools[1].name == "users.Users.Create"
@patch("requests.Session.request")
def test_get_tools_with_service_filter(self, mock_request, mock_gateway_response):
"""Test filtering tools by service."""
mock_response = Mock()
mock_response.json.return_value = mock_gateway_response
mock_response.status_code = 200
mock_request.return_value = mock_response
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
tools = toolkit.get_tools(service_filter="users")
assert len(tools) == 2
for tool in tools:
assert "users" in tool.name
@patch("requests.Session.request")
def test_get_tools_with_include(self, mock_request, mock_gateway_response):
"""Test including specific tools."""
mock_response = Mock()
mock_response.json.return_value = mock_gateway_response
mock_response.status_code = 200
mock_request.return_value = mock_response
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
tools = toolkit.get_tools(include=["users.Users.Get"])
assert len(tools) == 1
assert tools[0].name == "users.Users.Get"
@patch("requests.Session.request")
def test_get_tools_with_exclude(self, mock_request, mock_gateway_response):
"""Test excluding specific tools."""
mock_response = Mock()
mock_response.json.return_value = mock_gateway_response
mock_response.status_code = 200
mock_request.return_value = mock_response
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
tools = toolkit.get_tools(exclude=["users.Users.Create"])
assert len(tools) == 1
assert tools[0].name == "users.Users.Get"
@patch("requests.Session.request")
def test_call_tool(self, mock_request):
"""Test calling a tool directly."""
mock_response = Mock()
mock_response.json.return_value = {"user": {"id": "user-123", "name": "Alice"}}
mock_response.status_code = 200
mock_request.return_value = mock_response
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
result = toolkit.call_tool("users.Users.Get", '{"id": "user-123"}')
result_data = json.loads(result)
assert result_data["user"]["id"] == "user-123"
@patch("requests.Session.request")
def test_connection_error(self, mock_request):
"""Test handling connection errors."""
mock_request.side_effect = requests.ConnectionError("Connection failed")
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
with pytest.raises(GoMicroConnectionError):
toolkit.refresh()
@patch("requests.Session.request")
def test_auth_error(self, mock_request):
"""Test handling authentication errors."""
mock_response = Mock()
mock_response.status_code = 401
mock_request.return_value = mock_response
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
with pytest.raises(GoMicroAuthError):
toolkit.refresh()
@patch("requests.Session.request")
def test_timeout(self, mock_request):
"""Test handling timeouts."""
mock_request.side_effect = requests.Timeout("Request timed out")
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
with pytest.raises(GoMicroConnectionError):
toolkit.refresh()
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"fmt"
"go-micro.dev/v5/debug/log"
"go-micro.dev/v5/util/ring"
"go-micro.dev/v5/internal/util/ring"
)
var (
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"sync"
"github.com/google/uuid"
"go-micro.dev/v5/util/ring"
"go-micro.dev/v5/internal/util/ring"
)
// Should stream from OS.
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"sync"
"time"
"go-micro.dev/v5/util/ring"
"go-micro.dev/v5/internal/util/ring"
)
type stats struct {
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"time"
"github.com/google/uuid"
"go-micro.dev/v5/util/ring"
"go-micro.dev/v5/internal/util/ring"
)
type memTracer struct {
+30 -3
View File
@@ -34,13 +34,40 @@ cd web-service
go run .
```
## Coming Soon
### [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
The following examples are planned:
**Run it:**
```bash
cd multi-service
go run .
```
### [deployment](./deployment/)
Docker Compose deployment with MCP gateway, Consul registry, and Jaeger tracing:
- Production-like architecture in one `docker-compose up`
- Standalone MCP gateway connected to service registry
- Distributed tracing with OpenTelemetry + Jaeger
### MCP Examples
See the [mcp/](./mcp/) directory for AI agent integration examples:
- **[hello](./mcp/hello/)** - Minimal MCP service (start here)
- **[crud](./mcp/crud/)** - CRUD contact book with full agent documentation
- **[workflow](./mcp/workflow/)** - Cross-service orchestration via AI agents
- **[documented](./mcp/documented/)** - All MCP features with auth scopes
### [agent-demo](./agent-demo/)
Multi-service project management app (Projects, Tasks, Team) with seed data and agent playground integration.
## Coming Soon
- **pubsub-events** - Event-driven architecture with NATS
- **grpc-integration** - Using go-micro with gRPC
- **production-ready** - Complete production-grade service with observability
## Prerequisites
+63
View File
@@ -0,0 +1,63 @@
# Agent Demo
A multi-service project management app that demonstrates AI agents interacting with Go Micro services through MCP.
## What's Included
Three services registered in a single process:
| Service | Endpoints | Description |
|---------|-----------|-------------|
| **ProjectService** | Create, Get, List | Manage projects with status tracking |
| **TaskService** | Create, List, Update | Tasks with assignees, priorities, and status |
| **TeamService** | Add, List, Get | Team members with roles and skills |
The demo starts with seed data: 2 projects, 7 tasks, and 4 team members.
## Run
```bash
go run main.go
```
Endpoints:
- **MCP Gateway:** http://localhost:3000
- **MCP Tools:** http://localhost:3000/mcp/tools
- **WebSocket:** ws://localhost:3000/mcp/ws
## Use with Claude Code
```json
{
"mcpServers": {
"demo": {
"command": "go",
"args": ["run", "main.go"],
"cwd": "examples/agent-demo"
}
}
}
```
## Example Prompts
Try these with Claude Code or any MCP client:
- "What projects do we have?"
- "Show me all tasks assigned to alice"
- "Create a high-priority task for bob to review the design mockups"
- "Who on the team knows Go?"
- "Give me a status update on the Website Redesign project"
- "What tasks are still todo on the API v2 migration?"
- "Assign the unassigned tasks to charlie"
- "Mark task-1 as done"
## What This Demonstrates
1. **Zero-config MCP** — Services become AI tools automatically from doc comments
2. **Cross-service orchestration** — An agent queries projects, tasks, and team in one conversation
3. **Rich tool descriptions**`description` struct tags and `@example` comments guide the agent
4. **Auth scopes** — Read and write operations have separate scopes
5. **`WithMCP` one-liner** — MCP gateway starts with a single option
See the [blog post](/blog/4) for a detailed walkthrough.
+454
View File
@@ -0,0 +1,454 @@
// Agent Demo — A multi-service project management app
//
// This example shows three Go Micro services (projects, tasks, team)
// working together through the MCP gateway, letting an AI agent
// manage projects using natural language.
//
// Run:
//
// go run main.go
//
// Then open the agent playground at http://localhost:8080/agent
// or connect Claude Code via: micro mcp serve
package main
import (
"context"
"fmt"
"strings"
"sync"
"time"
"go-micro.dev/v5"
"go-micro.dev/v5/gateway/mcp"
"go-micro.dev/v5/server"
)
// ---------------------------------------------------------------------------
// Projects service
// ---------------------------------------------------------------------------
type Project struct {
ID string `json:"id" description:"Unique project identifier"`
Name string `json:"name" description:"Project name"`
Description string `json:"description" description:"What the project is about"`
Status string `json:"status" description:"Project status: planning, active, or completed"`
CreatedAt time.Time `json:"created_at" description:"When the project was created"`
}
type CreateProjectRequest struct {
Name string `json:"name" description:"Project name (required)"`
Description string `json:"description" description:"Short description of the project"`
}
type CreateProjectResponse struct {
Project *Project `json:"project" description:"The newly created project"`
}
type GetProjectRequest struct {
ID string `json:"id" description:"Project ID to retrieve"`
}
type GetProjectResponse struct {
Project *Project `json:"project" description:"The requested project"`
}
type ListProjectsRequest struct {
Status string `json:"status,omitempty" description:"Filter by status: planning, active, completed (optional)"`
}
type ListProjectsResponse struct {
Projects []*Project `json:"projects" description:"List of matching projects"`
}
type ProjectService struct {
mu sync.RWMutex
projects map[string]*Project
nextID int
}
// Create creates a new project with the given name and description.
// Returns the project with a generated ID and initial status of "planning".
//
// @example {"name": "Website Redesign", "description": "Redesign the company website with new branding"}
func (s *ProjectService) Create(ctx context.Context, req *CreateProjectRequest, rsp *CreateProjectResponse) error {
s.mu.Lock()
defer s.mu.Unlock()
s.nextID++
p := &Project{
ID: fmt.Sprintf("proj-%d", s.nextID),
Name: req.Name,
Description: req.Description,
Status: "planning",
CreatedAt: time.Now(),
}
s.projects[p.ID] = p
rsp.Project = p
return nil
}
// Get retrieves a project by ID.
// Returns an error if the project does not exist.
//
// @example {"id": "proj-1"}
func (s *ProjectService) Get(ctx context.Context, req *GetProjectRequest, rsp *GetProjectResponse) error {
s.mu.RLock()
defer s.mu.RUnlock()
p, ok := s.projects[req.ID]
if !ok {
return fmt.Errorf("project %s not found", req.ID)
}
rsp.Project = p
return nil
}
// List returns all projects, optionally filtered by status.
// Valid status values: planning, active, completed.
//
// @example {"status": "active"}
func (s *ProjectService) List(ctx context.Context, req *ListProjectsRequest, rsp *ListProjectsResponse) error {
s.mu.RLock()
defer s.mu.RUnlock()
for _, p := range s.projects {
if req.Status == "" || p.Status == req.Status {
rsp.Projects = append(rsp.Projects, p)
}
}
return nil
}
// ---------------------------------------------------------------------------
// Tasks service
// ---------------------------------------------------------------------------
type Task struct {
ID string `json:"id" description:"Unique task identifier"`
ProjectID string `json:"project_id" description:"ID of the project this task belongs to"`
Title string `json:"title" description:"Short task title"`
Status string `json:"status" description:"Task status: todo, in_progress, or done"`
Assignee string `json:"assignee,omitempty" description:"Username of the person assigned"`
Priority string `json:"priority" description:"Priority: low, medium, or high"`
}
type CreateTaskRequest struct {
ProjectID string `json:"project_id" description:"Project ID to add the task to (required)"`
Title string `json:"title" description:"Task title (required)"`
Assignee string `json:"assignee,omitempty" description:"Username to assign (optional)"`
Priority string `json:"priority,omitempty" description:"Priority: low, medium, or high (default: medium)"`
}
type CreateTaskResponse struct {
Task *Task `json:"task" description:"The newly created task"`
}
type ListTasksRequest struct {
ProjectID string `json:"project_id,omitempty" description:"Filter by project ID (optional)"`
Assignee string `json:"assignee,omitempty" description:"Filter by assignee username (optional)"`
Status string `json:"status,omitempty" description:"Filter by status: todo, in_progress, done (optional)"`
}
type ListTasksResponse struct {
Tasks []*Task `json:"tasks" description:"List of matching tasks"`
}
type UpdateTaskRequest struct {
ID string `json:"id" description:"Task ID to update"`
Status string `json:"status,omitempty" description:"New status: todo, in_progress, or done"`
Assignee string `json:"assignee,omitempty" description:"New assignee username"`
}
type UpdateTaskResponse struct {
Task *Task `json:"task" description:"The updated task"`
}
type TaskService struct {
mu sync.RWMutex
tasks map[string]*Task
nextID int
}
// Create creates a new task in a project.
// Returns the task with a generated ID, initial status of "todo", and default priority of "medium".
//
// @example {"project_id": "proj-1", "title": "Design homepage mockup", "assignee": "alice", "priority": "high"}
func (s *TaskService) Create(ctx context.Context, req *CreateTaskRequest, rsp *CreateTaskResponse) error {
s.mu.Lock()
defer s.mu.Unlock()
s.nextID++
priority := req.Priority
if priority == "" {
priority = "medium"
}
t := &Task{
ID: fmt.Sprintf("task-%d", s.nextID),
ProjectID: req.ProjectID,
Title: req.Title,
Status: "todo",
Assignee: req.Assignee,
Priority: priority,
}
s.tasks[t.ID] = t
rsp.Task = t
return nil
}
// List returns tasks filtered by project, assignee, or status.
// All filters are optional; omit all to list every task.
//
// @example {"project_id": "proj-1", "status": "todo"}
func (s *TaskService) List(ctx context.Context, req *ListTasksRequest, rsp *ListTasksResponse) error {
s.mu.RLock()
defer s.mu.RUnlock()
for _, t := range s.tasks {
if req.ProjectID != "" && t.ProjectID != req.ProjectID {
continue
}
if req.Assignee != "" && t.Assignee != req.Assignee {
continue
}
if req.Status != "" && t.Status != req.Status {
continue
}
rsp.Tasks = append(rsp.Tasks, t)
}
return nil
}
// Update modifies a task's status or assignee.
// Only provided fields are changed; omitted fields stay the same.
// Returns an error if the task does not exist.
//
// @example {"id": "task-1", "status": "in_progress"}
func (s *TaskService) Update(ctx context.Context, req *UpdateTaskRequest, rsp *UpdateTaskResponse) error {
s.mu.Lock()
defer s.mu.Unlock()
t, ok := s.tasks[req.ID]
if !ok {
return fmt.Errorf("task %s not found", req.ID)
}
if req.Status != "" {
t.Status = req.Status
}
if req.Assignee != "" {
t.Assignee = req.Assignee
}
rsp.Task = t
return nil
}
// ---------------------------------------------------------------------------
// Team service
// ---------------------------------------------------------------------------
type Member struct {
Username string `json:"username" description:"Unique username"`
Name string `json:"name" description:"Display name"`
Role string `json:"role" description:"Role: engineer, designer, or manager"`
Skills []string `json:"skills" description:"List of skills (e.g. go, react, figma)"`
}
type AddMemberRequest struct {
Username string `json:"username" description:"Unique username (required)"`
Name string `json:"name" description:"Display name (required)"`
Role string `json:"role" description:"Role: engineer, designer, or manager"`
Skills []string `json:"skills,omitempty" description:"List of skills"`
}
type AddMemberResponse struct {
Member *Member `json:"member" description:"The added team member"`
}
type ListMembersRequest struct {
Role string `json:"role,omitempty" description:"Filter by role: engineer, designer, manager (optional)"`
Skill string `json:"skill,omitempty" description:"Filter by skill (optional, e.g. 'go' or 'react')"`
}
type ListMembersResponse struct {
Members []*Member `json:"members" description:"List of matching team members"`
}
type GetMemberRequest struct {
Username string `json:"username" description:"Username to look up"`
}
type GetMemberResponse struct {
Member *Member `json:"member" description:"The team member"`
}
type TeamService struct {
mu sync.RWMutex
members map[string]*Member
}
// Add adds a new team member.
// Returns the member with their assigned role and skills.
//
// @example {"username": "alice", "name": "Alice Chen", "role": "engineer", "skills": ["go", "react"]}
func (s *TeamService) Add(ctx context.Context, req *AddMemberRequest, rsp *AddMemberResponse) error {
s.mu.Lock()
defer s.mu.Unlock()
m := &Member{
Username: req.Username,
Name: req.Name,
Role: req.Role,
Skills: req.Skills,
}
s.members[m.Username] = m
rsp.Member = m
return nil
}
// List returns team members, optionally filtered by role or skill.
//
// @example {"role": "engineer"}
func (s *TeamService) List(ctx context.Context, req *ListMembersRequest, rsp *ListMembersResponse) error {
s.mu.RLock()
defer s.mu.RUnlock()
for _, m := range s.members {
if req.Role != "" && m.Role != req.Role {
continue
}
if req.Skill != "" && !hasSkill(m.Skills, req.Skill) {
continue
}
rsp.Members = append(rsp.Members, m)
}
return nil
}
// Get retrieves a team member by username.
// Returns an error if the member does not exist.
//
// @example {"username": "alice"}
func (s *TeamService) Get(ctx context.Context, req *GetMemberRequest, rsp *GetMemberResponse) error {
s.mu.RLock()
defer s.mu.RUnlock()
m, ok := s.members[req.Username]
if !ok {
return fmt.Errorf("member %s not found", req.Username)
}
rsp.Member = m
return nil
}
func hasSkill(skills []string, target string) bool {
for _, s := range skills {
if strings.EqualFold(s, target) {
return true
}
}
return false
}
// ---------------------------------------------------------------------------
// Main — wire everything together
// ---------------------------------------------------------------------------
func main() {
// Create the service
service := micro.New("demo",
micro.Address(":9090"),
// Start MCP gateway alongside the service
mcp.WithMCP(":3000"),
)
service.Init()
// Register all three handlers with scopes
service.Handle(
&ProjectService{projects: make(map[string]*Project)},
server.WithEndpointScopes("ProjectService.Create", "projects:write"),
server.WithEndpointScopes("ProjectService.Get", "projects:read"),
server.WithEndpointScopes("ProjectService.List", "projects:read"),
)
service.Handle(
&TaskService{tasks: make(map[string]*Task)},
server.WithEndpointScopes("TaskService.Create", "tasks:write"),
server.WithEndpointScopes("TaskService.List", "tasks:read"),
server.WithEndpointScopes("TaskService.Update", "tasks:write"),
)
service.Handle(
&TeamService{members: make(map[string]*Member)},
server.WithEndpointScopes("TeamService.Add", "team:write"),
server.WithEndpointScopes("TeamService.List", "team:read"),
server.WithEndpointScopes("TeamService.Get", "team:read"),
)
// Seed some demo data
seedData(service.Server())
fmt.Println()
fmt.Println(" Agent Demo")
fmt.Println()
fmt.Println(" MCP Gateway http://localhost:3000")
fmt.Println(" MCP Tools http://localhost:3000/mcp/tools")
fmt.Println(" WebSocket ws://localhost:3000/mcp/ws")
fmt.Println()
fmt.Println(" Try these prompts with Claude Code or the agent playground:")
fmt.Println()
fmt.Println(" \"What projects do we have?\"")
fmt.Println(" \"Create a task for alice to design the new landing page\"")
fmt.Println(" \"Show me all high-priority tasks that are still todo\"")
fmt.Println(" \"Who on the team knows React?\"")
fmt.Println(" \"Give me a status update on the Website Redesign project\"")
fmt.Println()
service.Run()
}
// seedData pre-populates the services with realistic demo data.
func seedData(srv server.Server) {
ctx := context.Background()
// Seed team members
team := &TeamService{members: make(map[string]*Member)}
for _, m := range []AddMemberRequest{
{Username: "alice", Name: "Alice Chen", Role: "engineer", Skills: []string{"go", "grpc", "kubernetes"}},
{Username: "bob", Name: "Bob Park", Role: "designer", Skills: []string{"figma", "css", "react"}},
{Username: "charlie", Name: "Charlie Kim", Role: "engineer", Skills: []string{"go", "react", "postgres"}},
{Username: "diana", Name: "Diana Flores", Role: "manager", Skills: []string{"project-management", "scrum"}},
} {
req := m
team.Add(ctx, &req, &AddMemberResponse{})
}
// Seed projects
projects := &ProjectService{projects: make(map[string]*Project)}
projects.Create(ctx, &CreateProjectRequest{
Name: "Website Redesign",
Description: "Redesign the company website with new branding and improved UX",
}, &CreateProjectResponse{})
projects.projects["proj-1"].Status = "active"
projects.Create(ctx, &CreateProjectRequest{
Name: "API v2 Migration",
Description: "Migrate all services from REST to gRPC with backward compatibility",
}, &CreateProjectResponse{})
projects.projects["proj-2"].Status = "planning"
// Seed tasks
tasks := &TaskService{tasks: make(map[string]*Task)}
for _, t := range []CreateTaskRequest{
{ProjectID: "proj-1", Title: "Design new homepage layout", Assignee: "bob", Priority: "high"},
{ProjectID: "proj-1", Title: "Implement responsive nav component", Assignee: "charlie", Priority: "high"},
{ProjectID: "proj-1", Title: "Write copy for about page", Priority: "medium"},
{ProjectID: "proj-1", Title: "Set up CI/CD for new site", Assignee: "alice", Priority: "medium"},
{ProjectID: "proj-2", Title: "Audit existing REST endpoints", Assignee: "alice", Priority: "high"},
{ProjectID: "proj-2", Title: "Design gRPC proto files", Priority: "medium"},
{ProjectID: "proj-2", Title: "Write migration guide", Assignee: "diana", Priority: "low"},
} {
req := t
tasks.Create(ctx, &req, &CreateTaskResponse{})
}
// Mark a couple tasks as in_progress
tasks.tasks["task-1"].Status = "in_progress"
tasks.tasks["task-5"].Status = "in_progress"
// Register the seeded handlers (replace the empty ones registered above)
// Note: in a real app these would be separate services. Here we register
// pre-seeded instances so the demo starts with data.
srv.Handle(srv.NewHandler(projects))
srv.Handle(srv.NewHandler(tasks))
srv.Handle(srv.NewHandler(team))
}
+13
View File
@@ -0,0 +1,13 @@
# Multi-stage build for a go-micro service
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /service .
FROM alpine:3.19
RUN apk --no-cache add ca-certificates
COPY --from=builder /service /service
ENTRYPOINT ["/service"]
+13
View File
@@ -0,0 +1,13 @@
# Standalone MCP gateway
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /gateway ./cmd/gateway
FROM alpine:3.19
RUN apk --no-cache add ca-certificates
COPY --from=builder /gateway /gateway
ENTRYPOINT ["/gateway"]
+116
View File
@@ -0,0 +1,116 @@
# Docker Compose Deployment Example
Run a go-micro service with MCP gateway, service registry, and distributed tracing in one command.
## Architecture
```
┌─────────┐ discover ┌──────────┐ RPC ┌─────────┐
│ Agent │ ─────────────→ │ MCP │ ──────────→ │ Your │
│ (Claude) │ MCP :3001 │ Gateway │ │ Service │
└─────────┘ └──────────┘ └─────────┘
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ Consul │ │ Jaeger │
│ Registry │ │ Tracing │
│ :8500 │ │ :16686 │
└──────────┘ └──────────┘
```
## Quick Start
```bash
docker-compose up
```
## Endpoints
| Service | URL |
|---------|-----|
| MCP Tools | http://localhost:3001/mcp/tools |
| Consul UI | http://localhost:8500 |
| Jaeger UI | http://localhost:16686 |
| Service RPC | http://localhost:9090 |
## Test
```bash
# List MCP tools
curl http://localhost:3001/mcp/tools | jq
# Call a tool
curl -X POST http://localhost:3001/mcp/call \
-H 'Content-Type: application/json' \
-d '{"tool": "myservice.Handler.Method", "arguments": {"key": "value"}}'
# View traces in Jaeger
open http://localhost:16686
```
## Connect Claude Code
```bash
# Claude Code can connect to the running MCP gateway
# Add to your Claude Code MCP settings:
```
```json
{
"mcpServers": {
"my-services": {
"url": "http://localhost:3001/mcp"
}
}
}
```
## Customizing
### Add Your Service
Replace the `app` service's build context with your service directory:
```yaml
app:
build:
context: ../path/to/your/service
dockerfile: Dockerfile
```
### Add More Services
```yaml
users:
build: ./users
environment:
MICRO_REGISTRY: consul
MICRO_REGISTRY_ADDRESS: consul:8500
orders:
build: ./orders
environment:
MICRO_REGISTRY: consul
MICRO_REGISTRY_ADDRESS: consul:8500
```
All services register with Consul. The MCP gateway discovers them automatically.
### Add Redis Cache
```yaml
redis:
image: redis:7-alpine
ports:
- "6379:6379"
```
Then set `MICRO_CACHE_ADDRESS=redis:6379` on your service.
### Production Considerations
- Add health checks to each service
- Use named volumes for Consul data persistence
- Configure rate limiting on the MCP gateway
- Set up TLS between services
- Use secrets management for API keys
+65
View File
@@ -0,0 +1,65 @@
# Go Micro + MCP Gateway deployment with Docker Compose
#
# This runs:
# 1. Consul — service registry (discovery)
# 2. App — your go-micro service(s)
# 3. MCP Gateway — standalone MCP gateway connected to Consul
# 4. Jaeger — distributed tracing UI
#
# Usage:
# docker-compose up
#
# Endpoints:
# MCP Tools: http://localhost:3001/mcp/tools
# Consul UI: http://localhost:8500
# Jaeger UI: http://localhost:16686
# Service: http://localhost:9090 (RPC)
services:
# --- Service Registry ---
consul:
image: consul:1.15
ports:
- "8500:8500"
command: agent -server -bootstrap-expect=1 -ui -client=0.0.0.0
# --- Your Go Micro Service ---
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "9090:9090"
environment:
MICRO_REGISTRY: consul
MICRO_REGISTRY_ADDRESS: consul:8500
MICRO_SERVER_ADDRESS: :9090
depends_on:
- consul
restart: unless-stopped
# --- MCP Gateway (standalone) ---
mcp-gateway:
build:
context: .
dockerfile: Dockerfile.gateway
ports:
- "3001:3001"
environment:
MICRO_REGISTRY: consul
MICRO_REGISTRY_ADDRESS: consul:8500
MCP_ADDRESS: :3001
OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4318
depends_on:
- consul
- app
restart: unless-stopped
# --- Tracing ---
jaeger:
image: jaegertracing/all-in-one:1.53
ports:
- "16686:16686" # UI
- "4318:4318" # OTLP HTTP
environment:
COLLECTOR_OTLP_ENABLED: "true"
+38
View File
@@ -0,0 +1,38 @@
# Graceful Stop Demo
This example demonstrates the intended shutdown behavior after the gRPC graceful-stop patch.
## Run
```bash
go run ./examples/graceful-stop
```
## Expected behavior
- one long-running RPC starts
- shutdown begins while that RPC is still running
- new RPCs stop being accepted shortly after shutdown starts
- the in-flight RPC is allowed to finish
Typical output:
```text
long RPC is running; starting shutdown
new RPC rejected after shutdown began: ...
long RPC completed: slept for 1500ms
done
```
There may be a small race window where the first post-stop RPC is still accepted once before subsequent new RPCs are rejected. The important part is that in-flight RPCs are drained while new RPCs are cut off.
## Automated check
```bash
go test ./server/grpc -run TestGracefulStopRejectsNewRPCsButAllowsInFlightRPCs -v
```
## Environment
- no special environment variables are required
- the demo may print a TLS warning from `go-micro`; it is unrelated to this change
+129
View File
@@ -0,0 +1,129 @@
package main
import (
"context"
"fmt"
"log"
"net"
"time"
micro "go-micro.dev/v5"
"go-micro.dev/v5/client"
grpcclient "go-micro.dev/v5/client/grpc"
"go-micro.dev/v5/registry"
"go-micro.dev/v5/server"
grpcserver "go-micro.dev/v5/server/grpc"
)
type SleepRequest struct {
DelayMS int `json:"delay_ms"`
}
type SleepResponse struct {
Message string `json:"message"`
}
type Sleeper struct {
started chan struct{}
}
func (s *Sleeper) Sleep(ctx context.Context, req *SleepRequest, rsp *SleepResponse) error {
select {
case s.started <- struct{}{}:
default:
}
timer := time.NewTimer(time.Duration(req.DelayMS) * time.Millisecond)
defer timer.Stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
}
rsp.Message = fmt.Sprintf("slept for %dms", req.DelayMS)
return nil
}
func main() {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
log.Fatal(err)
}
defer listener.Close()
addr := listener.Addr().String()
reg := registry.NewMemoryRegistry()
handler := &Sleeper{started: make(chan struct{}, 1)}
service := micro.New("grace-demo",
micro.HandleSignal(false),
micro.Registry(reg),
micro.Server(grpcserver.NewServer(
server.Registry(reg),
server.Name("grace-demo"),
server.Address(addr),
grpcserver.Listener(listener),
grpcserver.GracefulStopTimeout(3*time.Second),
)),
micro.Client(grpcclient.NewClient(
client.Registry(reg),
client.ContentType("application/grpc+json"),
client.DialTimeout(200*time.Millisecond),
client.RequestTimeout(5*time.Second),
)),
)
if err := service.Handle(handler); err != nil {
log.Fatal(err)
}
if err := service.Start(); err != nil {
log.Fatal(err)
}
log.Printf("service started on %s", addr)
longDone := make(chan error, 1)
go func() {
req := service.Client().NewRequest("grace-demo", "Sleeper.Sleep", &SleepRequest{DelayMS: 1500})
rsp := &SleepResponse{}
longDone <- service.Client().Call(context.Background(), req, rsp, client.WithAddress(addr))
if rsp.Message != "" {
log.Printf("long RPC completed: %s", rsp.Message)
}
}()
<-handler.started
log.Printf("long RPC is running; starting shutdown")
stopDone := make(chan error, 1)
go func() {
stopDone <- service.Stop()
}()
deadline := time.Now().Add(500 * time.Millisecond)
for time.Now().Before(deadline) {
callCtx, cancel := context.WithTimeout(context.Background(), 150*time.Millisecond)
req := service.Client().NewRequest("grace-demo", "Sleeper.Sleep", &SleepRequest{DelayMS: 50})
rsp := &SleepResponse{}
err = service.Client().Call(callCtx, req, rsp, client.WithAddress(addr))
cancel()
if err != nil {
log.Printf("new RPC rejected after shutdown began: %v", err)
break
}
log.Printf("new RPC still accepted during shutdown: %s", rsp.Message)
time.Sleep(10 * time.Millisecond)
}
if err := <-longDone; err != nil {
log.Fatalf("long RPC failed: %v", err)
}
if err := <-stopDone; err != nil {
log.Fatalf("service stop failed: %v", err)
}
log.Printf("done")
}
+12 -47
View File
@@ -6,7 +6,6 @@ import (
"log"
"go-micro.dev/v5"
"go-micro.dev/v5/client"
)
// Request and Response types
@@ -30,11 +29,7 @@ func (g *Greeter) Hello(ctx context.Context, req *Request, rsp *Response) error
func main() {
// Create a new service
service := micro.New(
micro.Name("greeter"),
micro.Version("latest"),
micro.Address(":8080"),
)
service := micro.New("greeter", micro.Address(":8080"))
// Initialize the service
service.Init()
@@ -44,47 +39,17 @@ func main() {
log.Fatal(err)
}
// Run the service in a goroutine
go func() {
if err := service.Run(); err != nil {
log.Fatal(err)
}
}()
fmt.Println("Starting greeter service on :8080")
fmt.Println()
fmt.Println("Test with:")
fmt.Println(" curl -XPOST \\")
fmt.Println(" -H 'Content-Type: application/json' \\")
fmt.Println(" -H 'Micro-Endpoint: Greeter.Hello' \\")
fmt.Println(" -d '{\"name\": \"Alice\"}' \\")
fmt.Println(" http://localhost:8080")
// Wait for service to start
fmt.Println("Service started on :8080")
fmt.Println("Testing the service...")
// Create a client to test the service
c := service.Client()
// Make a request
req := c.NewRequest("greeter", "Greeter.Hello", &Request{Name: "World"})
rsp := &Response{}
if err := c.Call(context.Background(), req, rsp); err != nil {
log.Printf("Error calling service: %v", err)
} else {
fmt.Printf("Response: %s\n", rsp.Message)
// Run the service
if err := service.Run(); err != nil {
log.Fatal(err)
}
// Make another request
req2 := c.NewRequest("greeter", "Greeter.Hello", &Request{Name: "Go Micro"})
rsp2 := &Response{}
if err := c.Call(context.Background(), req2, rsp2); err != nil {
log.Printf("Error calling service: %v", err)
} else {
fmt.Printf("Response: %s\n", rsp2.Message)
}
// Test with HTTP client
fmt.Println("\nYou can also test with curl:")
fmt.Println("curl -X POST http://localhost:8080 \\")
fmt.Println(" -H 'Content-Type: application/json' \\")
fmt.Println(" -H 'Micro-Endpoint: Greeter.Hello' \\")
fmt.Println(" -d '{\"name\": \"Alice\"}'")
// Keep service running
select {}
}
+52 -4
View File
@@ -19,6 +19,36 @@ cd hello
go run main.go
```
### [crud](./crud/) - CRUD Contact Book
A realistic service with create, read, update, delete, list, and search operations. Shows how to document a full API for agents with `@example` tags, `description` struct tags, validation errors, and partial updates.
**Run it:**
```bash
cd crud
go run main.go
```
### [workflow](./workflow/) - Cross-Service Orchestration
Three services (Inventory, Orders, Notifications) showing how an AI agent orchestrates multi-step workflows: search products, check stock, reserve inventory, place order, send confirmation — all from a single natural language request.
**Run it:**
```bash
cd workflow
go run main.go
```
### [platform](./platform/) - Agent Platform Showcase
A complete platform (Users, Posts, Comments, Mail) mirroring [micro/blog](https://github.com/micro/blog). Shows how existing microservices become agent-accessible with zero code changes — agents can sign up, write posts, comment, tag, and send mail through natural language.
**Run it:**
```bash
cd platform
go run main.go
```
### [documented](./documented/) - Full-Featured Example
Complete example showing all MCP features with a user service.
@@ -127,12 +157,30 @@ Just write Go comments - documentation is extracted automatically:
### ✅ MCP Command Line
```bash
micro mcp serve # Start with stdio
micro mcp serve --address :3000 # Start with HTTP
micro mcp list # List tools
micro mcp test <tool-name> # Test a tool
# Start MCP server
micro mcp serve # Stdio (for Claude Code)
micro mcp serve --address :3000 # HTTP/SSE (for web agents)
# List available tools
micro mcp list # Human-readable list
micro mcp list --json # JSON output
# Test a tool
micro mcp test <tool-name> '{"key": "value"}'
# Generate documentation
micro mcp docs # Markdown format
micro mcp docs --format json # JSON format
micro mcp docs --output tools.md # Save to file
# Export to different formats
micro mcp export langchain # Python LangChain tools
micro mcp export openapi # OpenAPI 3.0 spec
micro mcp export json # Raw JSON definitions
```
For detailed examples, see [CLI Examples](../../cmd/micro/mcp/EXAMPLES.md).
### ✅ Zero Configuration
- No manual tool registration
+74
View File
@@ -0,0 +1,74 @@
# CRUD Contact Book Example
A complete CRUD service with MCP integration — the kind of service you'd actually build in production.
## What This Shows
- **6 operations**: Create, Get, Update, Delete, List, Search
- **Rich documentation**: Every handler has doc comments with `@example` tags
- **Struct tag descriptions**: All fields have `description` tags for agents
- **Input validation**: Required field checks with clear error messages
- **Partial updates**: Update only changes non-empty fields
- **Seed data**: Starts with 3 contacts so agents can explore immediately
## Run
```bash
go run .
```
## Test
```bash
# List all MCP tools
curl http://localhost:3001/mcp/tools | jq
# Create a contact
curl -X POST http://localhost:3001/mcp/call \
-H 'Content-Type: application/json' \
-d '{"tool": "contacts.Contacts.Create", "arguments": {"name": "Dave", "email": "dave@example.com"}}'
# Search contacts
curl -X POST http://localhost:3001/mcp/call \
-H 'Content-Type: application/json' \
-d '{"tool": "contacts.Contacts.Search", "arguments": {"query": "engineer"}}'
```
## Use with Claude Code
```bash
micro mcp serve
```
Then ask: "List all contacts and find the engineers."
## Key Patterns
### Doc Comments for Agents
```go
// Create adds a new contact to the book. Name and email are required.
//
// @example {"name": "Dave Wilson", "email": "dave@example.com", "role": "Engineer"}
func (h *Contacts) Create(ctx context.Context, req *CreateRequest, rsp *CreateResponse) error {
```
### Struct Tag Descriptions
```go
type Contact struct {
ID string `json:"id" description:"Unique contact identifier"`
Name string `json:"name" description:"Full name"`
Email string `json:"email" description:"Email address"`
}
```
### Partial Updates
Only update fields that are provided (non-empty), so agents can change one field without overwriting others:
```go
if req.Name != "" {
contact.Name = req.Name
}
```
+278
View File
@@ -0,0 +1,278 @@
// CRUD example: a contact book service with full MCP integration.
//
// This shows a realistic service with create, read, update, delete, and
// search operations, all automatically exposed as MCP tools with rich
// documentation for AI agents.
//
// Run:
//
// go run .
//
// MCP tools: http://localhost:3001/mcp/tools
// Test: curl http://localhost:3001/mcp/tools | jq
package main
import (
"context"
"fmt"
"log"
"strings"
"sync"
"go-micro.dev/v5"
"go-micro.dev/v5/gateway/mcp"
)
// --- Types ---
// Contact represents a person in the contact book.
type Contact struct {
ID string `json:"id" description:"Unique contact identifier"`
Name string `json:"name" description:"Full name"`
Email string `json:"email" description:"Email address"`
Phone string `json:"phone" description:"Phone number in E.164 format"`
Role string `json:"role" description:"Job title or role"`
Notes string `json:"notes" description:"Free-text notes about this contact"`
}
type CreateRequest struct {
Name string `json:"name" description:"Full name (required)"`
Email string `json:"email" description:"Email address (required)"`
Phone string `json:"phone" description:"Phone number"`
Role string `json:"role" description:"Job title or role"`
Notes string `json:"notes" description:"Free-text notes"`
}
type CreateResponse struct {
Contact *Contact `json:"contact" description:"The newly created contact"`
}
type GetRequest struct {
ID string `json:"id" description:"Contact ID to look up"`
}
type GetResponse struct {
Contact *Contact `json:"contact" description:"The requested contact"`
}
type UpdateRequest struct {
ID string `json:"id" description:"Contact ID to update (required)"`
Name string `json:"name" description:"New name (leave empty to keep current)"`
Email string `json:"email" description:"New email (leave empty to keep current)"`
Phone string `json:"phone" description:"New phone (leave empty to keep current)"`
Role string `json:"role" description:"New role (leave empty to keep current)"`
Notes string `json:"notes" description:"New notes (leave empty to keep current)"`
}
type UpdateResponse struct {
Contact *Contact `json:"contact" description:"The updated contact"`
}
type DeleteRequest struct {
ID string `json:"id" description:"Contact ID to delete"`
}
type DeleteResponse struct {
Deleted bool `json:"deleted" description:"True if the contact was deleted"`
}
type ListRequest struct {
}
type ListResponse struct {
Contacts []*Contact `json:"contacts" description:"All contacts in the book"`
}
type SearchRequest struct {
Query string `json:"query" description:"Search term to match against name, email, role, or notes"`
}
type SearchResponse struct {
Contacts []*Contact `json:"contacts" description:"Contacts matching the search query"`
}
// --- Handler ---
// Contacts manages a contact book with CRUD operations.
type Contacts struct {
mu sync.RWMutex
store map[string]*Contact
counter int
}
func NewContacts() *Contacts {
c := &Contacts{store: make(map[string]*Contact)}
// Seed with example data
c.store["c-1"] = &Contact{ID: "c-1", Name: "Alice Johnson", Email: "alice@example.com", Phone: "+1-555-0101", Role: "Engineer", Notes: "Backend team lead"}
c.store["c-2"] = &Contact{ID: "c-2", Name: "Bob Smith", Email: "bob@example.com", Phone: "+1-555-0102", Role: "Designer", Notes: "UI/UX specialist"}
c.store["c-3"] = &Contact{ID: "c-3", Name: "Carol Davis", Email: "carol@example.com", Phone: "+1-555-0103", Role: "PM", Notes: "Leads the platform team"}
c.counter = 3
return c
}
// Create adds a new contact to the book. Name and email are required.
//
// @example {"name": "Dave Wilson", "email": "dave@example.com", "role": "Engineer"}
func (h *Contacts) Create(ctx context.Context, req *CreateRequest, rsp *CreateResponse) error {
if req.Name == "" {
return fmt.Errorf("name is required")
}
if req.Email == "" {
return fmt.Errorf("email is required")
}
h.mu.Lock()
defer h.mu.Unlock()
h.counter++
id := fmt.Sprintf("c-%d", h.counter)
contact := &Contact{
ID: id,
Name: req.Name,
Email: req.Email,
Phone: req.Phone,
Role: req.Role,
Notes: req.Notes,
}
h.store[id] = contact
rsp.Contact = contact
return nil
}
// Get retrieves a single contact by ID.
//
// @example {"id": "c-1"}
func (h *Contacts) Get(ctx context.Context, req *GetRequest, rsp *GetResponse) error {
if req.ID == "" {
return fmt.Errorf("id is required")
}
h.mu.RLock()
defer h.mu.RUnlock()
contact, ok := h.store[req.ID]
if !ok {
return fmt.Errorf("contact %s not found", req.ID)
}
rsp.Contact = contact
return nil
}
// Update modifies an existing contact. Only non-empty fields are updated,
// so you can change just the email without affecting other fields.
//
// @example {"id": "c-1", "role": "Senior Engineer"}
func (h *Contacts) Update(ctx context.Context, req *UpdateRequest, rsp *UpdateResponse) error {
if req.ID == "" {
return fmt.Errorf("id is required")
}
h.mu.Lock()
defer h.mu.Unlock()
contact, ok := h.store[req.ID]
if !ok {
return fmt.Errorf("contact %s not found", req.ID)
}
if req.Name != "" {
contact.Name = req.Name
}
if req.Email != "" {
contact.Email = req.Email
}
if req.Phone != "" {
contact.Phone = req.Phone
}
if req.Role != "" {
contact.Role = req.Role
}
if req.Notes != "" {
contact.Notes = req.Notes
}
rsp.Contact = contact
return nil
}
// Delete removes a contact from the book permanently.
//
// @example {"id": "c-1"}
func (h *Contacts) Delete(ctx context.Context, req *DeleteRequest, rsp *DeleteResponse) error {
if req.ID == "" {
return fmt.Errorf("id is required")
}
h.mu.Lock()
defer h.mu.Unlock()
if _, ok := h.store[req.ID]; !ok {
return fmt.Errorf("contact %s not found", req.ID)
}
delete(h.store, req.ID)
rsp.Deleted = true
return nil
}
// List returns all contacts in the book.
//
// @example {}
func (h *Contacts) List(ctx context.Context, req *ListRequest, rsp *ListResponse) error {
h.mu.RLock()
defer h.mu.RUnlock()
for _, c := range h.store {
rsp.Contacts = append(rsp.Contacts, c)
}
return nil
}
// Search finds contacts matching a query string. Matches against name,
// email, role, and notes fields (case-insensitive).
//
// @example {"query": "engineer"}
func (h *Contacts) Search(ctx context.Context, req *SearchRequest, rsp *SearchResponse) error {
if req.Query == "" {
return fmt.Errorf("query is required")
}
h.mu.RLock()
defer h.mu.RUnlock()
q := strings.ToLower(req.Query)
for _, c := range h.store {
if strings.Contains(strings.ToLower(c.Name), q) ||
strings.Contains(strings.ToLower(c.Email), q) ||
strings.Contains(strings.ToLower(c.Role), q) ||
strings.Contains(strings.ToLower(c.Notes), q) {
rsp.Contacts = append(rsp.Contacts, c)
}
}
return nil
}
func main() {
service := micro.New("contacts",
micro.Address(":9010"),
mcp.WithMCP(":3001"),
)
service.Init()
if err := service.Handle(NewContacts()); err != nil {
log.Fatal(err)
}
fmt.Println("Contacts service running on :9010")
fmt.Println("MCP tools available at http://localhost:3001/mcp/tools")
fmt.Println()
fmt.Println("Try asking an AI agent:")
fmt.Println(" 'List all contacts'")
fmt.Println(" 'Find engineers in the contact book'")
fmt.Println(" 'Add a new contact for Eve at eve@example.com'")
fmt.Println(" 'Update Alice's role to Staff Engineer'")
if err := service.Run(); err != nil {
log.Fatal(err)
}
}
+13 -36
View File
@@ -90,53 +90,30 @@ func (u *Users) CreateUser(ctx context.Context, req *CreateUserRequest, rsp *Cre
func main() {
// Create service
service := micro.NewService(
micro.Name("users"),
micro.Version("1.0.0"),
service := micro.New("users",
micro.Address(":9090"),
// Start MCP gateway alongside the service
mcp.WithMCP(":3000"),
)
service.Init()
// Register handler with pre-populated test data
usersService := &Users{
users: map[string]*User{
"user-1": {
ID: "user-1",
Name: "John Doe",
Email: "john@example.com",
Age: 25,
},
"user-2": {
ID: "user-2",
Name: "Jane Smith",
Email: "jane@example.com",
Age: 30,
// Register handler with pre-populated test data.
// Documentation is automatically extracted from method comments.
// Use WithEndpointScopes to declare required auth scopes per endpoint.
if err := service.Handle(
&Users{
users: map[string]*User{
"user-1": {ID: "user-1", Name: "John Doe", Email: "john@example.com", Age: 25},
"user-2": {ID: "user-2", Name: "Jane Smith", Email: "jane@example.com", Age: 30},
},
},
}
// Register handler - documentation is automatically extracted from method comments.
// Use WithEndpointScopes to declare required auth scopes per endpoint.
handler := service.Server().NewHandler(
usersService,
server.WithEndpointScopes("Users.GetUser", "users:read"),
server.WithEndpointScopes("Users.CreateUser", "users:write"),
)
if err := service.Server().Handle(handler); err != nil {
); err != nil {
log.Fatal(err)
}
// Start MCP gateway on port 3000
go func() {
log.Println("Starting MCP gateway on :3000")
if err := mcp.ListenAndServe(":3000", mcp.Options{
Registry: service.Options().Registry,
}); err != nil {
log.Printf("MCP gateway error: %v", err)
}
}()
log.Println("Users service starting...")
log.Println("Service: users")
log.Println("Endpoints:")
+10 -23
View File
@@ -37,38 +37,25 @@ type HelloResponse struct {
func main() {
// Create service
service := micro.NewService(
micro.Name("greeter"),
micro.Version("1.0.0"),
service := micro.New("greeter",
micro.Address(":9090"),
// Start MCP gateway alongside the service
mcp.WithMCP(":3000"),
)
service.Init()
// Register handler - documentation extracted automatically from comments!
handler := service.Server().NewHandler(new(Greeter))
if err := service.Server().Handle(handler); err != nil {
// Register handler docs extracted automatically from comments
if err := service.Handle(new(Greeter)); err != nil {
log.Fatal(err)
}
// Start MCP gateway on port 3000
go func() {
log.Println("Starting MCP gateway on :3000")
if err := mcp.ListenAndServe(":3000", mcp.Options{
Registry: service.Options().Registry,
}); err != nil {
log.Printf("MCP gateway error: %v", err)
}
}()
log.Println("Greeter service starting...")
log.Println("Service: greeter")
log.Println("Endpoint: Greeter.SayHello")
log.Println("Service: http://localhost:9090")
log.Println("MCP Gateway: http://localhost:3000")
log.Println("")
log.Println("Test with:")
log.Println(" curl http://localhost:3000/mcp/tools")
log.Println("")
log.Println("Or use with Claude Code:")
log.Println("MCP Tools: http://localhost:3000/mcp/tools")
log.Println()
log.Println("Use with Claude Code:")
log.Println(" micro mcp serve")
// Run service
+114
View File
@@ -0,0 +1,114 @@
# Platform Example: AI Agents Meet Real Microservices
This example mirrors the [micro/blog](https://github.com/micro/blog) platform — a real microblogging application built on Go Micro. It demonstrates how existing microservices become AI-accessible through MCP with **zero changes to business logic**.
## Services
| Service | Endpoints | Description |
|---------|-----------|-------------|
| **Users** | Signup, Login, GetProfile, UpdateStatus, List | Account management and authentication |
| **Posts** | Create, Read, Update, Delete, List, TagPost, UntagPost, ListTags | Blog posts with markdown and tagging |
| **Comments** | Create, List, Delete | Threaded comments on posts |
| **Mail** | Send, Read | Internal messaging between users |
## Running
```bash
go run .
```
MCP tools available at: http://localhost:3001/mcp/tools
## Agent Scenarios
These are realistic multi-step workflows an AI agent can complete:
### 1. New User Onboarding
```
"Sign up a new user called carol, then write a welcome post introducing herself"
```
The agent will: call Signup → use the returned user ID → call Posts.Create
### 2. Content Creation
```
"Log in as alice and write a blog post about Go concurrency patterns, then tag it with 'golang' and 'concurrency'"
```
The agent will: call Login → call Posts.Create → call TagPost twice
### 3. Social Interaction
```
"List all posts, find the welcome post, and comment on it as bob saying 'Great to be here!'"
```
The agent will: call Posts.List → pick the right post → call Comments.Create
### 4. Cross-Service Workflow
```
"Send a mail from alice to bob welcoming him, then check bob's inbox to confirm delivery"
```
The agent will: call Mail.Send → call Mail.Read to verify
### 5. Platform Overview
```
"Show me all users, all posts, and all tags currently in use"
```
The agent will: call Users.List, Posts.List, and ListTags (potentially in parallel)
## How It Works
The key insight: **you don't need to write any agent-specific code**. The MCP gateway discovers services from the registry, extracts tool schemas from Go types, and generates descriptions from doc comments.
```go
service := micro.New("platform",
micro.Address(":9090"),
mcp.WithMCP(":3001"), // This one line makes everything AI-accessible
)
service.Handle(&Users{})
service.Handle(&Posts{})
service.Handle(&Comments{})
service.Handle(&Mail{})
```
Each handler method becomes an MCP tool. The `@example` tags in doc comments give agents sample inputs to learn from.
## Connecting to Claude Code
Add to your Claude Code MCP config:
```json
{
"mcpServers": {
"platform": {
"command": "curl",
"args": ["-s", "http://localhost:3001/mcp/tools"]
}
}
}
```
Or use stdio transport:
```bash
micro mcp serve --registry mdns
```
## Architecture
```
Agent (Claude, GPT, etc.)
MCP Gateway (:3001) ← Discovers services, generates tools
Go Micro RPC (:9090) ← Standard service mesh
├── UserService ← Signup, Login, Profile
├── PostService ← CRUD + Tags
├── CommentService ← Threaded comments
└── MailService ← Internal messaging
```
## Relation to micro/blog
This example is a simplified, self-contained version of [micro/blog](https://github.com/micro/blog). The real platform splits each service into its own binary with protobuf definitions. This example uses Go structs directly for simplicity, but the MCP integration works identically either way — the gateway discovers services from the registry regardless of how they're implemented.
+774
View File
@@ -0,0 +1,774 @@
// Platform example: AI agents interacting with a real microservices platform.
//
// This example mirrors the micro/blog platform (https://github.com/micro/blog)
// — a microblogging platform built on Go Micro with Users, Posts, Comments,
// and Mail services. It demonstrates how existing microservices become
// AI-accessible through MCP with zero changes to business logic.
//
// The services run as a single binary for convenience. In production,
// each would be a separate process discovered via the registry.
//
// Run:
//
// go run .
//
// MCP tools: http://localhost:3001/mcp/tools
//
// Agent scenarios:
//
// "Sign me up as alice with password secret123"
// "Log in as alice and write a blog post about Go concurrency"
// "List all posts and comment on the first one"
// "Send a welcome email to alice"
// "Tag the Go concurrency post with 'golang' and 'tutorial'"
// "Show me alice's profile and all her posts"
package main
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"log"
"sort"
"strings"
"sync"
"time"
"go-micro.dev/v5"
"go-micro.dev/v5/gateway/mcp"
"go-micro.dev/v5/server"
)
// ---------------------------------------------------------------------------
// Users service — account registration, login, profiles
// ---------------------------------------------------------------------------
type User struct {
ID string `json:"id" description:"Unique user identifier"`
Name string `json:"name" description:"Display name"`
Status string `json:"status" description:"Bio or status message"`
CreatedAt int64 `json:"created_at" description:"Unix timestamp of account creation"`
}
type SignupRequest struct {
Name string `json:"name" description:"Username (required, 3-20 characters)"`
Password string `json:"password" description:"Password (required, minimum 6 characters)"`
}
type SignupResponse struct {
User *User `json:"user" description:"The newly created user account"`
Token string `json:"token" description:"Session token for authenticated requests"`
}
type LoginRequest struct {
Name string `json:"name" description:"Username"`
Password string `json:"password" description:"Password"`
}
type LoginResponse struct {
User *User `json:"user" description:"The authenticated user"`
Token string `json:"token" description:"Session token for authenticated requests"`
}
type GetProfileRequest struct {
ID string `json:"id" description:"User ID to look up"`
}
type GetProfileResponse struct {
User *User `json:"user" description:"The user profile"`
}
type UpdateStatusRequest struct {
ID string `json:"id" description:"User ID"`
Status string `json:"status" description:"New bio or status message"`
}
type UpdateStatusResponse struct {
User *User `json:"user" description:"Updated user profile"`
}
type ListUsersRequest struct{}
type ListUsersResponse struct {
Users []*User `json:"users" description:"All registered users"`
}
type Users struct {
mu sync.RWMutex
users map[string]*User
passwords map[string]string // name -> password (plaintext for demo only)
tokens map[string]string // token -> user ID
nextID int
}
func NewUsers() *Users {
return &Users{
users: make(map[string]*User),
passwords: make(map[string]string),
tokens: make(map[string]string),
}
}
// Signup creates a new user account and returns a session token.
// The username must be unique. Use the returned token for authenticated operations.
//
// @example {"name": "alice", "password": "secret123"}
func (s *Users) Signup(ctx context.Context, req *SignupRequest, rsp *SignupResponse) error {
if req.Name == "" || len(req.Name) < 3 {
return fmt.Errorf("name must be at least 3 characters")
}
if req.Password == "" || len(req.Password) < 6 {
return fmt.Errorf("password must be at least 6 characters")
}
s.mu.Lock()
defer s.mu.Unlock()
// Check uniqueness
for _, u := range s.users {
if strings.EqualFold(u.Name, req.Name) {
return fmt.Errorf("username %q is already taken", req.Name)
}
}
s.nextID++
user := &User{
ID: fmt.Sprintf("user-%d", s.nextID),
Name: req.Name,
CreatedAt: time.Now().Unix(),
}
s.users[user.ID] = user
s.passwords[req.Name] = req.Password
token := generateToken()
s.tokens[token] = user.ID
rsp.User = user
rsp.Token = token
return nil
}
// Login authenticates a user and returns a session token.
// Returns an error if the credentials are invalid.
//
// @example {"name": "alice", "password": "secret123"}
func (s *Users) Login(ctx context.Context, req *LoginRequest, rsp *LoginResponse) error {
s.mu.RLock()
defer s.mu.RUnlock()
pass, ok := s.passwords[req.Name]
if !ok || pass != req.Password {
return fmt.Errorf("invalid username or password")
}
// Find user by name
for _, u := range s.users {
if u.Name == req.Name {
token := generateToken()
s.tokens[token] = u.ID
rsp.User = u
rsp.Token = token
return nil
}
}
return fmt.Errorf("user not found")
}
// GetProfile retrieves a user's public profile by ID.
//
// @example {"id": "user-1"}
func (s *Users) GetProfile(ctx context.Context, req *GetProfileRequest, rsp *GetProfileResponse) error {
s.mu.RLock()
defer s.mu.RUnlock()
u, ok := s.users[req.ID]
if !ok {
return fmt.Errorf("user %s not found", req.ID)
}
rsp.User = u
return nil
}
// UpdateStatus sets a user's bio or status message.
//
// @example {"id": "user-1", "status": "Writing about Go and microservices"}
func (s *Users) UpdateStatus(ctx context.Context, req *UpdateStatusRequest, rsp *UpdateStatusResponse) error {
s.mu.Lock()
defer s.mu.Unlock()
u, ok := s.users[req.ID]
if !ok {
return fmt.Errorf("user %s not found", req.ID)
}
u.Status = req.Status
rsp.User = u
return nil
}
// List returns all registered users on the platform.
//
// @example {}
func (s *Users) List(ctx context.Context, req *ListUsersRequest, rsp *ListUsersResponse) error {
s.mu.RLock()
defer s.mu.RUnlock()
for _, u := range s.users {
rsp.Users = append(rsp.Users, u)
}
return nil
}
// ---------------------------------------------------------------------------
// Posts service — blog posts with markdown and tags
// ---------------------------------------------------------------------------
type Post struct {
ID string `json:"id" description:"Unique post identifier"`
Title string `json:"title" description:"Post title"`
Content string `json:"content" description:"Post body in markdown"`
AuthorID string `json:"author_id" description:"ID of the post author"`
AuthorName string `json:"author_name" description:"Display name of the author"`
Tags []string `json:"tags,omitempty" description:"Post tags for categorization"`
CreatedAt int64 `json:"created_at" description:"Unix timestamp of creation"`
UpdatedAt int64 `json:"updated_at" description:"Unix timestamp of last update"`
}
type CreatePostRequest struct {
Title string `json:"title" description:"Post title (required)"`
Content string `json:"content" description:"Post body in markdown (required)"`
AuthorID string `json:"author_id" description:"Author's user ID (required)"`
AuthorName string `json:"author_name" description:"Author's display name (required)"`
}
type CreatePostResponse struct {
Post *Post `json:"post" description:"The newly created post"`
}
type ReadPostRequest struct {
ID string `json:"id" description:"Post ID to retrieve"`
}
type ReadPostResponse struct {
Post *Post `json:"post" description:"The requested post"`
}
type UpdatePostRequest struct {
ID string `json:"id" description:"Post ID to update (required)"`
Title string `json:"title" description:"New title"`
Content string `json:"content" description:"New content in markdown"`
}
type UpdatePostResponse struct {
Post *Post `json:"post" description:"The updated post"`
}
type DeletePostRequest struct {
ID string `json:"id" description:"Post ID to delete"`
}
type DeletePostResponse struct {
Message string `json:"message" description:"Confirmation message"`
}
type ListPostsRequest struct {
AuthorID string `json:"author_id,omitempty" description:"Filter by author ID (optional)"`
}
type ListPostsResponse struct {
Posts []*Post `json:"posts" description:"Posts in reverse chronological order"`
Total int `json:"total" description:"Total number of matching posts"`
}
type TagPostRequest struct {
PostID string `json:"post_id" description:"Post to tag"`
Tag string `json:"tag" description:"Tag to add (lowercase, no spaces)"`
}
type TagPostResponse struct {
Post *Post `json:"post" description:"Post with updated tags"`
}
type UntagPostRequest struct {
PostID string `json:"post_id" description:"Post to untag"`
Tag string `json:"tag" description:"Tag to remove"`
}
type UntagPostResponse struct {
Post *Post `json:"post" description:"Post with updated tags"`
}
type ListTagsRequest struct{}
type ListTagsResponse struct {
Tags []string `json:"tags" description:"All tags in use, sorted alphabetically"`
}
type Posts struct {
mu sync.RWMutex
posts map[string]*Post
nextID int
}
func NewPosts() *Posts {
return &Posts{posts: make(map[string]*Post)}
}
// Create publishes a new blog post. Title, content, author_id, and author_name
// are required. Content supports markdown formatting.
//
// @example {"title": "Getting Started with Go Micro", "content": "Go Micro makes it easy to build microservices...", "author_id": "user-1", "author_name": "alice"}
func (s *Posts) Create(ctx context.Context, req *CreatePostRequest, rsp *CreatePostResponse) error {
if req.Title == "" {
return fmt.Errorf("title is required")
}
if req.Content == "" {
return fmt.Errorf("content is required")
}
if req.AuthorID == "" {
return fmt.Errorf("author_id is required")
}
s.mu.Lock()
defer s.mu.Unlock()
s.nextID++
now := time.Now().Unix()
post := &Post{
ID: fmt.Sprintf("post-%d", s.nextID),
Title: req.Title,
Content: req.Content,
AuthorID: req.AuthorID,
AuthorName: req.AuthorName,
CreatedAt: now,
UpdatedAt: now,
}
s.posts[post.ID] = post
rsp.Post = post
return nil
}
// Read retrieves a single blog post by ID.
//
// @example {"id": "post-1"}
func (s *Posts) Read(ctx context.Context, req *ReadPostRequest, rsp *ReadPostResponse) error {
s.mu.RLock()
defer s.mu.RUnlock()
p, ok := s.posts[req.ID]
if !ok {
return fmt.Errorf("post %s not found", req.ID)
}
rsp.Post = p
return nil
}
// Update modifies a blog post's title and/or content.
// Only non-empty fields are updated.
//
// @example {"id": "post-1", "title": "Updated Title", "content": "New content here..."}
func (s *Posts) Update(ctx context.Context, req *UpdatePostRequest, rsp *UpdatePostResponse) error {
s.mu.Lock()
defer s.mu.Unlock()
p, ok := s.posts[req.ID]
if !ok {
return fmt.Errorf("post %s not found", req.ID)
}
if req.Title != "" {
p.Title = req.Title
}
if req.Content != "" {
p.Content = req.Content
}
p.UpdatedAt = time.Now().Unix()
rsp.Post = p
return nil
}
// Delete removes a blog post permanently.
//
// @example {"id": "post-1"}
func (s *Posts) Delete(ctx context.Context, req *DeletePostRequest, rsp *DeletePostResponse) error {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.posts[req.ID]; !ok {
return fmt.Errorf("post %s not found", req.ID)
}
delete(s.posts, req.ID)
rsp.Message = fmt.Sprintf("post %s deleted", req.ID)
return nil
}
// List returns blog posts in reverse chronological order.
// Optionally filter by author_id to see a specific user's posts.
//
// @example {"author_id": "user-1"}
func (s *Posts) List(ctx context.Context, req *ListPostsRequest, rsp *ListPostsResponse) error {
s.mu.RLock()
defer s.mu.RUnlock()
for _, p := range s.posts {
if req.AuthorID != "" && p.AuthorID != req.AuthorID {
continue
}
rsp.Posts = append(rsp.Posts, p)
}
sort.Slice(rsp.Posts, func(i, j int) bool {
return rsp.Posts[i].CreatedAt > rsp.Posts[j].CreatedAt
})
rsp.Total = len(rsp.Posts)
return nil
}
// TagPost adds a tag to a post. Tags are useful for categorization
// and discovery. Duplicate tags are ignored.
//
// @example {"post_id": "post-1", "tag": "golang"}
func (s *Posts) TagPost(ctx context.Context, req *TagPostRequest, rsp *TagPostResponse) error {
if req.PostID == "" || req.Tag == "" {
return fmt.Errorf("post_id and tag are required")
}
s.mu.Lock()
defer s.mu.Unlock()
p, ok := s.posts[req.PostID]
if !ok {
return fmt.Errorf("post %s not found", req.PostID)
}
tag := strings.ToLower(strings.TrimSpace(req.Tag))
for _, t := range p.Tags {
if t == tag {
rsp.Post = p
return nil
}
}
p.Tags = append(p.Tags, tag)
p.UpdatedAt = time.Now().Unix()
rsp.Post = p
return nil
}
// UntagPost removes a tag from a post.
//
// @example {"post_id": "post-1", "tag": "golang"}
func (s *Posts) UntagPost(ctx context.Context, req *UntagPostRequest, rsp *UntagPostResponse) error {
s.mu.Lock()
defer s.mu.Unlock()
p, ok := s.posts[req.PostID]
if !ok {
return fmt.Errorf("post %s not found", req.PostID)
}
filtered := make([]string, 0, len(p.Tags))
for _, t := range p.Tags {
if t != req.Tag {
filtered = append(filtered, t)
}
}
p.Tags = filtered
p.UpdatedAt = time.Now().Unix()
rsp.Post = p
return nil
}
// ListTags returns all tags currently in use across all posts.
//
// @example {}
func (s *Posts) ListTags(ctx context.Context, req *ListTagsRequest, rsp *ListTagsResponse) error {
s.mu.RLock()
defer s.mu.RUnlock()
seen := make(map[string]bool)
for _, p := range s.posts {
for _, t := range p.Tags {
seen[t] = true
}
}
for t := range seen {
rsp.Tags = append(rsp.Tags, t)
}
sort.Strings(rsp.Tags)
return nil
}
// ---------------------------------------------------------------------------
// Comments service — threaded comments on posts
// ---------------------------------------------------------------------------
type Comment struct {
ID string `json:"id" description:"Unique comment identifier"`
PostID string `json:"post_id" description:"ID of the post this comment belongs to"`
Content string `json:"content" description:"Comment text"`
AuthorID string `json:"author_id" description:"ID of the comment author"`
AuthorName string `json:"author_name" description:"Display name of the author"`
CreatedAt int64 `json:"created_at" description:"Unix timestamp of creation"`
}
type CreateCommentRequest struct {
PostID string `json:"post_id" description:"Post to comment on (required)"`
Content string `json:"content" description:"Comment text (required)"`
AuthorID string `json:"author_id" description:"Author's user ID (required)"`
AuthorName string `json:"author_name" description:"Author's display name (required)"`
}
type CreateCommentResponse struct {
Comment *Comment `json:"comment" description:"The newly created comment"`
}
type ListCommentsRequest struct {
PostID string `json:"post_id,omitempty" description:"Filter by post ID (optional)"`
AuthorID string `json:"author_id,omitempty" description:"Filter by author ID (optional)"`
}
type ListCommentsResponse struct {
Comments []*Comment `json:"comments" description:"Matching comments"`
}
type DeleteCommentRequest struct {
ID string `json:"id" description:"Comment ID to delete"`
}
type DeleteCommentResponse struct {
Message string `json:"message" description:"Confirmation message"`
}
type Comments struct {
mu sync.RWMutex
comments []*Comment
nextID int
}
// Create adds a comment to a blog post. Post ID, content, author_id,
// and author_name are all required.
//
// @example {"post_id": "post-1", "content": "Great article! Very helpful.", "author_id": "user-2", "author_name": "bob"}
func (s *Comments) Create(ctx context.Context, req *CreateCommentRequest, rsp *CreateCommentResponse) error {
if req.PostID == "" {
return fmt.Errorf("post_id is required")
}
if req.Content == "" {
return fmt.Errorf("content is required")
}
if req.AuthorID == "" {
return fmt.Errorf("author_id is required")
}
s.mu.Lock()
defer s.mu.Unlock()
s.nextID++
comment := &Comment{
ID: fmt.Sprintf("comment-%d", s.nextID),
PostID: req.PostID,
Content: req.Content,
AuthorID: req.AuthorID,
AuthorName: req.AuthorName,
CreatedAt: time.Now().Unix(),
}
s.comments = append(s.comments, comment)
rsp.Comment = comment
return nil
}
// List returns comments, optionally filtered by post or author.
// Use post_id to get all comments on a specific post.
//
// @example {"post_id": "post-1"}
func (s *Comments) List(ctx context.Context, req *ListCommentsRequest, rsp *ListCommentsResponse) error {
s.mu.RLock()
defer s.mu.RUnlock()
for _, c := range s.comments {
if req.PostID != "" && c.PostID != req.PostID {
continue
}
if req.AuthorID != "" && c.AuthorID != req.AuthorID {
continue
}
rsp.Comments = append(rsp.Comments, c)
}
return nil
}
// Delete removes a comment by ID.
//
// @example {"id": "comment-1"}
func (s *Comments) Delete(ctx context.Context, req *DeleteCommentRequest, rsp *DeleteCommentResponse) error {
s.mu.Lock()
defer s.mu.Unlock()
for i, c := range s.comments {
if c.ID == req.ID {
s.comments = append(s.comments[:i], s.comments[i+1:]...)
rsp.Message = fmt.Sprintf("comment %s deleted", req.ID)
return nil
}
}
return fmt.Errorf("comment %s not found", req.ID)
}
// ---------------------------------------------------------------------------
// Mail service — internal messaging between users
// ---------------------------------------------------------------------------
type MailMessage struct {
ID string `json:"id" description:"Unique message identifier"`
From string `json:"from" description:"Sender username"`
To string `json:"to" description:"Recipient username"`
Subject string `json:"subject" description:"Message subject line"`
Body string `json:"body" description:"Message body text"`
Read bool `json:"read" description:"Whether the message has been read"`
CreatedAt int64 `json:"created_at" description:"Unix timestamp of when the message was sent"`
}
type SendMailRequest struct {
From string `json:"from" description:"Sender username (required)"`
To string `json:"to" description:"Recipient username (required)"`
Subject string `json:"subject" description:"Message subject (required)"`
Body string `json:"body" description:"Message body (required)"`
}
type SendMailResponse struct {
Message *MailMessage `json:"message" description:"The sent message"`
}
type ReadMailRequest struct {
User string `json:"user" description:"Username to read inbox for"`
}
type ReadMailResponse struct {
Messages []*MailMessage `json:"messages" description:"Inbox messages, newest first"`
}
type Mail struct {
mu sync.RWMutex
messages []*MailMessage
nextID int
}
// Send delivers a message to another user on the platform.
// Both sender and recipient are identified by username.
//
// @example {"from": "alice", "to": "bob", "subject": "Welcome!", "body": "Hey Bob, welcome to the platform!"}
func (s *Mail) Send(ctx context.Context, req *SendMailRequest, rsp *SendMailResponse) error {
if req.From == "" || req.To == "" {
return fmt.Errorf("from and to are required")
}
if req.Subject == "" {
return fmt.Errorf("subject is required")
}
s.mu.Lock()
defer s.mu.Unlock()
s.nextID++
msg := &MailMessage{
ID: fmt.Sprintf("mail-%d", s.nextID),
From: req.From,
To: req.To,
Subject: req.Subject,
Body: req.Body,
CreatedAt: time.Now().Unix(),
}
s.messages = append(s.messages, msg)
rsp.Message = msg
return nil
}
// Read returns all messages in a user's inbox, newest first.
//
// @example {"user": "alice"}
func (s *Mail) Read(ctx context.Context, req *ReadMailRequest, rsp *ReadMailResponse) error {
if req.User == "" {
return fmt.Errorf("user is required")
}
s.mu.Lock()
defer s.mu.Unlock()
for i := len(s.messages) - 1; i >= 0; i-- {
if s.messages[i].To == req.User {
s.messages[i].Read = true
rsp.Messages = append(rsp.Messages, s.messages[i])
}
}
return nil
}
// ---------------------------------------------------------------------------
// Main — wire up all services with MCP gateway
// ---------------------------------------------------------------------------
func main() {
service := micro.New("platform",
micro.Address(":9090"),
mcp.WithMCP(":3001"),
)
service.Init()
users := NewUsers()
posts := NewPosts()
// Seed some demo data so agents have something to work with
seedData(users, posts)
service.Handle(users)
service.Handle(posts)
service.Handle(&Comments{})
service.Handle(&Mail{},
server.WithEndpointScopes("Mail.Send", "mail:write"),
server.WithEndpointScopes("Mail.Read", "mail:read"),
)
printBanner()
if err := service.Run(); err != nil {
log.Fatal(err)
}
}
func seedData(users *Users, posts *Posts) {
// Create demo users
var aliceRsp SignupResponse
users.Signup(context.Background(), &SignupRequest{
Name: "alice", Password: "secret123",
}, &aliceRsp)
var bobRsp SignupResponse
users.Signup(context.Background(), &SignupRequest{
Name: "bob", Password: "secret123",
}, &bobRsp)
// Alice writes a welcome post
var postRsp CreatePostResponse
posts.Create(context.Background(), &CreatePostRequest{
Title: "Welcome to the Platform",
Content: "This is the first post on our new blogging platform. Built with Go Micro, every service is automatically accessible to AI agents through MCP.",
AuthorID: aliceRsp.User.ID,
AuthorName: "alice",
}, &postRsp)
// Tag it
posts.TagPost(context.Background(), &TagPostRequest{
PostID: postRsp.Post.ID, Tag: "welcome",
}, &TagPostResponse{})
posts.TagPost(context.Background(), &TagPostRequest{
PostID: postRsp.Post.ID, Tag: "go-micro",
}, &TagPostResponse{})
}
func printBanner() {
fmt.Println()
fmt.Println(" Platform Demo — AI-Native Microservices")
fmt.Println()
fmt.Println(" Services: Users, Posts, Comments, Mail")
fmt.Println(" MCP Tools: http://localhost:3001/mcp/tools")
fmt.Println(" RPC: localhost:9090")
fmt.Println()
fmt.Println(" Seeded: alice (user-1), bob (user-2)")
fmt.Println(" 1 post with tags [welcome, go-micro]")
fmt.Println()
fmt.Println(" Try asking an agent:")
fmt.Println()
fmt.Println(` "Sign up a new user called carol"`)
fmt.Println(` "Log in as alice and write a post about Go concurrency patterns"`)
fmt.Println(` "List all posts and comment on the welcome post as bob"`)
fmt.Println(` "Tag alice's post with 'tutorial' and 'golang'"`)
fmt.Println(` "Send a mail from alice to bob welcoming him to the platform"`)
fmt.Println(` "Show me bob's inbox"`)
fmt.Println(` "List all users and show me all tags in use"`)
fmt.Println()
}
func generateToken() string {
b := make([]byte, 16)
rand.Read(b)
return hex.EncodeToString(b)
}
+67
View File
@@ -0,0 +1,67 @@
# Workflow Example: Cross-Service Orchestration
An e-commerce scenario with three services (Inventory, Orders, Notifications) that demonstrates how AI agents orchestrate multi-step workflows across services — no glue code, no workflow engine.
## The Workflow
When a user says _"Order a ThinkPad for alice and send her a confirmation"_, the agent figures out the steps:
```
1. InventoryService.Search → Find the product
2. InventoryService.CheckStock → Verify availability
3. InventoryService.ReserveStock → Decrement inventory
4. OrderService.PlaceOrder → Create the order
5. NotificationService.Send → Email confirmation
```
No code connects these steps — the agent reads the tool descriptions and chains the calls itself.
## Run
```bash
go run .
```
## Services
| Service | Tools | Purpose |
|---------|-------|---------|
| InventoryService | Search, CheckStock, ReserveStock | Product catalog and stock management |
| OrderService | PlaceOrder, GetOrder, ListOrders | Order creation and lookup |
| NotificationService | Send, List | Email/SMS/Slack notifications |
## Example Prompts
Try these with Claude Code (`micro mcp serve`) or any MCP-compatible agent:
- "What laptops do you have in stock?"
- "Order a ThinkPad for alice@example.com and send her a confirmation"
- "Check if 'The Go Programming Language' is available" (it's out of stock!)
- "Order 3 Go Gopher t-shirts for bob@example.com, reserve the stock, and notify him via Slack"
- "Show me all orders and notifications for alice"
## Why This Matters
Traditional approach:
```go
// 50+ lines of glue code wiring services together
func handleOrder(req OrderRequest) {
product, err := inventoryClient.CheckStock(req.SKU)
if err != nil { ... }
if product.InStock < req.Quantity { ... }
_, err = inventoryClient.ReserveStock(req.SKU, req.Quantity)
if err != nil { ... }
order, err := orderClient.PlaceOrder(...)
if err != nil { ... }
_, err = notificationClient.Send(...)
// ...
}
```
Agent approach:
```
User: "Order a ThinkPad for alice and confirm via email"
Agent: [reads tool descriptions, chains 5 calls, handles the out-of-stock case]
```
The agent handles the orchestration. You just write the individual services with good documentation.
+393
View File
@@ -0,0 +1,393 @@
// Workflow example: cross-service orchestration via AI agents.
//
// This example runs three services (Inventory, Orders, Notifications) and
// demonstrates how an AI agent can orchestrate a multi-step workflow:
//
// 1. Check inventory for a product
// 2. Place an order if in stock
// 3. Send a confirmation notification
//
// The agent figures out the right sequence of calls on its own — no
// workflow engine, no glue code, just natural language.
//
// Run:
//
// go run .
//
// MCP tools: http://localhost:3001/mcp/tools
package main
import (
"context"
"fmt"
"log"
"strings"
"sync"
"time"
"go-micro.dev/v5"
"go-micro.dev/v5/gateway/mcp"
)
// ---------------------------------------------------------------------------
// Inventory service
// ---------------------------------------------------------------------------
type Product struct {
SKU string `json:"sku" description:"Stock keeping unit identifier"`
Name string `json:"name" description:"Product name"`
Price float64 `json:"price" description:"Unit price in USD"`
InStock int `json:"in_stock" description:"Number of units available"`
Category string `json:"category" description:"Product category"`
}
type CheckStockRequest struct {
SKU string `json:"sku" description:"Product SKU to check"`
}
type CheckStockResponse struct {
Product *Product `json:"product" description:"Product details with current stock level"`
}
type SearchProductsRequest struct {
Query string `json:"query" description:"Search term to match against product name or category"`
Category string `json:"category,omitempty" description:"Filter by category: electronics, clothing, books (optional)"`
}
type SearchProductsResponse struct {
Products []*Product `json:"products" description:"Products matching the search criteria"`
}
type ReserveStockRequest struct {
SKU string `json:"sku" description:"Product SKU to reserve"`
Quantity int `json:"quantity" description:"Number of units to reserve"`
}
type ReserveStockResponse struct {
Reserved bool `json:"reserved" description:"True if stock was successfully reserved"`
Remaining int `json:"remaining" description:"Units remaining after reservation"`
Message string `json:"message" description:"Human-readable result message"`
}
type InventoryService struct {
mu sync.RWMutex
products map[string]*Product
}
// CheckStock returns the current stock level for a product.
// Use this before placing an order to verify availability.
//
// @example {"sku": "LAPTOP-001"}
func (s *InventoryService) CheckStock(ctx context.Context, req *CheckStockRequest, rsp *CheckStockResponse) error {
s.mu.RLock()
defer s.mu.RUnlock()
p, ok := s.products[req.SKU]
if !ok {
return fmt.Errorf("product %s not found", req.SKU)
}
rsp.Product = p
return nil
}
// Search finds products by name or category. Use this to help
// users find what they're looking for before checking stock.
//
// @example {"query": "laptop"}
func (s *InventoryService) Search(ctx context.Context, req *SearchProductsRequest, rsp *SearchProductsResponse) error {
s.mu.RLock()
defer s.mu.RUnlock()
q := strings.ToLower(req.Query)
for _, p := range s.products {
if req.Category != "" && !strings.EqualFold(p.Category, req.Category) {
continue
}
if q == "" || strings.Contains(strings.ToLower(p.Name), q) || strings.Contains(strings.ToLower(p.Category), q) {
rsp.Products = append(rsp.Products, p)
}
}
return nil
}
// ReserveStock decrements inventory for a product. Call this after
// confirming stock is available. Returns an error if insufficient stock.
//
// @example {"sku": "LAPTOP-001", "quantity": 1}
func (s *InventoryService) ReserveStock(ctx context.Context, req *ReserveStockRequest, rsp *ReserveStockResponse) error {
s.mu.Lock()
defer s.mu.Unlock()
p, ok := s.products[req.SKU]
if !ok {
return fmt.Errorf("product %s not found", req.SKU)
}
if p.InStock < req.Quantity {
rsp.Reserved = false
rsp.Remaining = p.InStock
rsp.Message = fmt.Sprintf("insufficient stock: requested %d but only %d available", req.Quantity, p.InStock)
return nil
}
p.InStock -= req.Quantity
rsp.Reserved = true
rsp.Remaining = p.InStock
rsp.Message = fmt.Sprintf("reserved %d units of %s", req.Quantity, p.Name)
return nil
}
// ---------------------------------------------------------------------------
// Orders service
// ---------------------------------------------------------------------------
type Order struct {
ID string `json:"id" description:"Unique order identifier"`
Customer string `json:"customer" description:"Customer name or email"`
SKU string `json:"sku" description:"Product SKU ordered"`
Quantity int `json:"quantity" description:"Number of units"`
Total float64 `json:"total" description:"Total order amount in USD"`
Status string `json:"status" description:"Order status: pending, confirmed, shipped, delivered"`
CreatedAt time.Time `json:"created_at" description:"When the order was placed"`
}
type PlaceOrderRequest struct {
Customer string `json:"customer" description:"Customer name or email (required)"`
SKU string `json:"sku" description:"Product SKU to order (required)"`
Quantity int `json:"quantity" description:"Number of units (required, must be positive)"`
}
type PlaceOrderResponse struct {
Order *Order `json:"order" description:"The newly created order"`
}
type GetOrderRequest struct {
ID string `json:"id" description:"Order ID to look up"`
}
type GetOrderResponse struct {
Order *Order `json:"order" description:"The requested order"`
}
type ListOrdersRequest struct {
Customer string `json:"customer,omitempty" description:"Filter by customer (optional)"`
Status string `json:"status,omitempty" description:"Filter by status (optional)"`
}
type ListOrdersResponse struct {
Orders []*Order `json:"orders" description:"Matching orders"`
}
type OrderService struct {
mu sync.RWMutex
orders map[string]*Order
nextID int
// In a real app this would be a client to the inventory service
inventory *InventoryService
}
// PlaceOrder creates a new order. Stock must be reserved first via
// InventoryService.ReserveStock — this service does not check inventory.
//
// @example {"customer": "alice@example.com", "sku": "LAPTOP-001", "quantity": 1}
func (s *OrderService) PlaceOrder(ctx context.Context, req *PlaceOrderRequest, rsp *PlaceOrderResponse) error {
if req.Customer == "" {
return fmt.Errorf("customer is required")
}
if req.SKU == "" {
return fmt.Errorf("sku is required")
}
if req.Quantity <= 0 {
return fmt.Errorf("quantity must be positive")
}
// Look up price
s.inventory.mu.RLock()
p, ok := s.inventory.products[req.SKU]
s.inventory.mu.RUnlock()
if !ok {
return fmt.Errorf("product %s not found", req.SKU)
}
s.mu.Lock()
defer s.mu.Unlock()
s.nextID++
order := &Order{
ID: fmt.Sprintf("ORD-%04d", s.nextID),
Customer: req.Customer,
SKU: req.SKU,
Quantity: req.Quantity,
Total: p.Price * float64(req.Quantity),
Status: "confirmed",
CreatedAt: time.Now(),
}
s.orders[order.ID] = order
rsp.Order = order
return nil
}
// GetOrder retrieves an order by ID.
//
// @example {"id": "ORD-0001"}
func (s *OrderService) GetOrder(ctx context.Context, req *GetOrderRequest, rsp *GetOrderResponse) error {
s.mu.RLock()
defer s.mu.RUnlock()
o, ok := s.orders[req.ID]
if !ok {
return fmt.Errorf("order %s not found", req.ID)
}
rsp.Order = o
return nil
}
// ListOrders returns orders, optionally filtered by customer or status.
//
// @example {"customer": "alice@example.com"}
func (s *OrderService) ListOrders(ctx context.Context, req *ListOrdersRequest, rsp *ListOrdersResponse) error {
s.mu.RLock()
defer s.mu.RUnlock()
for _, o := range s.orders {
if req.Customer != "" && o.Customer != req.Customer {
continue
}
if req.Status != "" && o.Status != req.Status {
continue
}
rsp.Orders = append(rsp.Orders, o)
}
return nil
}
// ---------------------------------------------------------------------------
// Notifications service
// ---------------------------------------------------------------------------
type Notification struct {
ID string `json:"id" description:"Notification identifier"`
Recipient string `json:"recipient" description:"Who received the notification"`
Subject string `json:"subject" description:"Notification subject line"`
Body string `json:"body" description:"Notification body text"`
Channel string `json:"channel" description:"Delivery channel: email, sms, or slack"`
SentAt time.Time `json:"sent_at" description:"When the notification was sent"`
}
type SendNotificationRequest struct {
Recipient string `json:"recipient" description:"Email address, phone number, or Slack handle"`
Subject string `json:"subject" description:"Subject line (required)"`
Body string `json:"body" description:"Message body (required)"`
Channel string `json:"channel,omitempty" description:"Channel: email (default), sms, or slack"`
}
type SendNotificationResponse struct {
Notification *Notification `json:"notification" description:"The sent notification with delivery details"`
}
type ListNotificationsRequest struct {
Recipient string `json:"recipient,omitempty" description:"Filter by recipient (optional)"`
}
type ListNotificationsResponse struct {
Notifications []*Notification `json:"notifications" description:"Sent notifications"`
}
type NotificationService struct {
mu sync.RWMutex
notifications []*Notification
nextID int
}
// Send delivers a notification to a recipient via the specified channel.
// Use this to confirm orders, alert users, or send updates.
// Defaults to email if no channel is specified.
//
// @example {"recipient": "alice@example.com", "subject": "Order Confirmed", "body": "Your order ORD-0001 has been confirmed.", "channel": "email"}
func (s *NotificationService) Send(ctx context.Context, req *SendNotificationRequest, rsp *SendNotificationResponse) error {
if req.Recipient == "" {
return fmt.Errorf("recipient is required")
}
if req.Subject == "" {
return fmt.Errorf("subject is required")
}
if req.Body == "" {
return fmt.Errorf("body is required")
}
channel := req.Channel
if channel == "" {
channel = "email"
}
s.mu.Lock()
defer s.mu.Unlock()
s.nextID++
n := &Notification{
ID: fmt.Sprintf("notif-%d", s.nextID),
Recipient: req.Recipient,
Subject: req.Subject,
Body: req.Body,
Channel: channel,
SentAt: time.Now(),
}
s.notifications = append(s.notifications, n)
rsp.Notification = n
return nil
}
// List returns sent notifications, optionally filtered by recipient.
//
// @example {"recipient": "alice@example.com"}
func (s *NotificationService) List(ctx context.Context, req *ListNotificationsRequest, rsp *ListNotificationsResponse) error {
s.mu.RLock()
defer s.mu.RUnlock()
for _, n := range s.notifications {
if req.Recipient != "" && n.Recipient != req.Recipient {
continue
}
rsp.Notifications = append(rsp.Notifications, n)
}
return nil
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
func main() {
service := micro.New("shop",
micro.Address(":9090"),
mcp.WithMCP(":3001"),
)
service.Init()
inventory := &InventoryService{products: map[string]*Product{
"LAPTOP-001": {SKU: "LAPTOP-001", Name: "ThinkPad X1 Carbon", Price: 1299.99, InStock: 15, Category: "electronics"},
"LAPTOP-002": {SKU: "LAPTOP-002", Name: "MacBook Air M3", Price: 1099.00, InStock: 8, Category: "electronics"},
"PHONE-001": {SKU: "PHONE-001", Name: "Pixel 8 Pro", Price: 899.00, InStock: 23, Category: "electronics"},
"BOOK-001": {SKU: "BOOK-001", Name: "Designing Data-Intensive Applications", Price: 45.99, InStock: 50, Category: "books"},
"BOOK-002": {SKU: "BOOK-002", Name: "The Go Programming Language", Price: 39.99, InStock: 0, Category: "books"},
"SHIRT-001": {SKU: "SHIRT-001", Name: "Go Gopher T-Shirt", Price: 24.99, InStock: 100, Category: "clothing"},
}}
orders := &OrderService{
orders: make(map[string]*Order),
inventory: inventory,
}
notifications := &NotificationService{}
service.Handle(inventory)
service.Handle(orders)
service.Handle(notifications)
fmt.Println()
fmt.Println(" Shop Workflow Demo")
fmt.Println()
fmt.Println(" MCP Tools: http://localhost:3001/mcp/tools")
fmt.Println()
fmt.Println(" Try asking an agent:")
fmt.Println()
fmt.Println(" \"What laptops do you have in stock?\"")
fmt.Println(" \"Order a ThinkPad for alice@example.com and send her a confirmation\"")
fmt.Println(" \"Check if 'The Go Programming Language' is available\"")
fmt.Println(" \"Show me all orders for alice@example.com\"")
fmt.Println(" \"Order 3 Go Gopher t-shirts for bob@example.com, reserve the stock, and notify him\"")
fmt.Println()
if err := service.Run(); err != nil {
log.Fatal(err)
}
}
+78
View File
@@ -0,0 +1,78 @@
// 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"
)
// -- 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 := micro.New("users", micro.Address(":9001"))
orders := micro.New("orders", micro.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 := micro.NewGroup(users, orders)
fmt.Println("Starting users (:9001) and orders (:9002) in a single binary")
if err := g.Run(); err != nil {
log.Fatal(err)
}
}
+5 -5
View File
@@ -63,22 +63,22 @@ func homeHandler(w http.ResponseWriter, r *http.Request) {
func usersHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
// Return all users
userList := make([]*User, 0, len(users))
for _, user := range users {
userList = append(userList, user)
}
json.NewEncoder(w).Encode(userList)
}
func userHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
// Extract user ID from path
id := r.URL.Path[len("/users/"):]
user, exists := users[id]
if !exists {
w.WriteHeader(http.StatusNotFound)
@@ -87,7 +87,7 @@ func userHandler(w http.ResponseWriter, r *http.Request) {
})
return
}
json.NewEncoder(w).Encode(user)
}
+259
View File
@@ -0,0 +1,259 @@
package mcp
import (
"bytes"
"context"
"encoding/json"
"log"
"net/http"
"net/http/httptest"
"testing"
"go-micro.dev/v5/auth"
"go-micro.dev/v5/client"
"go-micro.dev/v5/registry"
)
// benchServer creates a Server with N pre-populated tools.
func benchServer(n int, opts Options) *Server {
if opts.Logger == nil {
opts.Logger = log.New(log.Writer(), "", 0)
}
if opts.Context == nil {
opts.Context = context.Background()
}
if opts.Client == nil {
opts.Client = client.DefaultClient
}
if opts.Registry == nil {
opts.Registry = registry.DefaultRegistry
}
s := &Server{
opts: opts,
tools: make(map[string]*Tool, n),
limiters: make(map[string]*rateLimiter),
}
for i := 0; i < n; i++ {
name := toolName(i)
s.tools[name] = &Tool{
Name: name,
Description: "Benchmark tool " + name,
InputSchema: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"id": map[string]interface{}{
"type": "string",
"description": "Resource identifier",
},
},
"required": []interface{}{"id"},
},
Service: "bench",
Endpoint: "Handler.Method",
}
}
return s
}
func toolName(i int) string {
return "bench.Handler.Method" + string(rune('A'+i%26))
}
// --- Benchmarks ---
// BenchmarkListTools measures tool listing throughput.
// This is the most common MCP operation — agents call it on every session start.
func BenchmarkListTools(b *testing.B) {
for _, numTools := range []int{10, 50, 100} {
b.Run(toolCountLabel(numTools), func(b *testing.B) {
s := benchServer(numTools, Options{})
req := httptest.NewRequest("GET", "/mcp/tools", nil)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
w := httptest.NewRecorder()
s.handleListTools(w, req)
if w.Code != http.StatusOK {
b.Fatalf("unexpected status %d", w.Code)
}
}
})
}
}
// BenchmarkListToolsParallel measures concurrent tool listing.
func BenchmarkListToolsParallel(b *testing.B) {
s := benchServer(50, Options{})
req := httptest.NewRequest("GET", "/mcp/tools", nil)
b.ResetTimer()
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
w := httptest.NewRecorder()
s.handleListTools(w, req)
}
})
}
// BenchmarkToolLookup measures tool name resolution from the tools map.
func BenchmarkToolLookup(b *testing.B) {
for _, numTools := range []int{10, 50, 100, 500} {
b.Run(toolCountLabel(numTools), func(b *testing.B) {
s := benchServer(numTools, Options{})
name := toolName(numTools / 2) // look up a tool in the middle
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
s.toolsMu.RLock()
_, ok := s.tools[name]
s.toolsMu.RUnlock()
if !ok {
b.Fatal("tool not found")
}
}
})
}
}
// BenchmarkAuthInspect measures auth token inspection overhead.
func BenchmarkAuthInspect(b *testing.B) {
ma := &mockAuth{
accounts: map[string]*auth.Account{
"valid-token": {
ID: "bench-user",
Scopes: []string{"read", "write"},
},
},
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
acc, err := ma.Inspect("valid-token")
if err != nil || acc.ID != "bench-user" {
b.Fatal("unexpected result")
}
}
}
// BenchmarkScopeCheck measures scope validation overhead per tool call.
func BenchmarkScopeCheck(b *testing.B) {
accountScopes := []string{"users:read", "users:write", "orders:read", "admin"}
requiredScopes := []string{"users:write"}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
hasScope(accountScopes, requiredScopes)
}
}
// BenchmarkAuditRecord measures audit record creation overhead.
func BenchmarkAuditRecord(b *testing.B) {
var records int
s := benchServer(10, Options{
AuditFunc: func(r AuditRecord) {
records++
},
})
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
s.opts.AuditFunc(AuditRecord{
TraceID: "trace-123",
Tool: "bench.Handler.MethodA",
AccountID: "user-1",
Allowed: true,
})
}
}
// BenchmarkRateLimiter measures rate limiter check overhead.
func BenchmarkRateLimiter(b *testing.B) {
s := benchServer(10, Options{
RateLimit: &RateLimitConfig{
RequestsPerSecond: 1000000, // Very high so it doesn't block
Burst: 1000000,
},
})
// Initialize limiters for tools
for name := range s.tools {
s.limiters[name] = newRateLimiter(s.opts.RateLimit.RequestsPerSecond, s.opts.RateLimit.Burst)
}
name := toolName(0)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
s.limitersMu.RLock()
l := s.limiters[name]
s.limitersMu.RUnlock()
l.Allow()
}
}
// BenchmarkJSONEncodeTool measures JSON serialization of tool definitions.
func BenchmarkJSONEncodeTool(b *testing.B) {
tool := &Tool{
Name: "myservice.Users.GetUser",
Description: "Retrieve a user by their unique ID. Returns the full profile.",
InputSchema: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"id": map[string]interface{}{
"type": "string",
"description": "User ID in UUID format",
},
},
"required": []interface{}{"id"},
},
Scopes: []string{"users:read"},
Service: "myservice",
Endpoint: "Users.GetUser",
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
var buf bytes.Buffer
json.NewEncoder(&buf).Encode(tool)
}
}
// BenchmarkJSONDecodeCallRequest measures parsing of incoming tool call requests.
func BenchmarkJSONDecodeCallRequest(b *testing.B) {
body := []byte(`{"tool":"myservice.Users.GetUser","arguments":{"id":"user-123"}}`)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
var req struct {
Tool string `json:"tool"`
Arguments map[string]interface{} `json:"arguments"`
}
json.Unmarshal(body, &req)
}
}
// --- Helpers ---
func toolCountLabel(n int) string {
switch {
case n >= 500:
return "500_tools"
case n >= 100:
return "100_tools"
case n >= 50:
return "50_tools"
default:
return "10_tools"
}
}
+147
View File
@@ -0,0 +1,147 @@
package mcp
import (
"fmt"
"sync"
"time"
)
// CircuitBreakerConfig configures circuit breaking for the MCP gateway.
// When a downstream service fails repeatedly, the circuit opens and
// subsequent calls are rejected immediately until the service recovers.
type CircuitBreakerConfig struct {
// MaxFailures is the number of consecutive failures before the circuit opens.
// Default: 5
MaxFailures int
// Timeout is how long the circuit stays open before allowing a probe request.
// Default: 30s
Timeout time.Duration
// MaxHalfOpen is the number of probe requests allowed in the half-open state.
// If they all succeed, the circuit closes. If any fail, it re-opens.
// Default: 1
MaxHalfOpen int
}
// circuitState represents the state of a circuit breaker.
type circuitState int
const (
circuitClosed circuitState = iota // healthy, requests flow through
circuitOpen // tripped, requests are rejected
circuitHalfOpen // testing recovery with limited requests
)
func (s circuitState) String() string {
switch s {
case circuitClosed:
return "closed"
case circuitOpen:
return "open"
case circuitHalfOpen:
return "half-open"
default:
return "unknown"
}
}
// circuitBreaker tracks failure state for a single tool/service endpoint.
type circuitBreaker struct {
mu sync.Mutex
state circuitState
failures int
maxFailures int
timeout time.Duration
maxHalfOpen int
halfOpenUsed int
lastFailure time.Time
}
func newCircuitBreaker(cfg CircuitBreakerConfig) *circuitBreaker {
maxFailures := cfg.MaxFailures
if maxFailures <= 0 {
maxFailures = 5
}
timeout := cfg.Timeout
if timeout <= 0 {
timeout = 30 * time.Second
}
maxHalfOpen := cfg.MaxHalfOpen
if maxHalfOpen <= 0 {
maxHalfOpen = 1
}
return &circuitBreaker{
state: circuitClosed,
maxFailures: maxFailures,
timeout: timeout,
maxHalfOpen: maxHalfOpen,
}
}
// Allow checks whether a request should be allowed through.
// Returns nil if allowed, error if the circuit is open.
func (cb *circuitBreaker) Allow() error {
cb.mu.Lock()
defer cb.mu.Unlock()
switch cb.state {
case circuitClosed:
return nil
case circuitOpen:
if time.Since(cb.lastFailure) > cb.timeout {
cb.state = circuitHalfOpen
cb.halfOpenUsed = 0
return nil
}
return fmt.Errorf("circuit breaker open (consecutive failures: %d)", cb.failures)
case circuitHalfOpen:
if cb.halfOpenUsed < cb.maxHalfOpen {
cb.halfOpenUsed++
return nil
}
return fmt.Errorf("circuit breaker half-open (probe limit reached)")
}
return nil
}
// RecordSuccess records a successful call. If half-open, closes the circuit.
func (cb *circuitBreaker) RecordSuccess() {
cb.mu.Lock()
defer cb.mu.Unlock()
cb.failures = 0
cb.state = circuitClosed
}
// RecordFailure records a failed call. May trip the circuit open.
func (cb *circuitBreaker) RecordFailure() {
cb.mu.Lock()
defer cb.mu.Unlock()
cb.failures++
cb.lastFailure = time.Now()
switch cb.state {
case circuitClosed:
if cb.failures >= cb.maxFailures {
cb.state = circuitOpen
}
case circuitHalfOpen:
// Probe failed, re-open
cb.state = circuitOpen
}
}
// State returns the current circuit state.
func (cb *circuitBreaker) State() circuitState {
cb.mu.Lock()
defer cb.mu.Unlock()
// Check for automatic transition from open -> half-open
if cb.state == circuitOpen && time.Since(cb.lastFailure) > cb.timeout {
cb.state = circuitHalfOpen
cb.halfOpenUsed = 0
}
return cb.state
}
+154
View File
@@ -0,0 +1,154 @@
package mcp
import (
"testing"
"time"
)
func TestCircuitBreaker_ClosedAllowsRequests(t *testing.T) {
cb := newCircuitBreaker(CircuitBreakerConfig{MaxFailures: 3, Timeout: time.Second})
if err := cb.Allow(); err != nil {
t.Fatalf("expected closed circuit to allow, got: %v", err)
}
if cb.State() != circuitClosed {
t.Fatalf("expected closed state, got %s", cb.State())
}
}
func TestCircuitBreaker_OpensAfterMaxFailures(t *testing.T) {
cb := newCircuitBreaker(CircuitBreakerConfig{MaxFailures: 3, Timeout: time.Minute})
// 2 failures: still closed
cb.RecordFailure()
cb.RecordFailure()
if cb.State() != circuitClosed {
t.Fatalf("expected closed after 2 failures, got %s", cb.State())
}
// 3rd failure: trips open
cb.RecordFailure()
if cb.State() != circuitOpen {
t.Fatalf("expected open after 3 failures, got %s", cb.State())
}
// Requests should be rejected
if err := cb.Allow(); err == nil {
t.Fatal("expected open circuit to reject")
}
}
func TestCircuitBreaker_SuccessResetsFailures(t *testing.T) {
cb := newCircuitBreaker(CircuitBreakerConfig{MaxFailures: 3, Timeout: time.Minute})
cb.RecordFailure()
cb.RecordFailure()
cb.RecordSuccess() // resets
cb.RecordFailure()
cb.RecordFailure()
// Should still be closed (only 2 consecutive failures)
if cb.State() != circuitClosed {
t.Fatalf("expected closed after reset, got %s", cb.State())
}
}
func TestCircuitBreaker_HalfOpenAfterTimeout(t *testing.T) {
cb := newCircuitBreaker(CircuitBreakerConfig{
MaxFailures: 1,
Timeout: 50 * time.Millisecond,
MaxHalfOpen: 1,
})
cb.RecordFailure()
if cb.State() != circuitOpen {
t.Fatalf("expected open, got %s", cb.State())
}
time.Sleep(60 * time.Millisecond)
// Should transition to half-open
if cb.State() != circuitHalfOpen {
t.Fatalf("expected half-open after timeout, got %s", cb.State())
}
// One probe request should be allowed
if err := cb.Allow(); err != nil {
t.Fatalf("expected half-open to allow probe, got: %v", err)
}
// Second should be rejected (maxHalfOpen=1, already used)
if err := cb.Allow(); err == nil {
t.Fatal("expected half-open to reject after max probes")
}
}
func TestCircuitBreaker_HalfOpenSuccessCloses(t *testing.T) {
cb := newCircuitBreaker(CircuitBreakerConfig{
MaxFailures: 1,
Timeout: 50 * time.Millisecond,
})
cb.RecordFailure()
time.Sleep(60 * time.Millisecond)
// Allow probe
if err := cb.Allow(); err != nil {
t.Fatalf("expected probe allowed: %v", err)
}
// Probe succeeds -> circuit closes
cb.RecordSuccess()
if cb.State() != circuitClosed {
t.Fatalf("expected closed after successful probe, got %s", cb.State())
}
}
func TestCircuitBreaker_HalfOpenFailureReopens(t *testing.T) {
cb := newCircuitBreaker(CircuitBreakerConfig{
MaxFailures: 1,
Timeout: 50 * time.Millisecond,
})
cb.RecordFailure()
time.Sleep(60 * time.Millisecond)
// Allow probe
cb.Allow()
// Probe fails -> circuit re-opens
cb.RecordFailure()
if cb.State() != circuitOpen {
t.Fatalf("expected open after failed probe, got %s", cb.State())
}
}
func TestCircuitBreaker_Defaults(t *testing.T) {
cb := newCircuitBreaker(CircuitBreakerConfig{})
if cb.maxFailures != 5 {
t.Fatalf("expected default maxFailures=5, got %d", cb.maxFailures)
}
if cb.timeout != 30*time.Second {
t.Fatalf("expected default timeout=30s, got %s", cb.timeout)
}
if cb.maxHalfOpen != 1 {
t.Fatalf("expected default maxHalfOpen=1, got %d", cb.maxHalfOpen)
}
}
func TestCircuitBreaker_StateString(t *testing.T) {
tests := []struct {
state circuitState
want string
}{
{circuitClosed, "closed"},
{circuitOpen, "open"},
{circuitHalfOpen, "half-open"},
{circuitState(99), "unknown"},
}
for _, tt := range tests {
if got := tt.state.String(); got != tt.want {
t.Errorf("state %d: got %q, want %q", tt.state, got, tt.want)
}
}
}
@@ -0,0 +1,18 @@
apiVersion: v2
name: mcp-gateway
description: Go Micro MCP Gateway - Expose microservices as AI-accessible tools via the Model Context Protocol
type: application
version: 0.1.0
appVersion: "0.1.0"
keywords:
- go-micro
- mcp
- ai
- microservices
- gateway
home: https://go-micro.dev
sources:
- https://github.com/micro/go-micro
maintainers:
- name: go-micro
url: https://github.com/micro/go-micro
@@ -0,0 +1,89 @@
# MCP Gateway Helm Chart
Deploy the Go Micro MCP Gateway on Kubernetes. The gateway discovers go-micro services via a registry and exposes them as AI-accessible tools through the Model Context Protocol.
## Quick Start
```bash
helm install mcp-gateway ./deploy/helm/mcp-gateway \
--set gateway.registry=consul \
--set gateway.registryAddress=consul:8500
```
## Configuration
| Parameter | Description | Default |
|-----------|-------------|---------|
| `replicaCount` | Number of gateway replicas | `1` |
| `image.repository` | Container image | `ghcr.io/micro/mcp-gateway` |
| `image.tag` | Image tag (defaults to appVersion) | `""` |
| `gateway.address` | Listen address | `:3000` |
| `gateway.registry` | Registry backend (mdns, consul, etcd) | `consul` |
| `gateway.registryAddress` | Registry address | `consul:8500` |
| `gateway.rateLimit` | Requests/second per tool (0=unlimited) | `0` |
| `gateway.rateBurst` | Rate limit burst size | `20` |
| `gateway.auth` | Enable JWT authentication | `false` |
| `gateway.audit` | Enable audit logging | `false` |
| `gateway.scopes` | Per-tool scope requirements | `[]` |
| `service.type` | Kubernetes service type | `ClusterIP` |
| `service.port` | Service port | `3000` |
| `ingress.enabled` | Enable ingress | `false` |
| `autoscaling.enabled` | Enable HPA | `false` |
| `autoscaling.minReplicas` | Minimum replicas | `1` |
| `autoscaling.maxReplicas` | Maximum replicas | `10` |
## Examples
### Production with Consul
```bash
helm install mcp-gateway ./deploy/helm/mcp-gateway \
--set replicaCount=3 \
--set gateway.registry=consul \
--set gateway.registryAddress=consul.default.svc:8500 \
--set gateway.auth=true \
--set gateway.audit=true \
--set gateway.rateLimit=100 \
--set autoscaling.enabled=true
```
### With Ingress (nginx)
```bash
helm install mcp-gateway ./deploy/helm/mcp-gateway \
--set ingress.enabled=true \
--set ingress.className=nginx \
--set ingress.hosts[0].host=mcp.example.com \
--set ingress.hosts[0].paths[0].path=/ \
--set ingress.hosts[0].paths[0].pathType=Prefix \
--set ingress.tls[0].secretName=mcp-tls \
--set ingress.tls[0].hosts[0]=mcp.example.com
```
### With Scopes
```bash
helm install mcp-gateway ./deploy/helm/mcp-gateway \
--set gateway.auth=true \
--set 'gateway.scopes[0]=blog.Blog.Create=blog:write' \
--set 'gateway.scopes[1]=blog.Blog.Delete=blog:admin'
```
## Architecture
```
Kubernetes Cluster
┌──────────────────────────────────────────────────────────┐
│ │
│ ┌─────────┐ MCP ┌─────────────┐ RPC ┌──────┐ │
│ │ Ingress │ ───────> │ MCP Gateway │ ──────> │ Svc │ │
│ │ │ │ (N pods) │ │ Pods │ │
│ └─────────┘ └─────────────┘ └──────┘ │
│ │ │ │
│ v v │
│ ┌──────────┐ │
│ │ Consul │ │
│ │ Registry │ │
│ └──────────┘ │
└──────────────────────────────────────────────────────────┘
```
@@ -0,0 +1,31 @@
MCP Gateway has been deployed.
1. Get the gateway URL:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "mcp-gateway.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "mcp-gateway.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else }}
kubectl port-forward --namespace {{ .Release.Namespace }} svc/{{ include "mcp-gateway.fullname" . }} {{ .Values.service.port }}:{{ .Values.service.port }}
echo http://127.0.0.1:{{ .Values.service.port }}
{{- end }}
2. List available MCP tools:
curl http://<GATEWAY_URL>/mcp/tools | jq
3. Connect Claude Code:
Add to your MCP settings:
{
"mcpServers": {
"my-services": {
"url": "http://<GATEWAY_URL>/mcp"
}
}
}

Some files were not shown because too many files have changed in this diff Show More