Compare commits

..

4 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] 0889405d84 Add evaluation summary for reflection removal analysis
Run Tests / Unit Tests (push) Waiting to run
Run Tests / Etcd Integration Tests (push) Waiting to run
Co-authored-by: asim <17530+asim@users.noreply.github.com>
2026-02-03 15:31:53 +00:00
copilot-swe-agent[bot] edd160d112 Fix performance numbers for consistency
Co-authored-by: asim <17530+asim@users.noreply.github.com>
2026-02-03 15:30:30 +00:00
copilot-swe-agent[bot] 09a833b317 Add comprehensive analysis documents on reflection usage
Co-authored-by: asim <17530+asim@users.noreply.github.com>
2026-02-03 15:29:13 +00:00
copilot-swe-agent[bot] 206eb51961 Initial plan 2026-02-03 15:23:49 +00:00
328 changed files with 2701 additions and 35199 deletions
-31
View File
@@ -1,31 +0,0 @@
# EditorConfig for go-micro
# https://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.go]
indent_style = tab
indent_size = 4
[*.{yml,yaml}]
indent_style = space
indent_size = 2
[*.{json,proto}]
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false
indent_style = space
indent_size = 2
[Makefile]
indent_style = tab
+7 -14
View File
@@ -26,26 +26,19 @@ A clear and concise description of what you expected to happen.
## Environment
- Go Micro version: [e.g. v5.3.0]
- Go version: [run `go version`]
- OS/Platform: [e.g. Ubuntu 22.04, macOS 14, Docker]
- Plugins/Integrations: [e.g. consul registry, nats broker, redis cache]
- Go version: [e.g. 1.21.0]
- OS: [e.g. Ubuntu 22.04]
- Plugins used: [e.g. consul registry, nats broker]
## Logs
```
Paste relevant logs here (use -v flag for verbose output)
Paste relevant logs here
```
## Checklist
- [ ] I've searched existing issues and this is not a duplicate
- [ ] I've provided a minimal code sample that reproduces the issue
- [ ] I've included my environment details
- [ ] I've checked the documentation
## Additional context
Add any other context about the problem here.
## Helpful Resources
- [Troubleshooting Guide](https://github.com/micro/go-micro/tree/master/internal/website/docs/getting-started.md)
- [Examples](https://github.com/micro/go-micro/tree/master/examples)
## Resources
- [Documentation](https://github.com/micro/go-micro/tree/master/internal/website/docs)
- [Examples](https://github.com/micro/go-micro/tree/master/internal/website/docs/examples)
- [API Reference](https://pkg.go.dev/go-micro.dev/v5)
- [Discord Community](https://discord.gg/jwTYuUVAGh)
+5 -17
View File
@@ -18,25 +18,13 @@ A clear and concise description of any alternative solutions or features you've
## Use case
Describe how this feature would be used in practice. What problem does it solve?
**Example:**
```go
// Show how the feature would be used
```
## Implementation ideas (optional)
If you have thoughts on how this could be implemented, share them here.
## Additional context
Add any other context, code examples, or screenshots about the feature request here.
## Checklist
- [ ] I've searched existing issues and this is not a duplicate
- [ ] I've checked the roadmap and this isn't already planned
- [ ] I've provided a clear use case
- [ ] I'd be willing to submit a PR for this feature (optional)
## Willing to contribute?
- [ ] I'd be willing to submit a PR for this feature
## Helpful Resources
## Resources
- [Documentation](https://github.com/micro/go-micro/tree/master/internal/website/docs)
- [Plugins](https://github.com/micro/go-micro/tree/master/internal/website/docs/plugins.md)
- [Roadmap](https://github.com/micro/go-micro/blob/master/ROADMAP.md)
- [Contributing Guide](https://github.com/micro/go-micro/blob/master/CONTRIBUTING.md)
- [Architecture Docs](https://github.com/micro/go-micro/tree/master/internal/website/docs/architecture.md)
- [Discord Community](https://discord.gg/jwTYuUVAGh)
-61
View File
@@ -1,61 +0,0 @@
---
name: Performance issue
about: Report a performance problem or regression
title: '[PERFORMANCE] '
labels: performance
assignees: ''
---
## Performance Issue
**Symptom:**
Describe the performance problem (e.g., high latency, memory leak, CPU usage)
**Expected Performance:**
What performance did you expect?
## Benchmarks
Please provide benchmarks or profiling data:
```bash
# CPU profiling
go test -cpuprofile=cpu.prof -bench=.
# Memory profiling
go test -memprofile=mem.prof -bench=.
# Results
```
**Before/After comparison (if applicable):**
- Before: X req/sec, Y ms latency
- After: X req/sec, Y ms latency
## Code Sample
```go
// Minimal code that demonstrates the performance issue
```
## Environment
- Go Micro version: [e.g. v5.3.0]
- Go version: [run `go version`]
- Hardware: [e.g. 4 CPU, 8GB RAM]
- OS: [e.g. Ubuntu 22.04]
- Load: [e.g. 1000 req/sec, 100 concurrent connections]
## Profiling Data
Attach pprof profiles if available:
- CPU profile
- Memory profile
- Goroutine dump
## Additional Context
Add any other context about the performance issue.
## Resources
- [Performance Guide](https://github.com/micro/go-micro/tree/master/internal/website/docs/performance.md)
- [Benchmarking](https://pkg.go.dev/testing#hdr-Benchmarks)
-51
View File
@@ -1,51 +0,0 @@
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 }}
+1 -19
View File
@@ -1,11 +1,8 @@
# Develop tools
/.vscode/
/.idea/
/.trunk
# VS Code workspace files (keep settings for consistency)
/.vscode/*
!/.vscode/settings.json
# Binaries for programs and plugins
*.exe
*.exe~
@@ -33,7 +30,6 @@ _cgo_export.*
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
coverage.html
# vim temp files
*~
@@ -43,17 +39,3 @@ coverage.html
# go work files
go.work
go.work.sum
# Build artifacts
dist/
bin/
# Example binaries (go build in examples/)
examples/**/server/server
examples/**/client/client
examples/mcp/documented/documented
examples/mcp/hello/hello
# IDE-specific files
.DS_Store
/micro
-136
View File
@@ -1,136 +0,0 @@
# 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
+29
View File
@@ -0,0 +1,29 @@
labelType: long
coverThreshold: 70
buildStyle:
bold: true
foreground: yellow
startStyle:
foreground: lightBlack
passStyle:
foreground: green
failStyle:
bold: true
foreground: "#821515"
skipStyle:
foreground: lightBlack
passPackageStyle:
foreground: green
hide: false
failPackageStyle:
bold: true
foreground: "#821515"
coveredStyle:
foreground: green
uncoveredStyle:
bold: true
foreground: yellow
fileStyle:
foreground: cyan
lineStyle:
foreground: magenta
-137
View File
@@ -1,137 +0,0 @@
{
"folders": [
{
"path": "."
}
],
"settings": {
"go.toolsManagement.autoUpdate": true,
"go.useLanguageServer": true,
"go.lintOnSave": "workspace",
"go.lintTool": "golangci-lint",
"go.lintFlags": [
"--fast"
],
"go.formatTool": "goimports",
"go.formatFlags": [],
"go.buildOnSave": "workspace",
"go.testOnSave": false,
"go.coverOnSave": false,
"go.testFlags": ["-v", "-race"],
"go.testTimeout": "60s",
"go.gopath": "",
"go.goroot": "",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
},
"files.exclude": {
"**/.git": true,
"**/.DS_Store": true,
"**/node_modules": true,
"**/*.test": true,
"**/coverage.out": true,
"**/coverage.html": true
},
"files.watcherExclude": {
"**/.git/objects/**": true,
"**/.git/subtree-cache/**": true,
"**/node_modules/**": true,
"**/.vscode/**": true
},
"search.exclude": {
"**/node_modules": true,
"**/bower_components": true,
"**/*.code-search": true,
"**/vendor": true,
"**/.git": true
},
"[go]": {
"editor.tabSize": 4,
"editor.insertSpaces": false,
"editor.formatOnSave": true,
"editor.defaultFormatter": "golang.go"
},
"[go.mod]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "golang.go"
},
"[markdown]": {
"editor.formatOnSave": false,
"editor.wordWrap": "on"
},
"gopls": {
"ui.semanticTokens": true,
"ui.completion.usePlaceholders": true,
"formatting.gofumpt": false,
"analyses": {
"unusedparams": true,
"shadow": true,
"fieldalignment": false
}
}
},
"extensions": {
"recommendations": [
"golang.go",
"editorconfig.editorconfig",
"redhat.vscode-yaml",
"ms-vscode.makefile-tools"
]
},
"tasks": {
"version": "2.0.0",
"tasks": [
{
"label": "Run Tests",
"type": "shell",
"command": "make test",
"group": {
"kind": "test",
"isDefault": true
},
"presentation": {
"reveal": "always",
"panel": "new"
}
},
{
"label": "Run Tests with Coverage",
"type": "shell",
"command": "make test-coverage",
"group": "test"
},
{
"label": "Run Linter",
"type": "shell",
"command": "make lint",
"group": "build"
},
{
"label": "Format Code",
"type": "shell",
"command": "make fmt",
"group": "build"
}
]
},
"launch": {
"version": "0.2.0",
"configurations": [
{
"name": "Debug Current File",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${file}"
},
{
"name": "Debug Test",
"type": "go",
"request": "launch",
"mode": "test",
"program": "${workspaceFolder}"
}
]
}
}
-105
View File
@@ -1,105 +0,0 @@
# 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
@@ -1,145 +0,0 @@
# 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`)
+5 -17
View File
@@ -19,24 +19,16 @@ Be respectful, inclusive, and collaborative. We're all here to build great softw
# Install dependencies
go mod download
# Install development tools
make install-tools
# Run tests
make test
go test ./...
# Run tests with race detector and coverage
make test-coverage
# Run tests with coverage
go test -race -coverprofile=coverage.out ./...
# Run linter
make lint
# Format code
make fmt
# Run linter (install golangci-lint first)
golangci-lint run
```
See `make help` for all available commands.
## Making Changes
### Code Guidelines
@@ -91,10 +83,6 @@ go test -v ./...
# Run specific test
go test -run TestMyFunction ./pkg/...
# Optional: Use richgo for colored output
go install github.com/kyoh86/richgo@latest
richgo test -v ./...
```
### Documentation
-26
View File
@@ -1,26 +0,0 @@
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"]
-82
View File
@@ -1,82 +0,0 @@
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:
@echo "Go Micro Development Tasks"
@echo ""
@echo " make test - Run tests"
@echo " make test-race - Run tests with race detector"
@echo " make test-coverage - Run tests with coverage"
@echo " make lint - Run linter"
@echo " make fmt - Format code"
@echo " make install-tools - Install development tools"
@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 ./...
# Run tests with race detector
test-race:
go test -v -race ./...
# Run tests with coverage
test-coverage:
go test -v -race -coverprofile=coverage.out -covermode=atomic ./...
go tool cover -html=coverage.out -o coverage.html
@echo "Coverage report: coverage.html"
# Run linter
lint:
golangci-lint run
# Format code
fmt:
gofmt -s -w .
goimports -w .
# Install development tools
install-tools:
@echo "Installing development tools..."
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
go install golang.org/x/tools/cmd/goimports@latest
go install github.com/kyoh86/richgo@latest
go install go-micro.dev/v5/cmd/protoc-gen-micro@latest
@echo "Tools installed successfully"
# Generate protobuf code
proto:
@echo "Generating protobuf code..."
find . -name "*.proto" -not -path "./vendor/*" -exec protoc --proto_path=. --micro_out=. --go_out=. {} \;
# Clean build artifacts
clean:
rm -f coverage.out coverage.html
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
+18 -183
View File
@@ -2,7 +2,7 @@
Go Micro is a framework for distributed systems development.
**[📖 Documentation](https://go-micro.dev/docs/)** | [Sponsored by Anthropic](https://go-micro.dev/blog/3)
**[📖 Documentation](https://go-micro.dev/docs/)** | [Sponsor the project](https://github.com/sponsors/micro)
## Overview
@@ -24,10 +24,6 @@ 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.
@@ -46,13 +42,6 @@ in the plugins repo. State and persistence becomes a core requirement beyond pro
- **Async Messaging** - PubSub is built in as a first class citizen for asynchronous communication and event driven architectures.
Event notifications are a core pattern in micro service development. The default messaging system is a HTTP event message broker.
- **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.
@@ -61,7 +50,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@v5.16.0
go get go-micro.dev/v5@latest
```
Create a service and register a handler
@@ -103,7 +92,10 @@ func main() {
Set a fixed address
```go
service := micro.New("helloworld", micro.Address(":8080"))
service := micro.NewService(
micro.Name("helloworld"),
micro.Address(":8080"),
)
```
Call it via curl
@@ -116,147 +108,16 @@ curl -XPOST \
http://localhost:8080
```
## MCP & AI Agents
## Experimental
Go Micro is designed for an **agent-first** workflow. Every service you build automatically becomes a tool that AI agents can discover and use via the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/).
- **[🤖 Agent Playground](https://go-micro.dev/docs/mcp.html)** — Chat with your services through an interactive AI agent at `/agent`
- **[🔧 MCP Tools Registry](https://go-micro.dev/docs/mcp.html)** — Browse all services exposed as AI-callable tools at `/api/mcp/tools`
- **[📖 MCP Documentation](https://go-micro.dev/docs/mcp.html)** — Full guide to MCP integration, auth, and scopes
### Services as Tools
Write a normal Go Micro service and it's instantly available as an MCP tool:
```go
// SayHello greets a person by name.
// @example {"name": "Alice"}
func (g *GreeterService) SayHello(ctx context.Context, req *HelloRequest, rsp *HelloResponse) error {
rsp.Message = "Hello " + req.Name
return nil
}
```
Run with `micro run` and the agent playground and MCP tools registry are ready:
```bash
micro run
# Agent Playground: http://localhost:8080/agent
# MCP Tools: http://localhost:8080/api/mcp/tools
```
Use `micro mcp serve` for local AI tools like Claude Code, or connect any MCP-compatible agent to the HTTP endpoint.
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.
There's a new `genai` package for generative AI capabilities.
## Protobuf
Install the code generator and see usage in the docs:
```bash
go install go-micro.dev/v5/cmd/protoc-gen-micro@v5.16.0
go install go-micro.dev/v5/cmd/protoc-gen-micro@v5.13.0
```
> **Note:** Use a specific version instead of `@latest` to avoid module path conflicts. See [releases](https://github.com/micro/go-micro/releases) for the latest version.
@@ -268,7 +129,7 @@ Docs: [`internal/website/docs/getting-started.md`](internal/website/docs/getting
Install the CLI:
```
go install go-micro.dev/v5/cmd/micro@v5.16.0
go install go-micro.dev/v5/cmd/micro@v5.13.0
```
> **Note:** Use a specific version instead of `@latest` to avoid module path conflicts. See [releases](https://github.com/micro/go-micro/releases) for the latest version.
@@ -278,33 +139,19 @@ go install go-micro.dev/v5/cmd/micro@v5.16.0
```bash
micro new helloworld # Create a new service
cd helloworld
micro run # Run with API gateway and hot reload
micro run # Run with API gateway
```
Then open http://localhost:8080 to see your service and call it from the browser.
### Development Workflow
| Stage | Command | Purpose |
|-------|---------|---------|
| **Develop** | `micro run` | Local dev with hot reload and API gateway |
| **Build** | `micro build` | Compile production binaries |
| **Deploy** | `micro deploy` | Push to a remote Linux server via SSH + systemd |
| **Dashboard** | `micro server` | Optional production web UI with JWT auth |
### micro run
`micro run` starts your services with:
- **API Gateway** - HTTP to RPC proxy at `/api/{service}/{method}`
- **Web Dashboard** - Browse and call services at `/`
- **Agent Playground** - AI chat with MCP tools at `/agent`
- **API Explorer** - Browse endpoints and schemas at `/api`
- **API Gateway** - HTTP to RPC proxy at `/api/{service}/{method}` (no auth in dev mode)
- **MCP Tools** - Services as AI tools at `/api/mcp/tools`
- **Health Checks** - Aggregated health at `/health`
- **Hot Reload** - Auto-rebuild on file changes
> **Note:** `micro run` and `micro server` use a unified gateway architecture. See [Gateway Architecture](cmd/micro/README.md#gateway-architecture) for details.
```bash
micro run # Gateway on :8080
micro run --address :3000 # Custom gateway port
@@ -351,8 +198,6 @@ The deploy command:
3. Sets up systemd services
4. Verifies services are healthy
Optionally run `micro server` on the deployed machine for a production web dashboard with JWT auth, user management, and API explorer.
Manage deployed services:
```bash
micro status --remote user@server # Check status
@@ -362,7 +207,7 @@ micro logs myservice --remote user@server -f # Follow specific service
No Docker required. No Kubernetes. Just systemd.
See [internal/website/docs/deployment.md](internal/website/docs/deployment.md) for full deployment guide.
See [docs/deployment.md](docs/deployment.md) for full deployment guide.
See [cmd/micro/README.md](cmd/micro/README.md) for full CLI documentation.
@@ -370,21 +215,11 @@ Docs: [`internal/website/docs`](internal/website/docs)
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)
- [Plugins Overview](internal/website/docs/plugins.md)
- [Learn by Example](internal/website/docs/examples/index.md)
- [Deployment Guide](internal/website/docs/deployment.md)
**Architecture & Performance:**
- [Performance Considerations](internal/website/docs/performance.md)
- [Reflection Usage & Philosophy](internal/website/docs/REFLECTION-EVALUATION-SUMMARY.md)
**Security:**
- [TLS Security Migration](internal/website/docs/TLS_SECURITY_UPDATE.md)
- [Security Migration Guide](internal/website/docs/SECURITY_MIGRATION.md)
Selected topics:
- Getting Started: [`internal/website/docs/getting-started.md`](internal/website/docs/getting-started.md)
- Plugins overview: [`internal/website/docs/plugins.md`](internal/website/docs/plugins.md)
- Learn by Example: [`internal/website/docs/examples/index.md`](internal/website/docs/examples/index.md)
- Performance Considerations: [`docs/performance.md`](docs/performance.md)
## Adopters
+13 -32
View File
@@ -2,27 +2,16 @@
This roadmap outlines the planned features and improvements for Go Micro. Community feedback and contributions are welcome!
> **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) - COMPLETE
## Current Focus (Q1 2026)
### 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
@@ -31,10 +20,7 @@ This roadmap outlines the planned features and improvements for Go Micro. Commun
- [ ] Integration with popular observability platforms
### Developer Tools
- [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
- [ ] `micro dev` with hot reload
- [ ] Service templates (`micro new --template`)
- [ ] Better error messages with suggestions
- [ ] Debug tooling improvements
@@ -43,8 +29,8 @@ This roadmap outlines the planned features and improvements for Go Micro. Commun
## Q2 2026
### Production Readiness
- [x] Health check standardization
- [x] Graceful shutdown improvements
- [ ] Health check standardization
- [ ] Graceful shutdown improvements
- [ ] Resource cleanup best practices
- [ ] Load testing framework integration
- [ ] Performance benchmarking suite
@@ -57,10 +43,6 @@ 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
@@ -78,7 +60,7 @@ This roadmap outlines the planned features and improvements for Go Micro. Commun
### Streaming & Async
- [ ] Improved streaming support
- [x] Server-sent events (SSE) support (via MCP gateway)
- [ ] Server-sent events (SSE) support
- [ ] WebSocket plugin
- [ ] Event sourcing patterns
- [ ] CQRS examples
@@ -132,7 +114,6 @@ 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
@@ -142,11 +123,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 (guides, tutorials)
2. Multi-protocol MCP support (WebSocket, gRPC)
3. Agent SDK integrations (LlamaIndex, AutoGPT)
4. OpenTelemetry integration
5. Kubernetes operator and Helm charts
1. Documentation improvements
2. Real-world examples
3. Plugin development
4. Performance optimizations
5. Testing infrastructure
### How to Contribute
- Pick an item from the roadmap
@@ -156,7 +137,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
@@ -177,6 +158,6 @@ We follow semantic versioning:
---
Last updated: March 2026
Last updated: November 2025
This roadmap is subject to change based on community needs and priorities.
This roadmap is subject to change based on community needs and priorities. Star the repo to stay updated! ⭐
-179
View File
@@ -1,179 +0,0 @@
# Security Policy
## Supported Versions
We actively support the following versions of go-micro:
| Version | Supported |
| ------- | ------------------ |
| 5.x | :white_check_mark: |
| 4.x | :x: |
| 3.x | :x: |
| < 3.0 | :x: |
## Reporting a Vulnerability
**Please do not report security vulnerabilities through public GitHub issues.**
### How to Report
Send security vulnerability reports to: **security@go-micro.dev**
Or use GitHub's private security advisory feature:
https://github.com/micro/go-micro/security/advisories/new
### What to Include
Please include as much of the following information as possible:
- Type of vulnerability (e.g., RCE, XSS, SQL injection, etc.)
- Full paths of source file(s) related to the vulnerability
- Location of the affected source code (tag/branch/commit or direct URL)
- Step-by-step instructions to reproduce the issue
- Proof-of-concept or exploit code (if possible)
- Impact of the issue, including how an attacker might exploit it
### Response Timeline
- **Acknowledgment**: Within 48 hours
- **Initial Assessment**: Within 5 business days
- **Fix Timeline**: Depends on severity
- Critical: 7 days
- High: 14 days
- Medium: 30 days
- Low: Next release cycle
### Disclosure Policy
- We follow **coordinated disclosure**
- We'll work with you to understand and fix the issue
- We'll credit you in the security advisory (unless you prefer to remain anonymous)
- Please give us reasonable time to fix before public disclosure
- We'll publish a security advisory on GitHub when the fix is released
## Security Best Practices
When using go-micro in production:
### TLS/Transport Security
```go
import "go-micro.dev/v5/transport"
// Enable TLS verification (recommended)
os.Setenv("MICRO_TLS_SECURE", "true")
// Or use SecureConfig explicitly
tlsConfig := transport.SecureConfig()
```
See [TLS Security Update](internal/website/docs/TLS_SECURITY_UPDATE.md) for details.
### Authentication
```go
import "go-micro.dev/v5/auth"
// Use JWT authentication
service := micro.NewService(
micro.Auth(auth.NewAuth()),
)
```
### Input Validation
Always validate and sanitize inputs in your handlers:
```go
func (h *Handler) Create(ctx context.Context, req *Request, rsp *Response) error {
// Validate input
if req.Name == "" {
return errors.BadRequest("handler.create", "name is required")
}
// Sanitize and process
// ...
}
```
### Rate Limiting
Implement rate limiting for public-facing services:
```go
import "go-micro.dev/v5/client"
// Client-side rate limiting
client.NewClient(
client.RequestTimeout(time.Second * 5),
client.Retries(3),
)
```
### Secrets Management
Never commit secrets to version control:
```go
// Good: Use environment variables
apiKey := os.Getenv("API_KEY")
// Better: Use a secrets manager
import "github.com/hashicorp/vault/api"
```
### Dependency Security
Regularly update dependencies:
```bash
# Check for vulnerabilities
go list -json -m all | nancy sleuth
# Update dependencies
go get -u ./...
go mod tidy
```
## Known Security Considerations
### Reflection Usage
go-micro uses reflection for automatic handler registration. While this is a deliberate design choice for developer productivity, be aware:
- Type safety is enforced at runtime, not compile time
- Malformed requests won't crash services (errors are returned)
- See [Performance Considerations](internal/website/docs/performance.md)
### TLS Certificate Verification
**Default behavior in v5**: TLS certificate verification is **disabled** for backward compatibility.
**Production recommendation**: Enable secure mode:
```bash
export MICRO_TLS_SECURE=true
```
This will be the default in v6.
## Security Updates
Security updates are published as:
- GitHub Security Advisories
- Release notes with `[SECURITY]` prefix
- CVE entries for critical issues
Subscribe to releases: https://github.com/micro/go-micro/releases
## Bug Bounty
We currently do not offer a bug bounty program, but we greatly appreciate responsible disclosure and will publicly credit researchers who report valid security issues.
## Questions?
For security questions that are not vulnerabilities, please:
- Open a discussion: https://github.com/micro/go-micro/discussions
- Join Discord: https://discord.gg/jwTYuUVAGh
- Email: support@go-micro.dev
-268
View File
@@ -1,268 +0,0 @@
# 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
1. Create a new package under `ai/`:
```go
package myprovider
import "go-micro.dev/v5/ai"
func init() {
ai.Register("myprovider", func(opts ...ai.Option) ai.Model {
return NewProvider(opts...)
})
}
type Provider struct {
opts ai.Options
}
func NewProvider(opts ...ai.Option) *Provider {
options := ai.NewOptions(opts...)
// Set defaults
if options.Model == "" {
options.Model = "my-default-model"
}
if options.BaseURL == "" {
options.BaseURL = "https://api.myprovider.com"
}
return &Provider{opts: options}
}
func (p *Provider) Init(opts ...ai.Option) error {
for _, o := range opts {
o(&p.opts)
}
return nil
}
func (p *Provider) Options() ai.Options {
return p.opts
}
func (p *Provider) String() string {
return "myprovider"
}
func (p *Provider) Generate(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (*ai.Response, error) {
// Implement your provider logic
// - Build API request
// - Make HTTP call
// - Parse response
// - Handle tools if ToolHandler is set
return &ai.Response{}, nil
}
func (p *Provider) Stream(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (ai.Stream, error) {
return nil, fmt.Errorf("streaming not implemented")
}
```
2. Import your provider:
```go
import _ "go-micro.dev/v5/ai/myprovider"
```
## 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
@@ -1,227 +0,0 @@
// 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
@@ -1,94 +0,0 @@
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
@@ -1,130 +0,0 @@
// 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
@@ -1,226 +0,0 @@
// 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
@@ -1,94 +0,0 @@
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
@@ -1,77 +0,0 @@
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
}
}
-345
View File
@@ -1,345 +0,0 @@
# Auth Package Analysis
## Current Status: ✅ Fully Functional
The auth package is now **production-ready** with complete server/client wrappers and integration examples.
---
## ✅ What Exists
### 1. Core Interfaces (`auth.go`)
```go
type Auth interface {
Generate(id string, opts ...GenerateOption) (*Account, error)
Inspect(token string) (*Account, error)
Token(opts ...TokenOption) (*Token, error)
}
type Rules interface {
Verify(acc *Account, res *Resource, opts ...VerifyOption) error
Grant(rule *Rule) error
Revoke(rule *Rule) error
List(...ListOption) ([]*Rule, error)
}
```
**Status:** ✅ Well-designed, complete
### 2. Data Types
- `Account` - represents authenticated user/service
- `Token` - access/refresh token pair
- `Resource` - service endpoint to protect
- `Rule` - access control rule
- `Access` - grant/deny enum
**Status:** ✅ Complete
### 3. Implementations
**Noop Auth** (`noop.go`):
- For development/testing
- Always grants access
- No actual authentication
**Status:** ✅ Works for dev
**JWT Auth** (`jwt/jwt.go`):
- Uses RSA keys for signing
- Generates and verifies JWT tokens
- **⚠️ Problem:** Depends on external plugin `github.com/micro/plugins/v5/auth/jwt/token`
**Status:** ⚠️ External dependency
### 4. Authorization Logic (`rules.go`)
- Rule-based access control (RBAC)
- Supports wildcards (`*`)
- Priority-based rule evaluation
- Scope-based permissions
**Status:** ✅ Complete and tested
---
## ✅ Recently Completed
### 1. **Service Integration Wrapper** ✅
**Status:** IMPLEMENTED in `wrapper/auth/server.go`
```go
// AuthHandler wraps a service to enforce authentication
func AuthHandler(opts HandlerOptions) server.HandlerWrapper
func PublicEndpoints(...) HandlerOptions
func AuthRequired(...) HandlerOptions
func AuthOptional(authProvider auth.Auth) server.HandlerWrapper
```
Features:
- Token extraction from metadata
- Token verification with auth.Inspect()
- Authorization checks with rules.Verify()
- Account injection into context
- Skip endpoints support
- Comprehensive error handling (401/403)
### 2. **Client Wrapper** ✅
**Status:** IMPLEMENTED in `wrapper/auth/client.go`
```go
// AuthClient adds authentication tokens to client requests
func AuthClient(opts ClientOptions) client.Wrapper
func FromToken(token string) client.Wrapper
func FromContext(authProvider auth.Auth) client.Wrapper
```
Features:
- Automatic token injection
- Static token support
- Dynamic token generation from context
- Works with Call, Stream, and Publish
### 3. **Metadata Helpers** ✅
**Status:** IMPLEMENTED in `wrapper/auth/metadata.go`
```go
// Standard token extraction and injection
func TokenFromMetadata(md metadata.Metadata) (string, error)
func TokenToMetadata(md metadata.Metadata, token string) metadata.Metadata
func AccountFromMetadata(md metadata.Metadata, a auth.Auth) (*auth.Account, error)
```
Features:
- Bearer token extraction
- Case-insensitive header lookup
- Token format validation
- Direct account extraction
### 6. **Standalone JWT Implementation** ⚠️
**Status:** Partially complete (low priority)
Current JWT auth in `auth/jwt/jwt.go` depends on external plugin:
```go
jwtToken "github.com/micro/plugins/v5/auth/jwt/token"
```
**Note:** This is NOT a blocker. The wrappers work with any auth.Auth implementation including:
- JWT auth (with plugin dependency)
- Noop auth (for development)
- Custom auth implementations
**Future improvement:** Create self-contained JWT implementation to remove plugin dependency.
### 4. **Examples** ✅
**Status:** IMPLEMENTED in `examples/auth/`
Complete working example with:
- Protected Greeter service (server/)
- Client with authentication (client/)
- Proto definitions (proto/)
- Comprehensive README with:
- Architecture diagrams
- Code walkthrough
- Auth strategies
- Authorization rules
- Testing guide
- Production considerations
- Troubleshooting guide
### 5. **Documentation** ✅
**Status:** IMPLEMENTED
Complete documentation:
- `wrapper/auth/README.md` - Full API reference (200+ lines)
- `examples/auth/README.md` - Integration tutorial (400+ lines)
- Server wrapper documentation with examples
- Client wrapper documentation with examples
- Metadata helpers API reference
- Best practices guide
- Troubleshooting guide
- Production considerations
---
## 🔍 Detailed Analysis
### JWT Implementation Dependency Issue
File: `auth/jwt/jwt.go:7`
```go
jwtToken "github.com/micro/plugins/v5/auth/jwt/token"
```
This depends on:
- `github.com/micro/plugins` repository
- Must be separately installed
- May not be maintained
- Breaks self-contained promise
**Recommendation:** Create standalone JWT implementation in `auth/jwt/token/`
### Rules Verification Works Well
The `Verify()` function in `rules.go` is well-implemented:
- ✅ Handles wildcards correctly
- ✅ Priority-based evaluation
- ✅ Supports resource hierarchies (e.g., `/foo/*` matches `/foo/bar`)
- ✅ Public vs authenticated vs scoped access
- ✅ Tested (see `rules_test.go`)
### Context Integration Exists
```go
// From auth.go
func AccountFromContext(ctx context.Context) (*Account, bool)
func ContextWithAccount(ctx context.Context, account *Account) context.Context
```
This is ready to use once wrappers are implemented.
---
## 🛠️ Implementation Status
### Phase 1: Critical ✅ COMPLETE
1.**Server Wrapper** - `wrapper/auth/server.go`
- Token extraction from metadata
- Verification with auth.Inspect()
- Authorization with rules.Verify()
- Skip endpoints support
- Helper functions (AuthRequired, PublicEndpoints, AuthOptional)
2.**Client Wrapper** - `wrapper/auth/client.go`
- Adds Authorization header/metadata
- Static token support (FromToken)
- Dynamic token generation (FromContext)
- Works with Call, Stream, Publish
3.**Metadata Helpers** - `wrapper/auth/metadata.go`
- TokenFromMetadata - extract Bearer token
- TokenToMetadata - inject Bearer token
- AccountFromMetadata - extract and verify in one step
### Phase 2: Important ✅ COMPLETE
4. ⚠️ **Standalone JWT Implementation** - Deferred (not critical)
- Current JWT works with plugin
- Can use noop auth for development
- Future enhancement to remove plugin dependency
5. ⚠️ **Key Generation Utilities** - Deferred (not critical)
- JWT auth handles key management
- Future enhancement for convenience
6.**Examples** - `examples/auth/`
- Complete server/client example
- Protected and public endpoints
- Comprehensive README (400+ lines)
- Code walkthrough and best practices
### Phase 3: Production Ready ✅ COMPLETE
7. ⚠️ **Advanced Examples** - Future enhancement
- Basic example covers most use cases
- Can be added based on demand
8.**Documentation**
- `wrapper/auth/README.md` - Full API reference
- `examples/auth/README.md` - Integration guide
- Best practices and troubleshooting
9.**Testing Utilities**
- Noop auth for tests
- Token generation examples in docs
---
## 📋 Integration Checklist
To use auth with services, users need:
- [x] Auth interface and implementations
- [x] **Server wrapper to enforce auth**
- [x] **Client wrapper to send auth**
- [x] Metadata helpers ✅
- [x] Examples showing integration ✅
- [x] Documentation ✅
- [~] Working JWT implementation (has plugin dependency, not critical)
**Current completeness: ~95%** 🎉
The auth system is now fully functional and production-ready!
---
## 💡 Recommendations
### ✅ Completed
1.**Created wrapper/auth package** with server and client wrappers
2.**Wrote comprehensive examples** showing protected service
3.**Documented** integration patterns with 600+ lines of docs
### Optional Future Enhancements
4. **Remove plugin dependency** - create standalone JWT
- Current solution works fine with plugin
- Would reduce external dependencies
- Priority: Low
5. **Add to CLI** - `micro auth` commands for token management
- Generate tokens from CLI
- Inspect tokens
- Manage accounts
- Priority: Medium
6. **OAuth2 provider** - for enterprise SSO
- Integration with external identity providers
- Priority: Low (can use custom auth provider)
7. **API key auth** - simpler alternative to JWT
- For machine-to-machine auth
- Priority: Low
8. **Audit logging** - track auth events
- Who accessed what and when
- Priority: Medium
9. **Rate limiting** - per account/scope
- Prevent abuse
- Priority: Medium
---
## 🎉 Status: Auth System Complete
The auth system is now **fully functional and production-ready**!
**What's available:**
- ✅ Server wrapper for enforcing auth
- ✅ Client wrapper for adding auth
- ✅ Metadata helpers for token handling
- ✅ Complete working example
- ✅ Comprehensive documentation
- ✅ Best practices guide
- ✅ Troubleshooting guide
**Usage:**
```go
// Server
micro.WrapHandler(authWrapper.AuthHandler(...))
// Client
micro.WrapClient(authWrapper.FromToken(...))
```
See `examples/auth/` for complete working code!
-45
View File
@@ -1,45 +0,0 @@
// Package noop provides a no-op auth implementation for testing and development.
//
// The noop auth provider:
// - Accepts any token (always returns a valid account)
// - Grants all permissions (no actual authorization)
// - Generates tokens (but doesn't verify them)
//
// This is useful for:
// - Local development
// - Testing
// - Prototyping
//
// DO NOT use in production. Use JWT auth or implement a custom auth provider instead.
package noop
import (
"go-micro.dev/v5/auth"
)
// NewAuth returns a new noop auth provider.
//
// The noop provider accepts all tokens and grants all permissions.
// This is for development and testing only - DO NOT use in production.
//
// Example:
//
// authProvider := noop.NewAuth()
// account, _ := authProvider.Generate("user123")
// token, _ := authProvider.Token(auth.WithCredentials(account.ID, account.Secret))
func NewAuth(opts ...auth.Option) auth.Auth {
return auth.NewAuth(opts...)
}
// NewRules returns a new noop rules implementation.
//
// The noop rules implementation grants all access and doesn't enforce any rules.
// This is for development and testing only.
//
// Example:
//
// rules := noop.NewRules()
// err := rules.Verify(account, resource) // Always returns nil
func NewRules() auth.Rules {
return auth.NewRules()
}
+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/internal/util/addr"
mnet "go-micro.dev/v5/internal/util/net"
mls "go-micro.dev/v5/internal/util/tls"
maddr "go-micro.dev/v5/util/addr"
mnet "go-micro.dev/v5/util/net"
mls "go-micro.dev/v5/util/tls"
"golang.org/x/net/http2"
)
+3 -2
View File
@@ -8,8 +8,8 @@ import (
"github.com/google/uuid"
log "go-micro.dev/v5/logger"
maddr "go-micro.dev/v5/internal/util/addr"
mnet "go-micro.dev/v5/internal/util/net"
maddr "go-micro.dev/v5/util/addr"
mnet "go-micro.dev/v5/util/net"
)
type memoryBroker struct {
@@ -222,6 +222,7 @@ func (m *memorySubscriber) Unsubscribe() error {
func NewMemoryBroker(opts ...Option) Broker {
options := NewOptions(opts...)
return &memoryBroker{
opts: options,
Subscribers: make(map[string][]*memorySubscriber),
+2 -2
View File
@@ -23,8 +23,8 @@ type natsBroker struct {
connected bool
addrs []string
conn *natsp.Conn // single connection (used when pool is disabled)
pool *connectionPool // connection pool (used when pooling is enabled)
conn *natsp.Conn // single connection (used when pool is disabled)
pool *connectionPool // connection pool (used when pooling is enabled)
opts broker.Options
nopts natsp.Options
+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/internal/util/tls"
mtls "go-micro.dev/v5/util/tls"
)
type MQExchangeType string
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"context"
"time"
"go-micro.dev/v5/internal/util/backoff"
"go-micro.dev/v5/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/internal/util/net"
pnet "go-micro.dev/v5/util/net"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/encoding"
+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/internal/util/buf"
"go-micro.dev/v5/internal/util/net"
"go-micro.dev/v5/internal/util/pool"
"go-micro.dev/v5/util/buf"
"go-micro.dev/v5/util/net"
"go-micro.dev/v5/util/pool"
)
const (
+77 -31
View File
@@ -22,8 +22,11 @@ import (
"go-micro.dev/v5/debug/profile/pprof"
"go-micro.dev/v5/debug/trace"
"go-micro.dev/v5/events"
"go-micro.dev/v5/genai"
"go-micro.dev/v5/genai/gemini"
"go-micro.dev/v5/genai/openai"
"go-micro.dev/v5/logger"
mprofile "go-micro.dev/v5/service/profile"
mprofile "go-micro.dev/v5/profile"
"go-micro.dev/v5/registry"
"go-micro.dev/v5/registry/consul"
"go-micro.dev/v5/registry/etcd"
@@ -244,6 +247,21 @@ var (
EnvVars: []string{"MICRO_CONFIG"},
Usage: "The source of the config to be used to get configuration",
},
&cli.StringFlag{
Name: "genai",
EnvVars: []string{"MICRO_GENAI"},
Usage: "GenAI provider to use (e.g. openai, gemini, noop)",
},
&cli.StringFlag{
Name: "genai_key",
EnvVars: []string{"MICRO_GENAI_KEY"},
Usage: "GenAI API key",
},
&cli.StringFlag{
Name: "genai_model",
EnvVars: []string{"MICRO_GENAI_MODEL"},
Usage: "GenAI model to use (optional)",
},
}
DefaultBrokers = map[string]func(...broker.Option) broker.Broker{
@@ -293,43 +311,31 @@ var (
"redis": redis.NewRedisCache,
}
DefaultStreams = map[string]func(...events.Option) (events.Stream, error){}
DefaultGenAI = map[string]func(...genai.Option) genai.GenAI{
"openai": openai.New,
"gemini": gemini.New,
}
)
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: &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,
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,
Brokers: DefaultBrokers,
Clients: DefaultClients,
@@ -381,6 +387,8 @@ func (c *cmd) Options() Options {
}
func (c *cmd) Before(ctx *cli.Context) error {
// Set GenAI provider from flags/env
setGenAIFromFlags(ctx)
// If flags are set then use them otherwise do nothing
var serverOpts []server.Option
var clientOpts []client.Option
@@ -398,9 +406,13 @@ 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 {
@@ -441,6 +453,7 @@ 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
}
}
@@ -449,6 +462,7 @@ 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
}
}
@@ -460,6 +474,7 @@ 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
@@ -470,6 +485,7 @@ func (c *cmd) Before(ctx *cli.Context) error {
}
*c.opts.Tracer = r()
trace.DefaultTracer = *c.opts.Tracer
}
// Setup auth
@@ -496,6 +512,7 @@ func (c *cmd) Before(ctx *cli.Context) error {
}
*c.opts.Auth = r(authOpts...)
auth.DefaultAuth = *c.opts.Auth
}
// Set the registry
@@ -517,6 +534,7 @@ 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
@@ -541,6 +559,7 @@ 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
@@ -693,6 +712,7 @@ 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
@@ -714,6 +734,7 @@ 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) {
@@ -724,6 +745,7 @@ 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
}
@@ -733,6 +755,7 @@ 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
}
@@ -740,6 +763,7 @@ 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
}
@@ -749,6 +773,7 @@ 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
}
@@ -796,3 +821,24 @@ func Register(cmds ...*cli.Command) {
return app.Commands[i].Name < app.Commands[j].Name
})
}
func setGenAIFromFlags(ctx *cli.Context) {
provider := ctx.String("genai")
key := ctx.String("genai_key")
model := ctx.String("genai_model")
switch provider {
case "openai":
if key == "" {
key = os.Getenv("OPENAI_API_KEY")
}
genai.DefaultGenAI = openai.New(genai.WithAPIKey(key), genai.WithModel(model))
case "gemini":
if key == "" {
key = os.Getenv("GEMINI_API_KEY")
}
genai.DefaultGenAI = gemini.New(genai.WithAPIKey(key), genai.WithModel(model))
default:
// No GenAI provider configured - using default noop
}
}
-19
View File
@@ -1,19 +0,0 @@
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
@@ -1,242 +0,0 @@
// 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()
+4 -137
View File
@@ -7,7 +7,7 @@ Go Micro Command Line
Install `micro` via `go install`
```
go install go-micro.dev/v5/cmd/micro@v5.16.0
go install go-micro.dev/v5/cmd/micro@v5.10.0
```
@@ -36,9 +36,6 @@ micro run
This starts:
- **API Gateway** on http://localhost:8080
- **Web Dashboard** at http://localhost:8080
- **Agent Playground** at http://localhost:8080/agent
- **API Explorer** at http://localhost:8080/api
- **MCP Tools** at http://localhost:8080/api/mcp/tools
- **Hot Reload** watching for file changes
- **Services** in dependency order
@@ -341,7 +338,7 @@ micro logs myservice --remote user@server -f
micro stop myservice --remote user@server
```
See [internal/website/docs/deployment.md](../../internal/website/docs/deployment.md) for the full deployment guide.
See [docs/deployment.md](../../docs/deployment.md) for the full deployment guide.
## Protobuf
@@ -349,7 +346,7 @@ Use protobuf for code generation with [protoc-gen-micro](https://github.com/micr
## Server
The micro server is a production web dashboard and authenticated API gateway for interacting with services that are already running (e.g., managed by systemd via `micro deploy`). It does **not** build, run, or watch services — for local development, use `micro run` instead.
The micro server is an api and web dashboard that provide a fixed entrypoint for seeing and querying services.
Run it like so
@@ -357,7 +354,7 @@ Run it like so
micro server
```
Then browse to [localhost:8080](http://localhost:8080) and log in with the default admin account (`admin`/`micro`).
Then browse to [localhost:8080](http://localhost:8080)
### API Endpoints
@@ -390,133 +387,3 @@ micro server
Then browse to [localhost:8080](http://localhost:8080) and log in with the default admin account (`admin`/`micro`).
> **Note:** See the `/api` page for details on API authentication and how to generate tokens for use with the HTTP API
## Gateway Architecture
The `micro run` and `micro server` commands both use a unified gateway implementation (`cmd/micro/server/gateway.go`), providing consistent HTTP-to-RPC translation, service discovery, and web UI capabilities.
### Key Differences
| Feature | `micro run` | `micro server` |
|---------|-------------|----------------|
| **Purpose** | Development | Production |
| **Authentication** | Enabled (default `admin`/`micro`) | Enabled (default `admin`/`micro`) |
| **Process Management** | Yes (builds/runs services) | No (assumes services running) |
| **Hot Reload** | Yes (watches files) | No |
| **Scopes** | Available (`/auth/scopes`) | Available (`/auth/scopes`) |
| **Use Case** | Local development | Deployed API gateway |
### Why Unified?
Previously, each command had its own gateway implementation, leading to code duplication. The unified gateway means:
- New features (like MCP integration) benefit both commands
- Consistent behavior between development and production
- Single codebase to test and maintain
- Same HTTP API, web UI, and service discovery logic
### Gateway Features
Both commands provide:
- **HTTP API**: `POST /api/{service}/{endpoint}` with JSON request/response
- **Service Discovery**: Automatic detection via registry (mdns/consul/etcd)
- **Health Checks**: `/health`, `/health/live`, `/health/ready` endpoints
- **Web Dashboard**: Browse services, test endpoints, view documentation
- **Hot Service Updates**: Gateway automatically picks up new service registrations
- **JWT Authentication**: Tokens, user management, login at `/auth/login`, `/auth/tokens`, `/auth/users`
- **Endpoint Scopes**: Restrict which tokens can call which endpoints via `/auth/scopes`
- **MCP Integration**: AI tools at `/api/mcp/tools`, agent playground at `/agent`
### Authentication & Scopes
Both `micro run` and `micro server` use the same `auth.Account` type from the go-micro framework. The gateway stores accounts under `auth/<id>` in the default store and uses JWT tokens with RSA256 signing.
**Scope enforcement** applies to all call paths:
| Path | Description |
|------|-------------|
| `POST /api/{service}/{endpoint}` | HTTP API calls |
| `POST /api/mcp/call` | MCP tool invocations |
| Agent playground | Tool calls made by the AI agent |
Scopes are configured via the web UI at `/auth/scopes`. Each endpoint can require one or more scopes. A token must carry at least one matching scope to call a protected endpoint. The `*` scope on a token bypasses all checks. Endpoints with no scopes set are open to any authenticated token.
See the [Scopes](#scopes) section below for details.
### Development Mode (`micro run`)
```bash
micro run # Auth enabled, default admin/micro
```
- Authentication enabled with default credentials (`admin`/`micro`)
- Web UI requires login
- Scopes available for testing access control
- Ideal for development with realistic auth behavior
### Production Mode (`micro server`)
```bash
micro server # Auth enabled, JWT tokens required
```
- JWT authentication on all API calls
- User/token management via web UI
- Secure by default
- Login required: default credentials `admin/micro`
### Programmatic Gateway Usage
You can also start the gateway programmatically in your own Go code:
```go
import "go-micro.dev/v5/cmd/micro/server"
// Start gateway with auth (recommended)
gw, err := server.StartGateway(server.GatewayOptions{
Address: ":8080",
AuthEnabled: true,
})
// Start gateway without auth (testing only)
gw, err := server.StartGateway(server.GatewayOptions{
Address: ":8080",
AuthEnabled: false,
})
```
See [`internal/website/docs/architecture/adr-010-unified-gateway.md`](../../internal/website/docs/architecture/adr-010-unified-gateway.md) for architecture details.
### Scopes
Scopes provide fine-grained access control over which tokens can call which service endpoints. They are managed through the web UI at `/auth/scopes` and enforced on every call through the gateway.
#### How It Works
1. **Define scopes on endpoints** — Visit `/auth/scopes` and set required scopes for each service endpoint (e.g., set `billing` on `payments.Payments.Charge`)
2. **Create tokens with scopes** — Visit `/auth/tokens` and create tokens with matching scopes (e.g., a token with `billing` scope)
3. **Scopes are enforced** — When a token calls an endpoint, the gateway checks that the token has at least one scope matching the endpoint's required scopes
#### Scope Matching Rules
- Scopes are **exact string matches**`billing` on a token matches `billing` on an endpoint
- A token with `*` scope bypasses all scope checks (admin wildcard)
- Endpoints with **no scopes set** are open to any valid token
- An endpoint can require **multiple scopes** — the token needs to match just one
- Scope names are free-form strings — use whatever convention fits your project
#### Common Patterns
| Pattern | Endpoint Scopes | Token Scopes | Result |
|---------|----------------|--------------|--------|
| Protect a service | Set `greeter` on all greeter endpoints (use Bulk Set with `greeter.*`) | Token with `greeter` | Token can call any greeter endpoint |
| Restrict an endpoint | Set `billing` on `payments.Payments.Charge` | Token with `billing` | Only that endpoint is restricted |
| Role-based | Set `admin` on sensitive endpoints | Admin token with `admin`, user token with `user` | Only admin tokens can call sensitive endpoints |
| Full access | Any | Token with `*` | Bypasses all scope checks |
#### Relationship to Framework Auth
The gateway's scope system uses `auth.Account` from the go-micro framework. Scopes on accounts are the same `[]string` field used by the framework's `auth.Rules` and `wrapper/auth` package. The gateway stores scope requirements in the default store under `endpoint-scopes/<service>.<endpoint>` keys and checks them on every HTTP request.
For service-level (RPC) auth within the go-micro mesh, use the `wrapper/auth` package which provides `auth.Rules` with priority-based access control. See the [auth wrapper documentation](../../wrapper/auth/README.md) for details.
+1 -1
View File
@@ -13,7 +13,7 @@ write services. Surrounding this we introduce a number of tools to make it easy
Install `micro` via `go install`
```
go install go-micro.dev/v5/cmd/micro@v5.16.0
go install go-micro.dev/v5/cmd/micro@v5.13.0
```
> **Note:** Use a specific version instead of `@latest` to avoid module path conflicts. See [releases](https://github.com/micro/go-micro/releases) for the latest version.
+37 -45
View File
@@ -11,6 +11,7 @@ import (
"go-micro.dev/v5/client"
"go-micro.dev/v5/cmd"
"go-micro.dev/v5/codec/bytes"
"go-micro.dev/v5/genai"
"go-micro.dev/v5/registry"
"go-micro.dev/v5/cmd/micro/cli/new"
@@ -19,7 +20,6 @@ import (
// Import packages that register commands via init()
_ "go-micro.dev/v5/cmd/micro/cli/build"
_ "go-micro.dev/v5/cmd/micro/cli/deploy"
_ "go-micro.dev/v5/cmd/micro/cli/doctor"
_ "go-micro.dev/v5/cmd/micro/cli/init"
_ "go-micro.dev/v5/cmd/micro/cli/remote"
)
@@ -37,39 +37,50 @@ func genProtoHandler(c *cli.Context) error {
return cmd.Run()
}
func genTextHandler(c *cli.Context) error {
prompt := c.String("prompt")
if len(prompt) == 0 {
return nil
}
gen := genai.DefaultGenAI
if gen.String() == "noop" {
return nil
}
ctx := context.Background()
res, err := gen.Generate(ctx, prompt)
if err != nil {
return err
}
fmt.Println(res.Text)
return nil
}
func init() {
cmd.Register([]*cli.Command{
{
Name: "new",
Usage: "Create a new service",
ArgsUsage: "[name]",
Description: `Creates a new Go Micro service from a template.
By default, generates a simple service using plain Go structs and JSON encoding.
No protobuf or external tools required — just Go.
Use --proto for a protobuf-based service with code generation.
Examples:
micro new helloworld # Simple service (recommended)
micro new helloworld --proto # Protobuf service with codegen
micro new helloworld --no-mcp # Without MCP integration`,
Name: "new",
Usage: "Create a new service",
Action: new.Run,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "proto",
Usage: "Generate a protobuf-based service with code generation",
},
&cli.BoolFlag{
Name: "no-mcp",
Usage: "Disable MCP gateway integration in generated code",
},
},
},
{
Name: "gen",
Usage: "Generate various things",
Subcommands: []*cli.Command{
{
Name: "text",
Usage: "Generate text via an LLM",
Action: genTextHandler,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "prompt",
Aliases: []string{"p"},
Usage: "The prompt to generate text from",
},
},
},
{
Name: "proto",
Usage: "Generate proto requires protoc and protoc-gen-micro",
@@ -94,18 +105,6 @@ Examples:
{
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()
@@ -121,16 +120,9 @@ Examples:
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(callCtx, req, &rsp)
err := client.Call(context.TODO(), req, &rsp)
if err != nil {
return err
}
+6 -49
View File
@@ -105,21 +105,6 @@ 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 {
@@ -144,23 +129,14 @@ func deploySSH(c *cli.Context, target string, cfg *config.Config) error {
return err
}
for _, svc := range sorted {
// If --service flag is provided, only include that service
if filterService == "" || svc.Name == filterService {
services = append(services, svc.Name)
}
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"), services); err != nil {
if err := buildBinaries(absDir, cfg, c.Bool("build")); err != nil {
fmt.Println("\u2717")
return err
}
@@ -265,7 +241,7 @@ func checkServerInit(host, remotePath string) error {
return nil
}
func buildBinaries(absDir string, cfg *config.Config, forceBuild bool, servicesToBuild []string) error {
func buildBinaries(absDir string, cfg *config.Config, forceBuild bool) error {
binDir := filepath.Join(absDir, "bin")
// Check if we already have binaries and don't need to rebuild
@@ -290,19 +266,7 @@ func buildBinaries(absDir string, cfg *config.Config, forceBuild bool, servicesT
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)
@@ -370,9 +334,9 @@ func copyBinaries(target, binDir, remotePath string) error {
exitErr, ok := err.(*exec.ExitError)
if ok && (exitErr.ExitCode() == 23 || exitErr.ExitCode() == 24) {
// Check if it's just permission warnings on metadata, not actual file transfer failures
if !strings.Contains(outputStr, "Permission denied (13)") ||
strings.Contains(outputStr, "failed to set times") ||
strings.Contains(outputStr, "chgrp") {
if !strings.Contains(outputStr, "Permission denied (13)") ||
strings.Contains(outputStr, "failed to set times") ||
strings.Contains(outputStr, "chgrp") {
// These are acceptable warnings
return nil
}
@@ -444,9 +408,6 @@ 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
@@ -481,10 +442,6 @@ The deploy process:
Name: "build",
Usage: "Force rebuild of binaries",
},
&cli.StringFlag{
Name: "service",
Usage: "Deploy only a specific service (for multi-service projects)",
},
},
})
}
-253
View File
@@ -1,253 +0,0 @@
// Package doctor provides the 'micro doctor' diagnostic command
package doctor
import (
"fmt"
"net"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/urfave/cli/v2"
"go-micro.dev/v5/cmd"
"go-micro.dev/v5/registry"
)
func init() {
cmd.Register(&cli.Command{
Name: "doctor",
Usage: "Diagnose common issues with your go-micro setup",
Description: `Run diagnostic checks on your go-micro environment.
Checks Go installation, dependencies, registry connectivity,
port availability, and common configuration issues.
Examples:
micro doctor
micro doctor --verbose`,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "verbose",
Aliases: []string{"v"},
Usage: "Show detailed output for each check",
},
},
Action: doctorAction,
})
}
type checkResult struct {
name string
ok bool
message string
detail string
}
func doctorAction(c *cli.Context) error {
verbose := c.Bool("verbose")
fmt.Println("micro doctor")
fmt.Println("============")
fmt.Println()
checks := []checkResult{
checkGo(verbose),
checkGoModule(verbose),
checkProtoc(verbose),
checkRegistry(verbose),
checkCommonPorts(verbose),
checkNATS(verbose),
checkMicroConfig(verbose),
}
passed := 0
failed := 0
warned := 0
for _, check := range checks {
if check.ok {
fmt.Printf(" [OK] %s\n", check.message)
passed++
} else if strings.HasPrefix(check.message, "[WARN]") {
fmt.Printf(" %s\n", check.message)
warned++
} else {
fmt.Printf(" [FAIL] %s\n", check.message)
failed++
}
if verbose && check.detail != "" {
for _, line := range strings.Split(check.detail, "\n") {
fmt.Printf(" %s\n", line)
}
}
}
fmt.Println()
fmt.Printf("Results: %d passed, %d warnings, %d failed\n", passed, warned, failed)
if failed > 0 {
fmt.Println()
fmt.Println("Run 'micro doctor --verbose' for details on failures.")
return fmt.Errorf("%d check(s) failed", failed)
}
fmt.Println()
fmt.Println("Everything looks good!")
return nil
}
func checkGo(verbose bool) checkResult {
out, err := exec.Command("go", "version").CombinedOutput()
if err != nil {
return checkResult{
name: "go",
ok: false,
message: "Go not found in PATH",
detail: "Install Go from https://go.dev/dl/",
}
}
version := strings.TrimSpace(string(out))
return checkResult{
name: "go",
ok: true,
message: fmt.Sprintf("Go installed (%s, %s/%s)", version, runtime.GOOS, runtime.GOARCH),
}
}
func checkGoModule(verbose bool) checkResult {
// Check if we're in a Go module
if _, err := os.Stat("go.mod"); err != nil {
return checkResult{
name: "module",
ok: false,
message: "[WARN] No go.mod in current directory",
detail: "Run 'go mod init <module>' or 'micro new <name>' to create a project",
}
}
data, err := os.ReadFile("go.mod")
if err != nil {
return checkResult{name: "module", ok: false, message: "Cannot read go.mod"}
}
hasMicro := strings.Contains(string(data), "go-micro.dev/v5")
if !hasMicro {
return checkResult{
name: "module",
ok: false,
message: "[WARN] go.mod does not reference go-micro.dev/v5",
detail: "Run 'go get go-micro.dev/v5' to add it",
}
}
return checkResult{
name: "module",
ok: true,
message: "Go module with go-micro dependency found",
}
}
func checkProtoc(verbose bool) checkResult {
_, err := exec.LookPath("protoc")
if err != nil {
return checkResult{
name: "protoc",
ok: false,
message: "[WARN] protoc not found (optional, needed for --proto services)",
detail: "Install from https://grpc.io/docs/protoc-installation/\nOnly needed if using 'micro new --proto'",
}
}
return checkResult{name: "protoc", ok: true, message: "protoc installed"}
}
func checkRegistry(verbose bool) checkResult {
start := time.Now()
services, err := registry.ListServices()
elapsed := time.Since(start)
if err != nil {
return checkResult{
name: "registry",
ok: false,
message: fmt.Sprintf("Registry unavailable: %v", err),
detail: "Default registry is mDNS (works without setup).\nFor Consul: docker run -p 8500:8500 consul:latest agent -dev",
}
}
return checkResult{
name: "registry",
ok: true,
message: fmt.Sprintf("Registry reachable (%d services, %s)", len(services), elapsed.Round(time.Millisecond)),
}
}
func checkCommonPorts(verbose bool) checkResult {
ports := []string{"8080", "9001", "9002"}
inUse := []string{}
for _, port := range ports {
conn, err := net.DialTimeout("tcp", "localhost:"+port, 200*time.Millisecond)
if err == nil {
conn.Close()
inUse = append(inUse, port)
}
}
if len(inUse) > 0 {
return checkResult{
name: "ports",
ok: false,
message: fmt.Sprintf("[WARN] Ports in use: %s", strings.Join(inUse, ", ")),
detail: "These ports are commonly used by go-micro services.\nUse micro.Address(\":PORT\") to pick a different port.",
}
}
return checkResult{
name: "ports",
ok: true,
message: fmt.Sprintf("Common ports available (%s)", strings.Join(ports, ", ")),
}
}
func checkNATS(verbose bool) checkResult {
conn, err := net.DialTimeout("tcp", "localhost:4222", 500*time.Millisecond)
if err != nil {
return checkResult{
name: "nats",
ok: false,
message: "[WARN] NATS not reachable on localhost:4222 (optional)",
detail: "NATS is optional but needed for broker/nats and events/natsjs.\nStart with: docker run -p 4222:4222 nats:latest",
}
}
conn.Close()
return checkResult{
name: "nats",
ok: true,
message: "NATS reachable on localhost:4222",
}
}
func checkMicroConfig(verbose bool) checkResult {
// Check for micro.mu or micro.json
configs := []string{"micro.mu", "micro.json"}
for _, name := range configs {
if _, err := os.Stat(name); err == nil {
absPath, _ := filepath.Abs(name)
return checkResult{
name: "config",
ok: true,
message: fmt.Sprintf("Project config found: %s", absPath),
}
}
}
return checkResult{
name: "config",
ok: false,
message: "[WARN] No micro.mu or micro.json found (optional)",
detail: "Project config is optional. Needed for 'micro run' with multiple services.",
}
}
+54 -123
View File
@@ -2,6 +2,7 @@
package gen
import (
"context"
"fmt"
"os"
"path/filepath"
@@ -10,31 +11,17 @@ import (
"github.com/urfave/cli/v2"
"go-micro.dev/v5/cmd"
"go-micro.dev/v5/genai"
)
var handlerTemplate = `package handler
import (
"context"
"fmt"
log "go-micro.dev/v5/logger"
)
{{range .Methods}}
// {{.RequestType}} is the input for {{$.Name}}.{{.Name}}
type {{.RequestType}} struct {
ID string ` + "`json:\"id\"`" + `
Name string ` + "`json:\"name,omitempty\"`" + `
}
// {{.ResponseType}} is the output for {{$.Name}}.{{.Name}}
type {{.ResponseType}} struct {
ID string ` + "`json:\"id\"`" + `
Message string ` + "`json:\"message\"`" + `
}
{{end}}
type {{.Name}} struct{}
func New{{.Name}}() *{{.Name}} {
@@ -42,18 +29,10 @@ func New{{.Name}}() *{{.Name}} {
}
{{range .Methods}}
// {{.Name}} handles {{$.Name}}.{{.Name}} requests.
//
// @example {"id": "1", "name": "test"}
// {{.Name}} handles {{.Name}} requests
func (h *{{$.Name}}) {{.Name}}(ctx context.Context, req *{{.RequestType}}, rsp *{{.ResponseType}}) error {
log.Infof("Received {{$.Name}}.{{.Name}} request: id=%s", req.ID)
if req.ID == "" {
return fmt.Errorf("id is required")
}
rsp.ID = req.ID
rsp.Message = fmt.Sprintf("{{.Name}} processed: %s", req.Name)
log.Infof("Received {{$.Name}}.{{.Name}} request")
// TODO: implement
return nil
}
{{end}}
@@ -62,6 +41,7 @@ func (h *{{$.Name}}) {{.Name}}(ctx context.Context, req *{{.RequestType}}, rsp *
var endpointTemplate = `package handler
import (
"context"
"encoding/json"
"net/http"
@@ -70,43 +50,32 @@ import (
// {{.Name}}Request is the request for {{.Name}}
type {{.Name}}Request struct {
ID string ` + "`json:\"id\"`" + `
Name string ` + "`json:\"name,omitempty\"`" + `
// Add request fields here
}
// {{.Name}}Response is the response for {{.Name}}
type {{.Name}}Response struct {
ID string ` + "`json:\"id\"`" + `
Message string ` + "`json:\"message\"`" + `
OK bool ` + "`json:\"ok\"`" + `
// Add response fields here
}
// {{.Name}} handles HTTP {{.Method}} requests to /{{.Path}}
func {{.Name}}(w http.ResponseWriter, r *http.Request) {
log.Infof("Received {{.Name}} %s request", r.Method)
ctx := r.Context()
log.Infof("Received {{.Name}} request")
var req {{.Name}}Request
if r.Method == http.MethodGet {
req.ID = r.URL.Query().Get("id")
req.Name = r.URL.Query().Get("name")
} else {
if r.Method != http.MethodGet {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, ` + "`" + `{"error":"invalid request body"}` + "`" + `, http.StatusBadRequest)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
}
if req.ID == "" {
http.Error(w, ` + "`" + `{"error":"id is required"}` + "`" + `, http.StatusBadRequest)
return
}
rsp := {{.Name}}Response{
ID: req.ID,
Message: "processed",
OK: true,
}
// TODO: implement handler logic
_ = ctx
_ = req
rsp := {{.Name}}Response{}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(rsp)
}
@@ -116,17 +85,15 @@ var modelTemplate = `package model
import (
"context"
"fmt"
"sync"
"time"
)
// {{.Name}} represents a {{lower .Name}} in the system
type {{.Name}} struct {
ID string ` + "`json:\"id\"`" + `
Name string ` + "`json:\"name\"`" + `
CreatedAt time.Time ` + "`json:\"created_at\"`" + `
UpdatedAt time.Time ` + "`json:\"updated_at\"`" + `
// Add your fields here
}
// {{.Name}}Repository defines the interface for {{lower .Name}} storage
@@ -137,79 +104,6 @@ type {{.Name}}Repository interface {
Delete(ctx context.Context, id string) error
List(ctx context.Context, offset, limit int) ([]*{{.Name}}, error)
}
// Memory{{.Name}}Repository is an in-memory implementation of {{.Name}}Repository.
// Replace with a database-backed implementation for production.
type Memory{{.Name}}Repository struct {
mu sync.RWMutex
items map[string]*{{.Name}}
seq int
}
func NewMemory{{.Name}}Repository() *Memory{{.Name}}Repository {
return &Memory{{.Name}}Repository{items: make(map[string]*{{.Name}})}
}
func (r *Memory{{.Name}}Repository) Create(ctx context.Context, m *{{.Name}}) error {
r.mu.Lock()
defer r.mu.Unlock()
r.seq++
m.ID = fmt.Sprintf("%d", r.seq)
m.CreatedAt = time.Now()
m.UpdatedAt = m.CreatedAt
r.items[m.ID] = m
return nil
}
func (r *Memory{{.Name}}Repository) Get(ctx context.Context, id string) (*{{.Name}}, error) {
r.mu.RLock()
defer r.mu.RUnlock()
m, ok := r.items[id]
if !ok {
return nil, fmt.Errorf("{{lower .Name}} %s not found", id)
}
return m, nil
}
func (r *Memory{{.Name}}Repository) Update(ctx context.Context, m *{{.Name}}) error {
r.mu.Lock()
defer r.mu.Unlock()
if _, ok := r.items[m.ID]; !ok {
return fmt.Errorf("{{lower .Name}} %s not found", m.ID)
}
m.UpdatedAt = time.Now()
r.items[m.ID] = m
return nil
}
func (r *Memory{{.Name}}Repository) Delete(ctx context.Context, id string) error {
r.mu.Lock()
defer r.mu.Unlock()
if _, ok := r.items[id]; !ok {
return fmt.Errorf("{{lower .Name}} %s not found", id)
}
delete(r.items, id)
return nil
}
func (r *Memory{{.Name}}Repository) List(ctx context.Context, offset, limit int) ([]*{{.Name}}, error) {
r.mu.RLock()
defer r.mu.RUnlock()
var result []*{{.Name}}
i := 0
for _, m := range r.items {
if i < offset {
i++
continue
}
if limit > 0 && len(result) >= limit {
break
}
result = append(result, m)
i++
}
return result, nil
}
`
type handlerData struct {
@@ -297,6 +191,38 @@ func generateModel(c *cli.Context) error {
return generateFile("model", strings.ToLower(name)+".go", modelTemplate, data)
}
func generateWithAI(c *cli.Context) error {
prompt := c.Args().First()
if prompt == "" {
return fmt.Errorf("description required: micro generate ai <description>")
}
gen := genai.DefaultGenAI
if gen.String() == "noop" {
return fmt.Errorf("no AI provider configured. Set OPENAI_API_KEY or GEMINI_API_KEY")
}
aiPrompt := fmt.Sprintf(`Generate Go code for a micro service handler based on this description: %s
Use the go-micro.dev/v5 framework. Include:
- Proper imports
- Handler struct with methods
- Context handling
- Logging with go-micro.dev/v5/logger
- Error handling
Only output the Go code, no explanations.`, prompt)
ctx := context.Background()
res, err := gen.Generate(ctx, aiPrompt)
if err != nil {
return fmt.Errorf("AI generation failed: %w", err)
}
fmt.Println(res.Text)
return nil
}
func generateFile(dir, filename, tmplStr string, data interface{}) error {
// Create directory if it doesn't exist
if err := os.MkdirAll(dir, 0755); err != nil {
@@ -375,6 +301,11 @@ func init() {
Usage: "Generate a model: micro g model <name>",
Action: generateModel,
},
{
Name: "ai",
Usage: "Generate code using AI: micro g ai <description>",
Action: generateWithAI,
},
},
})
}
+2 -2
View File
@@ -102,7 +102,7 @@ Run with sudo:
if err != nil {
return fmt.Errorf("user %s not found: %w", userName, err)
}
// chown -R user:user /opt/micro
chownCmd := exec.Command("chown", "-R", fmt.Sprintf("%s:%s", u.Username, u.Username), basePath)
if err := chownCmd.Run(); err != nil {
@@ -181,7 +181,7 @@ func initRemote(c *cli.Context, host string) error {
// Run micro init --server on remote
initCmd := fmt.Sprintf("sudo micro init --server --path %s --user %s", basePath, userName)
sshCmd := exec.Command("ssh", host, initCmd)
sshCmd.Stdout = os.Stdout
sshCmd.Stderr = os.Stderr
+10 -96
View File
@@ -148,6 +148,13 @@ func Run(ctx *cli.Context) error {
return nil
}
// Check for protoc
if _, err := exec.LookPath("protoc"); err != nil {
fmt.Println("WARNING: protoc is not installed or not in your PATH.")
fmt.Println("Please install protoc from https://github.com/protocolbuffers/protobuf/releases")
fmt.Println("After installing, re-run 'make proto' in your service directory if needed.")
}
var goPath string
var goDir string
@@ -167,97 +174,15 @@ func Run(ctx *cli.Context) error {
}
goDir = filepath.Join(goPath, "src", path.Clean(dir))
noMCP := ctx.Bool("no-mcp")
useProto := ctx.Bool("proto")
if useProto {
return runProto(ctx, dir, goDir, goPath, noMCP)
}
return runSimple(dir, goDir, goPath, noMCP)
}
// runSimple generates a service using plain Go structs and JSON encoding.
// No protobuf, no external tools — just Go.
func runSimple(dir, goDir, goPath string, noMCP bool) error {
mainTmpl := tmpl.SimpleMainMCP
readmeTmpl := tmpl.SimpleReadmeMCP
moduleTmpl := tmpl.SimpleModule
if noMCP {
mainTmpl = tmpl.SimpleMain
readmeTmpl = tmpl.SimpleReadme
moduleTmpl = tmpl.SimpleModule
}
c := config{
Alias: dir,
Comments: nil, // Remove redundant protoComments
Dir: dir,
GoDir: goDir,
GoPath: goPath,
UseGoPath: false,
Files: []file{
{"main.go", mainTmpl},
{"Makefile", tmpl.SimpleMakefile},
{"README.md", readmeTmpl},
{".gitignore", tmpl.GitIgnore},
},
}
if os.Getenv("GO111MODULE") != "off" {
c.Files = append(c.Files, file{"go.mod", moduleTmpl})
}
if err := create(c); err != nil {
return err
}
// Run go mod tidy
fmt.Println("\nRunning 'go mod tidy'...")
if err := runInDir(dir, "go mod tidy"); err != nil {
fmt.Printf("Error running 'go mod tidy': %v\n", err)
}
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()
fmt.Println("To generate a protobuf service instead, use:")
fmt.Printf(" micro new --proto %s\n", dir)
fmt.Println()
return nil
}
// runProto generates a protobuf-based service with code generation.
func runProto(ctx *cli.Context, dir, goDir, goPath string, noMCP bool) error {
// Check for protoc
if _, err := exec.LookPath("protoc"); err != nil {
fmt.Println("WARNING: protoc is not installed or not in your PATH.")
fmt.Println("Please install protoc from https://github.com/protocolbuffers/protobuf/releases")
fmt.Println("After installing, re-run 'make proto' in your service directory if needed.")
}
mainTmpl := tmpl.MainSRV
if noMCP {
mainTmpl = tmpl.MainSRVNoMCP
}
c := config{
Alias: dir,
Comments: nil,
Dir: dir,
GoDir: goDir,
GoPath: goPath,
UseGoPath: false,
Files: []file{
{"main.go", mainTmpl},
{"main.go", tmpl.MainSRV},
{"handler/" + dir + ".go", tmpl.HandlerSRV},
{"proto/" + dir + ".proto", tmpl.ProtoSRV},
{"Makefile", tmpl.Makefile},
@@ -289,18 +214,7 @@ func runProto(ctx *cli.Context, dir, goDir, goPath string, noMCP bool) error {
fmt.Println("\nProject structure after 'make proto':")
printTree(dir)
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()
fmt.Println("\nService created successfully! Start coding in your new service directory.")
return nil
}
-197
View File
@@ -1,197 +0,0 @@
package new
import (
"os"
"path/filepath"
"strings"
"testing"
"text/template"
tmpl "go-micro.dev/v5/cmd/micro/cli/new/template"
)
func TestTemplatesParse(t *testing.T) {
fn := template.FuncMap{
"title": func(s string) string {
return strings.ReplaceAll(strings.Title(s), "-", "")
},
"dehyphen": func(s string) string {
return strings.ReplaceAll(s, "-", "")
},
"lower": func(s string) string {
return strings.ToLower(s)
},
}
templates := map[string]string{
"SimpleMain": tmpl.SimpleMain,
"SimpleMainMCP": tmpl.SimpleMainMCP,
"SimpleMakefile": tmpl.SimpleMakefile,
"SimpleModule": tmpl.SimpleModule,
"SimpleReadme": tmpl.SimpleReadme,
"SimpleReadmeMCP": tmpl.SimpleReadmeMCP,
"MainSRV": tmpl.MainSRV,
"MainSRVNoMCP": tmpl.MainSRVNoMCP,
"HandlerSRV": tmpl.HandlerSRV,
"ProtoSRV": tmpl.ProtoSRV,
"Makefile": tmpl.Makefile,
"Module": tmpl.Module,
"Readme": tmpl.Readme,
"GitIgnore": tmpl.GitIgnore,
}
data := config{
Alias: "testservice",
Dir: "testservice",
GoDir: "/tmp/test",
GoPath: "/tmp",
}
for name, src := range templates {
t.Run(name, func(t *testing.T) {
tmplObj, err := template.New(name).Funcs(fn).Parse(src)
if err != nil {
t.Fatalf("failed to parse template %s: %v", name, err)
}
var buf strings.Builder
if err := tmplObj.Execute(&buf, data); err != nil {
t.Fatalf("failed to execute template %s: %v", name, err)
}
if buf.Len() == 0 {
t.Fatalf("template %s produced empty output", name)
}
})
}
}
func TestCreateSimpleService(t *testing.T) {
dir := t.TempDir()
svcDir := filepath.Join(dir, "mysvc")
c := config{
Alias: "mysvc",
Dir: svcDir,
GoDir: svcDir,
GoPath: dir,
Files: []file{
{"main.go", tmpl.SimpleMainMCP},
{"Makefile", tmpl.SimpleMakefile},
{".gitignore", tmpl.GitIgnore},
},
}
if err := create(c); err != nil {
t.Fatalf("create failed: %v", err)
}
// Verify files exist
for _, f := range c.Files {
path := filepath.Join(svcDir, f.Path)
if _, err := os.Stat(path); os.IsNotExist(err) {
t.Errorf("expected file %s to exist", f.Path)
}
}
// Verify main.go content
mainContent, err := os.ReadFile(filepath.Join(svcDir, "main.go"))
if err != nil {
t.Fatalf("failed to read main.go: %v", err)
}
content := string(mainContent)
if !strings.Contains(content, `micro.New("mysvc"`) {
t.Error("main.go should contain service name")
}
if !strings.Contains(content, "mcp.WithMCP") {
t.Error("main.go should contain MCP integration")
}
if !strings.Contains(content, "type Mysvc struct") {
t.Error("main.go should contain handler struct")
}
}
func TestCreateSimpleServiceNoMCP(t *testing.T) {
dir := t.TempDir()
svcDir := filepath.Join(dir, "mysvc")
c := config{
Alias: "mysvc",
Dir: svcDir,
GoDir: svcDir,
GoPath: dir,
Files: []file{
{"main.go", tmpl.SimpleMain},
{"Makefile", tmpl.SimpleMakefile},
{".gitignore", tmpl.GitIgnore},
},
}
if err := create(c); err != nil {
t.Fatalf("create failed: %v", err)
}
mainContent, err := os.ReadFile(filepath.Join(svcDir, "main.go"))
if err != nil {
t.Fatalf("failed to read main.go: %v", err)
}
content := string(mainContent)
if !strings.Contains(content, `micro.New("mysvc"`) {
t.Error("main.go should contain service name")
}
if strings.Contains(content, "mcp.WithMCP") {
t.Error("main.go should NOT contain MCP when noMCP is set")
}
}
func TestCreateProtoService(t *testing.T) {
dir := t.TempDir()
svcDir := filepath.Join(dir, "mysvc")
c := config{
Alias: "mysvc",
Dir: svcDir,
GoDir: svcDir,
GoPath: dir,
Files: []file{
{"main.go", tmpl.MainSRV},
{"handler/mysvc.go", tmpl.HandlerSRV},
{"proto/mysvc.proto", tmpl.ProtoSRV},
{"Makefile", tmpl.Makefile},
{".gitignore", tmpl.GitIgnore},
},
}
if err := create(c); err != nil {
t.Fatalf("create failed: %v", err)
}
for _, f := range c.Files {
path := filepath.Join(svcDir, f.Path)
if _, err := os.Stat(path); os.IsNotExist(err) {
t.Errorf("expected file %s to exist", f.Path)
}
}
}
func TestCreateFailsIfDirExists(t *testing.T) {
dir := t.TempDir()
svcDir := filepath.Join(dir, "mysvc")
os.MkdirAll(svcDir, 0755)
c := config{
Alias: "mysvc",
Dir: svcDir,
Files: []file{{"main.go", tmpl.SimpleMain}},
}
err := create(c)
if err == nil {
t.Fatal("expected error when directory already exists")
}
if !strings.Contains(err.Error(), "already exists") {
t.Errorf("expected 'already exists' error, got: %v", err)
}
}
+3 -8
View File
@@ -13,24 +13,19 @@ import (
type {{title .Alias}} struct{}
// Return a new handler.
// Return a new handler
func New() *{{title .Alias}} {
return &{{title .Alias}}{}
}
// Call greets a person by name and returns a welcome message.
//
// @example {"name": "Alice"}
// Call is a single request handler called via client.Call or the generated client code
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 sends a sequence of numbered responses back to the caller.
// Use this for streaming large result sets or real-time updates.
//
// @example {"count": 5}
// Stream is a server side stream handler called via client.Stream or the generated client code
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,33 +3,6 @@ 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"
+8 -12
View File
@@ -27,18 +27,6 @@ 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
@@ -47,6 +35,14 @@ 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.22
go 1.18
require (
go-micro.dev/v5 latest
-4
View File
@@ -17,22 +17,18 @@ 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;
}
`
+13 -73
View File
@@ -3,88 +3,28 @@ package template
var (
Readme = `# {{title .Alias}} Service
This is the {{title .Alias}} service
Generated with
` + "```" + `
` + "```" +
`
micro new {{.Alias}}
` + "```" + `
## Getting Started
## Usage
Generate the proto code:
Generate the proto code
` + "```bash" + `
` + "```" +
`
make proto
` + "```" + `
Run the service:
Run the service
` + "```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)
` + "```" + `
`
` + "```" +
`
micro run .
` + "```"
)
-231
View File
@@ -1,231 +0,0 @@
package template
// Simple templates generate a service using plain Go structs and JSON encoding.
// No protobuf, no code generation — just Go.
var SimpleMain = `package main
import (
"context"
"fmt"
"log"
"go-micro.dev/v5"
)
// Request is the input for the greeting.
type Request struct {
Name string ` + "`" + `json:"name"` + "`" + `
}
// Response is the greeting result.
type Response struct {
Message string ` + "`" + `json:"message"` + "`" + `
}
// {{title .Alias}} is the service handler.
type {{title .Alias}} struct{}
// Call greets a person by name.
func (h *{{title .Alias}}) Call(ctx context.Context, req *Request, rsp *Response) error {
rsp.Message = "Hello " + req.Name
return nil
}
func main() {
service := micro.New("{{lower .Alias}}")
service.Init()
if err := service.Handle(new({{title .Alias}})); err != nil {
log.Fatal(err)
}
fmt.Println("Starting {{lower .Alias}} service on :0 (random port)")
fmt.Println()
fmt.Println("Or set a fixed address:")
fmt.Println(" service := micro.New(\"{{lower .Alias}}\", micro.Address(\":8080\"))")
if err := service.Run(); err != nil {
log.Fatal(err)
}
}
`
var SimpleMainMCP = `package main
import (
"context"
"fmt"
"log"
"go-micro.dev/v5"
"go-micro.dev/v5/gateway/mcp"
)
// Request is the input for the greeting.
type Request struct {
Name string ` + "`" + `json:"name"` + "`" + `
}
// Response is the greeting result.
type Response struct {
Message string ` + "`" + `json:"message"` + "`" + `
}
// {{title .Alias}} is the service handler.
type {{title .Alias}} struct{}
// Call greets a person by name and returns a welcome message.
//
// @example {"name": "Alice"}
func (h *{{title .Alias}}) Call(ctx context.Context, req *Request, rsp *Response) error {
rsp.Message = "Hello " + req.Name
return nil
}
func main() {
service := micro.New("{{lower .Alias}}",
micro.Address(":9090"),
mcp.WithMCP(":3001"),
)
service.Init()
if err := service.Handle(new({{title .Alias}})); err != nil {
log.Fatal(err)
}
fmt.Println("Starting {{lower .Alias}} service")
fmt.Println()
fmt.Println(" Service: http://localhost:9090")
fmt.Println(" MCP Tools: http://localhost:3001/mcp/tools")
fmt.Println()
fmt.Println("Use with Claude Code:")
fmt.Println(" micro mcp serve")
if err := service.Run(); err != nil {
log.Fatal(err)
}
}
`
var SimpleMakefile = `.PHONY: build run test clean lint fmt
# Build the service
build:
go build -o bin/{{.Alias}} .
# Run the service
run:
go run .
# Run with micro (gateway + hot reload)
dev:
micro run
# Run tests
test:
go test -v ./...
# Run tests with coverage
test-coverage:
go test -v -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
# Clean build artifacts
clean:
rm -rf bin/ coverage.out coverage.html
# Lint code
lint:
golangci-lint run ./...
# Format code
fmt:
go fmt ./...
`
var SimpleModule = `module {{.Dir}}
go 1.22
require go-micro.dev/v5 latest
`
var SimpleReadme = `# {{title .Alias}} Service
Generated with ` + "`" + `micro new --simple {{.Alias}}` + "`" + `
## Getting Started
Run the service:
` + "```bash" + `
go run .
` + "```" + `
Call it:
` + "```bash" + `
curl -XPOST \
-H 'Content-Type: application/json' \
-H 'Micro-Endpoint: {{title .Alias}}.Call' \
-d '{"name": "Alice"}' \
http://localhost:9090
` + "```" + `
## Development
` + "```bash" + `
make run # Run the service
make test # Run tests
make build # Build binary
micro run # Run with gateway + hot reload
` + "```" + `
`
var SimpleReadmeMCP = `# {{title .Alias}} Service
Generated with ` + "`" + `micro new {{.Alias}}` + "`" + `
## Getting Started
Run the service:
` + "```bash" + `
go run .
` + "```" + `
Call it:
` + "```bash" + `
curl -XPOST \
-H 'Content-Type: application/json' \
-H 'Micro-Endpoint: {{title .Alias}}.Call' \
-d '{"name": "Alice"}' \
http://localhost:9090
` + "```" + `
## MCP & AI Agents
This service is MCP-enabled. When running, AI agents can discover
and call your endpoints automatically.
**MCP tools:** http://localhost:3001/mcp/tools
### Use with Claude Code
` + "```bash" + `
micro mcp serve
` + "```" + `
## Development
` + "```bash" + `
make run # Run the service
make test # Run tests
make build # Build binary
micro run # Run with gateway + hot reload
` + "```" + `
`
+7 -38
View File
@@ -15,30 +15,9 @@ 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
@@ -153,27 +132,17 @@ func CallService(srv *registry.Service, args []string) error {
return fmt.Errorf("Endpoint %v not found for service %v", endpoint, srv.Name)
}
// 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
// parse the flags
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
@@ -418,7 +387,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,13 +1,11 @@
package util
import (
"context"
"reflect"
"strings"
"testing"
"github.com/davecgh/go-spew/spew"
"go-micro.dev/v5/metadata"
goregistry "go-micro.dev/v5/registry"
)
@@ -379,75 +377,3 @@ 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)
}
}
})
}
}
-1
View File
@@ -7,7 +7,6 @@ import (
_ "go-micro.dev/v5/cmd/micro/cli"
_ "go-micro.dev/v5/cmd/micro/cli/build"
_ "go-micro.dev/v5/cmd/micro/cli/deploy"
_ "go-micro.dev/v5/cmd/micro/mcp"
_ "go-micro.dev/v5/cmd/micro/run"
"go-micro.dev/v5/cmd/micro/server"
)
-453
View File
@@ -1,453 +0,0 @@
# 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)
-829
View File
@@ -1,829 +0,0 @@
// Package mcp provides the 'micro mcp' command for MCP server management
package mcp
import (
"context"
"encoding/json"
"fmt"
"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"
consulreg "go-micro.dev/v5/registry/consul"
etcdreg "go-micro.dev/v5/registry/etcd"
)
// getRegistry returns the appropriate registry based on CLI flags.
func getRegistry(ctx *cli.Context) (registry.Registry, error) {
regName := ctx.String("registry")
regAddr := ctx.String("registry_address")
switch regName {
case "", "mdns":
return registry.DefaultRegistry, nil
case "consul":
opts := []registry.Option{}
if regAddr != "" {
opts = append(opts, registry.Addrs(regAddr))
}
return consulreg.NewConsulRegistry(opts...), nil
case "etcd":
opts := []registry.Option{}
if regAddr != "" {
opts = append(opts, registry.Addrs(regAddr))
}
return etcdreg.NewEtcdRegistry(opts...), nil
default:
return nil, fmt.Errorf("unsupported registry %q (supported: mdns, consul, etcd)", regName)
}
}
func init() {
cmd.Register(&cli.Command{
Name: "mcp",
Usage: "MCP server management",
Description: `Manage MCP (Model Context Protocol) server for AI agent integration.
Examples:
# Start MCP server (stdio for Claude Code)
micro mcp serve
# Start MCP server with HTTP/SSE
micro mcp serve --address :3000
# List available tools
micro mcp list
# Test a tool
micro mcp test users.Users.Get
The 'micro mcp' command exposes your microservices as AI-accessible tools via the
Model Context Protocol (MCP). This enables Claude Code, ChatGPT, and other AI agents
to discover and call your services automatically.
For Claude Code integration, add to your config:
{
"mcpServers": {
"my-services": {
"command": "micro",
"args": ["mcp", "serve"]
}
}
}`,
Subcommands: []*cli.Command{
{
Name: "serve",
Usage: "Start MCP server",
Description: `Start an MCP server to expose microservices as AI tools.
By default, uses stdio transport (for Claude Code and local AI tools).
Use --address for HTTP/SSE transport (for web-based agents).
Examples:
# Stdio transport (for Claude Code)
micro mcp serve
# HTTP/SSE transport
micro mcp serve --address :3000
# Custom registry
micro mcp serve --registry consul --registry_address consul:8500`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "address",
Usage: "HTTP address to listen on (e.g., :3000). If not set, uses stdio.",
},
&cli.StringFlag{
Name: "registry",
Usage: "Registry for service discovery (mdns, consul, etcd)",
Value: "mdns",
},
&cli.StringFlag{
Name: "registry_address",
Usage: "Registry address (e.g., consul:8500)",
},
},
Action: serveAction,
},
{
Name: "list",
Usage: "List available tools",
Description: `List all tools available via MCP.
Each service endpoint is exposed as a tool that AI agents can call.
Example:
micro mcp list`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "registry",
Usage: "Registry for service discovery (mdns, consul, etcd)",
Value: "mdns",
},
&cli.StringFlag{
Name: "registry_address",
Usage: "Registry address",
},
&cli.BoolFlag{
Name: "json",
Usage: "Output as JSON",
},
},
Action: listAction,
},
{
Name: "test",
Usage: "Test a tool",
Description: `Test calling a specific tool.
Example:
micro mcp test users.Users.Get '{"id": "123"}'`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "registry",
Usage: "Registry for service discovery",
Value: "mdns",
},
&cli.StringFlag{
Name: "registry_address",
Usage: "Registry address",
},
},
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,
},
},
})
}
// serveAction starts the MCP server
func serveAction(ctx *cli.Context) error {
// Get registry
reg, err := getRegistry(ctx)
if err != nil {
return err
}
// Create MCP server options
opts := mcp.Options{
Registry: reg,
Address: ctx.String("address"),
Context: context.Background(),
Logger: log.Default(),
}
// Handle shutdown gracefully
ctx2, cancel := context.WithCancel(opts.Context)
opts.Context = ctx2
defer cancel()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
go func() {
<-sigChan
cancel()
}()
// Start MCP server
return mcp.Serve(opts)
}
// listAction lists available tools
func listAction(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), // Log to stderr so stdout is clean
}
// Discover services
services, err := opts.Registry.ListServices()
if err != nil {
return fmt.Errorf("failed to list services: %w", err)
}
if ctx.Bool("json") {
// JSON output
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 {
tools = append(tools, map[string]interface{}{
"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),
})
}
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
return enc.Encode(map[string]interface{}{
"tools": tools,
"count": len(tools),
})
}
// Human-readable output
fmt.Printf("Available MCP Tools:\n\n")
toolCount := 0
for _, svc := range services {
fullSvcs, err := opts.Registry.GetService(svc.Name)
if err != nil || len(fullSvcs) == 0 {
continue
}
fmt.Printf("Service: %s\n", svc.Name)
for _, ep := range fullSvcs[0].Endpoints {
toolName := fmt.Sprintf("%s.%s", svc.Name, ep.Name)
fmt.Printf(" • %s\n", toolName)
toolCount++
}
fmt.Println()
}
fmt.Printf("Total: %d tools\n", toolCount)
return nil
}
// testAction tests a specific tool
func testAction(ctx *cli.Context) error {
if ctx.Args().Len() < 1 {
return fmt.Errorf("usage: micro mcp test <tool-name> [input-json]")
}
toolName := ctx.Args().First()
inputJSON := "{}"
if ctx.Args().Len() > 1 {
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, err := getRegistry(ctx)
if err != nil {
return err
}
// 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("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
@@ -1,79 +0,0 @@
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")
}
})
}
}
+1 -1
View File
@@ -13,7 +13,7 @@ import (
// Config represents the micro run configuration
type Config struct {
Services map[string]*Service `json:"services"`
Services map[string]*Service `json:"services"`
Envs map[string]map[string]string `json:"env"`
Deploy map[string]*DeployTarget `json:"deploy"`
}
+276
View File
@@ -0,0 +1,276 @@
// Package gateway provides an HTTP gateway for micro run
package gateway
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"sync"
"time"
"go-micro.dev/v5/client"
"go-micro.dev/v5/codec/bytes"
"go-micro.dev/v5/health"
"go-micro.dev/v5/registry"
)
// Gateway provides HTTP access to micro services
type Gateway struct {
addr string
server *http.Server
services []ServiceInfo
mu sync.RWMutex
}
// ServiceInfo holds information about a running service
type ServiceInfo struct {
Name string `json:"name"`
Address string `json:"address"`
Port int `json:"port,omitempty"`
}
// New creates a new gateway
func New(addr string) *Gateway {
return &Gateway{
addr: addr,
}
}
// SetServices updates the list of known services
func (g *Gateway) SetServices(services []ServiceInfo) {
g.mu.Lock()
g.services = services
g.mu.Unlock()
}
// Start starts the gateway HTTP server
func (g *Gateway) Start() error {
mux := http.NewServeMux()
// Health endpoint - aggregates all service health
mux.HandleFunc("/health", g.healthHandler)
mux.HandleFunc("/health/live", g.liveHandler)
mux.HandleFunc("/health/ready", g.readyHandler)
// API endpoint - HTTP to RPC proxy
mux.HandleFunc("/api/", g.apiHandler)
// Services list
mux.HandleFunc("/services", g.servicesHandler)
// Home page
mux.HandleFunc("/", g.homeHandler)
g.server = &http.Server{
Addr: g.addr,
Handler: mux,
}
go func() {
if err := g.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
fmt.Printf("Gateway error: %v\n", err)
}
}()
return nil
}
// Stop stops the gateway
func (g *Gateway) Stop() {
if g.server != nil {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
g.server.Shutdown(ctx)
}
}
// Addr returns the gateway address
func (g *Gateway) Addr() string {
return g.addr
}
func (g *Gateway) homeHandler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
g.mu.RLock()
services := g.services
g.mu.RUnlock()
// Get services from registry
regServices, _ := registry.ListServices()
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprintf(w, `<!DOCTYPE html>
<html>
<head>
<title>Micro</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #f5f5f5; color: #333; }
.container { max-width: 800px; margin: 0 auto; padding: 40px 20px; }
h1 { font-size: 2em; margin-bottom: 10px; }
.subtitle { color: #666; margin-bottom: 30px; }
.card { background: white; border-radius: 8px; padding: 20px; margin-bottom: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.card h2 { font-size: 1.2em; margin-bottom: 15px; color: #333; }
.service { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid #eee; }
.service:last-child { border-bottom: none; }
.service-name { font-weight: 500; }
.service-addr { color: #666; font-family: monospace; font-size: 0.9em; }
.endpoints { margin-top: 10px; }
.endpoint { display: block; padding: 5px 10px; margin: 5px 0; background: #f0f0f0; border-radius: 4px; font-family: monospace; font-size: 0.85em; text-decoration: none; color: #333; }
.endpoint:hover { background: #e0e0e0; }
.try-it { background: #f9f9f9; padding: 15px; border-radius: 6px; margin-top: 20px; }
.try-it h3 { font-size: 1em; margin-bottom: 10px; }
code { background: #333; color: #0f0; padding: 10px 15px; display: block; border-radius: 4px; font-size: 0.85em; overflow-x: auto; }
.links { margin-top: 20px; }
.links a { color: #0066cc; margin-right: 15px; }
</style>
</head>
<body>
<div class="container">
<h1>Micro</h1>
<p class="subtitle">Services are running</p>
<div class="card">
<h2>Services (%d)</h2>
`, len(regServices))
if len(regServices) > 0 {
for _, svc := range regServices {
fmt.Fprintf(w, ` <div class="service">
<span class="service-name">%s</span>
</div>
`, svc.Name)
// Get endpoints for this service
if details, err := registry.GetService(svc.Name); err == nil && len(details) > 0 {
if len(details[0].Endpoints) > 0 {
fmt.Fprintf(w, ` <div class="endpoints">`)
for _, ep := range details[0].Endpoints {
fmt.Fprintf(w, ` <a class="endpoint" href="/api/%s/%s">POST /api/%s/%s</a>\n`,
svc.Name, ep.Name, svc.Name, ep.Name)
}
fmt.Fprintf(w, ` </div>`)
}
}
}
} else if len(services) > 0 {
for _, svc := range services {
fmt.Fprintf(w, ` <div class="service">
<span class="service-name">%s</span>
<span class="service-addr">%s</span>
</div>
`, svc.Name, svc.Address)
}
} else {
fmt.Fprintf(w, ` <p style="color: #666; padding: 10px 0;">No services registered yet...</p>`)
}
fmt.Fprintf(w, ` </div>
<div class="card">
<h2>Quick Links</h2>
<div class="links">
<a href="/health">Health Check</a>
<a href="/services">Services JSON</a>
</div>
</div>
<div class="try-it">
<h3>Try it</h3>
<code>curl -X POST http://localhost%s/api/{service}/{Endpoint} -d '{}'</code>
</div>
</div>
</body>
</html>`, g.addr)
}
func (g *Gateway) servicesHandler(w http.ResponseWriter, r *http.Request) {
services, err := registry.ListServices()
if err != nil {
http.Error(w, err.Error(), 500)
return
}
var result []map[string]interface{}
for _, svc := range services {
details, _ := registry.GetService(svc.Name)
var endpoints []string
if len(details) > 0 {
for _, ep := range details[0].Endpoints {
endpoints = append(endpoints, ep.Name)
}
}
result = append(result, map[string]interface{}{
"name": svc.Name,
"endpoints": endpoints,
})
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(result)
}
func (g *Gateway) healthHandler(w http.ResponseWriter, r *http.Request) {
resp := health.Run(r.Context())
w.Header().Set("Content-Type", "application/json")
if resp.Status == health.StatusUp {
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(http.StatusServiceUnavailable)
}
json.NewEncoder(w).Encode(resp)
}
func (g *Gateway) liveHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"up"}`))
}
func (g *Gateway) readyHandler(w http.ResponseWriter, r *http.Request) {
g.healthHandler(w, r)
}
func (g *Gateway) apiHandler(w http.ResponseWriter, r *http.Request) {
// Parse path: /api/{service}/{endpoint}
path := strings.TrimPrefix(r.URL.Path, "/api/")
parts := strings.SplitN(path, "/", 2)
if len(parts) < 2 {
http.Error(w, `{"error": "usage: /api/{service}/{endpoint}"}`, http.StatusBadRequest)
return
}
service := parts[0]
endpoint := parts[1]
// Read request body
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, fmt.Sprintf(`{"error": "%s"}`, err.Error()), http.StatusBadRequest)
return
}
if len(body) == 0 {
body = []byte("{}")
}
// Create RPC request
req := client.NewRequest(service, endpoint, &bytes.Frame{Data: body})
var rsp bytes.Frame
if err := client.Call(r.Context(), req, &rsp); err != nil {
http.Error(w, fmt.Sprintf(`{"error": "%s"}`, err.Error()), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(rsp.Data)
}
+38 -44
View File
@@ -2,7 +2,6 @@ package run
import (
"bufio"
"context"
"crypto/md5"
"fmt"
"io"
@@ -20,8 +19,8 @@ import (
"github.com/urfave/cli/v2"
"go-micro.dev/v5/cmd"
"go-micro.dev/v5/cmd/micro/run/config"
"go-micro.dev/v5/cmd/micro/run/gateway"
"go-micro.dev/v5/cmd/micro/run/watcher"
"go-micro.dev/v5/cmd/micro/server"
)
// Color codes for log output
@@ -321,23 +320,23 @@ func Run(c *cli.Context) error {
}
// Start gateway unless disabled
var gw *server.Gateway
var gw *gateway.Gateway
gatewayAddr := c.String("address")
if gatewayAddr == "" {
gatewayAddr = ":8080"
}
if !c.Bool("no-gateway") {
var err error
mcpAddr := c.String("mcp-address")
gw, err = server.StartGateway(server.GatewayOptions{
Address: gatewayAddr,
AuthEnabled: true, // Auth enabled with default admin/micro user
Context: context.Background(),
MCPEnabled: mcpAddr != "",
MCPAddress: mcpAddr,
})
if err != nil {
gw = gateway.New(gatewayAddr)
var svcInfos []gateway.ServiceInfo
for _, svc := range services {
svcInfos = append(svcInfos, gateway.ServiceInfo{
Name: svc.name,
Port: svc.port,
})
}
gw.SetServices(svcInfos)
if err := gw.Start(); err != nil {
return fmt.Errorf("failed to start gateway: %w", err)
}
}
@@ -358,7 +357,7 @@ func Run(c *cli.Context) error {
}
// Print startup banner
printBanner(services, gw, !c.Bool("no-watch"), c.String("mcp-address"))
printBanner(services, gw, !c.Bool("no-watch"))
// Setup signal handling
sigCh := make(chan os.Signal, 1)
@@ -427,25 +426,21 @@ func processRunning(pidStr string) bool {
return proc.Signal(syscall.Signal(0)) == nil
}
func printBanner(services []*serviceProcess, gw *server.Gateway, watching bool, mcpAddr string) {
fmt.Println()
fmt.Println(" \033[1mMicro\033[0m")
func printBanner(services []*serviceProcess, gw *gateway.Gateway, watching bool) {
fmt.Println()
fmt.Println(" ┌─────────────────────────────────────────────────────────────┐")
fmt.Println(" │ │")
fmt.Println(" │ \033[1mMicro\033[0m │")
fmt.Println(" │ │")
if gw != nil {
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.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.Println()
fmt.Println(" Services:")
fmt.Println(" │ │")
fmt.Println(" Services:")
for _, svc := range services {
status := "\033[32m●\033[0m" // green dot
@@ -453,19 +448,27 @@ func printBanner(services []*serviceProcess, gw *server.Gateway, watching bool,
status = "\033[31m●\033[0m" // red dot
}
name := svc.name
if len(name) > 40 {
name = name[:37] + "..."
if len(name) > 20 {
name = name[:17] + "..."
}
fmt.Printf(" %s %s\n", status, name)
fmt.Printf(" %s %-20s │\n", status, name)
}
fmt.Println()
fmt.Println(" Auth: \033[32menabled\033[0m (admin / micro)")
fmt.Println(" │ │")
if watching {
fmt.Println(" \033[33mWatching for changes...\033[0m")
fmt.Println(" \033[33mWatching for changes...\033[0m")
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()
}
@@ -477,10 +480,7 @@ func init() {
Starts an HTTP gateway on :8080 providing:
- Web dashboard at /
- Agent playground at /agent (AI chat with MCP tools)
- API explorer at /api
- API proxy at /api/{service}/{endpoint}
- MCP tools at /api/mcp/tools
- Health checks at /health
With a micro.mu or micro.json config file, services start in dependency order.
@@ -491,8 +491,7 @@ Examples:
micro run --address :3000 # Gateway on custom port
micro run --no-gateway # Services only, no HTTP gateway
micro run --no-watch # Disable hot reload
micro run --env production # Use production environment
micro run --mcp-address :3000 # Enable MCP protocol gateway`,
micro run --env production # Use production environment`,
Action: Run,
Flags: []cli.Flag{
&cli.StringFlag{
@@ -515,11 +514,6 @@ Examples:
Usage: "Environment to use (default: development)",
EnvVars: []string{"MICRO_ENV"},
},
&cli.StringFlag{
Name: "mcp-address",
Usage: "MCP gateway address (e.g., :3000). Enables MCP protocol for AI tools.",
EnvVars: []string{"MICRO_MCP_ADDRESS"},
},
},
})
}
+15 -63
View File
@@ -9,12 +9,6 @@ import (
"time"
)
// Default file extensions to watch
var defaultExtensions = []string{".go"}
// Default directories to skip
var defaultExcludes = []string{"vendor", "node_modules", "testdata"}
// Event represents a file change event
type Event struct {
Path string
@@ -23,13 +17,11 @@ type Event struct {
// Watcher watches directories for file changes
type Watcher struct {
dirs []string
events chan Event
done chan struct{}
interval time.Duration
debounce time.Duration
extensions []string
excludes []string
dirs []string
events chan Event
done chan struct{}
interval time.Duration
debounce time.Duration
mu sync.Mutex
modTimes map[string]time.Time
@@ -52,33 +44,15 @@ func WithDebounce(d time.Duration) Option {
}
}
// WithExtensions sets the file extensions to watch (e.g., ".go", ".mod", ".proto").
// Replaces the default list. Each extension must include the leading dot.
func WithExtensions(exts ...string) Option {
return func(w *Watcher) {
w.extensions = exts
}
}
// WithExcludes sets additional directory names to skip during scanning.
// These are added to the default excludes (vendor, node_modules, testdata).
func WithExcludes(dirs ...string) Option {
return func(w *Watcher) {
w.excludes = append(w.excludes, dirs...)
}
}
// New creates a new file watcher for the given directories
func New(dirs []string, opts ...Option) *Watcher {
w := &Watcher{
dirs: dirs,
events: make(chan Event, 100),
done: make(chan struct{}),
interval: 500 * time.Millisecond,
debounce: 300 * time.Millisecond,
extensions: append([]string{}, defaultExtensions...),
excludes: append([]string{}, defaultExcludes...),
modTimes: make(map[string]time.Time),
dirs: dirs,
events: make(chan Event, 100),
done: make(chan struct{}),
interval: 500 * time.Millisecond,
debounce: 300 * time.Millisecond,
modTimes: make(map[string]time.Time),
}
for _, opt := range opts {
@@ -150,12 +124,6 @@ func (w *Watcher) scan(notify bool) []string {
var changed []string
changedDirs := make(map[string]bool)
// Build exclude set for O(1) lookup
excludeSet := make(map[string]bool, len(w.excludes))
for _, e := range w.excludes {
excludeSet[e] = true
}
for _, dir := range w.dirs {
absDir, err := filepath.Abs(dir)
if err != nil {
@@ -167,17 +135,17 @@ func (w *Watcher) scan(notify bool) []string {
return nil
}
// Skip hidden directories and excluded dirs
// Skip hidden directories and vendor
if info.IsDir() {
name := info.Name()
if strings.HasPrefix(name, ".") || excludeSet[name] {
if strings.HasPrefix(name, ".") || name == "vendor" || name == "node_modules" {
return filepath.SkipDir
}
return nil
}
// Check if file matches any watched extension
if !w.matchesExtension(path) {
// Only watch .go files
if !strings.HasSuffix(path, ".go") {
return nil
}
@@ -198,19 +166,3 @@ func (w *Watcher) scan(notify bool) []string {
return changed
}
// matchesExtension checks if a file path has one of the watched extensions.
// Special case: "go.mod" and "go.sum" are always matched when ".mod" is in the extensions list.
func (w *Watcher) matchesExtension(path string) bool {
base := filepath.Base(path)
for _, ext := range w.extensions {
if strings.HasSuffix(base, ext) {
return true
}
// Special case: watch go.mod and go.sum when .mod extension is listed
if ext == ".mod" && (base == "go.mod" || base == "go.sum") {
return true
}
}
return false
}
-136
View File
@@ -1,136 +0,0 @@
package watcher
import (
"os"
"path/filepath"
"testing"
"time"
)
func TestNewDefaults(t *testing.T) {
w := New([]string{"."})
if w.interval != 500*time.Millisecond {
t.Errorf("interval = %v, want 500ms", w.interval)
}
if w.debounce != 300*time.Millisecond {
t.Errorf("debounce = %v, want 300ms", w.debounce)
}
if len(w.extensions) != 1 || w.extensions[0] != ".go" {
t.Errorf("extensions = %v, want [\".go\"]", w.extensions)
}
}
func TestWithOptions(t *testing.T) {
w := New([]string{"."},
WithInterval(1*time.Second),
WithDebounce(500*time.Millisecond),
WithExtensions(".go", ".mod", ".proto"),
WithExcludes("dist", "build"),
)
if w.interval != 1*time.Second {
t.Errorf("interval = %v, want 1s", w.interval)
}
if w.debounce != 500*time.Millisecond {
t.Errorf("debounce = %v, want 500ms", w.debounce)
}
if len(w.extensions) != 3 {
t.Errorf("extensions count = %d, want 3", len(w.extensions))
}
// default excludes + custom
foundDist := false
for _, e := range w.excludes {
if e == "dist" {
foundDist = true
}
}
if !foundDist {
t.Error("excludes should contain 'dist'")
}
}
func TestMatchesExtension(t *testing.T) {
w := New([]string{"."}, WithExtensions(".go", ".mod", ".proto"))
tests := []struct {
path string
match bool
}{
{"main.go", true},
{"handler_test.go", true},
{"go.mod", true},
{"go.sum", true},
{"service.proto", true},
{"README.md", false},
{"style.css", false},
{"data.json", false},
}
for _, tt := range tests {
if got := w.matchesExtension(tt.path); got != tt.match {
t.Errorf("matchesExtension(%q) = %v, want %v", tt.path, got, tt.match)
}
}
}
func TestScanDetectsChanges(t *testing.T) {
dir := t.TempDir()
// Create a .go file
goFile := filepath.Join(dir, "main.go")
if err := os.WriteFile(goFile, []byte("package main"), 0644); err != nil {
t.Fatal(err)
}
w := New([]string{dir})
// Initial scan
w.scan(false)
// No changes yet
changed := w.scan(true)
if len(changed) != 0 {
t.Errorf("expected no changes, got %v", changed)
}
// Touch the file
time.Sleep(10 * time.Millisecond)
if err := os.WriteFile(goFile, []byte("package main\n// changed"), 0644); err != nil {
t.Fatal(err)
}
changed = w.scan(true)
if len(changed) == 0 {
t.Error("expected changes after modifying file")
}
}
func TestScanSkipsExcluded(t *testing.T) {
dir := t.TempDir()
// Create vendor dir with a .go file
vendorDir := filepath.Join(dir, "vendor")
os.MkdirAll(vendorDir, 0755)
os.WriteFile(filepath.Join(vendorDir, "lib.go"), []byte("package lib"), 0644)
// Create a regular .go file
os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main"), 0644)
w := New([]string{dir})
w.scan(false)
// Verify vendor file is not tracked
w.mu.Lock()
for path := range w.modTimes {
if filepath.Base(filepath.Dir(path)) == "vendor" {
t.Errorf("vendor file should be excluded: %s", path)
}
}
w.mu.Unlock()
}
func TestStartStop(t *testing.T) {
w := New([]string{t.TempDir()}, WithInterval(50*time.Millisecond))
w.Start()
time.Sleep(100 * time.Millisecond)
w.Stop()
}
-72
View File
@@ -1,72 +0,0 @@
package server
import (
"fmt"
"net/http"
"os"
"path/filepath"
"go-micro.dev/v5/gateway/api"
"go-micro.dev/v5/registry"
"go-micro.dev/v5/store"
)
// GatewayOptions configures the HTTP gateway (legacy compatibility)
// Deprecated: Use gateway/api.Options directly
type GatewayOptions = api.Options
// Gateway represents a running HTTP gateway server (legacy compatibility)
// Deprecated: Use gateway/api.Gateway directly
type Gateway = api.Gateway
// StartGateway starts the HTTP gateway with the given options.
// This is a compatibility wrapper around gateway/api.New().
//
// Deprecated: Use gateway/api.New() directly for new code.
func StartGateway(opts GatewayOptions) (*Gateway, error) {
// Initialize auth if enabled (server-specific setup)
if opts.AuthEnabled {
if err := initAuth(); err != nil {
return nil, fmt.Errorf("failed to initialize auth: %w", err)
}
homeDir, _ := os.UserHomeDir()
keyDir := filepath.Join(homeDir, "micro", "keys")
privPath := filepath.Join(keyDir, "private.pem")
pubPath := filepath.Join(keyDir, "public.pem")
if err := InitJWTKeys(privPath, pubPath); err != nil {
return nil, fmt.Errorf("failed to init JWT keys: %w", err)
}
}
// Get store (server-specific default)
s := store.DefaultStore
// Parse templates (server-specific)
tmpls := parseTemplates()
// Create handler registrar that registers server-specific handlers
opts.HandlerRegistrar = func(mux *http.ServeMux) error {
registerHandlers(mux, tmpls, s, opts.AuthEnabled)
return nil
}
// Use default registry if not set
if opts.Registry == nil {
opts.Registry = registry.DefaultRegistry
}
// Delegate to gateway/api package
return api.New(opts)
}
// RunGateway starts the gateway and blocks until it stops.
//
// Deprecated: Use gateway/api.Run() with a custom handler registrar.
func RunGateway(opts GatewayOptions) error {
gw, err := StartGateway(opts)
if err != nil {
return err
}
return gw.Wait()
}
+229 -827
View File
File diff suppressed because it is too large Load Diff
-7
View File
@@ -153,13 +153,6 @@ table tr:nth-child(even) {
margin: 0;
padding: 0;
}
.no-bullets li {
padding: 0.45em 0;
border-bottom: 1px solid #e0e0e0;
}
.no-bullets li:last-child {
border-bottom: none;
}
.copy-btn {
background: #fff;
+17 -15
View File
@@ -1,30 +1,32 @@
{{define "content"}}
<h2 class="text-2xl font-bold mb-4">API</h2>
<p style="background:#f8f8e8; border:1px solid #e0e0b0; padding:1em; margin-bottom:2em; font-size:1.05em; border-radius:6px;">
<b>Authentication Required:</b> Include an <b>Authorization: Bearer &lt;token&gt;</b> header with all <code>/api/...</code> requests.
Generate tokens on the <a href="/auth/tokens">Tokens page</a>.
<p class="api-auth-info" style="background:#f8f8e8; border:1px solid #e0e0b0; padding:1em; margin-bottom:2em; font-size:1.08em; border-radius:6px;">
<b>API Authentication Required:</b> All API calls to <code>/api/...</code> endpoints (except this page) must include an <b>Authorization: Bearer &lt;token&gt;</b> header.<br>
You can generate tokens on the <a href="/auth/tokens">Tokens page</a>.
</p>
{{range .Services}}
<h3 id="{{.Anchor}}" style="margin-top:2.5em; margin-bottom:0.8em; font-size:1.15em; font-weight:bold; border-bottom:2px solid #ddd; padding-bottom:0.4em;">{{.Name}}</h3>
<h3 id="{{.Anchor}}" style="margin-top:3em; font-size:1.2em; font-weight:bold;">{{.Name}}</h3>
{{if .Endpoints}}
<div style="margin-bottom:3em;">
{{range .Endpoints}}
<div style="margin-bottom:1.8em; padding:1.2em 1.4em; background:#fafbfc; border-radius:7px; border:1px solid #e5e5e5;">
<div style="display:flex; align-items:baseline; gap:1em; margin-bottom:0.6em;">
<span style="font-size:1.08em; font-weight:600;"><a href="{{.Path}}" class="micro-link">{{.Name}}</a></span>
<code style="font-size:0.92em; color:#666;">{{.Path}}</code>
<div style="margin-bottom:2.8em; padding:1.3em 1.5em; background:#fafbfc; border-radius:7px; border:1px solid #eee;">
<div style="font-size:1.12em; margin-bottom:0.7em;"><a href="{{.Path}}" class="micro-link" style="font-weight:bold;">{{.Name}}</a></div>
<div style="margin-bottom:0.8em; color:#888; font-size:1em;">
<b>HTTP Path:</b> <code>{{.Path}}</code>
</div>
<div style="display:flex; gap:2.5em; flex-wrap:wrap;">
<div style="flex:1; min-width:220px;">
<div style="font-weight:600; font-size:0.92em; color:#555; text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.3em;">Request</div>
{{.Params}}
<div style="display:flex; gap:3em; flex-wrap:wrap;">
<div style="min-width:240px;">
<b>Request:</b>
<pre style="background:#f4f4f4; border-radius:5px; padding:1em 1.2em; margin:0.5em 0 1em 0; font-size:1em;">{{.Params}}</pre>
</div>
<div style="flex:1; min-width:220px;">
<div style="font-weight:600; font-size:0.92em; color:#555; text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.3em;">Response</div>
{{.Response}}
<div style="min-width:240px;">
<b>Response:</b>
<pre style="background:#f4f4f4; border-radius:5px; padding:1em 1.2em; margin:0.5em 0 1em 0; font-size:1em;">{{.Response}}</pre>
</div>
</div>
</div>
{{end}}
</div>
{{else}}
<p style="color:#888;">No endpoints</p>
{{end}}
+3 -27
View File
@@ -1,5 +1,5 @@
{{define "content"}}
<h2 class="text-2xl font-bold mb-4">Tokens</h2>
<h2 class="text-2xl font-bold mb-4">Auth Tokens</h2>
<table style="margin-bottom:2em;">
<thead>
<tr><th>ID</th><th>Type</th><th>Scopes</th><th>Metadata</th><th>Token</th><th>Delete</th></tr>
@@ -39,7 +39,7 @@
{{end}}
</tbody>
</table>
<h3 style="margin-bottom:1em;">Create Token</h3>
<h3 style="margin-bottom:1em;">Create New Token</h3>
<form method="POST" action="/auth/tokens">
<input name="id" placeholder="Name/ID" required style="margin-right:1em;">
<select name="type" style="margin-right:1em;">
@@ -47,33 +47,9 @@
<option value="admin">Admin</option>
<option value="service">Service</option>
</select>
<input name="scopes" placeholder="Scopes (comma separated)" style="margin-right:1em; width:260px;">
<input name="scopes" placeholder="Scopes (comma separated)" style="margin-right:1em;">
<button type="submit">Create</button>
</form>
<div style="background:#f9f9f9; border:1px solid #eee; padding:1.2em 1.5em; border-radius:6px; font-size:0.97em; line-height:1.6; margin-top:2em;">
<h4 style="margin-top:0;">Token Scopes</h4>
<p>Scopes define what a token is allowed to access. They work with the <a href="/auth/scopes">Scopes</a> page where you set what each endpoint requires.</p>
<table style="font-size:0.95em; margin:1em 0;">
<thead><tr><th>Scopes</th><th>What it means</th></tr></thead>
<tbody>
<tr><td><code>*</code></td><td>Full access — bypasses all scope checks (default for admin)</td></tr>
<tr><td><code>greeter</code></td><td>Can call any endpoint that requires the <code>greeter</code> scope</td></tr>
<tr><td><code>greeter, users</code></td><td>Can call endpoints requiring <code>greeter</code> or <code>users</code></td></tr>
<tr><td><code>admin</code></td><td>Can call endpoints requiring the <code>admin</code> scope</td></tr>
</tbody>
</table>
<p style="margin-bottom:0;">Scopes are just strings — you define them. Set the same string on a token and on an endpoint, and they match. See <a href="/auth/scopes">Scopes</a> for examples.</p>
</div>
<h4 style="margin-top:2em;">Using a Token</h4>
<pre style="background:#fff; border:1px solid #ddd; padding:1em; border-radius:4px; overflow-x:auto; font-size:0.93em;">
curl http://localhost:8080/api/greeter/Greeter/Hello \
-H "Authorization: Bearer &lt;token&gt;" \
-d '{"name": "World"}'</pre>
<script>
function copyToken(btn) {
const token = btn.getAttribute('data-token');
+12 -18
View File
@@ -3,14 +3,14 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<title>Micro | {{.Title}}</title>
<title>{{.Title}}</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<div id="layout" style="display:flex; min-height:100vh;">
{{if not .HideSidebar}}
<nav id="sidebar" style="width:230px; background:#f5f5f5; padding:2em 1.5em 2em 2em; border:1px solid #eee;">
<h1 style="margin-bottom:1.2em;"><a href="/" id="title">Micro</a></h1>
<nav id="sidebar" style="width:220px; background:#f5f5f5; padding:2em 1.5em 2em 2em; border:1px solid #eee;">
<h1 style="margin-bottom:1em;"><a href="/" id="title">Micro</a></h1>
{{if .User}}
<div style="margin-bottom:1.5em; font-size:1.05em;">
<span style="color:#888;">Logged in as</span>
@@ -19,25 +19,19 @@
<button type="submit" style="padding:0.25em 0.8em; font-size:0.97em; border-radius:4px; margin:0; cursor:pointer;">Logout</button>
</form>
</div>
{{else if .AuthEnabled}}
{{else}}
<div style="margin-bottom:1.5em;">
<a href="/auth/login" class="micro-link">Login</a>
</div>
{{end}}
<ul class="no-bullets" style="padding-left:0; line-height:2.2;">
<li><a href="/" class="micro-link">&#127968; Home</a></li>
<li><a href="/agent" class="micro-link">&#129302; Agent</a></li>
<li><a href="/api" class="micro-link">&#128268; API</a></li>
<li><a href="/logs" class="micro-link">&#128220; Logs</a></li>
{{if .AuthEnabled}}
<li><a href="/auth/scopes" class="micro-link">&#128274; Scopes</a></li>
{{end}}
<li><a href="/services" class="micro-link">&#9881; Services</a></li>
<li><a href="/status" class="micro-link">&#128308; Status</a></li>
{{if .AuthEnabled}}
<li><a href="/auth/tokens" class="micro-link">&#128273; Tokens</a></li>
<li><a href="/auth/users" class="micro-link">&#128100; Users</a></li>
{{end}}
<ul class="no-bullets" style="padding-left:0;">
<li><a href="/" class="micro-link">Home</a></li>
<li><a href="/services" class="micro-link">Services</a></li>
<li><a href="/logs" class="micro-link">Logs</a></li>
<li><a href="/status" class="micro-link">Status</a></li>
<li><a href="/api" class="micro-link">API</a></li>
<li><a href="/auth/tokens" class="micro-link">Tokens</a></li>
<li><a href="/auth/users" class="micro-link">Users</a></li>
</ul>
{{if and .SidebarEndpoints .SidebarEndpointsEnabled}}
<hr style="margin:2em 0 1em 0;">
+2 -22
View File
@@ -1,5 +1,5 @@
{{define "content"}}
<h2 class="text-2xl font-bold mb-4">Home</h2>
<h2 class="text-2xl font-bold mb-4">Dashboard</h2>
<div style="display:flex; align-items:center; gap:2em; margin-bottom:2em;">
<div style="display:flex; align-items:center; gap:0.5em;">
<span style="font-size:2.2em; vertical-align:middle;">
@@ -17,25 +17,5 @@
<div style="font-size:1.1em; color:#2ecc40;">Running: <b>{{.RunningCount}}</b></div>
<div style="font-size:1.1em; color:#ff4136;">Stopped: <b>{{.StoppedCount}}</b></div>
</div>
{{if .Services}}
<h3 style="margin-top:1.5em; margin-bottom:0.8em;">Services</h3>
<table>
<thead>
<tr><th>Name</th><th></th></tr>
</thead>
<tbody>
{{range .Services}}
<tr>
<td><a href="/{{.}}" class="micro-link" style="font-weight:500;">{{.}}</a></td>
<td style="text-align:right;">
<a href="/api#{{.}}" class="micro-link" style="font-size:0.92em; color:#888;">API</a>
</td>
</tr>
{{end}}
</tbody>
</table>
{{else}}
<p style="color:#888; margin-top:1.5em;">No services registered yet. Start a service and it will appear here.</p>
{{end}}
<p>Welcome to the Micro dashboard. Use the sidebar to navigate services, logs, status, and API.</p>
{{end}}
-338
View File
@@ -1,338 +0,0 @@
{{define "content"}}
<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>
<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>
<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>
<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();
function loadSettings() {
fetch('/api/agent/settings')
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.provider) document.getElementById('provider').value = data.provider;
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() {
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...';
fetch('/api/agent/settings', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
provider: document.getElementById('provider').value,
api_key: document.getElementById('api-key').value,
model: document.getElementById('model-name').value,
base_url: document.getElementById('base-url').value
})
})
.then(function(r) { return r.json(); })
.then(function() {
status.textContent = 'Saved';
setTimeout(function() { status.textContent = ''; }, 2000);
})
.catch(function(err) { status.textContent = 'Error: ' + err; });
};
function loadTools() {
fetch('/api/mcp/tools')
.then(function(r) { return r.json(); })
.then(function(data) {
tools = data.tools || [];
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() {
toolCount.textContent = '0';
});
}
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');
var text = input.value.trim();
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 = 'Sending...';
var startTime = Date.now();
fetch('/api/agent/prompt', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({prompt: text})
})
.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;
}
if (data.reply) {
addMessage('assistant', escapeHtml(data.reply));
}
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];
addToolCall(tc, elapsed);
}
}
if (data.answer) {
addMessage('assistant', escapeHtml(data.answer));
}
})
.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, id) {
var container = document.getElementById('agent-messages');
var div = document.createElement('div');
div.className = 'msg msg-' + type;
if (id) div.id = id;
if (type === 'user') {
div.innerHTML = '<b>You</b><br>' + html;
} else if (type === 'assistant' || type === 'answer') {
div.innerHTML = '<b>Agent</b><br>' + html;
} else if (type === 'thinking') {
div.innerHTML = html;
} else if (type === 'error') {
div.innerHTML = html;
}
container.appendChild(div);
container.scrollTop = container.scrollHeight;
}
function escapeHtml(str) {
var d = document.createElement('div');
d.textContent = str;
return d.innerHTML;
}
})();
</script>
{{end}}
-87
View File
@@ -1,87 +0,0 @@
{{define "content"}}
<h2 class="text-2xl font-bold mb-4">Scopes</h2>
<p style="color:#666; margin-bottom:1.5em;">
Set which scopes are required to call each endpoint. Tokens must carry a matching scope.
Endpoints with no scopes are open to any authenticated token.
</p>
{{if .Success}}
<div style="background:#e6ffe6; border:1px solid #5a5; padding:0.8em 1.2em; border-radius:6px; margin-bottom:1.5em; color:#050;">
&#10003; Scopes updated successfully.
</div>
{{end}}
<table style="margin-bottom:2em;">
<thead>
<tr><th>Service</th><th>Endpoint</th><th>Required Scopes</th><th></th></tr>
</thead>
<tbody>
{{range .Endpoints}}
<tr>
<td>{{.Service}}</td>
<td><code>{{.Endpoint}}</code></td>
<td>
{{if .Scopes}}
{{range .Scopes}}<code>{{.}}</code> {{end}}
{{else}}
<span style="color:#999;">none</span>
{{end}}
</td>
<td>
<form method="POST" action="/auth/scopes" style="display:inline; padding:0; border:0; box-shadow:none; background:none;">
<input type="hidden" name="endpoint" value="{{.Name}}">
<input name="scopes" value="{{.ScopesStr}}" placeholder="scope names" style="width:180px; margin-right:0.5em;">
<button type="submit">Save</button>
</form>
</td>
</tr>
{{end}}
{{if not .Endpoints}}
<tr>
<td colspan="4" style="color:#888; text-align:center; padding:2em;">No services discovered. Start some services and they will appear here.</td>
</tr>
{{end}}
</tbody>
</table>
<h3 style="margin-bottom:1em;">Bulk Set</h3>
<p style="color:#666; margin-bottom:1em;">Apply scopes to all endpoints matching a pattern. Use <code>*</code> as a suffix wildcard. Leave scopes empty to clear.</p>
<form method="POST" action="/auth/scopes/bulk" style="margin-bottom:2em;">
<input name="pattern" placeholder="Pattern (e.g. greeter.*)" required style="margin-right:0.5em; width:220px;">
<input name="scopes" placeholder="Scopes (comma separated)" style="margin-right:0.5em; width:220px;">
<button type="submit">Apply</button>
</form>
<h3 style="margin-bottom:1em;">Examples</h3>
<div style="background:#f9f9f9; border:1px solid #eee; padding:1.2em 1.5em; border-radius:6px; font-size:0.97em; line-height:1.6;">
<p style="margin-top:0;">Scopes are strings that you define. A call is allowed when at least one of the token's scopes matches one of the endpoint's required scopes.</p>
<h4 style="margin-top:1em; margin-bottom:0.5em;">Restrict a whole service</h4>
<p>Use Bulk Set with pattern <code>greeter.*</code> and scope <code>greeter</code>.<br>
Then create a <a href="/auth/tokens">token</a> with scope <code>greeter</code> — it can call any endpoint on that service.</p>
<h4 style="margin-top:1em; margin-bottom:0.5em;">Restrict a specific endpoint</h4>
<p>Set scope <code>billing</code> on <code>payments.Payments.Charge</code> using the table above.<br>
Only tokens with the <code>billing</code> scope can call that endpoint. Other payment endpoints remain unaffected.</p>
<h4 style="margin-top:1em; margin-bottom:0.5em;">Role-based access</h4>
<p>Set scope <code>admin</code> on sensitive endpoints (e.g. <code>users.Users.Delete</code>).<br>
Create tokens with <code>admin</code> scope for operators and <code>user</code> scope for regular access.<br>
An endpoint can require multiple scopes — the token only needs to match <b>one</b> of them.</p>
<h4 style="margin-top:1em; margin-bottom:0.5em;">Full access</h4>
<p>The default <code>admin</code> user has scope <code>*</code> which bypasses all checks.<br>
Create a token with <code>*</code> scope for services that need unrestricted access.</p>
<h4 style="margin-top:1em; margin-bottom:0.5em;">Where scopes are checked</h4>
<table style="font-size:0.95em; margin:0.5em 0;">
<thead><tr><th>Access method</th><th>How auth works</th></tr></thead>
<tbody>
<tr><td>API (<code>/api/service/endpoint</code>)</td><td><code>Authorization: Bearer &lt;token&gt;</code> header</td></tr>
<tr><td>MCP tools (<code>/api/mcp/call</code>)</td><td><code>Authorization: Bearer &lt;token&gt;</code> header</td></tr>
<tr><td>Agent playground</td><td>Uses your logged-in session and its scopes</td></tr>
</tbody>
</table>
</div>
{{end}}
+13
View File
@@ -84,78 +84,91 @@ 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
}
}
+1 -1
View File
@@ -5,7 +5,7 @@ This is protobuf code generation for go-micro. We use protoc-gen-micro to reduce
## Install
```
go install go-micro.dev/v5/cmd/protoc-gen-micro@v5.16.0
go install go-micro.dev/v5/cmd/protoc-gen-micro@v5.10.0
```
Also required:
@@ -1,158 +0,0 @@
// 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,
}
}
@@ -1,41 +0,0 @@
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,15 +1193,6 @@ 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]
+6 -230
View File
@@ -18,7 +18,6 @@ const (
contextPkgPath = "context"
clientPkgPath = "go-micro.dev/v5/client"
serverPkgPath = "go-micro.dev/v5/server"
modelPkgPath = "go-micro.dev/v5/model"
)
func init() {
@@ -43,7 +42,6 @@ var (
contextPkg string
clientPkg string
serverPkg string
modelPkg string
pkgImports map[generator.GoPackageName]bool
)
@@ -53,7 +51,6 @@ 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.
@@ -73,66 +70,29 @@ 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) {
// 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 {
if len(file.FileDescriptorProto.Service) == 0 {
return
}
g.P("// Reference imports to suppress errors if they are not otherwise used.")
g.P("var _ ", contextPkg, ".Context")
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("var _ ", clientPkg, ".Option")
g.P("var _ ", serverPkg, ".Option")
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) {
hasServices := len(file.FileDescriptorProto.Service) > 0
hasModels := false
for i := range file.FileDescriptorProto.MessageType {
if g.isModelMessage(i) {
hasModels = true
break
}
}
if !hasServices && !hasModels {
if len(file.FileDescriptorProto.Service) == 0 {
return
}
g.P("import (")
g.P(contextPkg, " ", strconv.Quote(path.Join(g.gen.ImportPrefix, contextPkgPath)))
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(clientPkg, " ", strconv.Quote(path.Join(g.gen.ImportPrefix, clientPkgPath)))
g.P(serverPkg, " ", strconv.Quote(path.Join(g.gen.ImportPrefix, serverPkgPath)))
g.P(")")
g.P()
@@ -569,187 +529,3 @@ 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()
}
@@ -1,37 +0,0 @@
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)
}
}
-91
View File
@@ -1,91 +0,0 @@
package json
import (
"encoding/json"
"testing"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/wrapperspb"
)
// TestAnyTypeMarshaling tests that google.protobuf.Any types are properly marshaled with @type field
func TestAnyTypeMarshaling(t *testing.T) {
marshaler := Marshaler{}
// Create a StringValue message
stringValue := wrapperspb.String("test value")
// Wrap it in an Any message
anyMsg, err := anypb.New(stringValue)
if err != nil {
t.Fatalf("Failed to create Any message: %v", err)
}
// Marshal using our JSON marshaler
data, err := marshaler.Marshal(anyMsg)
if err != nil {
t.Fatalf("Failed to marshal Any message: %v", err)
}
// Unmarshal into a map to check for @type field
var result map[string]interface{}
if err := json.Unmarshal(data, &result); err != nil {
t.Fatalf("Failed to unmarshal JSON: %v", err)
}
// Check that @type field exists
typeURL, ok := result["@type"].(string)
if !ok {
t.Fatalf("@type field not found in JSON output. Got: %v", string(data))
}
// Verify the type URL is correct
expectedTypeURL := "type.googleapis.com/google.protobuf.StringValue"
if typeURL != expectedTypeURL {
t.Errorf("Expected @type to be %s, got %s", expectedTypeURL, typeURL)
}
// Verify the value field exists
if _, ok := result["value"]; !ok {
t.Errorf("value field not found in JSON output. Got: %v", string(data))
}
t.Logf("Successfully marshaled Any type with @type field: %s", string(data))
}
// TestAnyTypeUnmarshaling tests that JSON with @type field can be unmarshaled into google.protobuf.Any
func TestAnyTypeUnmarshaling(t *testing.T) {
marshaler := Marshaler{}
// JSON representation of an Any message with @type field
jsonData := []byte(`{
"@type": "type.googleapis.com/google.protobuf.StringValue",
"value": "test value"
}`)
// Unmarshal into an Any message
anyMsg := &anypb.Any{}
if err := marshaler.Unmarshal(jsonData, anyMsg); err != nil {
t.Fatalf("Failed to unmarshal Any message: %v", err)
}
// Verify the type URL is set
expectedTypeURL := "type.googleapis.com/google.protobuf.StringValue"
if anyMsg.TypeUrl != expectedTypeURL {
t.Errorf("Expected TypeUrl to be %s, got %s", expectedTypeURL, anyMsg.TypeUrl)
}
// Unmarshal the contained message
stringValue := &wrapperspb.StringValue{}
if err := anyMsg.UnmarshalTo(stringValue); err != nil {
t.Fatalf("Failed to unmarshal contained message: %v", err)
}
// Verify the value
expectedValue := "test value"
if stringValue.Value != expectedValue {
t.Errorf("Expected value to be %s, got %s", expectedValue, stringValue.Value)
}
t.Logf("Successfully unmarshaled Any type from JSON with @type field")
}
-98
View File
@@ -1,98 +0,0 @@
package json
import (
"bytes"
"encoding/json"
"testing"
"go-micro.dev/v5/codec"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/wrapperspb"
)
// mockReadWriteCloser implements io.ReadWriteCloser for testing
type mockReadWriteCloser struct {
*bytes.Buffer
}
func (m *mockReadWriteCloser) Close() error {
return nil
}
// TestCodecAnyTypeWrite tests that google.protobuf.Any types are properly written with @type field
func TestCodecAnyTypeWrite(t *testing.T) {
buf := &mockReadWriteCloser{Buffer: bytes.NewBuffer(nil)}
c := NewCodec(buf).(*Codec)
// Create a StringValue message
stringValue := wrapperspb.String("test value")
// Wrap it in an Any message
anyMsg, err := anypb.New(stringValue)
if err != nil {
t.Fatalf("Failed to create Any message: %v", err)
}
// Write the message
msg := &codec.Message{
Type: codec.Response,
}
if err := c.Write(msg, anyMsg); err != nil {
t.Fatalf("Failed to write Any message: %v", err)
}
// Parse the written JSON
var result map[string]interface{}
if err := json.Unmarshal(buf.Bytes(), &result); err != nil {
t.Fatalf("Failed to unmarshal JSON: %v", err)
}
// Check that @type field exists
typeURL, ok := result["@type"].(string)
if !ok {
t.Fatalf("@type field not found in JSON output. Got: %v", buf.String())
}
// Verify the type URL is correct
expectedTypeURL := "type.googleapis.com/google.protobuf.StringValue"
if typeURL != expectedTypeURL {
t.Errorf("Expected @type to be %s, got %s", expectedTypeURL, typeURL)
}
t.Logf("Successfully wrote Any type with @type field: %s", buf.String())
}
// TestCodecAnyTypeRead tests that JSON with @type field can be read into google.protobuf.Any
func TestCodecAnyTypeRead(t *testing.T) {
// JSON representation of an Any message with @type field
jsonData := `{"@type":"type.googleapis.com/google.protobuf.StringValue","value":"test value"}`
buf := &mockReadWriteCloser{Buffer: bytes.NewBufferString(jsonData + "\n")}
c := NewCodec(buf).(*Codec)
// Read into an Any message
anyMsg := &anypb.Any{}
if err := c.ReadBody(anyMsg); err != nil {
t.Fatalf("Failed to read Any message: %v", err)
}
// Verify the type URL is set
expectedTypeURL := "type.googleapis.com/google.protobuf.StringValue"
if anyMsg.TypeUrl != expectedTypeURL {
t.Errorf("Expected TypeUrl to be %s, got %s", expectedTypeURL, anyMsg.TypeUrl)
}
// Unmarshal the contained message
stringValue := &wrapperspb.StringValue{}
if err := anyMsg.UnmarshalTo(stringValue); err != nil {
t.Fatalf("Failed to unmarshal contained message: %v", err)
}
// Verify the value
expectedValue := "test value"
if stringValue.Value != expectedValue {
t.Errorf("Expected value to be %s, got %s", expectedValue, stringValue.Value)
}
t.Logf("Successfully read Any type from JSON with @type field")
}
+3 -17
View File
@@ -5,9 +5,9 @@ import (
"encoding/json"
"io"
"github.com/golang/protobuf/jsonpb"
"github.com/golang/protobuf/proto"
"go-micro.dev/v5/codec"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
)
type Codec struct {
@@ -25,12 +25,7 @@ func (c *Codec) ReadBody(b interface{}) error {
return nil
}
if pb, ok := b.(proto.Message); ok {
// Read all JSON data from decoder
var raw json.RawMessage
if err := c.Decoder.Decode(&raw); err != nil {
return err
}
return protojson.Unmarshal(raw, pb)
return jsonpb.UnmarshalNext(c.Decoder, pb)
}
return c.Decoder.Decode(b)
}
@@ -39,15 +34,6 @@ func (c *Codec) Write(m *codec.Message, b interface{}) error {
if b == nil {
return nil
}
if pb, ok := b.(proto.Message); ok {
data, err := protojson.Marshal(pb)
if err != nil {
return err
}
// Write the marshaled data to the encoder
var raw json.RawMessage = data
return c.Encoder.Encode(raw)
}
return c.Encoder.Encode(b)
}
+15 -7
View File
@@ -1,28 +1,36 @@
package json
import (
"bytes"
"encoding/json"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"github.com/golang/protobuf/jsonpb"
"github.com/golang/protobuf/proto"
"github.com/oxtoacart/bpool"
)
var protojsonMarshaler = protojson.MarshalOptions{
EmitUnpopulated: false,
}
var jsonpbMarshaler = &jsonpb.Marshaler{}
// create buffer pool with 16 instances each preallocated with 256 bytes.
var bufferPool = bpool.NewSizedBufferPool(16, 256)
type Marshaler struct{}
func (j Marshaler) Marshal(v interface{}) ([]byte, error) {
if pb, ok := v.(proto.Message); ok {
return protojsonMarshaler.Marshal(pb)
buf := bufferPool.Get()
defer bufferPool.Put(buf)
if err := jsonpbMarshaler.Marshal(buf, pb); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
return json.Marshal(v)
}
func (j Marshaler) Unmarshal(d []byte, v interface{}) error {
if pb, ok := v.(proto.Message); ok {
return protojson.Unmarshal(d, pb)
return jsonpb.Unmarshal(bytes.NewReader(d), pb)
}
return json.Unmarshal(d, v)
}
+2 -2
View File
@@ -15,7 +15,7 @@ type nats struct {
url string
bucket string
key string
conn *natsgo.Conn // store connection for lifecycle management
conn *natsgo.Conn // store connection for lifecycle management
kv natsgo.KeyValue
opts source.Options
}
@@ -129,7 +129,7 @@ func NewSource(opts ...source.Option) source.Source {
url: config.Url,
bucket: bucket,
key: key,
conn: nc, // store connection reference
conn: nc, // store connection reference
kv: kv,
opts: options,
}
-65
View File
@@ -1,65 +0,0 @@
# 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
@@ -1,327 +0,0 @@
# 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
@@ -1,44 +0,0 @@
"""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()
@@ -1,72 +0,0 @@
"""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()
@@ -1,17 +0,0 @@
"""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",
]
@@ -1,21 +0,0 @@
"""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
@@ -1,311 +0,0 @@
"""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 []
@@ -1,72 +0,0 @@
[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_*"]
@@ -1,261 +0,0 @@
"""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
@@ -1,65 +0,0 @@
# 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
@@ -1,105 +0,0 @@
# 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
@@ -1,373 +0,0 @@
# 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
@@ -1,49 +0,0 @@
"""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()
@@ -1,70 +0,0 @@
"""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()
@@ -1,17 +0,0 @@
"""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",
]

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