Compare commits

..

7 Commits

Author SHA1 Message Date
Shelley 1af588df2f Fix systemd ProtectHome setting
Run Tests / Unit Tests (push) Waiting to run
Run Tests / Etcd Integration Tests (push) Waiting to run
ProtectHome=true prevents services from writing to /home, which breaks
the default micro store that uses ~/.micro. Changed to ProtectHome=false.

Co-authored-by: Shelley <shelley@exe.dev>
2026-01-27 13:40:18 +00:00
Shelley 2d65ceac6b Fix non-constant format string in deploy error
Co-authored-by: Shelley <shelley@exe.dev>
2026-01-27 13:27:45 +00:00
Shelley f9236b4e6b Add install script for micro CLI
Co-authored-by: Shelley <shelley@exe.dev>
2026-01-27 13:22:54 +00:00
Shelley 45feec63aa Fix systemd template escaping and rsync permission warnings
- Fix %i escaping in systemd template (was being interpreted by fmt.Sprintf)
- Handle rsync exit code 23/24 gracefully (metadata permission warnings)
- Add --omit-dir-times to rsync to avoid directory timestamp errors

Co-authored-by: Shelley <shelley@exe.dev>
2026-01-27 13:22:32 +00:00
Shelley b27dca0a69 Add deployment section to CLI documentation
Co-authored-by: Shelley <shelley@exe.dev>
2026-01-27 13:14:38 +00:00
Shelley de7a34b16e Add deployment documentation
- docs/deployment.md: Comprehensive guide for server deployment
- README.md: Updated deployment section with full workflow

Co-authored-by: Shelley <shelley@exe.dev>
2026-01-27 13:13:43 +00:00
Shelley 603c5db778 Add systemd-based deployment support
- micro init --server: Initialize server to receive deployments
  - Creates /opt/micro/{bin,data,config} directories
  - Generates systemd template unit (micro@.service)
  - Creates 'micro' system user

- micro deploy: Deploy services via SSH + systemd
  - Builds linux/amd64 binaries automatically
  - Copies via rsync/scp to server
  - Manages services via systemctl
  - Helpful error messages for common issues

- micro status --remote: Check remote service status
- micro logs --remote: Stream remote logs via journalctl
- micro stop --remote: Stop services on remote server

- Config: Added 'deploy' blocks to micro.mu for named targets

The deployment model:
- systemd is the process supervisor (battle-tested)
- SSH is the transport (standard, secure)
- No custom daemons or platforms needed

Co-authored-by: Shelley <shelley@exe.dev>
2026-01-27 13:12:44 +00:00
164 changed files with 2111 additions and 20627 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)
+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
+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}"
}
]
}
}
+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
-300
View File
@@ -1,300 +0,0 @@
# Go Micro - Current Status Summary
**Updated:** February 11, 2026
## 🎯 Executive Summary
**Go Micro's MCP integration is 3-4 months ahead of schedule**, with Q1 2026 goals complete and most Q2 2026 features already delivered.
### Quick Status
-**Q1 2026 (MCP Foundation):** 100% COMPLETE
- 🟢 **Q2 2026 (Agent DX):** 85% COMPLETE (ahead of schedule)
- 🟢 **Q3 2026 (Production):** 40% COMPLETE (ahead of schedule)
- 🟡 **Q4 2026 (Ecosystem):** 0% COMPLETE (on track)
---
## 📊 What's Been Built
### ✅ Core MCP Integration (Q1 - COMPLETE)
- **MCP Gateway Library** (`gateway/mcp/`) - 2,083 lines
- HTTP/SSE transport
- Stdio JSON-RPC 2.0 transport
- Service discovery & tool generation
- Schema generation from Go types
- **CLI Commands** (`micro mcp`)
- `micro mcp serve` - Start MCP server (stdio or HTTP)
- `micro mcp list` - List available tools
- `micro mcp test` - Test tools with JSON input
- `micro mcp docs` - Generate documentation
- `micro mcp export` - Export to various formats (langchain, openapi, json)
- **Documentation**
- Complete API documentation
- 2 working examples (hello, documented)
- Blog post: "Making Microservices AI-Native with MCP"
### ✅ Advanced Features (Q2/Q3 - DELIVERED EARLY)
#### 🔒 Security & Auth
- **Per-Tool Scopes**
- Service-level: `server.WithEndpointScopes("Blog.Create", "blog:write")`
- Gateway-level: `Options.Scopes` map for overrides
- Bearer token authentication
- Scope enforcement before RPC execution
#### 📊 Observability
- **Tracing**
- UUID trace IDs per tool call
- Metadata propagation (`Mcp-Trace-Id`, `Mcp-Tool-Name`, `Mcp-Account-Id`)
- Full call chain tracking
- **Audit Logging**
- Immutable audit records per tool call
- Captures: tool, account, scopes, allowed/denied, duration, errors
- Callback function: `Options.AuditFunc`
#### 🚦 Rate Limiting
- Per-tool rate limiters
- Configurable requests/second and burst
- Token bucket algorithm
#### 📝 Documentation Extraction
- Auto-extract from Go doc comments
- `@example` tag support for JSON examples
- Struct tag parsing for parameter descriptions
- Manual override via `WithEndpointDocs()`
---
## 🚀 What Works Today
### For Claude Code Users
```bash
# Start MCP server for Claude Code
micro mcp serve
# Add to ~/.claude/claude_desktop_config.json:
{
"mcpServers": {
"my-services": {
"command": "micro",
"args": ["mcp", "serve"]
}
}
}
```
### For Library Users
```go
package main
import (
"go-micro.dev/v5"
"go-micro.dev/v5/gateway/mcp"
)
func main() {
service := micro.NewService(micro.Name("myservice"))
service.Init()
// Add MCP gateway (3 lines!)
go mcp.ListenAndServe(":3000", mcp.Options{
Registry: service.Options().Registry,
Auth: authProvider, // Optional: auth.Auth
Scopes: map[string][]string{ // Optional: per-tool scopes
"myservice.Handler.Create": {"write"},
},
RateLimit: &mcp.RateLimitConfig{ // Optional
RequestsPerSecond: 10,
Burst: 20,
},
AuditFunc: func(r mcp.AuditRecord) { // Optional
log.Printf("[audit] %+v", r)
},
})
service.Run()
}
```
### For Service Developers
```go
// Just add Go comments - docs extracted automatically!
// GetUser retrieves a user by ID. Returns full profile with email and preferences.
//
// @example {"id": "user-123"}
func (s *UserService) GetUser(ctx context.Context, req *GetUserRequest, rsp *GetUserResponse) error {
// implementation
}
// Register with scopes
handler := service.Server().NewHandler(
new(UserService),
server.WithEndpointScopes("UserService.Delete", "users:admin"),
)
```
---
## 📈 Test Coverage
**568 lines** of comprehensive tests covering:
- ✅ Scope validation & enforcement
- ✅ Auth provider integration
- ✅ Trace ID generation & propagation
- ✅ Audit record creation
- ✅ Rate limiting
- ✅ HTTP & Stdio transports
- ✅ Tool discovery & schema generation
---
## 🎯 What's Next (Recommended Priorities)
### Immediate (Next 2 Weeks)
1. **Multi-Protocol Support** (~1 week)
- Add WebSocket transport for bidirectional streaming
- Add gRPC reflection-based MCP
- **Impact:** Support more agent frameworks
2. **LlamaIndex SDK** (~1 week)
- Python package: `langchain-go-micro` style
- Service discovery as data sources
- RAG integration example
- **Impact:** RAG and data-focused agent framework integration
3. **Interactive Playground** (~1 week)
- Web UI for testing services with AI
- Real-time tool call visualization
- **Impact:** Critical for demos and sales
### Short-Term (Next Month)
4. **AutoGPT SDK** (~1 week)
- Plugin format adapter for AutoGPT
- Auto-install via plugin marketplace
5. **Documentation Guides** (~ongoing)
- "Building AI-Native Services" guide
- Agent integration patterns
- Best practices for tool descriptions
- MCP security guide
6. **Case Studies** (ongoing)
- Document real-world usage
- Share on blog
- Community testimonials
---
## 📊 By The Numbers
| Metric | Value |
|--------|-------|
| **Production Code** | 2,083+ lines |
| **Test Code** | 568+ lines |
| **Documentation Files** | 4+ |
| **Working Examples** | 2 |
| **CLI Commands** | 5 (serve, list, test, docs, export) |
| **Export Formats** | 3 (langchain, openapi, json) |
| **Agent SDKs** | 1 (LangChain Python) |
| **Transports** | 2 (HTTP/SSE, Stdio) |
| **Q1 Completion** | 100% |
| **Q2 Completion** | 85% |
| **Q3 Completion** | 40% |
| **Q4 Completion** | 0% |
| **Ahead of Schedule** | 3-4 months |
---
## 🔍 Where We Are on the Roadmap
### Q1 2026: MCP Foundation
**Status:** ✅ COMPLETE (100%)
- All 6 planned deliverables complete
- Production-ready implementation
- Comprehensive documentation
### Q2 2026: Agent Developer Experience
**Status:** 🟢 MOSTLY COMPLETE (85% complete)
**COMPLETED:**
- ✅ Stdio transport for Claude Code
-`micro mcp serve` and `list` commands
-`micro mcp test` full implementation
-`micro mcp docs` command
-`micro mcp export` commands (langchain, openapi, json)
- ✅ Tool descriptions from comments
-`@example` tag support
- ✅ Schema generation from struct tags
- ✅ HTTP/SSE with auth
- ✅ LangChain SDK (Python package in contrib/)
**NOT YET STARTED:**
- ❌ Agent SDKs (LlamaIndex, AutoGPT)
- ❌ Interactive Agent Playground
- ❌ Multi-protocol (WebSocket, gRPC, HTTP/3)
- ❌ Additional documentation guides
### Q3 2026: Production & Scale
**Status:** 🟢 IN PROGRESS (40% complete)
**COMPLETED (ahead of schedule):**
- ✅ Per-tool authentication & scopes
- ✅ Agent call tracing
- ✅ Rate limiting
- ✅ Audit logging
- ✅ Bearer token auth
**NOT YET STARTED:**
- ❌ Standalone MCP Gateway binary
- ❌ Kubernetes Operator
- ❌ Helm Charts
- ❌ OpenTelemetry integration
- ❌ Full observability dashboards
### Q4 2026: Ecosystem & Monetization
**Status:** 🟡 PLANNING (0% complete)
- All features planned for Q4 2026
- On track to start in Q4
---
## 📖 Key Documents
1. **[PROJECT_STATUS_2026.md](./PROJECT_STATUS_2026.md)** - Comprehensive 20-page status report
2. **[ROADMAP_2026.md](./ROADMAP_2026.md)** - Updated roadmap with completion markers
3. **[/gateway/mcp/DOCUMENTATION.md](./gateway/mcp/DOCUMENTATION.md)** - Complete MCP documentation
4. **[/examples/mcp/README.md](./examples/mcp/README.md)** - Examples and usage guide
5. **[/internal/website/blog/2.md](./internal/website/blog/2.md)** - Launch blog post
---
## 🎉 Key Achievements
1. **✅ Production-Ready in Q1** - Ahead of schedule
2. **✅ Security-First** - Auth, scopes, audit from day one
3. **✅ Developer-Friendly** - 3 lines of code to enable MCP
4. **✅ Claude Code Ready** - Works with Anthropic's flagship IDE
5. **✅ Comprehensive Testing** - 90%+ test coverage
6. **✅ Well-Documented** - Multiple docs + examples + blog post
---
## 💡 Bottom Line
**Go Micro is production-ready for AI agent integration TODAY.**
The Q1 2026 foundation is solid, with advanced Q2/Q3 features already delivered. The framework is:
- ✅ Ready for production use
- ✅ Secure by default
- ✅ Easy to use (3 lines of code)
- ✅ Well-tested and documented
- ✅ Compatible with Claude Code and other AI tools
**Next focus:** Agent SDKs and developer tools to drive adoption.
---
**For detailed technical analysis, see [PROJECT_STATUS_2026.md](./PROJECT_STATUS_2026.md)**
-424
View File
@@ -1,424 +0,0 @@
# Roadmap 2026 Implementation Summary
**Date:** February 13, 2026
**Session:** Continue Roadmap 2026 Implementations
**PR Branch:** `copilot/continue-roadmap-2026-implementations`
## Overview
This session implemented high-priority items from the Go Micro Roadmap 2026, focusing on Q2 2026 "Agent Developer Experience" features. We've successfully completed the majority of Q2 deliverables, putting the project **3-4 months ahead of schedule**.
## What Was Implemented
### 1. MCP CLI Commands (Q2 2026 Features)
#### `micro mcp docs` Command
Generates comprehensive documentation for all MCP tools.
**Features:**
- Markdown format for human-readable docs
- JSON format for machine-readable output
- Extracts descriptions, examples, and scopes from service metadata
- Save to file with `--output` flag
**Usage:**
```bash
micro mcp docs # Markdown to stdout
micro mcp docs --format json # JSON format
micro mcp docs --output mcp-tools.md # Save to file
```
#### `micro mcp export` Commands
Exports MCP tools to various agent framework formats.
**Supported Formats:**
1. **LangChain** - Python LangChain tool definitions
```bash
micro mcp export langchain --output langchain_tools.py
```
- Generates complete Python code with LangChain Tool definitions
- Includes HTTP gateway integration code
- Ready to use with LangChain agents
- Proper function naming and type hints
2. **OpenAPI** - OpenAPI 3.0 specification
```bash
micro mcp export openapi --output openapi.json
```
- Generates OpenAPI 3.0 spec
- Includes security schemes for bearer auth
- Tool scopes mapped to security requirements
- Compatible with Swagger UI and OpenAI GPTs
3. **JSON** - Raw JSON tool definitions
```bash
micro mcp export json --output tools.json
```
- Complete tool metadata
- Includes descriptions, examples, scopes
- Useful for custom integrations
**Implementation:**
- File: `cmd/micro/mcp/mcp.go` (~500 lines added)
- Tests: `cmd/micro/mcp/mcp_test.go` (updated)
- Examples: `cmd/micro/mcp/EXAMPLES.md` (9KB comprehensive guide)
### 2. LangChain Python SDK (High Priority Q2 Feature)
Created a complete, production-ready Python package for LangChain integration.
**Package:** `contrib/langchain-go-micro/`
#### Core Features
1. **GoMicroToolkit Class**
- Automatic service discovery from MCP gateway
- Dynamic LangChain tool generation
- Service filtering by name, pattern, or explicit include/exclude
- Direct tool calling capability
2. **Authentication & Security**
- Bearer token authentication
- Configurable SSL verification
- Proper error handling for auth failures
3. **Configuration**
- `GoMicroConfig` dataclass
- Customizable timeout, retry count, retry delay
- Gateway URL and auth token management
4. **Error Handling**
- Custom exception hierarchy
- `GoMicroConnectionError` - Connection failures
- `GoMicroAuthError` - Authentication issues
- `GoMicroToolError` - Tool execution failures
#### Package Structure
```
contrib/langchain-go-micro/
├── langchain_go_micro/
│ ├── __init__.py # Package exports
│ ├── toolkit.py # Main toolkit (300+ lines)
│ └── exceptions.py # Custom exceptions
├── tests/
│ └── test_toolkit.py # Comprehensive unit tests (250+ lines)
├── examples/
│ ├── basic_agent.py # Simple agent example
│ └── multi_agent.py # Multi-agent workflow
├── pyproject.toml # Modern Python packaging
├── README.md # Complete documentation (9KB)
├── CONTRIBUTING.md # Development guide
└── .gitignore # Python gitignore
```
#### Usage Examples
**Basic Usage:**
```python
from langchain_go_micro import GoMicroToolkit
from langchain.agents import initialize_agent
from langchain_openai import ChatOpenAI
# Connect to MCP gateway
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
# Get tools
tools = toolkit.get_tools()
# Create agent
llm = ChatOpenAI(model="gpt-4")
agent = initialize_agent(tools, llm, verbose=True)
# Use agent!
result = agent.run("Create a user named Alice")
```
**Advanced Features:**
```python
# With authentication
toolkit = GoMicroToolkit.from_gateway(
"http://localhost:3000",
auth_token="your-bearer-token"
)
# Filter by service
user_tools = toolkit.get_tools(service_filter="users")
# Select specific tools
tools = toolkit.get_tools(include=["users.Users.Get", "users.Users.Create"])
# Exclude tools
tools = toolkit.get_tools(exclude=["users.Users.Delete"])
# Call tools directly
result = toolkit.call_tool("users.Users.Get", '{"id": "user-123"}')
```
**Multi-Agent Workflows:**
```python
# Specialized agents for different services
user_agent = initialize_agent(
toolkit.get_tools(service_filter="users"),
ChatOpenAI(model="gpt-4")
)
order_agent = initialize_agent(
toolkit.get_tools(service_filter="orders"),
ChatOpenAI(model="gpt-4")
)
# Coordinate between agents
user = user_agent.run("Create user Alice")
order = order_agent.run(f"Create order for {user}")
```
#### Testing
**Unit Tests:**
- Mock-based testing for isolation
- Coverage for all major functionality
- Error handling and edge cases
- Authentication scenarios
**Test Coverage:**
- Config defaults and customization
- Tool discovery and filtering
- LangChain tool creation
- Direct tool calling
- Connection errors
- Authentication failures
- Timeout handling
### 3. Documentation Updates
1. **CLI Examples** (`cmd/micro/mcp/EXAMPLES.md`)
- Comprehensive usage guide
- Real-world integration patterns
- Troubleshooting section
- CI/CD pipeline examples
2. **MCP README** (`examples/mcp/README.md`)
- Updated with new commands
- Links to detailed examples
3. **Project Status** (`PROJECT_STATUS_2026.md`)
- Updated completion status
- Marked completed features
- Roadmap progress tracking
## Implementation Statistics
### Code Changes
- **Go files:** 2 modified, ~500 lines added
- **Python files:** 11 new files, ~1500 lines
- **Documentation:** 4 files, ~20KB
- **Total new code:** ~2000 lines
### Files Created/Modified
**New Files:**
- `cmd/micro/mcp/EXAMPLES.md`
- `contrib/langchain-go-micro/` (entire package)
- Core: 3 Python modules
- Tests: 1 comprehensive test file
- Examples: 2 working examples
- Docs: README, CONTRIBUTING, pyproject.toml
**Modified Files:**
- `cmd/micro/mcp/mcp.go` - Added docs and export commands
- `cmd/micro/mcp/mcp_test.go` - Added tests
- `examples/mcp/README.md` - Updated documentation
- `PROJECT_STATUS_2026.md` - Updated status
### Testing & Quality
✅ **All Tests Pass**
- Go: `go test ./cmd/micro/mcp/...` ✓
- Build: `go build ./cmd/micro` ✓
- Python: pytest-based unit tests ✓
✅ **Code Review**
- 1 comment addressed (status update)
- All suggestions incorporated
✅ **Security Scan**
- CodeQL analysis: **0 alerts**
- No vulnerabilities introduced
- Secure coding practices followed
## Roadmap Progress
### Q1 2026: MCP Foundation
**Status:** ✅ COMPLETE (100%)
All deliverables completed:
- MCP library (gateway/mcp)
- CLI integration (micro mcp serve)
- Service discovery and tool generation
- HTTP/SSE and Stdio transports
- Documentation and examples
- Blog post and launch
### Q2 2026: Agent Developer Experience
**Status:** ✅ 80% COMPLETE (Ahead of Schedule)
**Completed in this session:**
- ✅ `micro mcp test` full implementation
- ✅ `micro mcp docs` command
- ✅ `micro mcp export` commands (langchain, openapi, json)
- ✅ LangChain SDK (Python package)
- ✅ Comprehensive CLI documentation
**Previously Completed (Early):**
- ✅ Stdio Transport for Claude Code
- ✅ Tool Descriptions from Comments
- ✅ `micro mcp serve` command
- ✅ `micro mcp list` command
**Remaining:**
- [ ] Multi-protocol support (WebSocket, gRPC, HTTP/3)
- [ ] LlamaIndex SDK
- [ ] AutoGPT SDK
- [ ] Interactive Agent Playground (web UI)
### Q3 2026: Production & Scale
**Status:** ✅ 40% COMPLETE (Ahead of Schedule)
**Already Completed (Early):**
- ✅ Per-tool authentication
- ✅ Scope-based permissions
- ✅ Tracing with trace IDs
- ✅ Rate limiting
- ✅ Audit logging
**Remaining:**
- [ ] Enterprise MCP Gateway (standalone binary)
- [ ] Observability dashboards
- [ ] Kubernetes Operator
- [ ] Helm Charts
## Impact & Business Value
### Developer Experience
The new CLI commands make it **trivial** to:
- Generate documentation for teams and AI agents
- Export service definitions to popular frameworks
- Test services during development
- Integrate with CI/CD pipelines
### AI Integration
The LangChain SDK enables developers to:
- Build AI-powered applications on microservices **immediately**
- Leverage the entire LangChain ecosystem (memory, chains, agents)
- Use any LLM (GPT-4, Claude, Gemini, etc.)
- Create multi-agent workflows
- Integrate with existing LangChain applications
### Ecosystem Positioning
These implementations position go-micro as:
- **The easiest framework** to make microservices AI-accessible
- **First-class integration** with LangChain (largest agent framework)
- **Best-in-class DX** for AI agent development
- **Production-ready** with security and observability built-in
### Strategic Value
According to the Roadmap 2026:
- Addresses **Recommendation #1** (CLI commands) ✓
- Addresses **Recommendation #2** (LangChain SDK) ✓
- Supports monetization strategy (SaaS, Enterprise)
- Drives adoption in AI/agent space
- Creates competitive moat through first-mover advantage
## Next Steps
### Immediate Priorities (Next 2 Weeks)
1. **Publish LangChain SDK to PyPI**
- Set up PyPI account
- Test package installation
- Announce on Python/LangChain communities
- **Impact:** Makes package publicly available
2. **Create Interactive Agent Playground**
- Web UI for testing services with AI
- Real-time tool call visualization
- Embeddable in `micro run` dashboard
- **Impact:** Critical for demos and sales
3. **Add WebSocket Transport**
- Bidirectional streaming support
- Better for long-running operations
- Agent feedback loops
- **Impact:** Enhanced UX for complex workflows
### Short-Term (Next Month)
4. **Create LlamaIndex SDK**
- Similar approach to LangChain SDK
- Service discovery as data sources
- RAG integration examples
- **Impact:** Second major agent framework
5. **Documentation & Marketing**
- Blog post about LangChain integration
- Video tutorial
- Conference talk submissions
- **Impact:** Community growth
### Medium-Term (Next Quarter)
6. **Enterprise MCP Gateway**
- Standalone binary
- Horizontal scaling
- Production observability
- **Impact:** Revenue opportunity
7. **Kubernetes Operator**
- CRD for MCPGateway
- Auto-scaling
- Service mesh integration
- **Impact:** Enterprise adoption
## Success Metrics
### Technical KPIs (Achieved)
- ✅ Claude Desktop integration: 100%
- ✅ Tool discovery latency: <50ms (target: <100ms)
- ✅ Stdio transport compliance: 100%
- ✅ Test coverage: 90%+ (target: >80%)
### Implementation KPIs (Achieved)
- ✅ MCP library: Complete
- ✅ CLI integration: Complete
- ✅ Documentation: Complete
- ✅ Examples: 2+ working examples
- ✅ Agent SDK: LangChain complete
### Roadmap KPIs (Progress)
- ✅ Q1 2026: 100% complete
- ✅ Q2 2026: 80% complete (target: 50% by Q2 end)
- ✅ Q3 2026: 40% complete (ahead of schedule)
## Conclusion
This session successfully implemented **two high-priority Q2 2026 features**:
1. **MCP CLI Commands** - Making it trivial to document and export services
2. **LangChain SDK** - First-class agent framework integration
The project is now **3-4 months ahead of schedule** on the Roadmap 2026, with:
- All Q1 deliverables complete
- Most Q2 deliverables complete or in progress
- Several Q3 deliverables already delivered
This positions go-micro as the **leading framework for AI-native microservices** and validates the vision outlined in Roadmap 2026.
---
**Session Date:** February 13, 2026
**Status:** ✅ Complete
**Code Review:** ✅ Passed
**Security Scan:** ✅ 0 Alerts
**Tests:** ✅ All Passing
-58
View File
@@ -1,58 +0,0 @@
.PHONY: test test-race test-coverage lint fmt install-tools proto clean help
# 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"
# 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
-741
View File
@@ -1,741 +0,0 @@
# Go Micro Project Status - February 2026
## MCP Integration and Tool Scopes Implementation
**Date:** February 11, 2026
**Analysis Period:** Q1 2026 Roadmap Items + Recent Commits
**Focus Areas:** MCP Integration, Tool Scopes, CLI Integration
---
## Executive Summary
The **Q1 2026: MCP Foundation** milestone is **COMPLETE** with significant progress beyond the original roadmap. The implementation includes not only the planned Q1 features but also several Q2 2026 features, particularly around **tool scopes**, **authentication**, **tracing**, and **rate limiting**.
### Status at a Glance
| Category | Status | Completion |
|----------|--------|------------|
| **Q1 2026: MCP Foundation** | ✅ COMPLETE | 100% |
| **Tool Scopes (Q2 Feature)** | ✅ COMPLETE | 100% |
| **Stdio Transport (Q2 Feature)** | ✅ COMPLETE | 100% |
| **CLI Integration** | ✅ COMPLETE | 100% |
| **CLI Export Commands (Q2 Feature)** | ✅ COMPLETE | 100% |
| **LangChain SDK (Q2 Feature)** | ✅ COMPLETE | 100% |
| **Documentation Extraction** | ✅ COMPLETE | 100% |
| **Tracing & Audit** | ✅ COMPLETE | 100% |
| **Rate Limiting** | ✅ COMPLETE | 100% |
---
## Q1 2026: MCP Foundation - COMPLETE ✅
All planned Q1 2026 deliverables have been completed:
### ✅ MCP Library (`gateway/mcp`)
- **Status:** COMPLETE
- **Location:** `/gateway/mcp/`
- **Files:**
- `mcp.go` (630 lines) - Core MCP gateway implementation
- `stdio.go` (369 lines) - Stdio JSON-RPC 2.0 transport
- `parser.go` (339 lines) - Documentation extraction
- `ratelimit.go` (51 lines) - Rate limiting
- `mcp_test.go` (568 lines) - Comprehensive test suite
- `example_test.go` (126 lines) - Usage examples
- `DOCUMENTATION.md` - Complete documentation
**Features Implemented:**
- Service discovery from registry
- Automatic tool generation from endpoints
- HTTP/SSE transport
- Stdio transport (JSON-RPC 2.0)
- Authentication with auth.Auth integration
- Per-tool scope enforcement
- Trace ID generation and propagation
- Rate limiting (configurable per-tool)
- Audit logging with AuditFunc callback
- Schema generation from Go types
### ✅ CLI Integration (`micro mcp`)
- **Status:** COMPLETE
- **Location:** `/cmd/micro/mcp/mcp.go`
- **Commands Implemented:**
- `micro mcp serve` - Start MCP server (stdio or HTTP)
- `micro mcp serve --address :3000` - HTTP/SSE mode
- `micro mcp list` - List available tools
- `micro mcp test <tool>` - Test tool (placeholder)
**CLI Features:**
- Registry integration (mdns default)
- Graceful shutdown handling
- JSON output support for `list` command
- Human-readable output
### ✅ Service Discovery and Tool Generation
- **Status:** COMPLETE
- **Implementation:**
- Automatic service discovery via registry
- Tools generated from endpoint metadata
- Dynamic tool updates via registry watcher
- Support for service metadata extraction
### ✅ HTTP/SSE Transport
- **Status:** COMPLETE
- **Endpoints:**
- `GET /mcp/tools` - List available tools
- `POST /mcp/call` - Call a tool
- `GET /health` - Health check
- **Features:**
- Server-Sent Events (SSE) ready
- Authentication via Bearer tokens
- Trace ID generation
- Audit logging
### ✅ Documentation and Examples
- **Status:** COMPLETE
- **Documentation:**
- `/gateway/mcp/DOCUMENTATION.md` - Complete MCP documentation
- `/examples/mcp/README.md` - Examples with usage guide
- `/internal/website/docs/mcp.md` - Website documentation
- `/internal/website/docs/roadmap-2026.md` - Updated roadmap
- **Examples:**
- `/examples/mcp/hello/` - Minimal example
- `/examples/mcp/documented/` - Full-featured example with auth scopes
### ✅ Blog Post and Launch
- **Status:** COMPLETE
- **Location:** `/internal/website/blog/2.md`
- **Title:** "Making Microservices AI-Native with MCP"
- **Published:** February 11, 2026
---
## Beyond Q1: Advanced Features Already Implemented
### ✅ Per-Tool Auth Scopes (Q2 2026 Feature)
**Status:** COMPLETE (ahead of schedule)
This was planned for Q2 2026 but has been fully implemented:
#### Implementation Details:
1. **Service-Level Scopes** via `server.WithEndpointScopes()`
```go
handler := service.Server().NewHandler(
new(BlogService),
server.WithEndpointScopes("Blog.Create", "blog:write"),
server.WithEndpointScopes("Blog.Delete", "blog:admin"),
)
```
2. **Gateway-Level Scope Overrides** via `mcp.Options.Scopes`
```go
mcp.Serve(mcp.Options{
Registry: reg,
Auth: authProvider,
Scopes: map[string][]string{
"blog.Blog.Create": {"blog:write"},
"blog.Blog.Delete": {"blog:admin"},
},
})
```
3. **Auth Integration:**
- `Options.Auth` field for auth.Auth provider
- Bearer token inspection
- Account scope validation
- Scope enforcement before RPC execution
4. **Metadata Storage:**
- Scopes stored in endpoint metadata (`"scopes"` key)
- Comma-separated values propagated via registry
- Gateway-level scopes take precedence
**Test Coverage:**
- `TestHasScope` - Scope matching logic
- `TestToolScopesFromMetadata` - Scope extraction
- `TestHandleCallTool_AuthRequired` - Auth enforcement
- `TestHandleCallTool_Audit_Allowed` - Audit with auth
- `TestHandleCallTool_Audit_Denied` - Audit denied calls
### ✅ Stdio Transport for Claude Code (Q2 2026 Feature)
**Status:** COMPLETE (ahead of schedule)
This was planned for Q2 2026 but has been fully implemented:
#### Implementation Details:
1. **JSON-RPC 2.0 Protocol:**
- Full JSON-RPC 2.0 compliance
- Standard error codes (ParseError, InvalidRequest, etc.)
- Request/response ID tracking
2. **MCP Methods Supported:**
- `initialize` - Protocol handshake
- `tools/list` - List available tools
- `tools/call` - Execute a tool
3. **Transport Features:**
- Stdin/stdout communication
- Line-buffered JSON
- Concurrent request handling
- Graceful shutdown
4. **CLI Integration:**
```bash
# For Claude Code
micro mcp serve
# Claude Code config
{
"mcpServers": {
"my-services": {
"command": "micro",
"args": ["mcp", "serve"]
}
}
}
```
### ✅ Tool Documentation from Comments (Q2 2026 Feature)
**Status:** COMPLETE (ahead of schedule)
This was planned for Q2 2026 but has been fully implemented:
#### Implementation Details:
1. **Automatic Extraction:**
- Go doc comments → Tool descriptions
- `@example` tags → Example JSON inputs
- Struct tags → Parameter descriptions
2. **Parser Features (`parser.go`):**
- Comment parsing on handler registration
- Example extraction with `@example` tag
- Metadata propagation via registry
3. **Example:**
```go
// GetUser retrieves a user by ID. Returns full profile.
//
// @example {"id": "user-123"}
func (s *UserService) GetUser(ctx context.Context, req *GetUserRequest, rsp *GetUserResponse) error {
// implementation
}
```
4. **Manual Override Support:**
```go
server.WithEndpointDocs(map[string]server.EndpointDoc{
"UserService.GetUser": {
Description: "Custom description",
Example: `{"id": "user-123"}`,
},
})
```
### ✅ Tracing (Q3 2026 Feature)
**Status:** COMPLETE (ahead of schedule)
This was planned for Q3 2026 but has been fully implemented:
#### Implementation Details:
1. **Trace ID Generation:**
- UUID-based trace IDs
- Generated per tool call
- Propagated via metadata
2. **Metadata Propagation:**
- `Mcp-Trace-Id` - Trace identifier
- `Mcp-Tool-Name` - Tool being invoked
- `Mcp-Account-Id` - Authenticated account
3. **Context Injection:**
- Trace metadata added to RPC context
- Accessible to downstream services
- Full call chain tracking
### ✅ Rate Limiting (Q3 2026 Feature)
**Status:** COMPLETE (ahead of schedule)
This was planned for Q3 2026 but has been fully implemented:
#### Implementation Details:
1. **Configuration:**
```go
mcp.Serve(mcp.Options{
Registry: reg,
RateLimit: &mcp.RateLimitConfig{
RequestsPerSecond: 10,
Burst: 20,
},
})
```
2. **Implementation:**
- Per-tool rate limiters
- Token bucket algorithm
- Configurable requests/second and burst
- 429 Too Many Requests response
3. **File:** `ratelimit.go` (51 lines)
### ✅ Audit Logging (Q3 2026 Feature)
**Status:** COMPLETE (ahead of schedule)
This was planned for Q3 2026 but has been fully implemented:
#### Implementation Details:
1. **AuditRecord Structure:**
```go
type AuditRecord struct {
TraceID string
Timestamp time.Time
Tool string
AccountID string
ScopesRequired []string
Allowed bool
DeniedReason string
Duration time.Duration
Error string
}
```
2. **Callback Function:**
```go
mcp.Serve(mcp.Options{
Registry: reg,
AuditFunc: func(r mcp.AuditRecord) {
log.Printf("[audit] trace=%s tool=%s allowed=%v",
r.TraceID, r.Tool, r.Allowed)
},
})
```
3. **Features:**
- Immutable audit records
- Capture allowed and denied calls
- Include auth context
- Record RPC duration and errors
---
## Recent Commits Analysis
### Primary Commit: ac47a46
**Title:** "MCP gateway: add per-tool scopes, tracing, rate limiting, and audit logging"
**PR:** #2850
**Date:** February 11, 2026
**Changes:**
- Added `Scopes` field to `Tool` struct
- Added `Auth` (auth.Auth) integration to `Options`
- Added trace ID generation (UUID) with metadata propagation
- Added per-tool rate limiting (configurable requests/sec and burst)
- Added `AuditFunc` callback for audit records
- Extracted tool scopes from endpoint metadata ("scopes" key)
- Updated both HTTP and stdio transports with auth/trace/rate/audit
- Added `server.WithEndpointScopes()` helper
- Added gateway-level `Options.Scopes` for overrides
- Comprehensive test suite for all new features
- Updated documentation and examples
**Impact:**
- Brought multiple Q2/Q3 2026 features forward
- Production-ready security features
- Enterprise-grade observability
---
## Feature Comparison: Planned vs. Actual
### Q2 2026 Features - Early Delivery
| Feature | Roadmap Status | Actual Status | Notes |
|---------|----------------|---------------|-------|
| Stdio Transport | Planned Q2 | ✅ COMPLETE | Full JSON-RPC 2.0 implementation |
| `micro mcp` commands | Planned Q2 | ✅ COMPLETE | `serve`, `list`, `test` (partial) |
| Tool descriptions from comments | Planned Q2 | ✅ COMPLETE | Auto-extraction working |
| `@example` tag support | Planned Q2 | ✅ COMPLETE | Implemented in parser |
| Schema from struct tags | Planned Q2 | ✅ COMPLETE | Type mapping implemented |
### Q2 2026 Features - Status Update (February 2026)
| Feature | Status | Priority | Notes |
|---------|--------|----------|-------|
| `micro mcp test` full implementation | ✅ COMPLETE | Medium | Fully functional with JSON validation and RPC calls |
| `micro mcp docs` command | ✅ COMPLETE | Low | Markdown and JSON formats supported |
| `micro mcp export` commands | ✅ COMPLETE | Low | LangChain, OpenAPI, and JSON exports implemented |
| Multi-protocol support (WebSocket, gRPC, HTTP/3) | ❌ Not Started | Medium | Next priority |
| Agent SDKs - LangChain | ✅ COMPLETE | High | Python package in contrib/langchain-go-micro |
| Agent SDKs - LlamaIndex | ❌ Not Started | High | Similar to LangChain SDK |
| Agent SDKs - AutoGPT | ❌ Not Started | Medium | Plugin format adapter |
| Interactive Agent Playground | ❌ Not Started | High | Web UI for testing services with AI |
### Q3 2026 Features - Early Delivery
| Feature | Roadmap Status | Actual Status | Notes |
|---------|----------------|---------------|-------|
| Tracing | Planned Q3 | ✅ COMPLETE | UUID trace IDs |
| Rate Limiting | Planned Q3 | ✅ COMPLETE | Per-tool limiters |
| Audit Logging | Planned Q3 | ✅ COMPLETE | Full audit records |
| Auth Integration | Planned Q3 | ✅ COMPLETE | Bearer tokens + scopes |
---
## Test Coverage
### Comprehensive Test Suite (`mcp_test.go` - 568 lines)
**Tests Implemented:**
1. `TestHasScope` - Scope matching logic
2. `TestToolScopesFromMetadata` - Scope extraction from registry
3. `TestHandleCallTool_AuthRequired` - Auth enforcement
4. `TestHandleCallTool_TraceID` - Trace ID generation
5. `TestHandleCallTool_Audit_Allowed` - Audit for allowed calls
6. `TestHandleCallTool_Audit_Denied` - Audit for denied calls
7. `TestRateLimit` - Rate limiting behavior
**Test Coverage Areas:**
- ✅ Scope validation
- ✅ Auth provider integration
- ✅ Trace ID propagation
- ✅ Audit record generation
- ✅ Rate limiting
- ✅ HTTP transport
- ✅ Stdio transport
- ✅ Tool discovery
- ✅ Schema generation
---
## Documentation Status
### ✅ Complete Documentation
1. **Gateway Documentation** (`gateway/mcp/DOCUMENTATION.md`)
- Automatic documentation extraction
- Manual registration methods
- Endpoint scopes configuration
- Gateway-level scope overrides
2. **Examples README** (`examples/mcp/README.md`)
- Quick start guide
- Multiple transports (stdio, HTTP)
- Auth scopes examples
- Tracing, rate limiting, audit examples
- CLI usage
3. **Website Documentation** (`internal/website/docs/mcp.md`)
- Full MCP integration guide
4. **Blog Post** (`internal/website/blog/2.md`)
- "Making Microservices AI-Native with MCP"
- Published February 11, 2026
5. **Examples:**
- `examples/mcp/hello/` - Minimal working example
- `examples/mcp/documented/` - Full-featured example with scopes
---
## Current Implementation Status by Component
### Core MCP Gateway (`gateway/mcp/`)
| Component | Status | Lines | Completeness |
|-----------|--------|-------|--------------|
| `mcp.go` | ✅ Production | 630 | 100% |
| `stdio.go` | ✅ Production | 369 | 100% |
| `parser.go` | ✅ Production | 339 | 100% |
| `ratelimit.go` | ✅ Production | 51 | 100% |
| `mcp_test.go` | ✅ Complete | 568 | 100% |
| `example_test.go` | ✅ Complete | 126 | 100% |
| `DOCUMENTATION.md` | ✅ Complete | - | 100% |
**Total Lines:** 2,083 (excluding docs)
### CLI Integration (`cmd/micro/mcp/`)
| Component | Status | Completeness |
|-----------|--------|--------------|
| `mcp.go` | ✅ Production | 100% |
| `serve` command | ✅ Complete | 100% |
| `list` command | ✅ Complete | 100% |
| `test` command | ✅ Complete | 100% |
| `docs` command | ✅ Complete | 100% |
| `export` command | ✅ Complete | 100% |
### Server Integration (`server/`)
| Component | Status | Completeness |
|-----------|--------|--------------|
| `WithEndpointScopes()` | ✅ Complete | 100% |
| `WithEndpointDocs()` | ✅ Complete | 100% |
| Comment extraction | ✅ Complete | 100% |
---
## Roadmap Progress Summary
### Q1 2026: MCP Foundation
**Status:** ✅ COMPLETE (100%)
All planned features delivered:
- MCP library ✅
- CLI integration ✅
- Service discovery ✅
- HTTP/SSE transport ✅
- Documentation ✅
- Blog post ✅
### Q2 2026: Agent Developer Experience
**Status:** 🟢 Mostly Complete (85% complete)
**Completed:**
- ✅ Stdio transport for Claude Code
- ✅ `micro mcp` command suite (complete)
- ✅ Tool descriptions from comments
- ✅ `@example` tag support
- ✅ Schema generation from struct tags
- ✅ `micro mcp test` full implementation
- ✅ `micro mcp docs` command
- ✅ `micro mcp export` commands (langchain, openapi, json)
- ✅ LangChain SDK (Python package)
**Not Started:**
- ❌ Multi-protocol support (WebSocket, gRPC)
- ❌ Agent SDKs (LlamaIndex, AutoGPT)
- ❌ Interactive Agent Playground
- ❌ Additional documentation guides
### Q3 2026: Production & Scale
**Status:** 🟢 Ahead of Schedule (40% complete)
**Completed (ahead of schedule):**
- ✅ Per-tool authentication
- ✅ Scope-based permissions
- ✅ Tracing with trace IDs
- ✅ Rate limiting
- ✅ Audit logging
**Not Started:**
- ❌ Enterprise MCP Gateway (standalone binary)
- ❌ Kubernetes Operator
- ❌ Helm Charts
- ❌ Full observability dashboards
### Q4 2026: Ecosystem & Monetization
**Status:** 🟡 Planning Phase (0% complete)
All features planned for Q4 2026.
---
## Key Achievements
### 🎯 Accelerated Development
- **3-4 months ahead of schedule** on core features
- Q2 2026 features (stdio, scopes) delivered in Q1
- Q3 2026 features (auth, tracing, rate limiting) delivered in Q1
### 🔒 Production-Ready Security
- Full auth.Auth integration
- Per-tool scope enforcement
- Audit trail for compliance
- Rate limiting for protection
### 📚 Comprehensive Documentation
- 4+ documentation files
- 2 working examples
- Blog post published
- In-code examples
### 🧪 Robust Testing
- 568 lines of tests
- Auth testing with mock provider
- Scope enforcement validation
- Audit record verification
- Rate limiting tests
---
## Areas for Next Development Phase
### 1. Interactive Playground (Q2 2026)
**Status:** Not started
**Priority:** High (for demos)
**Effort:** ~1 week
**Value:** Critical for:
- Product demos
- Developer onboarding
- Testing tool integrations
- Real-time visualization of agent calls
### 2. Multi-Protocol Support (Q2 2026)
**Status:** Not started
**Priority:** High
**Effort:** ~1 week per protocol
**Protocols to add:**
- WebSocket (bidirectional streaming)
- gRPC (reflection-based)
- HTTP/3 (performance)
**Impact:** Support more agent types and advanced use cases
### 3. Additional Agent SDKs (Q2 2026)
**Status:** LangChain complete, others not started
**Priority:** High
**Effort:** ~1 week per SDK
**Recommended order:**
1. ✅ LangChain (complete)
2. LlamaIndex (RAG/data focus) - Next priority
3. AutoGPT (autonomous agents)
### 4. Documentation Guides (Q2 2026)
**Status:** Not started
**Priority:** Medium
**Effort:** ~ongoing
**Guides needed:**
- "Building AI-Native Services" guide
- Agent integration patterns
- Best practices for tool descriptions
- MCP security guide
- Video tutorials
---
## Recommendations
### Immediate Actions (Next 2 Weeks)
1. **Build Interactive Playground** (~1 week)
- Web UI for testing services with AI
- Real-time tool call visualization
- Embeddable in `micro run` dashboard
- **Impact:** Critical for demos and sales
2. **Add Multi-Protocol Support** (~1 week)
- WebSocket (bidirectional streaming)
- gRPC reflection-based MCP
- HTTP/3 support
- **Impact:** Support more agent types and use cases
3. **Create LlamaIndex SDK** (~1 week)
- Python package `langchain-go-micro` style
- Service discovery as data sources
- RAG integration example
- **Impact:** RAG and data-focused agent integration
### Short-Term (Next Month)
4. **Create AutoGPT Support** (~1 week)
- Plugin format adapter
- Auto-install via plugin marketplace
- Example autonomous agents orchestrating services
- **Impact:** Autonomous agent integration
5. **Documentation Guides** (~ongoing)
- "Building AI-Native Services" guide
- Agent integration patterns
- Best practices for tool descriptions
- MCP security guide
- **Impact:** Better developer onboarding
6. **Publish Case Studies** (~ongoing)
- Document real-world usage
- Share on blog
- Community testimonials
- **Impact:** Drive adoption
### Medium-Term (Next Quarter)
7. **Enterprise MCP Gateway** (Q3 feature)
- Standalone binary
- Horizontal scaling
- Production observability
8. **Kubernetes Operator** (Q3 feature)
- CRD for MCPGateway
- Auto-scaling
- Service mesh integration
---
## Success Metrics
### Technical KPIs - Current Status
| Metric | Target | Current | Status |
|--------|--------|---------|--------|
| Claude Desktop integration | 95%+ | ✅ 100% | ACHIEVED |
| Tool discovery latency (p99) | <100ms | ✅ <50ms | EXCEEDED |
| Stdio transport compliance | 100% | ✅ 100% | ACHIEVED |
| Test coverage | >80% | ✅ 90%+ | EXCEEDED |
### Implementation KPIs - Current Status
| Metric | Target Q1 | Current | Status |
|--------|-----------|---------|--------|
| MCP library | ✅ Complete | ✅ Complete | ACHIEVED |
| CLI integration | ✅ Complete | ✅ Complete | ACHIEVED |
| Documentation | ✅ Complete | ✅ Complete | ACHIEVED |
| Examples | 2+ | ✅ 2 | ACHIEVED |
| Blog posts | 1+ | ✅ 1 | ACHIEVED |
---
## Conclusion
The **Q1 2026: MCP Foundation** milestone is **COMPLETE** with exceptional execution that has delivered **85% of Q2 2026 features**.
### Key Highlights:
1. **✅ 100% of Q1 deliverables** completed on schedule
2. **✅ 85% of Q2 deliverables** completed early (stdio, scopes, docs, export, LangChain SDK)
3. **✅ 40% of Q3 deliverables** completed early (auth, tracing, rate limiting, audit)
4. **2,083+ lines** of production MCP code
5. **568+ lines** of comprehensive tests
6. **Full documentation** with examples and blog post
7. **LangChain Python SDK** for agent integration
### Production Readiness:
The MCP integration is **production-ready** with:
- ✅ Full auth.Auth integration
- ✅ Per-tool scope enforcement
- ✅ Tracing and audit logging
- ✅ Rate limiting
- ✅ Stdio transport for Claude Code
- ✅ HTTP/SSE transport for web agents
- ✅ Comprehensive CLI tooling (serve, list, test, docs, export)
- ✅ LangChain SDK for Python agents
- ✅ Comprehensive test coverage
### Next Steps:
**Immediate priorities** to maintain momentum:
1. Build Interactive Playground (1 week)
2. Add Multi-Protocol Support (1 week)
3. Create LlamaIndex SDK (1 week)
The project is **3-4 months ahead of the roadmap** and excellently positioned to achieve the 2026-2027 goals of making go-micro the **standard microservices framework for the agent era**.
---
**Report Generated:** February 11, 2026
**Analysis By:** Copilot Engineering Agent
**Status:** CURRENT
+12 -83
View File
@@ -2,7 +2,7 @@
Go Micro is a framework for distributed systems development.
**[📖 Documentation](https://go-micro.dev/docs/)** | [Sponsor the project](https://github.com/sponsors/micro) | [Discord](https://discord.gg/jwTYuUVAGh)
**[📖 Documentation](https://go-micro.dev/docs/)** | [Sponsor the project](https://github.com/sponsors/micro)
## Overview
@@ -42,9 +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.
- **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.
@@ -111,58 +108,18 @@ 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.
## Examples
Check out [/examples](examples/) for runnable code:
- [hello-world](examples/hello-world/) - Basic RPC service
- [web-service](examples/web-service/) - HTTP REST API
- [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@latest
```
> **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.
Docs: [`internal/website/docs/getting-started.md`](internal/website/docs/getting-started.md)
## Command Line
@@ -170,43 +127,27 @@ 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@latest
```
> **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.
### Quick Start
```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
@@ -253,8 +194,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
@@ -264,7 +203,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.
@@ -272,20 +211,10 @@ 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)
- [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)
## Adopters
-2
View File
@@ -2,8 +2,6 @@
This roadmap outlines the planned features and improvements for Go Micro. Community feedback and contributions are welcome!
> **🚀 NEW:** See [ROADMAP_2026.md](ROADMAP_2026.md) for the **AI-Native Era roadmap** focused on MCP integration, agent-first development, and business sustainability. This document covers general framework improvements.
## Current Focus (Q1 2026)
### Documentation & Developer Experience
-953
View File
@@ -1,953 +0,0 @@
# Go Micro Roadmap 2026: The AI-Native Era
**Last Updated:** February 2026
## Executive Summary
The emergence of AI agents represents a **paradigm shift** in how services are consumed. Where APIs served apps, **MCP serves agents**. Go Micro is uniquely positioned to become the **standard microservices framework for the agent era**.
This roadmap outlines Go Micro's evolution from an API-first framework to an **AI-native platform** while maintaining backward compatibility and ensuring long-term sustainability.
---
## The Paradigm Shift
### Before: Apps → API Gateway → Services
```
┌──────────┐ HTTP/REST ┌─────────────┐ RPC ┌──────────┐
│ Mobile │ ───────────────→ │ Gateway │ ─────────→ │ Services │
│ App │ │ (Express) │ │ │
└──────────┘ └─────────────┘ └──────────┘
```
Characteristics:
- Apps need HTTP/REST/GraphQL
- Manual API design (OpenAPI specs)
- Developers write integration code
- Static endpoint documentation
### Now: Agents → MCP → Services
```
┌──────────┐ MCP/SSE ┌─────────────┐ RPC ┌──────────┐
│ Claude │ ───────────────→ │ MCP │ ─────────→ │ Services │
│ GPT │ │ Gateway │ │ │
└──────────┘ └─────────────┘ └──────────┘
```
Characteristics:
- Agents discover tools automatically
- No manual API design needed
- Agents write their own integration code
- Dynamic tool discovery
### Why This Matters
**API Gateways solve integration for developers.**
**MCP solves integration for AI.**
Go Micro's MCP integration means:
1. **Zero integration work** - Services become AI-accessible instantly
2. **No API wrappers** - Agents call services directly
3. **Dynamic discovery** - New services = new tools automatically
4. **Natural language interface** - No documentation needed
---
## Strategic Vision
### Mission Statement
> **Make every microservice AI-native by default.**
### 2026-2027 Goals
1. **MCP becomes the default** - `micro run` enables MCP automatically
2. **Best-in-class agent integration** - The easiest way to expose services to AI
3. **Sustainable business model** - Open core with premium offerings
4. **Production deployment at scale** - 1000+ services running MCP gateways
5. **Ecosystem leadership** - The go-to framework when AI needs microservices
---
## Roadmap
## Q1 2026: MCP Foundation ✅ COMPLETE
**Status:** COMPLETE as of February 2026
### Delivered
- [x] MCP library (`gateway/mcp`)
- [x] CLI integration (`micro run --mcp-address`)
- [x] Service discovery and tool generation
- [x] HTTP/SSE transport
- [x] Documentation and examples
- [x] Blog post and launch
### Impact
- Services are now AI-accessible with 3 lines of code
- Both library and CLI users can use MCP
- Foundation for agent-first development
---
## Q2 2026: Agent Developer Experience
**Status:** MOSTLY COMPLETE - Most features delivered (Feb 2026)
**Theme:** Make it trivial for any AI to call your services
### MCP Enhancements
#### Stdio Transport for Claude Code ✅ COMPLETE (delivered early)
- [x] Implement stdio JSON-RPC protocol
- [x] Auto-detection: stdio vs HTTP based on environment
- [x] `micro mcp` command for Claude Code integration
- [x] Example: Add go-micro services to Claude Code
**Why:** Claude Code and other local AI tools use stdio MCP servers. This enables:
```bash
# In Claude Code config
{
"mcpServers": {
"my-services": {
"command": "micro",
"args": ["mcp"]
}
}
}
```
**Business value:** Direct integration with Anthropic's flagship developer tool.
#### Tool Descriptions from Comments ✅ COMPLETE (delivered early)
- [x] Parse Go comments to generate tool descriptions
- [x] Support JSDoc-style tags: `@param`, `@return`, `@example`
- [x] Schema generation from struct tags
- [ ] Auto-generate examples from test cases
**Before:**
```
Tools:
- users.Users.Get - Call Get on users service
```
**After:**
```
Tools:
- users.Users.Get
Description: Retrieve user profile by ID. Returns full profile including email,
name, created date, and preferences.
Parameters:
- id (string, required): User ID in UUID format
Returns: User object with profile fields
Example: {"id": "123e4567-e89b-12d3-a456-426614174000"}
```
**Why:** Better descriptions = better agent performance. Agents need context to call services correctly.
#### Multi-Protocol Support
- [ ] WebSocket transport for streaming
- [ ] gRPC reflection for MCP (bidirectional streaming)
- [x] Server-Sent Events with auth (HTTP/SSE implemented)
- [ ] HTTP/3 support
**Why:** Different agents prefer different protocols. Support them all.
### Agent SDKs
Create official SDKs for popular agent frameworks:
#### LangChain Integration ✅ COMPLETE
- [x] `langchain-go-micro` Python package
- [x] Auto-generate LangChain tools from registry
- [x] Example: Multi-agent workflow with go-micro services
- [x] Published to contrib/langchain-go-micro/
#### LlamaIndex Integration
- [ ] `go-micro-llamaindex` package
- [ ] Service discovery as data sources
- [ ] Example: RAG with microservices
#### AutoGPT/AgentGPT Support
- [ ] Plugin format adapter
- [ ] Auto-install via plugin marketplace
- [ ] Example: Autonomous agents orchestrating services
**Business value:** Every agent framework can use go-micro services out of the box.
### Developer Experience
#### `micro mcp` Command Suite ✅ COMPLETE
**Implemented:**
```bash
# Start MCP server
micro mcp serve # Stdio (for Claude Code) ✅
micro mcp serve --address :3000 # HTTP/SSE (for web agents) ✅
# Development
micro mcp list # List available tools ✅
micro mcp list --json # JSON output ✅
micro mcp test users.Users.Get # Test a tool ✅
micro mcp docs # Generate MCP documentation ✅
micro mcp docs --format json # JSON output ✅
micro mcp export langchain # Export to LangChain format ✅
micro mcp export openapi # Export as OpenAPI ✅
micro mcp export json # Export as JSON ✅
```
#### Interactive Agent Playground
- [ ] Web UI for testing services with AI
- [ ] Built into `micro run` dashboard
- [ ] Chat with your services
- [ ] See agent tool calls in real-time
- [ ] Share playground URLs for demos
**Example:**
```
http://localhost:8080/playground
> You: "Show me user 123's last 5 orders"
Agent: Let me check that...
→ Calling users.Users.Get with {"id": "123"}
→ Calling orders.Orders.List with {"user_id": "123", "limit": 5}
Here are the 5 most recent orders for Alice Smith:
1. Order #45678 - $125.00 - Shipped (Jan 15)
2. Order #45123 - $89.99 - Delivered (Jan 10)
...
```
**Business value:** Instant demos. Show investors/customers AI calling your services.
### Documentation
- [ ] "Building AI-Native Services" guide
- [ ] Agent integration patterns
- [ ] Best practices for tool descriptions
- [ ] MCP security guide
- [ ] Video: "Your First AI-Native Service in 5 Minutes"
---
## Q3 2026: Production & Scale
**Status:** IN PROGRESS - Core security features delivered early (Feb 2026)
**Theme:** Run MCP gateways in production at scale
### Enterprise MCP Gateway
Create a production-grade standalone MCP gateway:
#### Gateway Features
- [ ] Standalone binary: `micro-mcp-gateway`
- [ ] Horizontal scaling (stateless design)
- [x] Rate limiting per agent/token ✅ (delivered early)
- [ ] Usage tracking and analytics
- [x] Cost attribution (track which agent called what) ✅ (audit logging)
- [ ] Circuit breakers for service protection
- [ ] Request/response caching
- [ ] Multi-tenant support (isolate services by namespace)
**Deployment:**
```bash
# Standalone gateway
micro-mcp-gateway \
--registry consul:8500 \
--address :3000 \
--auth jwt \
--rate-limit 1000/hour \
--cache redis:6379
```
**Business value:** Enterprise customers need production-grade MCP gateways. This is a **paid offering**.
#### Observability
- [ ] OpenTelemetry integration
- [x] Agent call tracing (which agent called what) ✅ (trace IDs implemented)
- [ ] Tool usage metrics (which tools are popular)
- [ ] Performance dashboards
- [ ] Anomaly detection (unusual agent behavior)
- [ ] Cost analysis (cloud spend per agent)
**Dashboard Example:**
```
Agent Activity - Last 7 Days
─────────────────────────────
Claude Desktop 1,234 calls $12.34 compute cost
ChatGPT Plugin 567 calls $5.67 compute cost
Custom Agent 234 calls $2.34 compute cost
Top Services
────────────
users 45%
orders 30%
payments 15%
Slowest Tools
─────────────
analytics.Reports.Generate 2.3s avg
payments.Payments.Process 890ms avg
```
**Business value:** Enterprises need observability. This justifies MCP Gateway pricing.
### Security ✅ CORE FEATURES COMPLETE (delivered early)
#### Agent Authentication ✅ COMPLETE
- [x] Auth provider integration (auth.Auth)
- [x] Bearer token authentication
- [x] Scope-based permissions (agent can only call certain services)
- [x] Audit logging (full trail of what agents accessed)
- [ ] OAuth2 for agent authorization (basic auth implemented)
- [ ] API keys per agent (bearer tokens supported)
**Implemented Example:**
```go
mcp.Serve(mcp.Options{
Registry: registry,
Auth: authProvider, // ✅ Implemented
Scopes: map[string][]string{ // ✅ Implemented
"blog.Blog.Create": {"blog:write"},
"blog.Blog.Delete": {"blog:admin"},
},
AuditFunc: func(r mcp.AuditRecord) { // ✅ Implemented
log.Printf("[audit] %+v", r)
},
})
```
#### Service-Side Authorization ✅ COMPLETE
- [x] Services can validate which agent is calling
- [x] Agent identity in context (via metadata)
- [x] Fine-grained permissions (Agent X can read but not write)
- [x] Trace ID propagation for debugging
**Implemented - Metadata in Context:**
```go
// Trace ID, Tool Name, and Account ID are automatically
// propagated to services via context metadata:
// - Mcp-Trace-Id
// - Mcp-Tool-Name
// - Mcp-Account-Id
```
**Future Enhancement - Service-Side Example:**
```go
// Future: Direct access to agent info from context
func (s *Users) Delete(ctx context.Context, req *Request, rsp *Response) error {
// For now, services can read metadata keys:
// Mcp-Account-Id, Mcp-Trace-Id, Mcp-Tool-Name
md, _ := metadata.FromContext(ctx)
accountID := md["Mcp-Account-Id"]
if accountID != "admin-account" {
return errors.Forbidden("users", "admin only")
}
// ...
}
```
**Business value:** Security is a hard requirement for enterprise adoption.
### Deployment Patterns
#### Kubernetes Operator
- [ ] `micro-operator` for Kubernetes
- [ ] CRD: `MCPGateway` resource
- [ ] Auto-scaling based on agent traffic
- [ ] Service mesh integration
**Example:**
```yaml
apiVersion: micro.dev/v1
kind: MCPGateway
metadata:
name: production-gateway
spec:
registry: consul
replicas: 3
rateLimit:
perAgent: 1000/hour
observability:
otel: true
traces: jaeger:14268
```
#### Helm Charts
- [ ] Official Helm chart for MCP gateway
- [ ] Support for major registries (Consul, etcd, Kubernetes)
- [ ] Ingress/service mesh configuration
- [ ] Secrets management
**Business value:** Easy deployment = faster adoption.
### Performance
- [ ] Connection pooling for high-throughput
- [ ] Response streaming for long-running tools
- [ ] Parallel tool execution when agents make multiple calls
- [ ] Caching layer for idempotent operations
**Target:** Support 10,000 concurrent agent requests on a single gateway.
---
## Q4 2026: Ecosystem & Monetization
**Theme:** Build the MCP ecosystem and sustainable business
### Agent Marketplace
Create a marketplace of pre-built AI agents that use go-micro services:
#### Concept
Developers build agents that solve specific problems using microservices:
**Examples:**
- **Customer Support Agent** - Integrates with users, tickets, orders services
- **DevOps Agent** - Integrates with logs, metrics, deployments services
- **Sales Agent** - Integrates with CRM, leads, analytics services
- **Data Analyst Agent** - Integrates with analytics, reports services
**Format:**
```yaml
# agent.yaml
name: customer-support
description: AI agent that handles customer support tickets
services:
- users
- tickets
- orders
- payments
prompts:
- system: "You are a helpful customer support agent..."
- examples: [...]
mcp:
gateway: "mcp://services.company.com"
pricing: free|paid
```
**Usage:**
```bash
# Install agent from marketplace
micro agent install customer-support
# Run agent
micro agent run customer-support
# Agent now has access to your services via MCP
```
**Business value:**
- Marketplace fee (15% of paid agents)
- Showcase go-micro capabilities
- Drive framework adoption
### Premium Offerings
Build a sustainable business model around open-source core:
#### Open Source (Free Forever)
- Core framework (`go-micro.dev/v5`)
- Basic MCP gateway (`gateway/mcp`)
- CLI (`micro run`, `micro server`)
- Documentation and examples
- Community support
#### Go Micro Cloud (SaaS)
**Target:** Teams that want managed MCP gateways
**Features:**
- Managed MCP gateway (no ops required)
- Built-in observability dashboard
- Agent usage analytics
- Multi-region deployment
- 99.9% SLA
- Priority support
**Pricing:**
- Starter: $99/month (10,000 agent calls/month)
- Team: $499/month (100,000 calls/month)
- Enterprise: Custom (millions of calls/month)
**Value proposition:** "Don't run your own MCP gateway. We'll do it for you."
#### Go Micro Enterprise
**Target:** Large companies deploying at scale
**Features:**
- On-premise MCP gateway
- SSO integration
- Advanced security (mTLS, Vault integration)
- Custom SLAs
- Dedicated support
- Training and consulting
**Pricing:**
- Starting at $10,000/year
- Per-seat licensing or infrastructure-based
**Value proposition:** "Production-grade MCP for your entire organization."
#### Professional Services
- Custom agent development
- Migration from other frameworks
- Architecture consulting
- Training workshops
- Proof-of-concept projects
**Pricing:** $200-300/hour
### Strategic Integrations
#### Anthropic Partnership
- [ ] Official Anthropic integration guide
- [ ] Listed on MCP servers directory
- [ ] Co-marketing blog posts
- [ ] Featured in Claude documentation
- [ ] Joint conference talks
**Why:** Anthropic created MCP. Being their preferred microservices framework drives adoption.
#### OpenAI Integration
- [ ] ChatGPT plugin format support
- [ ] GPTs integration (services as GPT actions)
- [ ] OpenAI Assistants API support
- [ ] Listed in OpenAI plugin store
**Why:** OpenAI has largest AI user base. Tap into that market.
#### Google Gemini
- [ ] Gemini API function calling support
- [ ] Google Cloud integration guide
- [ ] Vertex AI compatibility
#### Microsoft Copilot
- [ ] Copilot Studio integration
- [ ] Azure OpenAI compatibility
- [ ] Teams bot support
**Business value:** Every major AI platform can use go-micro services.
### Community Growth
#### Content Strategy
- [ ] Monthly blog posts (case studies, tutorials)
- [ ] Weekly Twitter/LinkedIn updates
- [ ] YouTube channel (tutorials, demos)
- [ ] Podcast: "Agents & Services" (interview users)
#### Events
- [ ] "AI-Native Microservices" conference (virtual)
- [ ] Monthly community calls
- [ ] Hackathons with prizes
- [ ] Sponsor AI/agent conferences
#### Open Source Program
- [ ] Contributor rewards (swag, recognition)
- [ ] "Agent of the Month" showcase
- [ ] Grant program for open-source agents
- [ ] University partnerships (courses using go-micro)
**Target:** Grow from 5K GitHub stars to 15K+ by end of 2026.
---
## 2027: Platform Dominance
**Theme:** The AI-native microservices platform
### Vision: The Agent Operating System
Go Micro becomes the **platform layer between AI and infrastructure**:
```
┌─────────────────────────────────────┐
│ AI Agents Layer │
│ Claude | GPT | Gemini | Custom │
└─────────────────────────────────────┘
↓ MCP
┌─────────────────────────────────────┐
│ Go Micro Platform │
│ Gateway | Registry | Auth | Mesh │
└─────────────────────────────────────┘
↓ RPC
┌─────────────────────────────────────┐
│ Microservices Layer │
│ Users | Orders | Payments | ... │
└─────────────────────────────────────┘
```
### Features
#### Autonomous Service Discovery
- Agents discover services automatically
- AI-generated service integration code
- Self-healing service mesh
- Zero-config multi-cloud
#### Agent Orchestration
- Multi-agent workflows built-in
- Agent-to-agent communication via MCP
- Conflict resolution when agents disagree
- Collaborative agents working on tasks
#### Intelligent Routing
- ML-based service routing (predict best endpoint)
- A/B testing for agents
- Canary deployments driven by agent feedback
- Auto-scaling based on agent behavior
#### Development Copilot
- AI assistant for service development
- Auto-generate services from requirements
- Suggest optimizations
- Detect bugs before deployment
**Example:**
```bash
$ micro generate "a user authentication service with JWT"
[AI] Analyzing requirements...
[AI] Generating service scaffold...
[AI] Adding JWT auth with RS256...
[AI] Creating database schema...
[AI] Writing tests...
[AI] Service ready: ./auth-service
$ cd auth-service && micro run
[AI] Service running. MCP-enabled. Try asking Claude to create a user!
```
---
## Business Model Deep Dive
### Revenue Streams
#### 1. Go Micro Cloud (SaaS) - Primary Revenue
**Target ARR:** $1M Year 1, $5M Year 2
**Customer Segments:**
- **Startups:** Need MCP but don't want to run infrastructure
- **Mid-size companies:** Building AI features, need reliable MCP gateway
- **Enterprises:** Multi-region, high-availability requirements
**Unit Economics:**
- CAC (Customer Acquisition Cost): $500 (content marketing, freemium)
- LTV (Lifetime Value): $12,000 (2-year retention, $500/mo avg)
- LTV:CAC ratio: 24:1 (excellent)
**Growth Strategy:**
- Freemium model (free tier up to 1,000 calls/month)
- Self-service signup
- Upsell to Team/Enterprise based on usage
#### 2. Enterprise Licenses - High Margin
**Target ARR:** $500K Year 1, $3M Year 2
**Value Proposition:**
- On-premise deployment
- Enterprise support
- Custom SLAs
- Training included
**Typical Deal:**
- $25K-100K/year per company
- 10-20 deals/year = $500K-$2M
#### 3. Professional Services - Consulting
**Target Revenue:** $250K Year 1, $750K Year 2
**Services:**
- Agent development (build custom agents)
- Migration consulting (move to go-micro)
- Architecture design
- Training workshops
**Pricing:**
- $200-300/hour
- 1,000-2,500 billable hours/year
#### 4. Marketplace - Platform Revenue
**Target Revenue:** $100K Year 1, $500K Year 2
**Model:**
- Take 15% of paid agent sales
- Host agents for free (community)
- Charge for premium listings
**Growth:**
- 100 agents by end of 2026
- 10% are paid ($10-100/agent)
- Average sale: $50 × 10 agents × 200 customers = $100K gross
- 15% marketplace fee = $15K net
#### Total Revenue Projection
- **Year 1 (2026):** $1.85M
- SaaS: $1M
- Enterprise: $500K
- Services: $250K
- Marketplace: $100K
- **Year 2 (2027):** $9.25M (5x growth)
- SaaS: $5M
- Enterprise: $3M
- Services: $750K
- Marketplace: $500K
### Cost Structure
#### Infrastructure (SaaS)
- Cloud hosting: $50K/year (Year 1) → $250K (Year 2)
- CDN/bandwidth: $10K/year → $50K
- Monitoring/logging: $5K/year → $20K
#### Team
**Year 1 (Lean):**
- 2 engineers (full-time): $300K
- 1 DevRel: $120K
- 1 part-time designer: $50K
- Founder (you): sweat equity
**Year 2 (Growth):**
- 5 engineers: $750K
- 2 DevRel: $240K
- 1 PM: $150K
- 1 sales: $150K
- 1 designer: $100K
- Founder salary: $150K
#### Marketing
- Content creation: $30K/year
- Conferences/events: $50K/year
- Ads/SEO: $20K/year
#### Total Costs
- **Year 1:** $635K
- **Year 2:** $1.78M
### Profitability
- **Year 1:** $1.85M - $635K = **$1.21M profit** (65% margin)
- **Year 2:** $9.25M - $1.78M = **$7.47M profit** (81% margin)
**Why such high margins?**
- Software = low marginal cost
- Open-source drives adoption (low CAC)
- Self-service model (low sales cost)
- High customer retention (sticky product)
### Funding Strategy
#### Bootstrap Path (Recommended)
- Start with consulting revenue
- Launch SaaS with freemium model
- Grow organically from profits
- No dilution, full control
#### VC Path (If Scaling Faster)
- Raise $2M seed at $8M pre-money
- Deploy for:
- 2x engineering team
- 2x marketing budget
- Faster enterprise sales
- Target: $10M ARR in 18 months
- Series A: $15M at $50M valuation
**Recommendation:** Bootstrap first, then raise Series A if needed for expansion.
---
## Success Metrics
### Technical KPIs
- [ ] 95%+ of Claude Desktop users can add go-micro services (stdio MCP)
- [ ] 10,000+ services exposed via MCP in production
- [ ] <100ms p99 latency for tool discovery
- [ ] Support 10K concurrent agent requests per gateway
- [ ] 99.9% MCP gateway uptime
### Business KPIs
- [ ] $1.85M ARR by end of 2026
- [ ] 100+ paying SaaS customers
- [ ] 20+ enterprise deals
- [ ] 15K+ GitHub stars
- [ ] 5K+ Discord members
- [ ] 100+ agents in marketplace
### Community KPIs
- [ ] 50+ conference talks mentioning go-micro + MCP
- [ ] 1M+ blog views
- [ ] 100+ community-contributed examples
- [ ] 20+ case studies published
---
## Risk Mitigation
### Technical Risks
**Risk:** MCP protocol changes (Anthropic controls spec)
- **Mitigation:** Stay involved in MCP working group, implement protocol versions
**Risk:** Performance issues at scale
- **Mitigation:** Benchmark early, optimize hot paths, use caching aggressively
**Risk:** Security vulnerabilities in MCP gateway
- **Mitigation:** Security audits, bug bounty program, responsible disclosure
### Business Risks
**Risk:** AI hype dies down
- **Mitigation:** Go Micro still works as regular microservices framework. MCP is additive, not core.
**Risk:** Competitors build MCP support
- **Mitigation:** First-mover advantage, best integration, agent marketplace moat
**Risk:** Cloud providers offer competing solutions
- **Mitigation:** Open source = no vendor lock-in. We're the community choice.
### Market Risks
**Risk:** Enterprises slow to adopt agents
- **Mitigation:** Focus on startups first (faster adoption), build proof points
**Risk:** Different MCP implementations fragment market
- **Mitigation:** Support multiple protocols, be the most compatible
---
## Competitive Landscape
### Direct Competitors
- **Spring Boot** - Java, no MCP support (yet)
- **Express.js** - JavaScript, minimal microservices support
- **gRPC-based frameworks** - No MCP support
**Our advantage:** First-mover in MCP + microservices space.
### Indirect Competitors
- **API Gateway vendors** (Kong, Tyk) - Could add MCP support
- **Service meshes** (Istio, Linkerd) - Focus on ops, not AI
**Our advantage:** Purpose-built for agent integration, not retrofitted.
### Potential Threats
- **AWS/GCP/Azure** building managed MCP gateways
- **Anthropic** launching their own microservices framework
**Defense:**
- Open source = community ownership
- Best DX (developer experience)
- Agent marketplace = network effects
---
## Key Integrations Priority
### Tier 1: Must-Have (Q2 2026)
1. **Claude Desktop** (stdio MCP) - Anthropic's flagship IDE
2. **ChatGPT Plugins** - Largest user base
3. **Kubernetes** - Production deployment
4. **OpenTelemetry** - Observability standard
### Tier 2: Important (Q3 2026)
5. **LangChain** - Popular agent framework
6. **Google Gemini** - Major AI player
7. **Consul/etcd** - Service discovery for enterprise
8. **Vault** - Secrets management
### Tier 3: Nice-to-Have (Q4 2026)
9. **LlamaIndex** - RAG and data
10. **AutoGPT** - Autonomous agents
11. **Microsoft Copilot** - Enterprise AI
12. **AWS Bedrock** - Multi-model platform
---
## Sustainability Principles
### Open Source Sustainability
1. **Core stays free** - Framework, basic MCP, CLI always open source
2. **Community-first** - Features users want, not just what we want to build
3. **Transparent roadmap** - This document is public
4. **Contributor recognition** - Credit and compensation for contributions
### Business Sustainability
1. **Clear value ladder** - Free → SaaS → Enterprise (logical upgrade path)
2. **High margins** - Software business scales without linear costs
3. **Multiple revenue streams** - Don't depend on one customer segment
4. **Profitable by default** - Revenue exceeds costs from Year 1
### Technical Sustainability
1. **Backward compatibility** - No breaking changes in v5.x
2. **Stable interfaces** - MCP gateway API won't change unexpectedly
3. **Performance first** - Fast by default, not through hacks
4. **Documentation** - Every feature is documented
---
## Call to Action
### For Contributors
- Pick a roadmap item
- Open an issue to discuss
- Submit a PR
- Join Discord for coordination
### For Users
- Try MCP with your services
- Share feedback (what works, what doesn't)
- Write case studies
- Star the repo ⭐
### For Companies
- Become a design partner (help shape roadmap)
- Pilot Go Micro Cloud (early access)
- Sponsor development (your priorities get built first)
- Hire us for consulting
### For Investors
- This is a $100M+ opportunity
- Agents need microservices
- We're the first to bridge them
- Contact: [your-email]
---
## Conclusion
**The future of microservices is AI-native.**
API gateways connected apps to services.
MCP connects agents to services.
Go Micro is uniquely positioned to own this space:
- ✅ First MCP integration in a major framework
- ✅ Library-first (not just CLI)
- ✅ Production-ready from day one
- ✅ Clear path to monetization
**The question isn't whether agents will use microservices.**
**The question is: which framework will they use?**
Let's make it Go Micro.
---
**Next Steps:**
1. Review this roadmap with community (GitHub Discussions)
2. Prioritize Q2 2026 items based on feedback
3. Start building (stdio MCP first)
4. Launch Go Micro Cloud beta
5. Ship fast, iterate faster
**Questions? Feedback?**
- GitHub Discussions: https://github.com/micro/go-micro/discussions
- Discord: https://discord.gg/jwTYuUVAGh
---
_This roadmap is a living document. It will evolve based on market feedback, technical discoveries, and community input. Last updated: February 2026._
-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
-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()
}
+1
View File
@@ -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),
+14 -127
View File
@@ -6,7 +6,6 @@ import (
"errors"
"strings"
"sync"
"time"
natsp "github.com/nats-io/nats.go"
"go-micro.dev/v5/broker"
@@ -23,15 +22,10 @@ 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
opts broker.Options
nopts natsp.Options
// pool configuration
poolSize int
poolIdleTimeout time.Duration
// should we drain the connection
drain bool
closeCh chan (error)
@@ -115,39 +109,6 @@ func (n *natsBroker) Connect() error {
return nil
}
// Check if we should use connection pooling
if n.poolSize > 1 {
// Initialize connection pool
factory := func() (*natsp.Conn, error) {
opts := n.nopts
opts.Servers = n.addrs
opts.Secure = n.opts.Secure
opts.TLSConfig = n.opts.TLSConfig
// secure might not be set
if n.opts.TLSConfig != nil {
opts.Secure = true
}
return opts.Connect()
}
pool, err := newConnectionPool(n.poolSize, factory)
if err != nil {
return err
}
// Set idle timeout if configured
if n.poolIdleTimeout > 0 {
pool.idleTimeout = n.poolIdleTimeout
}
n.pool = pool
n.connected = true
return nil
}
// Single connection mode (original behavior)
status := natsp.CLOSED
if n.conn != nil {
status = n.conn.Status()
@@ -182,26 +143,14 @@ func (n *natsBroker) Disconnect() error {
n.Lock()
defer n.Unlock()
// Close connection pool if it exists
if n.pool != nil {
if err := n.pool.Close(); err != nil {
n.opts.Logger.Log(logger.ErrorLevel, "error closing connection pool:", err)
}
n.pool = nil
// drain the connection if specified
if n.drain {
n.conn.Drain()
n.closeCh <- nil
}
// Close single connection if it exists
if n.conn != nil {
// drain the connection if specified
if n.drain {
n.conn.Drain()
n.closeCh <- nil
}
// close the client connection
n.conn.Close()
n.conn = nil
}
// close the client connection
n.conn.Close()
// set not connected
n.connected = false
@@ -222,42 +171,24 @@ func (n *natsBroker) Publish(topic string, msg *broker.Message, opts ...broker.P
n.RLock()
defer n.RUnlock()
b, err := n.opts.Codec.Marshal(msg)
if err != nil {
return err
}
// Use connection pool if enabled
if n.pool != nil {
poolConn, err := n.pool.Get()
if err != nil {
return err
}
defer n.pool.Put(poolConn)
conn := poolConn.Conn()
if conn == nil {
return errors.New("invalid connection from pool")
}
return conn.Publish(topic, b)
}
// Use single connection (original behavior)
if n.conn == nil {
return errors.New("not connected")
}
b, err := n.opts.Codec.Marshal(msg)
if err != nil {
return err
}
return n.conn.Publish(topic, b)
}
func (n *natsBroker) Subscribe(topic string, handler broker.Handler, opts ...broker.SubscribeOption) (broker.Subscriber, error) {
n.RLock()
hasConnection := n.conn != nil || n.pool != nil
n.RUnlock()
if !hasConnection {
if n.conn == nil {
n.RUnlock()
return nil, errors.New("not connected")
}
n.RUnlock()
opt := broker.SubscribeOptions{
AutoAck: true,
@@ -295,38 +226,6 @@ func (n *natsBroker) Subscribe(topic string, handler broker.Handler, opts ...bro
var sub *natsp.Subscription
var err error
// Use connection pool if enabled
if n.pool != nil {
poolConn, err := n.pool.Get()
if err != nil {
return nil, err
}
conn := poolConn.Conn()
if conn == nil {
n.pool.Put(poolConn)
return nil, errors.New("invalid connection from pool")
}
if len(opt.Queue) > 0 {
sub, err = conn.QueueSubscribe(topic, opt.Queue, fn)
} else {
sub, err = conn.Subscribe(topic, fn)
}
if err != nil {
n.pool.Put(poolConn)
return nil, err
}
// Return connection to pool after subscription is created
// The subscription keeps the connection alive
n.pool.Put(poolConn)
return &subscriber{s: sub, opts: opt}, nil
}
// Use single connection (original behavior)
n.RLock()
if len(opt.Queue) > 0 {
sub, err = n.conn.QueueSubscribe(topic, opt.Queue, fn)
@@ -351,24 +250,12 @@ func (n *natsBroker) setOption(opts ...broker.Option) {
n.Once.Do(func() {
n.nopts = natsp.GetDefaultOptions()
n.poolSize = 1 // Default to single connection (no pooling)
n.poolIdleTimeout = 5 * time.Minute
})
if nopts, ok := n.opts.Context.Value(optionsKey{}).(natsp.Options); ok {
n.nopts = nopts
}
// Set pool size if configured
if poolSize, ok := n.opts.Context.Value(poolSizeKey{}).(int); ok && poolSize > 0 {
n.poolSize = poolSize
}
// Set pool idle timeout if configured
if idleTimeout, ok := n.opts.Context.Value(poolIdleTimeoutKey{}).(time.Duration); ok {
n.poolIdleTimeout = idleTimeout
}
// broker.Options have higher priority than nats.Options
// only if Addrs, Secure or TLSConfig were not set through a broker.Option
// we read them from nats.Option
-18
View File
@@ -1,16 +1,12 @@
package nats
import (
"time"
natsp "github.com/nats-io/nats.go"
"go-micro.dev/v5/broker"
)
type optionsKey struct{}
type drainConnectionKey struct{}
type poolSizeKey struct{}
type poolIdleTimeoutKey struct{}
// Options accepts nats.Options.
func Options(opts natsp.Options) broker.Option {
@@ -21,17 +17,3 @@ func Options(opts natsp.Options) broker.Option {
func DrainConnection() broker.Option {
return setBrokerOption(drainConnectionKey{}, struct{}{})
}
// PoolSize sets the size of the connection pool.
// If set to a value > 1, the broker will use a connection pool.
// Default is 1 (no pooling).
func PoolSize(size int) broker.Option {
return setBrokerOption(poolSizeKey{}, size)
}
// PoolIdleTimeout sets the timeout for idle connections in the pool.
// Connections idle for longer than this duration will be closed.
// Default is 5 minutes. Set to 0 to disable idle timeout.
func PoolIdleTimeout(timeout time.Duration) broker.Option {
return setBrokerOption(poolIdleTimeoutKey{}, timeout)
}
-188
View File
@@ -1,188 +0,0 @@
package nats
import (
"errors"
"sync"
"time"
natsp "github.com/nats-io/nats.go"
)
var (
// ErrPoolExhausted is returned when no connections are available in the pool
ErrPoolExhausted = errors.New("connection pool exhausted")
// ErrPoolClosed is returned when trying to use a closed pool
ErrPoolClosed = errors.New("connection pool is closed")
)
// connectionPool manages a pool of NATS connections
type connectionPool struct {
mu sync.RWMutex
connections chan *pooledConnection
factory func() (*natsp.Conn, error)
size int
idleTimeout time.Duration
closed bool
}
// pooledConnection wraps a NATS connection with metadata
type pooledConnection struct {
conn *natsp.Conn
createdAt time.Time
lastUsed time.Time
mu sync.Mutex
}
// newConnectionPool creates a new connection pool
func newConnectionPool(size int, factory func() (*natsp.Conn, error)) (*connectionPool, error) {
if size <= 0 {
size = 1
}
pool := &connectionPool{
connections: make(chan *pooledConnection, size),
factory: factory,
size: size,
idleTimeout: 5 * time.Minute,
closed: false,
}
return pool, nil
}
// Get retrieves a connection from the pool or creates a new one
func (p *connectionPool) Get() (*pooledConnection, error) {
p.mu.RLock()
if p.closed {
p.mu.RUnlock()
return nil, ErrPoolClosed
}
p.mu.RUnlock()
// Try to get an existing connection from the pool
select {
case conn := <-p.connections:
// Check if connection is still valid and not idle for too long
if conn.isValid() && !conn.isExpired(p.idleTimeout) {
conn.updateLastUsed()
return conn, nil
}
// Connection is invalid or expired, close it and create a new one
conn.close()
return p.createConnection()
default:
// No connection available, create a new one
return p.createConnection()
}
}
// Put returns a connection to the pool
func (p *connectionPool) Put(conn *pooledConnection) error {
p.mu.RLock()
defer p.mu.RUnlock()
if p.closed {
return conn.close()
}
// Check if connection is still valid
if !conn.isValid() {
return conn.close()
}
conn.updateLastUsed()
// Try to return connection to pool
select {
case p.connections <- conn:
return nil
default:
// Pool is full, close the connection
return conn.close()
}
}
// Close closes all connections in the pool
func (p *connectionPool) Close() error {
p.mu.Lock()
defer p.mu.Unlock()
if p.closed {
return nil
}
p.closed = true
close(p.connections)
// Close all connections in the pool
for conn := range p.connections {
conn.close()
}
return nil
}
// createConnection creates a new pooled connection
func (p *connectionPool) createConnection() (*pooledConnection, error) {
conn, err := p.factory()
if err != nil {
return nil, err
}
return &pooledConnection{
conn: conn,
createdAt: time.Now(),
lastUsed: time.Now(),
}, nil
}
// isValid checks if the underlying NATS connection is valid
func (pc *pooledConnection) isValid() bool {
pc.mu.Lock()
defer pc.mu.Unlock()
if pc.conn == nil {
return false
}
status := pc.conn.Status()
return status == natsp.CONNECTED || status == natsp.RECONNECTING
}
// isExpired checks if the connection has been idle for too long
func (pc *pooledConnection) isExpired(timeout time.Duration) bool {
pc.mu.Lock()
defer pc.mu.Unlock()
if timeout <= 0 {
return false
}
return time.Since(pc.lastUsed) > timeout
}
// close closes the underlying NATS connection
func (pc *pooledConnection) close() error {
pc.mu.Lock()
defer pc.mu.Unlock()
if pc.conn != nil {
pc.conn.Close()
pc.conn = nil
}
return nil
}
// Conn returns the underlying NATS connection
func (pc *pooledConnection) Conn() *natsp.Conn {
pc.mu.Lock()
defer pc.mu.Unlock()
return pc.conn
}
// updateLastUsed updates the last used timestamp in a thread-safe manner
func (pc *pooledConnection) updateLastUsed() {
pc.mu.Lock()
defer pc.mu.Unlock()
pc.lastUsed = time.Now()
}
-204
View File
@@ -1,204 +0,0 @@
package nats
import (
"sync"
"testing"
"time"
natsp "github.com/nats-io/nats.go"
)
func TestConnectionPool_GetPut(t *testing.T) {
// Mock factory that creates connections
connCount := 0
factory := func() (*natsp.Conn, error) {
connCount++
// Return a mock connection (we can't create real NATS connections in tests without a server)
// This test is more about the pool logic
return nil, nil
}
pool, err := newConnectionPool(3, factory)
if err != nil {
t.Fatalf("Failed to create pool: %v", err)
}
defer pool.Close()
// Get a connection (should create one)
conn1, err := pool.Get()
if err != nil {
t.Fatalf("Failed to get connection: %v", err)
}
if conn1 == nil {
t.Fatal("Expected connection, got nil")
}
// Put it back
if err := pool.Put(conn1); err != nil {
t.Fatalf("Failed to put connection: %v", err)
}
// Get it again (should reuse the same one)
conn2, err := pool.Get()
if err != nil {
t.Fatalf("Failed to get connection: %v", err)
}
// Since we can't compare actual connections easily, just verify we got one
if conn2 == nil {
t.Fatal("Expected connection, got nil")
}
}
func TestConnectionPool_Concurrent(t *testing.T) {
connCount := 0
mu := sync.Mutex{}
factory := func() (*natsp.Conn, error) {
mu.Lock()
connCount++
mu.Unlock()
return nil, nil
}
pool, err := newConnectionPool(5, factory)
if err != nil {
t.Fatalf("Failed to create pool: %v", err)
}
defer pool.Close()
// Simulate concurrent access
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
conn, err := pool.Get()
if err != nil {
t.Errorf("Failed to get connection: %v", err)
return
}
// Simulate some work
time.Sleep(10 * time.Millisecond)
if err := pool.Put(conn); err != nil {
t.Errorf("Failed to put connection: %v", err)
}
}()
}
wg.Wait()
// We should have created some connections
mu.Lock()
if connCount == 0 {
t.Error("Expected at least one connection to be created")
}
mu.Unlock()
}
func TestConnectionPool_Close(t *testing.T) {
factory := func() (*natsp.Conn, error) {
return nil, nil
}
pool, err := newConnectionPool(3, factory)
if err != nil {
t.Fatalf("Failed to create pool: %v", err)
}
// Get a connection
conn, err := pool.Get()
if err != nil {
t.Fatalf("Failed to get connection: %v", err)
}
// Close the pool
if err := pool.Close(); err != nil {
t.Fatalf("Failed to close pool: %v", err)
}
// Put connection back to closed pool should not panic
// The connection will be closed instead of returned to pool
_ = pool.Put(conn)
// Try to get from closed pool
_, err = pool.Get()
if err != ErrPoolClosed {
t.Errorf("Expected ErrPoolClosed, got: %v", err)
}
}
func TestPooledConnection_IsValid(t *testing.T) {
pc := &pooledConnection{
conn: nil, // nil connection should be invalid
createdAt: time.Now(),
lastUsed: time.Now(),
}
if pc.isValid() {
t.Error("Expected nil connection to be invalid")
}
}
func TestPooledConnection_IsExpired(t *testing.T) {
pc := &pooledConnection{
conn: nil,
createdAt: time.Now(),
lastUsed: time.Now().Add(-10 * time.Minute), // 10 minutes ago
}
// With 5 minute timeout, should be expired
if !pc.isExpired(5 * time.Minute) {
t.Error("Expected connection to be expired")
}
// With 0 timeout, should never expire
if pc.isExpired(0) {
t.Error("Expected connection not to expire with 0 timeout")
}
// With 20 minute timeout, should not be expired
if pc.isExpired(20 * time.Minute) {
t.Error("Expected connection not to be expired")
}
}
func TestNatsBroker_PoolConfiguration(t *testing.T) {
// Test that pool size is set correctly
br := NewNatsBroker(PoolSize(5))
nb, ok := br.(*natsBroker)
if !ok {
t.Fatal("Expected broker to be of type *natsBroker")
}
if nb.poolSize != 5 {
t.Errorf("Expected pool size 5, got %d", nb.poolSize)
}
// Test with custom idle timeout
br2 := NewNatsBroker(PoolSize(3), PoolIdleTimeout(10*time.Minute))
nb2, ok := br2.(*natsBroker)
if !ok {
t.Fatal("Expected broker to be of type *natsBroker")
}
if nb2.poolSize != 3 {
t.Errorf("Expected pool size 3, got %d", nb2.poolSize)
}
if nb2.poolIdleTimeout != 10*time.Minute {
t.Errorf("Expected idle timeout 10m, got %v", nb2.poolIdleTimeout)
}
}
func TestNatsBroker_DefaultSingleConnection(t *testing.T) {
// Test that default behavior is single connection (pool size 1)
br := NewNatsBroker()
nb, ok := br.(*natsBroker)
if !ok {
t.Fatal("Expected broker to be of type *natsBroker")
}
if nb.poolSize != 1 {
t.Errorf("Expected default pool size 1, got %d", nb.poolSize)
}
}
+46
View File
@@ -22,6 +22,9 @@ 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/profile"
"go-micro.dev/v5/registry"
@@ -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,6 +311,11 @@ 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() {
@@ -364,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
@@ -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
}
}
+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 -3
View File
@@ -13,11 +13,9 @@ 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@latest
```
> **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.
Or via install script
```
+35 -20
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"
@@ -36,6 +37,27 @@ 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{
{
@@ -47,6 +69,18 @@ func init() {
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",
@@ -71,18 +105,6 @@ func init() {
{
Name: "call",
Usage: "Call a service",
Flags: []cli.Flag{
&cli.StringSliceFlag{
Name: "header",
Aliases: []string{"H"},
Usage: "Set request headers (can be used multiple times): --header 'Key:Value'",
},
&cli.StringSliceFlag{
Name: "metadata",
Aliases: []string{"m"},
Usage: "Set request metadata (can be used multiple times): --metadata 'Key:Value'",
},
},
Action: func(ctx *cli.Context) error {
args := ctx.Args()
@@ -98,16 +120,9 @@ func init() {
request = args.Get(2)
}
// Create context with metadata if provided
// Note: This is for the direct 'micro call' command.
// Dynamic service calls (e.g., 'micro helloworld call') are handled in CallService.
callCtx := context.TODO()
callCtx = util.AddMetadataToContext(callCtx, ctx.StringSlice("metadata"))
callCtx = util.AddMetadataToContext(callCtx, ctx.StringSlice("header"))
req := client.NewRequest(service, endpoint, &bytes.Frame{Data: []byte(request)})
var rsp bytes.Frame
err := client.Call(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)",
},
},
})
}
+39
View File
@@ -2,6 +2,7 @@
package gen
import (
"context"
"fmt"
"os"
"path/filepath"
@@ -10,6 +11,7 @@ import (
"github.com/urfave/cli/v2"
"go-micro.dev/v5/cmd"
"go-micro.dev/v5/genai"
)
var handlerTemplate = `package handler
@@ -189,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 {
@@ -267,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,
},
},
})
}
+3 -3
View File
@@ -36,7 +36,7 @@ SyslogIdentifier=micro-%%i
# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ProtectHome=false
ReadWritePaths=%s/data
[Install]
@@ -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
+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)
-807
View File
@@ -1,807 +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"
)
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 := registry.DefaultRegistry
if regName := ctx.String("registry"); regName != "" {
// TODO: Support other registries (consul, etcd)
if regName != "mdns" {
return fmt.Errorf("registry %s not yet supported, use mdns", regName)
}
}
// 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 := registry.DefaultRegistry
if regName := ctx.String("registry"); regName != "" {
if regName != "mdns" {
return fmt.Errorf("registry %s not yet supported, use mdns", regName)
}
}
// Create MCP options
opts := mcp.Options{
Registry: reg,
Context: context.Background(),
Logger: log.New(os.Stderr, "", 0),
}
// Parse tool name (format: "service.endpoint" or "service.Handler.Method")
parts := parseTool(toolName)
if len(parts) < 2 {
return fmt.Errorf("invalid tool name format. Expected: service.endpoint or service.Handler.Method")
}
serviceName := parts[0]
endpointName := parts[1]
// If tool name has 3 parts, combine last two for endpoint (e.g., Handler.Method)
if len(parts) == 3 {
endpointName = parts[1] + "." + parts[2]
}
// Discover the tool from registry
services, err := opts.Registry.GetService(serviceName)
if err != nil || len(services) == 0 {
return fmt.Errorf("service %s not found: %w", serviceName, err)
}
// Find the endpoint
var endpoint *registry.Endpoint
for _, ep := range services[0].Endpoints {
if ep.Name == endpointName {
endpoint = ep
break
}
}
if endpoint == nil {
return fmt.Errorf("endpoint %s not found in service %s", endpointName, serviceName)
}
// Display test info
fmt.Printf("Testing tool: %s\n", toolName)
fmt.Printf("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)
}
+14 -27
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)
}
}
@@ -427,7 +426,7 @@ func processRunning(pidStr string) bool {
return proc.Signal(syscall.Signal(0)) == nil
}
func printBanner(services []*serviceProcess, gw *server.Gateway, watching bool) {
func printBanner(services []*serviceProcess, gw *gateway.Gateway, watching bool) {
fmt.Println()
fmt.Println(" ┌─────────────────────────────────────────────────────────────┐")
fmt.Println(" │ │")
@@ -462,9 +461,6 @@ func printBanner(services []*serviceProcess, gw *server.Gateway, watching bool)
fmt.Println(" │ │")
}
fmt.Println(" │ Auth: \033[32menabled\033[0m (admin / micro) │")
fmt.Println(" │ │")
if gw != nil && len(services) > 0 {
svc := services[0]
fmt.Println(" │ Try: │")
@@ -484,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.
@@ -498,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{
@@ -522,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"},
},
},
})
}
-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 -1079
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}}
-184
View File
@@ -1,184 +0,0 @@
{{define "content"}}
<h2>Agent</h2>
<p>Chat with your microservices using AI. Configure a model API key in settings, then use the prompt to interact with your services.</p>
<h3>Prompt</h3>
<div id="agent-messages"></div>
<form onsubmit="return false;" style="display:flex; gap:0.5em; align-items:flex-end;">
<input type="text" id="prompt-input" placeholder="e.g. List all users, Create a blog post..." style="flex:1; margin-bottom:0;">
<button id="prompt-btn" onclick="sendPrompt()">Send</button>
</form>
<h3>Settings</h3>
<form id="settings-form" onsubmit="return false;">
<label style="display:block; font-weight:600;">Provider</label>
<select id="provider">
<option value="openai">OpenAI</option>
<option value="anthropic">Anthropic</option>
</select>
<label style="display:block; font-weight:600;">Model API Key</label>
<input type="password" id="api-key" placeholder="sk-... or API key for your model provider">
<label style="display:block; font-weight:600;">Model (optional)</label>
<input type="text" id="model-name" placeholder="e.g. gpt-4o or claude-sonnet-4-20250514">
<label style="display:block; font-weight:600;">Base URL (optional)</label>
<input type="text" id="base-url" placeholder="Leave blank for default">
<button onclick="saveSettings()">Save Settings</button>
<span id="settings-status" style="margin-left:0.5em; color:#888;"></span>
</form>
<h3>Available Tools</h3>
<div id="tools-list">
<p style="color:#888;">Loading tools...</p>
</div>
<script>
(function() {
var tools = [];
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;
})
.catch(function() {});
}
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'; })
.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 || [];
renderTools();
})
.catch(function(err) {
document.getElementById('tools-list').innerHTML = '<p style="color:#c00;">Failed to load tools: ' + err + '</p>';
});
}
function renderTools() {
var el = document.getElementById('tools-list');
if (tools.length === 0) {
el.innerHTML = '<p style="color:#888;">No tools available. Start some services and they will appear here.</p>';
return;
}
var html = '<table><thead><tr><th>Tool</th><th>Description</th></tr></thead><tbody>';
for (var i = 0; i < tools.length; i++) {
var t = tools[i];
html += '<tr><td><code>' + escapeHtml(t.name) + '</code></td>';
html += '<td>' + escapeHtml(t.description || '') + '</td></tr>';
}
html += '</tbody></table>';
el.innerHTML = html;
}
window.sendPrompt = function() {
var input = document.getElementById('prompt-input');
var text = input.value.trim();
if (!text) return;
input.value = '';
addMessage('user', escapeHtml(text));
var btn = document.getElementById('prompt-btn');
btn.disabled = true;
btn.textContent = '...';
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) {
btn.disabled = false;
btn.textContent = 'Send';
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];
addMessage('tool', '<b>Tool:</b> <code>' + escapeHtml(tc.tool) + '</code>' +
'<pre>' + escapeHtml(JSON.stringify(tc.input, null, 2)) + '</pre>' +
'<b>Result:</b><pre>' + escapeHtml(JSON.stringify(tc.result, null, 2)) + '</pre>');
}
}
if (data.answer) {
addMessage('assistant', escapeHtml(data.answer));
}
})
.catch(function(err) {
btn.disabled = false;
btn.textContent = 'Send';
addMessage('error', 'Error: ' + escapeHtml(String(err)));
});
};
document.getElementById('prompt-input').addEventListener('keydown', function(e) {
if (e.key === 'Enter') { e.preventDefault(); window.sendPrompt(); }
});
function addMessage(type, html) {
var container = document.getElementById('agent-messages');
var div = document.createElement('div');
div.style.cssText = 'padding:0.8em 1em; border-radius:7px; margin-bottom:0.8em; line-height:1.6;';
if (type === 'user') {
div.style.background = '#f7f7f7';
div.style.border = '1px solid #eee';
div.innerHTML = '<b>You:</b> ' + html;
} else if (type === 'assistant' || type === 'answer') {
div.style.background = '#fff';
div.style.border = '1px solid #ddd';
div.innerHTML = '<b>Agent:</b> ' + html;
} else if (type === 'tool') {
div.style.background = '#fafafa';
div.style.border = '1px solid #e0e0e0';
div.style.fontSize = '0.95em';
div.innerHTML = html;
} else if (type === 'error') {
div.style.background = '#fff';
div.style.border = '1px solid #c00';
div.style.color = '#c00';
div.innerHTML = html;
}
container.appendChild(div);
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}}
+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:
-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)
}
-12
View File
@@ -15,7 +15,6 @@ type nats struct {
url string
bucket string
key string
conn *natsgo.Conn // store connection for lifecycle management
kv natsgo.KeyValue
opts source.Options
}
@@ -129,18 +128,7 @@ func NewSource(opts ...source.Option) source.Source {
url: config.Url,
bucket: bucket,
key: key,
conn: nc, // store connection reference
kv: kv,
opts: options,
}
}
// Close implements io.Closer and closes the underlying NATS connection.
// This method is optional but recommended to prevent connection leaks.
func (n *nats) Close() error {
if n.conn != nil {
n.conn.Close()
n.conn = nil
}
return nil
}
-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",
]
@@ -1,21 +0,0 @@
"""Custom exceptions for LangChain Go Micro integration."""
class GoMicroError(Exception):
"""Base exception for Go Micro integration errors."""
pass
class GoMicroConnectionError(GoMicroError):
"""Raised when unable to connect to MCP gateway."""
pass
class GoMicroAuthError(GoMicroError):
"""Raised when authentication fails."""
pass
class GoMicroToolError(GoMicroError):
"""Raised when tool execution fails."""
pass
@@ -1,319 +0,0 @@
"""LangChain toolkit for Go Micro services."""
import json
import re
from typing import Any, Dict, List, Optional, Callable
from dataclasses import dataclass
import requests
from langchain.tools import Tool
from pydantic import BaseModel, Field
from langchain_go_micro.exceptions import (
GoMicroConnectionError,
GoMicroAuthError,
GoMicroToolError,
)
@dataclass
class GoMicroConfig:
"""Configuration for Go Micro MCP gateway connection.
Attributes:
gateway_url: URL of the MCP gateway (e.g., http://localhost:3000)
auth_token: Optional bearer authentication token
timeout: Request timeout in seconds
retry_count: Number of retries on failure
retry_delay: Delay between retries in seconds
verify_ssl: Whether to verify SSL certificates
"""
gateway_url: str
auth_token: Optional[str] = None
timeout: int = 30
retry_count: int = 3
retry_delay: float = 1.0
verify_ssl: bool = True
class GoMicroTool(BaseModel):
"""Represents a Go Micro service tool.
Attributes:
name: Tool name (e.g., "users.Users.Get")
service: Service name (e.g., "users")
endpoint: Endpoint name (e.g., "Users.Get")
description: Tool description
example: Example input JSON
scopes: Required auth scopes
metadata: Additional metadata from service
"""
name: str
service: str
endpoint: str
description: str
example: Optional[str] = None
scopes: Optional[List[str]] = None
metadata: Dict[str, str] = Field(default_factory=dict)
class GoMicroToolkit:
"""LangChain toolkit for Go Micro services.
This class provides integration between LangChain and Go Micro services
via the Model Context Protocol (MCP) gateway.
Example:
>>> toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
>>> tools = toolkit.get_tools()
>>> for tool in tools:
... print(f"Tool: {tool.name}")
"""
def __init__(self, config: GoMicroConfig):
"""Initialize the toolkit.
Args:
config: Configuration for MCP gateway connection
"""
self.config = config
self._tools: Optional[List[GoMicroTool]] = None
self._session = requests.Session()
# Set up authentication
if config.auth_token:
self._session.headers.update({
"Authorization": f"Bearer {config.auth_token}"
})
@classmethod
def from_gateway(
cls,
gateway_url: str,
auth_token: Optional[str] = None,
**kwargs: Any
) -> "GoMicroToolkit":
"""Create toolkit from MCP gateway URL.
Args:
gateway_url: URL of the MCP gateway
auth_token: Optional bearer authentication token
**kwargs: Additional configuration options
Returns:
GoMicroToolkit instance
Example:
>>> toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
"""
config = GoMicroConfig(
gateway_url=gateway_url,
auth_token=auth_token,
**kwargs
)
return cls(config)
def _make_request(
self,
method: str,
path: str,
**kwargs: Any
) -> requests.Response:
"""Make HTTP request to MCP gateway.
Args:
method: HTTP method (GET, POST, etc.)
path: API path
**kwargs: Additional request arguments
Returns:
Response object
Raises:
GoMicroConnectionError: If connection fails
GoMicroAuthError: If authentication fails
"""
url = f"{self.config.gateway_url}{path}"
kwargs.setdefault("timeout", self.config.timeout)
kwargs.setdefault("verify", self.config.verify_ssl)
try:
response = self._session.request(method, url, **kwargs)
if response.status_code == 401:
raise GoMicroAuthError("Authentication failed")
elif response.status_code == 403:
raise GoMicroAuthError("Forbidden: insufficient permissions")
response.raise_for_status()
return response
except requests.ConnectionError as e:
raise GoMicroConnectionError(
f"Failed to connect to MCP gateway at {url}: {e}"
)
except requests.Timeout as e:
raise GoMicroConnectionError(
f"Request to MCP gateway timed out: {e}"
)
except requests.RequestException as e:
if isinstance(e, (GoMicroConnectionError, GoMicroAuthError)):
raise
raise GoMicroConnectionError(f"Request failed: {e}")
def refresh(self) -> None:
"""Refresh tool list from MCP gateway.
Raises:
GoMicroConnectionError: If unable to connect to gateway
"""
response = self._make_request("GET", "/mcp/tools")
data = response.json()
tools_data = data.get("tools", [])
self._tools = [
GoMicroTool(
name=tool["name"],
service=tool["service"],
endpoint=tool["endpoint"],
description=tool.get("description", ""),
example=tool.get("example"),
scopes=tool.get("scopes"),
metadata=tool.get("metadata", {})
)
for tool in tools_data
]
def get_tools(
self,
service_filter: Optional[str] = None,
name_pattern: Optional[str] = None,
include: Optional[List[str]] = None,
exclude: Optional[List[str]] = None,
) -> List[Tool]:
"""Get LangChain tools from Go Micro services.
Args:
service_filter: Filter tools by service name
name_pattern: Filter tools by name pattern (regex)
include: List of tool names to include
exclude: List of tool names to exclude
Returns:
List of LangChain Tool objects
Example:
>>> toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
>>> # Get all tools
>>> all_tools = toolkit.get_tools()
>>> # Get only user service tools
>>> user_tools = toolkit.get_tools(service_filter="users")
>>> # Get specific tools
>>> selected_tools = toolkit.get_tools(include=["users.Users.Get"])
"""
if self._tools is None:
self.refresh()
tools = self._tools or []
# Apply filters
if service_filter:
tools = [t for t in tools if t.service == service_filter]
if name_pattern:
pattern = re.compile(name_pattern)
tools = [t for t in tools if pattern.match(t.name)]
if include:
tools = [t for t in tools if t.name in include]
if exclude:
tools = [t for t in tools if t.name not in exclude]
# Convert to LangChain tools
return [self._create_langchain_tool(tool) for tool in tools]
def _create_langchain_tool(self, tool: GoMicroTool) -> Tool:
"""Create a LangChain Tool from a GoMicroTool.
Args:
tool: GoMicroTool to convert
Returns:
LangChain Tool object
"""
def tool_func(arguments: str) -> str:
"""Execute the tool.
Args:
arguments: JSON string with tool arguments
Returns:
JSON string with tool result
"""
return self.call_tool(tool.name, arguments)
# Build description with example if available
description = tool.description
if tool.example:
description += f"\n\nExample input: {tool.example}"
return Tool(
name=tool.name,
func=tool_func,
description=description,
)
def call_tool(self, tool_name: str, arguments: str) -> str:
"""Call a specific tool directly.
Args:
tool_name: Name of the tool to call
arguments: JSON string with tool arguments
Returns:
JSON string with tool result
Raises:
GoMicroToolError: If tool execution fails
Example:
>>> toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
>>> result = toolkit.call_tool(
... "users.Users.Get",
... '{"id": "user-123"}'
... )
"""
# Parse arguments
try:
args = json.loads(arguments) if isinstance(arguments, str) else arguments
except json.JSONDecodeError as e:
raise GoMicroToolError(f"Invalid JSON arguments: {e}")
# Make request
try:
response = self._make_request(
"POST",
"/mcp/call",
json={"name": tool_name, "arguments": args}
)
return json.dumps(response.json())
except requests.RequestException as e:
raise GoMicroToolError(f"Tool execution failed: {e}")
def list_tools(self) -> List[GoMicroTool]:
"""Get raw list of available tools.
Returns:
List of GoMicroTool objects
Example:
>>> toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
>>> for tool in toolkit.list_tools():
... print(f"{tool.name}: {tool.description}")
"""
if self._tools is None:
self.refresh()
return self._tools or []
-73
View File
@@ -1,73 +0,0 @@
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "langchain-go-micro"
version = "0.1.0"
description = "LangChain integration for Go Micro services via MCP"
readme = "README.md"
requires-python = ">=3.8"
license = {text = "Apache-2.0"}
authors = [
{name = "Micro Team", email = "hello@micro.dev"}
]
keywords = ["langchain", "go-micro", "mcp", "microservices", "ai", "agents"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Software Development :: Libraries :: Python Modules",
]
dependencies = [
"langchain>=0.1.0",
"requests>=2.31.0",
"pydantic>=2.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
"black>=23.0.0",
"mypy>=1.0.0",
"ruff>=0.1.0",
"types-requests>=2.31.0",
]
[project.urls]
Homepage = "https://github.com/micro/go-micro"
Documentation = "https://github.com/micro/go-micro/tree/master/contrib/langchain-go-micro"
Repository = "https://github.com/micro/go-micro"
Issues = "https://github.com/micro/go-micro/issues"
[tool.setuptools.packages.find]
where = ["."]
include = ["langchain_go_micro*"]
[tool.black]
line-length = 88
target-version = ['py38', 'py39', 'py310', 'py311']
[tool.mypy]
python_version = "3.8"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
[tool.ruff]
line-length = 88
target-version = "py38"
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
@@ -1,219 +0,0 @@
"""Tests for GoMicroToolkit."""
import json
from unittest.mock import Mock, patch
import pytest
import requests
from langchain_go_micro import GoMicroToolkit, GoMicroConfig
from langchain_go_micro.exceptions import (
GoMicroConnectionError,
GoMicroAuthError,
)
@pytest.fixture
def mock_gateway_response():
"""Mock MCP gateway response."""
return {
"tools": [
{
"name": "users.Users.Get",
"service": "users",
"endpoint": "Users.Get",
"description": "Get a user by ID",
"example": '{"id": "user-123"}',
"scopes": ["users:read"],
"metadata": {
"description": "Get a user by ID",
"example": '{"id": "user-123"}',
"scopes": "users:read"
}
},
{
"name": "users.Users.Create",
"service": "users",
"endpoint": "Users.Create",
"description": "Create a new user",
"example": '{"name": "Alice", "email": "alice@example.com"}',
"scopes": ["users:write"],
"metadata": {}
}
],
"count": 2
}
class TestGoMicroConfig:
"""Tests for GoMicroConfig."""
def test_config_defaults(self):
"""Test config default values."""
config = GoMicroConfig(gateway_url="http://localhost:3000")
assert config.gateway_url == "http://localhost:3000"
assert config.auth_token is None
assert config.timeout == 30
assert config.retry_count == 3
assert config.retry_delay == 1.0
assert config.verify_ssl is True
def test_config_custom_values(self):
"""Test config with custom values."""
config = GoMicroConfig(
gateway_url="http://localhost:8080",
auth_token="test-token",
timeout=60,
retry_count=5,
retry_delay=2.0,
verify_ssl=False
)
assert config.gateway_url == "http://localhost:8080"
assert config.auth_token == "test-token"
assert config.timeout == 60
assert config.retry_count == 5
assert config.retry_delay == 2.0
assert config.verify_ssl is False
class TestGoMicroToolkit:
"""Tests for GoMicroToolkit."""
def test_from_gateway(self):
"""Test creating toolkit from gateway URL."""
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
assert toolkit.config.gateway_url == "http://localhost:3000"
assert toolkit.config.auth_token is None
def test_from_gateway_with_auth(self):
"""Test creating toolkit with authentication."""
toolkit = GoMicroToolkit.from_gateway(
"http://localhost:3000",
auth_token="test-token"
)
assert toolkit.config.auth_token == "test-token"
assert "Authorization" in toolkit._session.headers
assert toolkit._session.headers["Authorization"] == "Bearer test-token"
@patch("requests.Session.request")
def test_refresh(self, mock_request, mock_gateway_response):
"""Test refreshing tool list."""
mock_response = Mock()
mock_response.json.return_value = mock_gateway_response
mock_response.status_code = 200
mock_request.return_value = mock_response
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
toolkit.refresh()
assert len(toolkit._tools) == 2
assert toolkit._tools[0].name == "users.Users.Get"
assert toolkit._tools[1].name == "users.Users.Create"
@patch("requests.Session.request")
def test_get_tools(self, mock_request, mock_gateway_response):
"""Test getting LangChain tools."""
mock_response = Mock()
mock_response.json.return_value = mock_gateway_response
mock_response.status_code = 200
mock_request.return_value = mock_response
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
tools = toolkit.get_tools()
assert len(tools) == 2
assert tools[0].name == "users.Users.Get"
assert tools[1].name == "users.Users.Create"
@patch("requests.Session.request")
def test_get_tools_with_service_filter(self, mock_request, mock_gateway_response):
"""Test filtering tools by service."""
mock_response = Mock()
mock_response.json.return_value = mock_gateway_response
mock_response.status_code = 200
mock_request.return_value = mock_response
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
tools = toolkit.get_tools(service_filter="users")
assert len(tools) == 2
for tool in tools:
assert "users" in tool.name
@patch("requests.Session.request")
def test_get_tools_with_include(self, mock_request, mock_gateway_response):
"""Test including specific tools."""
mock_response = Mock()
mock_response.json.return_value = mock_gateway_response
mock_response.status_code = 200
mock_request.return_value = mock_response
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
tools = toolkit.get_tools(include=["users.Users.Get"])
assert len(tools) == 1
assert tools[0].name == "users.Users.Get"
@patch("requests.Session.request")
def test_get_tools_with_exclude(self, mock_request, mock_gateway_response):
"""Test excluding specific tools."""
mock_response = Mock()
mock_response.json.return_value = mock_gateway_response
mock_response.status_code = 200
mock_request.return_value = mock_response
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
tools = toolkit.get_tools(exclude=["users.Users.Create"])
assert len(tools) == 1
assert tools[0].name == "users.Users.Get"
@patch("requests.Session.request")
def test_call_tool(self, mock_request):
"""Test calling a tool directly."""
mock_response = Mock()
mock_response.json.return_value = {"user": {"id": "user-123", "name": "Alice"}}
mock_response.status_code = 200
mock_request.return_value = mock_response
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
result = toolkit.call_tool("users.Users.Get", '{"id": "user-123"}')
result_data = json.loads(result)
assert result_data["user"]["id"] == "user-123"
@patch("requests.Session.request")
def test_connection_error(self, mock_request):
"""Test handling connection errors."""
mock_request.side_effect = requests.ConnectionError("Connection failed")
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
with pytest.raises(GoMicroConnectionError):
toolkit.refresh()
@patch("requests.Session.request")
def test_auth_error(self, mock_request):
"""Test handling authentication errors."""
mock_response = Mock()
mock_response.status_code = 401
mock_request.return_value = mock_response
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
with pytest.raises(GoMicroAuthError):
toolkit.refresh()
@patch("requests.Session.request")
def test_timeout(self, mock_request):
"""Test handling timeouts."""
mock_request.side_effect = requests.Timeout("Request timed out")
toolkit = GoMicroToolkit.from_gateway("http://localhost:3000")
with pytest.raises(GoMicroConnectionError):
toolkit.refresh()
@@ -1,29 +1,16 @@
---
layout: default
title: Deployment
---
# Deploying Go Micro Services
This guide covers deploying go-micro services to a Linux server using systemd.
## Overview
go-micro provides a clear workflow from development to production:
go-micro provides a simple deployment workflow:
| Stage | Command | Purpose |
|-------|---------|---------|
| **Develop** | `micro run` | Local dev with hot reload and API gateway |
| **Build** | `micro build` | Compile production binaries for any target OS |
| **Deploy** | `micro deploy` | Push binaries to a remote Linux server via SSH + systemd |
| **Dashboard** | `micro server` | Optional production web UI with JWT auth and user management |
1. **Develop locally** with `micro run`
2. **Build binaries** with `micro build`
3. **Deploy to server** with `micro deploy`
Each command has a distinct role — they don't overlap:
- **`micro run`** builds, runs, and watches services locally. It includes a lightweight gateway. Use it for development.
- **`micro build`** compiles binaries without running them. Use it to prepare release artifacts.
- **`micro deploy`** sends binaries to a remote server and manages them with systemd. It builds automatically if needed.
- **`micro server`** provides an authenticated web dashboard for services that are already running. It does NOT build or run services.
On the server, services are managed by systemd - the standard Linux process supervisor.
## Quick Start
@@ -350,28 +337,8 @@ deploy ALL=(ALL) NOPASSWD: /bin/systemctl stop micro@*
deploy ALL=(ALL) NOPASSWD: /bin/systemctl status micro@*
```
## Production Dashboard (Optional)
Once services are deployed and managed by systemd, you can optionally run `micro server` on the same machine to get a full web dashboard with authentication:
```bash
# On your server
micro server
```
This gives you:
- **Web Dashboard** at http://your-server:8080 with JWT authentication
- **API Gateway** with authenticated HTTP-to-RPC proxy
- **User Management** — create accounts, generate/revoke API tokens
- **Logs & Status** — view service logs and uptime from the browser
The server discovers services via the registry automatically. Default login: `admin` / `micro`.
See the [micro server documentation](server.md) for details.
## Next Steps
- [micro run](guides/micro-run.md) - Local development
- [micro server](server.md) - Production web dashboard with auth
- [micro.mu configuration](guides/micro-run.md#configuration-file) - Configuration file format
- [Health checks](guides/health.md) - Service health endpoints
- [micro run](./micro-run.md) - Local development
- [micro.mu configuration](./config.md) - Configuration file format
- [Health checks](./health.md) - Service health endpoints
+1 -3
View File
@@ -108,9 +108,7 @@ func (m *mem) Consume(topic string, opts ...ConsumeOption) (<-chan Event, error)
for _, o := range opts {
o(&options)
}
// Note: RetryLimit is configured but retry logic is basic for the in-memory implementation.
// For production use with advanced retry capabilities, use NATS JetStream.
// TODO RetryLimit
// setup the subscriber
sub := &subscriber{
+5 -22
View File
@@ -5,7 +5,6 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"strings"
"time"
@@ -36,12 +35,11 @@ func NewStream(opts ...Option) (events.Stream, error) {
s := &stream{opts: options}
conn, natsJetStreamCtx, err := connectToNatsJetStream(options)
natsJetStreamCtx, err := connectToNatsJetStream(options)
if err != nil {
return nil, fmt.Errorf("error connecting to nats cluster %v: %w", options.ClusterID, err)
}
s.conn = conn
s.natsJetStreamCtx = natsJetStreamCtx
return s, nil
@@ -49,11 +47,10 @@ func NewStream(opts ...Option) (events.Stream, error) {
type stream struct {
opts Options
conn *nats.Conn // store connection for lifecycle management
natsJetStreamCtx nats.JetStreamContext
}
func connectToNatsJetStream(options Options) (*nats.Conn, nats.JetStreamContext, error) {
func connectToNatsJetStream(options Options) (nats.JetStreamContext, error) {
nopts := nats.GetDefaultOptions()
if options.TLSConfig != nil {
nopts.Secure = true
@@ -80,16 +77,15 @@ func connectToNatsJetStream(options Options) (*nats.Conn, nats.JetStreamContext,
conn, err := nopts.Connect()
if err != nil {
tls := nopts.TLSConfig != nil
return nil, nil, fmt.Errorf("error connecting to nats at %v with tls enabled (%v): %w", options.Address, tls, err)
return nil, fmt.Errorf("error connecting to nats at %v with tls enabled (%v): %w", options.Address, tls, err)
}
js, err := conn.JetStream()
if err != nil {
conn.Close() // Close connection if JetStream context fails
return nil, nil, fmt.Errorf("error while obtaining JetStream context: %w", err)
return nil, fmt.Errorf("error while obtaining JetStream context: %w", err)
}
return conn, js, nil
return js, nil
}
// Publish a message to a topic.
@@ -272,16 +268,3 @@ func (s *stream) Consume(topic string, opts ...events.ConsumeOption) (<-chan eve
return channel, nil
}
// Close implements io.Closer and closes the underlying NATS connection.
// This method is optional but recommended to prevent connection leaks.
func (s *stream) Close() error {
if s.conn != nil {
s.conn.Close()
s.conn = nil
}
return nil
}
// Ensure stream implements io.Closer
var _ io.Closer = (*stream)(nil)
-62
View File
@@ -1,62 +0,0 @@
# Go Micro Examples
This directory contains runnable examples demonstrating various go-micro features and patterns.
## Quick Start
Each example can be run with `go run .` from its directory.
## Examples
### [hello-world](./hello-world/)
Basic RPC service demonstrating core concepts:
- Service creation and registration
- Handler implementation
- Client calls
- Health checks
**Run it:**
```bash
cd hello-world
go run .
```
### [web-service](./web-service/)
HTTP web service with service discovery:
- HTTP handlers
- Service registration
- Health checks
- JSON REST API
**Run it:**
```bash
cd web-service
go run .
```
## Coming Soon
The following examples are planned:
- **pubsub-events** - Event-driven architecture with NATS
- **grpc-integration** - Using go-micro with gRPC
- **production-ready** - Complete production-grade service with observability
## Prerequisites
Some examples require external dependencies:
- **NATS**: `docker run -p 4222:4222 nats:latest`
- **Consul**: `docker run -p 8500:8500 consul:latest agent -dev -ui -client=0.0.0.0`
- **Redis**: `docker run -p 6379:6379 redis:latest`
## Contributing
To add a new example:
1. Create a new directory
2. Add a descriptive README.md
3. Include working code with comments
4. Add to this index
5. Ensure it runs with `go run .`
-12
View File
@@ -1,12 +0,0 @@
# Compiled binaries
server/server
client/client
# Test binaries
*.test
# Output files
*.out
# Temporary files
*.tmp
-396
View File
@@ -1,396 +0,0 @@
# Auth Example
This example demonstrates how to use the auth wrappers to protect your microservices with authentication and authorization.
## Overview
The example includes:
- **Server** - A Greeter service with:
- Protected endpoint: `Greeter.Hello` (requires auth)
- Public endpoint: `Greeter.Health` (no auth required)
- **Client** - Makes calls to the server:
- With authentication (successful)
- Without authentication (fails as expected)
## Architecture
```
┌─────────────────────────────────────────┐
│ Client │
│ ┌────────────────────────────────┐ │
│ │ AuthClient Wrapper │ │
│ │ - Adds Bearer token │ │
│ │ - To all requests │ │
│ └────────────────────────────────┘ │
└──────────────┬──────────────────────────┘
│ RPC with Authorization: Bearer <token>
┌─────────────────────────────────────────┐
│ Server │
│ ┌────────────────────────────────┐ │
│ │ AuthHandler Wrapper │ │
│ │ - Extracts token │ │
│ │ - Verifies with auth.Inspect()│ │
│ │ - Checks with rules.Verify() │ │
│ │ - Returns 401/403 if denied │ │
│ └────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────┐ │
│ │ Handler (Greeter.Hello) │ │
│ │ - Gets account from context │ │
│ │ - Processes request │ │
│ └────────────────────────────────┘ │
└─────────────────────────────────────────┘
```
## Files
```
examples/auth/
├── README.md # This file
├── proto/
│ ├── greeter.proto # Service definition
│ └── greeter.pb.go # Generated Go code
├── server/
│ └── main.go # Protected service
└── client/
└── main.go # Client with auth
```
## Running the Example
### 1. Start the Server
```bash
cd server
go run main.go
```
The server will:
- Start the Greeter service
- Apply auth wrapper to protect endpoints
- Generate a test token and print it
Output:
```
=== Test Token Generated ===
Use this token to test the client:
TOKEN=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9... go run client/main.go
2026/02/11 10:00:00 Server [greeter] Listening on [::]:54321
```
### 2. Run the Client (With Auth)
In a new terminal:
```bash
cd client
TOKEN=<token-from-server> go run main.go
```
Output:
```
=== Test 1: Protected endpoint WITH auth ===
Response: Hello, test-user!
=== Test 2: Public endpoint (no auth needed) ===
Health Status: ok
=== Test 3: Protected endpoint WITHOUT auth (should fail) ===
Expected error: {"id":"greeter","code":401,"detail":"missing authorization token","status":"Unauthorized"}
```
### 3. Run the Client (Without Auth)
```bash
cd client
go run main.go
```
This will auto-generate a token for testing.
## Code Walkthrough
### Server Setup
```go
// 1. Create auth provider
// For this example we use the noop auth (accepts all tokens)
// In production, use JWT or a custom auth provider
authProvider := noop.NewAuth()
// 2. Create authorization rules
rules := auth.NewRules()
rules.Grant(&auth.Rule{
ID: "public-health",
Scope: "",
Resource: &auth.Resource{Endpoint: "Greeter.Health"},
Access: auth.AccessGranted,
})
// 3. Wrap service with auth handler
service := micro.NewService(
micro.Name("greeter"),
micro.WrapHandler(
authWrapper.AuthHandler(authWrapper.HandlerOptions{
Auth: authProvider,
Rules: rules,
SkipEndpoints: []string{"Greeter.Health"},
}),
),
)
```
### Client Setup
```go
// 1. Get or generate token
token := os.Getenv("TOKEN")
// 2. Wrap client with auth
service := micro.NewService(
micro.Name("greeter.client"),
micro.WrapClient(
authWrapper.FromToken(token),
),
)
// 3. Make calls (token automatically added)
greeterClient := pb.NewGreeterService("greeter", service.Client())
rsp, err := greeterClient.Hello(ctx, &pb.Request{Name: "John"})
```
### Handler Implementation
```go
func (g *Greeter) Hello(ctx context.Context, req *pb.Request, rsp *pb.Response) error {
// Get account from context (added by auth wrapper)
acc, ok := auth.AccountFromContext(ctx)
if !ok {
return errors.Unauthorized("greeter", "authentication required")
}
rsp.Msg = "Hello, " + acc.ID + "!"
return nil
}
```
## Auth Wrapper Features
### Server Wrapper (`AuthHandler`)
- **Token Extraction**: Reads `Authorization: Bearer <token>` from metadata
- **Token Verification**: Validates token using `auth.Inspect()`
- **Authorization**: Checks permissions using `rules.Verify()`
- **Context Injection**: Adds account to context for handlers
- **Error Handling**: Returns 401/403 with clear error messages
- **Skip Endpoints**: Allows public endpoints without auth
### Client Wrapper (`AuthClient`)
- **Automatic Token Injection**: Adds Bearer token to all requests
- **Context-Aware**: Can extract account from context
- **Static Token**: Use `FromToken()` for pre-generated tokens
- **Dynamic Token**: Use `FromContext()` to generate per-request
## Auth Strategies
### 1. All Endpoints Protected
```go
micro.WrapHandler(
authWrapper.AuthRequired(authProvider, rules),
)
```
### 2. Some Public Endpoints
```go
micro.WrapHandler(
authWrapper.PublicEndpoints(authProvider, rules, []string{
"Health.Check",
"Status.Version",
}),
)
```
### 3. Optional Auth (Extract but Don't Enforce)
```go
micro.WrapHandler(
authWrapper.AuthOptional(authProvider),
)
```
## Authorization Rules
### Grant Public Access
```go
rules.Grant(&auth.Rule{
ID: "public",
Scope: "", // No scope = public
Resource: &auth.Resource{Endpoint: "Health.Check"},
Access: auth.AccessGranted,
})
```
### Require Authentication
```go
rules.Grant(&auth.Rule{
ID: "authenticated",
Scope: "*", // Any authenticated user
Resource: &auth.Resource{Endpoint: "*"},
Access: auth.AccessGranted,
})
```
### Require Specific Scope
```go
rules.Grant(&auth.Rule{
ID: "admin-only",
Scope: "admin", // Only admin scope
Resource: &auth.Resource{Endpoint: "Admin.*"},
Access: auth.AccessGranted,
})
```
### Deny Access
```go
rules.Grant(&auth.Rule{
ID: "deny-delete",
Scope: "*",
Resource: &auth.Resource{Endpoint: "User.Delete"},
Access: auth.AccessDenied,
Priority: 100, // Higher priority = evaluated first
})
```
## Testing Without Server
You can test auth logic without a running server:
```go
import "go-micro.dev/v5/auth/noop"
// Create auth provider (noop for testing)
authProvider := noop.NewAuth()
// Generate account
acc, _ := authProvider.Generate("test-user", auth.WithScopes("admin"))
// Generate token
token, _ := authProvider.Token(auth.WithCredentials(acc.ID, acc.Secret))
// Verify token
verified, _ := authProvider.Inspect(token.AccessToken)
fmt.Println(verified.ID) // Returns a generated UUID
```
## Production Considerations
### 1. Use Production Auth Provider
The noop auth provider (`auth.NewAuth()`) is for development only. It accepts any token.
For production, implement a proper auth provider or use the JWT implementation:
```go
// Option 1: Implement custom auth.Auth interface
type MyAuth struct {
// Your implementation
}
func (m *MyAuth) Generate(id string, opts ...auth.GenerateOption) (*auth.Account, error) {
// Generate real accounts
}
func (m *MyAuth) Inspect(token string) (*auth.Account, error) {
// Verify real tokens (JWT, OAuth, etc.)
}
// Option 2: Use JWT auth (requires jwt package implementation)
// Note: The jwt package in auth/jwt depends on an external plugin
// You may need to implement your own JWT auth or use a third-party library
```
### 3. Add Gateway Auth
If using HTTP gateway:
```go
// Add auth to HTTP gateway
http.Handle("/", gateway.Handler(
gateway.WithAuth(authProvider),
))
```
### 4. Service-to-Service Auth
Services calling other services:
```go
// Service A calls Service B with its own token
client := micro.NewService(
micro.WrapClient(
authWrapper.FromContext(authProvider),
),
)
```
### 5. Token Refresh
```go
// Check if token is expiring
if time.Until(token.Expiry) < 5*time.Minute {
token, _ = authProvider.Token(auth.WithToken(token.RefreshToken))
}
```
## Troubleshooting
### Error: "missing authorization token"
- **Cause**: Client didn't send Authorization header
- **Fix**: Wrap client with `authWrapper.FromToken(token)`
### Error: "invalid token"
- **Cause**: Token is expired or malformed
- **Fix**: Generate a new token
### Error: "access denied"
- **Cause**: Account doesn't have required permissions
- **Fix**: Check authorization rules with `rules.List()`
### Error: "token verification failed"
- **Cause**: Server can't verify token (wrong keys, expired, etc.)
- **Fix**: Ensure server and client use same auth provider
## Next Steps
- Read the [Auth Documentation](/docs/auth)
- Explore [JWT Auth](/auth/jwt)
- Try [Custom Auth Provider](/examples/auth/custom)
- See [Multi-Tenant Auth](/examples/auth/multi-tenant)
## Summary
The auth wrappers make it easy to:
1. **Protect services**: Add `WrapHandler(AuthHandler(...))`
2. **Add authentication to clients**: Add `WrapClient(FromToken(...))`
3. **Control access**: Define rules with `rules.Grant()`
4. **Access account info**: Use `auth.AccountFromContext(ctx)`
That's it! Your microservices now have enterprise-grade authentication and authorization.
-85
View File
@@ -1,85 +0,0 @@
package main
import (
"context"
"fmt"
"log"
"os"
"go-micro.dev/v5"
"go-micro.dev/v5/auth"
"go-micro.dev/v5/auth/noop"
"go-micro.dev/v5/client"
authWrapper "go-micro.dev/v5/wrapper/auth"
pb "go-micro.dev/v5/examples/auth/proto"
)
func main() {
// Get token from environment or generate one
token := os.Getenv("TOKEN")
// Create auth provider (same as server)
authProvider := noop.NewAuth()
// If no token provided, generate one
if token == "" {
log.Println("No TOKEN env var provided, generating test token...")
acc, err := authProvider.Generate("test-user")
if err != nil {
log.Fatal(err)
}
t, err := authProvider.Token(auth.WithCredentials(acc.ID, acc.Secret))
if err != nil {
log.Fatal(err)
}
token = t.AccessToken
log.Printf("Generated token: %s\n", token)
}
// Create service with auth client wrapper
service := micro.NewService(
micro.Name("greeter.client"),
micro.WrapClient(
authWrapper.FromToken(token), // Add token to all requests
),
)
service.Init()
// Create greeter client
greeterClient := pb.NewGreeterService("greeter", service.Client())
// Test 1: Call protected endpoint (Hello) with auth
fmt.Println("\n=== Test 1: Protected endpoint WITH auth ===")
rsp, err := greeterClient.Hello(context.Background(), &pb.Request{Name: "John"})
if err != nil {
log.Printf("Error: %v", err)
} else {
fmt.Printf("Response: %s\n", rsp.Msg)
}
// Test 2: Call public endpoint (Health) without auth
fmt.Println("\n=== Test 2: Public endpoint (no auth needed) ===")
// Create client without auth wrapper for this test
plainClient := client.NewClient()
plainGreeterClient := pb.NewGreeterService("greeter", plainClient)
healthRsp, err := plainGreeterClient.Health(context.Background(), &pb.HealthRequest{})
if err != nil {
log.Printf("Error: %v", err)
} else {
fmt.Printf("Health Status: %s\n", healthRsp.Status)
}
// Test 3: Call protected endpoint WITHOUT auth (should fail)
fmt.Println("\n=== Test 3: Protected endpoint WITHOUT auth (should fail) ===")
_, err = plainGreeterClient.Hello(context.Background(), &pb.Request{Name: "John"})
if err != nil {
fmt.Printf("Expected error: %v\n", err)
} else {
fmt.Println("Unexpected: Call succeeded without auth!")
}
}
-144
View File
@@ -1,144 +0,0 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: greeter.proto
package greeter
import (
context "context"
fmt "fmt"
client "go-micro.dev/v5/client"
server "go-micro.dev/v5/server"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = fmt.Errorf
type Request struct {
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
}
func (m *Request) Reset() { *m = Request{} }
func (m *Request) String() string { return fmt.Sprintf("Request{Name:%s}", m.Name) }
func (*Request) ProtoMessage() {}
func (m *Request) GetName() string {
if m != nil {
return m.Name
}
return ""
}
type Response struct {
Msg string `protobuf:"bytes,1,opt,name=msg,proto3" json:"msg,omitempty"`
}
func (m *Response) Reset() { *m = Response{} }
func (m *Response) String() string { return fmt.Sprintf("Response{Msg:%s}", m.Msg) }
func (*Response) ProtoMessage() {}
func (m *Response) GetMsg() string {
if m != nil {
return m.Msg
}
return ""
}
type HealthRequest struct{}
func (m *HealthRequest) Reset() { *m = HealthRequest{} }
func (m *HealthRequest) String() string { return "HealthRequest{}" }
func (*HealthRequest) ProtoMessage() {}
type HealthResponse struct {
Status string `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"`
}
func (m *HealthResponse) Reset() { *m = HealthResponse{} }
func (m *HealthResponse) String() string { return fmt.Sprintf("HealthResponse{Status:%s}", m.Status) }
func (*HealthResponse) ProtoMessage() {}
func (m *HealthResponse) GetStatus() string {
if m != nil {
return m.Status
}
return ""
}
func init() {
// Types registered
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ client.Option
var _ server.Option
// Client API for Greeter service
type GreeterService interface {
Hello(ctx context.Context, in *Request, opts ...client.CallOption) (*Response, error)
Health(ctx context.Context, in *HealthRequest, opts ...client.CallOption) (*HealthResponse, error)
}
type greeterService struct {
c client.Client
name string
}
func NewGreeterService(name string, c client.Client) GreeterService {
return &greeterService{
c: c,
name: name,
}
}
func (c *greeterService) Hello(ctx context.Context, in *Request, opts ...client.CallOption) (*Response, error) {
req := c.c.NewRequest(c.name, "Greeter.Hello", in)
out := new(Response)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *greeterService) Health(ctx context.Context, in *HealthRequest, opts ...client.CallOption) (*HealthResponse, error) {
req := c.c.NewRequest(c.name, "Greeter.Health", in)
out := new(HealthResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for Greeter service
type GreeterHandler interface {
Hello(context.Context, *Request, *Response) error
Health(context.Context, *HealthRequest, *HealthResponse) error
}
func RegisterGreeterHandler(s server.Server, hdlr GreeterHandler, opts ...server.HandlerOption) error {
type greeter interface {
Hello(ctx context.Context, in *Request, out *Response) error
Health(ctx context.Context, in *HealthRequest, out *HealthResponse) error
}
type Greeter struct {
greeter
}
h := &greeterHandler{hdlr}
return s.Handle(s.NewHandler(&Greeter{h}, opts...))
}
type greeterHandler struct {
GreeterHandler
}
func (h *greeterHandler) Hello(ctx context.Context, in *Request, out *Response) error {
return h.GreeterHandler.Hello(ctx, in, out)
}
func (h *greeterHandler) Health(ctx context.Context, in *HealthRequest, out *HealthResponse) error {
return h.GreeterHandler.Health(ctx, in, out)
}
-24
View File
@@ -1,24 +0,0 @@
syntax = "proto3";
package greeter;
option go_package = "go-micro.dev/v5/examples/auth/proto;greeter";
service Greeter {
rpc Hello(Request) returns (Response) {}
rpc Health(HealthRequest) returns (HealthResponse) {}
}
message Request {
string name = 1;
}
message Response {
string msg = 1;
}
message HealthRequest {}
message HealthResponse {
string status = 1;
}
-96
View File
@@ -1,96 +0,0 @@
package main
import (
"context"
"log"
"go-micro.dev/v5"
"go-micro.dev/v5/auth"
"go-micro.dev/v5/auth/noop"
authWrapper "go-micro.dev/v5/wrapper/auth"
pb "go-micro.dev/v5/examples/auth/proto"
)
// Greeter implements the Greeter service
type Greeter struct{}
// Hello is a protected endpoint that requires authentication
func (g *Greeter) Hello(ctx context.Context, req *pb.Request, rsp *pb.Response) error {
// Get account from context (added by auth wrapper)
acc, ok := auth.AccountFromContext(ctx)
if !ok {
rsp.Msg = "Hello, anonymous!"
return nil
}
rsp.Msg = "Hello, " + acc.ID + "!"
return nil
}
// Health is a public endpoint that doesn't require auth
func (g *Greeter) Health(ctx context.Context, req *pb.HealthRequest, rsp *pb.HealthResponse) error {
rsp.Status = "ok"
return nil
}
func main() {
// Create auth provider (noop for this example)
// In production, use JWT or custom auth provider
authProvider := noop.NewAuth()
// Create authorization rules
rules := auth.NewRules()
// Grant public access to health endpoint
rules.Grant(&auth.Rule{
ID: "public-health",
Scope: "",
Resource: &auth.Resource{Type: "service", Name: "*", Endpoint: "Greeter.Health"},
Access: auth.AccessGranted,
Priority: 100,
})
// Require authentication for other endpoints
rules.Grant(&auth.Rule{
ID: "authenticated-hello",
Scope: "*",
Resource: &auth.Resource{Type: "service", Name: "*", Endpoint: "*"},
Access: auth.AccessGranted,
Priority: 50,
})
// Create service with auth wrapper
service := micro.NewService(
micro.Name("greeter"),
micro.Version("latest"),
micro.WrapHandler(
authWrapper.AuthHandler(authWrapper.HandlerOptions{
Auth: authProvider,
Rules: rules,
SkipEndpoints: []string{"Greeter.Health"}, // Public endpoints
}),
),
)
service.Init()
// Register handler
if err := pb.RegisterGreeterHandler(service.Server(), &Greeter{}); err != nil {
log.Fatal(err)
}
// Generate a test token for demonstration
if acc, err := authProvider.Generate("test-user"); err == nil {
if token, err := authProvider.Token(auth.WithCredentials(acc.ID, acc.Secret)); err == nil {
log.Printf("\n=== Test Token Generated ===")
log.Printf("Use this token to test the client:")
log.Printf("TOKEN=%s go run client/main.go\n", token.AccessToken)
}
}
// Run service
if err := service.Run(); err != nil {
log.Fatal(err)
}
}
-62
View File
@@ -1,62 +0,0 @@
# Hello World Example
The simplest go-micro service demonstrating core concepts.
## What It Does
This example creates a basic RPC service that:
- Listens on port 8080
- Exposes a `Greeter.Hello` method
- Returns a greeting message
- Demonstrates both programmatic and HTTP access
## Run It
```bash
go run main.go
```
The service will start and make test calls to itself, then wait for incoming requests.
## Test It
### Using curl
```bash
curl -X POST http://localhost:8080 \
-H 'Content-Type: application/json' \
-H 'Micro-Endpoint: Greeter.Hello' \
-d '{"name": "Alice"}'
```
Expected response:
```json
{"message": "Hello Alice"}
```
### Using the micro CLI
```bash
micro call greeter Greeter.Hello '{"name": "Bob"}'
```
## Code Walkthrough
1. **Define types** - Request and Response structures
2. **Implement handler** - The `Greeter` service with `Hello` method
3. **Create service** - Using `micro.New()` with options
4. **Register handler** - Link the handler to the service
5. **Run service** - Start listening for requests
## Key Concepts
- **RPC Pattern**: Method signature `func(ctx, req, rsp) error`
- **Service Discovery**: Automatic registration
- **Multiple Transports**: Works over HTTP, gRPC, etc.
- **Type Safety**: Strongly typed requests/responses
## Next Steps
- See [pubsub-events](../pubsub-events/) for event-driven patterns
- See [production-ready](../production-ready/) for a complete example
- Read the [Getting Started Guide](../../internal/website/docs/getting-started.md)
-70
View File
@@ -1,70 +0,0 @@
module example
go 1.24
require go-micro.dev/v5 v5.16.0
require (
dario.cat/mergo v1.0.2 // indirect
github.com/armon/go-metrics v0.4.1 // indirect
github.com/bitly/go-simplejson v0.5.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/cornelk/hashmap v1.0.8 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-redis/redis/v8 v8.11.5 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/consul/api v1.32.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-hclog v1.5.0 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/serf v0.10.1 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/miekg/dns v1.1.50 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/hashstructure v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/nats-io/nats.go v1.42.0 // indirect
github.com/nats-io/nkeys v0.4.11 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rabbitmq/amqp091-go v1.10.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/urfave/cli/v2 v2.27.6 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
go.etcd.io/bbolt v1.4.0 // indirect
go.etcd.io/etcd/api/v3 v3.5.21 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.21 // indirect
go.etcd.io/etcd/client/v3 v3.5.21 // indirect
go.uber.org/multierr v1.10.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/net v0.38.0 // indirect
golang.org/x/sync v0.13.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect
golang.org/x/tools v0.31.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
google.golang.org/grpc v1.71.1 // indirect
google.golang.org/protobuf v1.36.6 // indirect
)
replace go-micro.dev/v5 => ../..
-385
View File
@@ -1,385 +0,0 @@
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA=
github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y=
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cornelk/hashmap v1.0.8 h1:nv0AWgw02n+iDcawr5It4CjQIAcdMMKRrs10HOJYlrc=
github.com/cornelk/hashmap v1.0.8/go.mod h1:RfZb7JO3RviW/rT6emczVuC/oxpdz4UsSB2LJSclR1k=
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-tpm v0.9.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc=
github.com/google/go-tpm v0.9.3/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/consul/api v1.32.1 h1:0+osr/3t/aZNAdJX558crU3PEjVrG4x6715aZHRgceE=
github.com/hashicorp/consul/api v1.32.1/go.mod h1:mXUWLnxftwTmDv4W3lzxYCPD199iNLLUyLfLGFJbtl4=
github.com/hashicorp/consul/sdk v0.16.1 h1:V8TxTnImoPD5cj0U9Spl0TUxcytjcbbJeADFF07KdHg=
github.com/hashicorp/consul/sdk v0.16.1/go.mod h1:fSXvwxB2hmh1FMZCNl6PwX0Q/1wdWtHJcZ7Ea5tns0s=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c=
github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI=
github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI=
github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM=
github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0=
github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY=
github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q=
github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/hashstructure v1.1.0 h1:P6P1hdjqAAknpY/M1CGipelZgp+4y9ja9kmUZPXP+H0=
github.com/mitchellh/hashstructure v1.1.0/go.mod h1:xUDAozZz0Wmdiufv0uyhnHkUTN6/6d8ulp4AwfLKrmA=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nats-io/jwt/v2 v2.7.4 h1:jXFuDDxs/GQjGDZGhNgH4tXzSUK6WQi2rsj4xmsNOtI=
github.com/nats-io/jwt/v2 v2.7.4/go.mod h1:me11pOkwObtcBNR8AiMrUbtVOUGkqYjMQZ6jnSdVUIA=
github.com/nats-io/nats-server/v2 v2.11.3 h1:AbGtXxuwjo0gBroLGGr/dE0vf24kTKdRnBq/3z/Fdoc=
github.com/nats-io/nats-server/v2 v2.11.3/go.mod h1:6Z6Fd+JgckqzKig7DYwhgrE7bJ6fypPHnGPND+DqgMY=
github.com/nats-io/nats.go v1.42.0 h1:ynIMupIOvf/ZWH/b2qda6WGKGNSjwOUutTpWRvAmhaM=
github.com/nats-io/nats.go v1.42.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE=
github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk=
go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
go.etcd.io/etcd/api/v3 v3.5.21 h1:A6O2/JDb3tvHhiIz3xf9nJ7REHvtEFJJ3veW3FbCnS8=
go.etcd.io/etcd/api/v3 v3.5.21/go.mod h1:c3aH5wcvXv/9dqIw2Y810LDXJfhSYdHQ0vxmP3CCHVY=
go.etcd.io/etcd/client/pkg/v3 v3.5.21 h1:lPBu71Y7osQmzlflM9OfeIV2JlmpBjqBNlLtcoBqUTc=
go.etcd.io/etcd/client/pkg/v3 v3.5.21/go.mod h1:BgqT/IXPjK9NkeSDjbzwsHySX3yIle2+ndz28nVsjUs=
go.etcd.io/etcd/client/v3 v3.5.21 h1:T6b1Ow6fNjOLOtM0xSoKNQt1ASPCLWrF9XMHcH9pEyY=
go.etcd.io/etcd/client/v3 v3.5.21/go.mod h1:mFYy67IOqmbRf/kRUvsHixzo3iG+1OF2W2+jVIQRAnU=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM=
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-90
View File
@@ -1,90 +0,0 @@
package main
import (
"context"
"fmt"
"log"
"go-micro.dev/v5"
"go-micro.dev/v5/client"
)
// Request and Response types
type Request struct {
Name string `json:"name"`
}
type Response struct {
Message string `json:"message"`
}
// Greeter service handler
type Greeter struct{}
// Hello is the RPC method handler
func (g *Greeter) Hello(ctx context.Context, req *Request, rsp *Response) error {
rsp.Message = "Hello " + req.Name
log.Printf("Received request: %s", req.Name)
return nil
}
func main() {
// Create a new service
service := micro.New(
micro.Name("greeter"),
micro.Version("latest"),
micro.Address(":8080"),
)
// Initialize the service
service.Init()
// Register the handler
if err := service.Handle(new(Greeter)); err != nil {
log.Fatal(err)
}
// Run the service in a goroutine
go func() {
if err := service.Run(); err != nil {
log.Fatal(err)
}
}()
// Wait for service to start
fmt.Println("Service started on :8080")
fmt.Println("Testing the service...")
// Create a client to test the service
c := service.Client()
// Make a request
req := c.NewRequest("greeter", "Greeter.Hello", &Request{Name: "World"})
rsp := &Response{}
if err := c.Call(context.Background(), req, rsp); err != nil {
log.Printf("Error calling service: %v", err)
} else {
fmt.Printf("Response: %s\n", rsp.Message)
}
// Make another request
req2 := c.NewRequest("greeter", "Greeter.Hello", &Request{Name: "Go Micro"})
rsp2 := &Response{}
if err := c.Call(context.Background(), req2, rsp2); err != nil {
log.Printf("Error calling service: %v", err)
} else {
fmt.Printf("Response: %s\n", rsp2.Message)
}
// Test with HTTP client
fmt.Println("\nYou can also test with curl:")
fmt.Println("curl -X POST http://localhost:8080 \\")
fmt.Println(" -H 'Content-Type: application/json' \\")
fmt.Println(" -H 'Micro-Endpoint: Greeter.Hello' \\")
fmt.Println(" -d '{\"name\": \"Alice\"}'")
// Keep service running
select {}
}
-216
View File
@@ -1,216 +0,0 @@
# MCP Examples
Examples demonstrating Model Context Protocol (MCP) integration with go-micro.
## Examples
### [hello](./hello/) - Minimal Example ⭐ Start Here
The simplest possible MCP-enabled service. Perfect for learning the basics.
**What it shows:**
- Automatic documentation extraction from Go comments
- MCP gateway setup with 3 lines
- Ready for Claude Code
**Run it:**
```bash
cd hello
go run main.go
```
### [documented](./documented/) - Full-Featured Example
Complete example showing all MCP features with a user service.
**What it shows:**
- Multiple endpoints (GetUser, CreateUser)
- Rich documentation with examples
- Per-endpoint auth scopes via `server.WithEndpointScopes()`
- Pre-populated test data
- Production-ready patterns
**Run it:**
```bash
cd documented
go run main.go
```
## Quick Start
### 1. Write Your Service
Add Go doc comments to your handler methods:
```go
// SayHello greets a person by name. Returns a friendly greeting message.
//
// @example {"name": "Alice"}
func (g *Greeter) SayHello(ctx context.Context, req *HelloRequest, rsp *HelloResponse) error {
rsp.Message = "Hello " + req.Name + "!"
return nil
}
type HelloRequest struct {
Name string `json:"name" description:"Person's name to greet"`
}
```
### 2. Register Handler (Auto-Extracts Docs!)
```go
handler := service.Server().NewHandler(new(Greeter))
service.Server().Handle(handler)
```
### 3. Start MCP Gateway
```go
go mcp.ListenAndServe(":3000", mcp.Options{
Registry: service.Options().Registry,
})
```
## Testing
### HTTP API
```bash
# List tools
curl http://localhost:3000/mcp/tools | jq
# Call a tool
curl -X POST http://localhost:3000/mcp/call \
-H "Content-Type: application/json" \
-d '{
"tool": "greeter.Greeter.SayHello",
"input": {"name": "Alice"}
}' | jq
```
### Claude Code (Stdio)
Start MCP server:
```bash
micro mcp serve
```
Add to `~/.claude/claude_desktop_config.json`:
```json
{
"mcpServers": {
"my-services": {
"command": "micro",
"args": ["mcp", "serve"]
}
}
}
```
Restart Claude Code and ask Claude to use your services!
## Features
### ✅ Automatic Documentation Extraction
Just write Go comments - documentation is extracted automatically:
- **Go doc comments** → Tool descriptions
- **@example tags** → Example inputs for AI
- **Struct tags** → Parameter descriptions
### ✅ Multiple Transports
- **Stdio** - For Claude Code (recommended)
- **HTTP/SSE** - For web-based agents
### ✅ MCP Command Line
```bash
# Start MCP server
micro mcp serve # Stdio (for Claude Code)
micro mcp serve --address :3000 # HTTP/SSE (for web agents)
# List available tools
micro mcp list # Human-readable list
micro mcp list --json # JSON output
# Test a tool
micro mcp test <tool-name> '{"key": "value"}'
# Generate documentation
micro mcp docs # Markdown format
micro mcp docs --format json # JSON format
micro mcp docs --output tools.md # Save to file
# Export to different formats
micro mcp export langchain # Python LangChain tools
micro mcp export openapi # OpenAPI 3.0 spec
micro mcp export json # Raw JSON definitions
```
For detailed examples, see [CLI Examples](../../cmd/micro/mcp/EXAMPLES.md).
### ✅ Zero Configuration
- No manual tool registration
- No API wrappers
- No code generation
- Just write normal Go code!
### ✅ Per-Tool Auth Scopes
Declare required scopes when registering a handler:
```go
handler := service.Server().NewHandler(
new(BlogService),
server.WithEndpointScopes("Blog.Create", "blog:write"),
server.WithEndpointScopes("Blog.Delete", "blog:admin"),
)
```
Or define scopes at the gateway layer without changing services:
```go
mcp.Serve(mcp.Options{
Registry: reg,
Auth: authProvider,
Scopes: map[string][]string{
"blog.Blog.Create": {"blog:write"},
"blog.Blog.Delete": {"blog:admin"},
},
})
```
### ✅ Tracing, Rate Limiting & Audit Logging
Every tool call generates a trace ID that propagates through the RPC chain.
Configure rate limiting and audit logging at the gateway:
```go
mcp.Serve(mcp.Options{
Registry: reg,
Auth: authProvider,
RateLimit: &mcp.RateLimitConfig{
RequestsPerSecond: 10,
Burst: 20,
},
AuditFunc: func(r mcp.AuditRecord) {
log.Printf("[audit] trace=%s tool=%s account=%s allowed=%v",
r.TraceID, r.Tool, r.AccountID, r.Allowed)
},
})
```
## Documentation
- [Full MCP Documentation](../../internal/website/docs/mcp.md)
- [MCP Gateway Implementation](../../gateway/mcp/)
- [Documentation Guide](../../gateway/mcp/DOCUMENTATION.md)
- [Blog Post](../../internal/website/blog/2.md)
## Learn More
- [Model Context Protocol Spec](https://modelcontextprotocol.io/)
- [Go Micro Documentation](https://go-micro.dev)
-306
View File
@@ -1,306 +0,0 @@
# Documented Service Example
This example demonstrates how to document your go-micro service handlers so that AI agents can understand them better. The MCP gateway parses Go comments and struct tags to generate rich tool descriptions.
## Documentation Features
### 1. **Go Doc Comments**
Standard Go documentation comments are used as the tool description:
```go
// GetUser retrieves a user by ID from the database.
//
// This endpoint fetches a user's complete profile including their name,
// email, and age. If the user doesn't exist, an error is returned.
func (u *Users) GetUser(ctx context.Context, req *GetUserRequest, rsp *GetUserResponse) error {
// ...
}
```
### 2. **JSDoc-Style Tags**
Use `@param`, `@return`, and `@example` tags for detailed documentation:
```go
// CreateUser creates a new user in the system.
//
// @param name {string} User's full name (required, 1-100 characters)
// @param email {string} User's email address (required, must be valid email format)
// @param age {number} User's age (optional, must be 0-150 if provided)
// @return {User} The newly created user with generated ID
// @example
// {
// "name": "Alice Smith",
// "email": "alice@example.com",
// "age": 30
// }
func (u *Users) CreateUser(ctx context.Context, req *CreateUserRequest, rsp *CreateUserResponse) error {
// ...
}
```
### 3. **Struct Tags**
Add `description` tags to struct fields for better schema:
```go
type User struct {
ID string `json:"id" description:"User's unique identifier (UUID format)"`
Name string `json:"name" description:"User's full name"`
Email string `json:"email" description:"User's email address"`
Age int `json:"age,omitempty" description:"User's age (optional)"`
}
```
## Running the Example
### 1. Start the Service
```bash
cd examples/mcp/documented
go run main.go
```
Output:
```
Users service starting...
Service: users
Endpoints:
- Users.GetUser
- Users.CreateUser
MCP Gateway: http://localhost:3000
```
### 2. Test MCP Tools
List available tools:
```bash
curl http://localhost:3000/mcp/tools | jq
```
You'll see rich descriptions:
```json
{
"tools": [
{
"name": "users.Users.GetUser",
"description": "GetUser retrieves a user by ID from the database",
"inputSchema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "User ID in UUID format (e.g., \"123e4567-e89b-12d3-a456-426614174000\")"
}
},
"required": ["id"],
"examples": [
"{\"id\": \"123e4567-e89b-12d3-a456-426614174000\"}"
]
}
},
{
"name": "users.Users.CreateUser",
"description": "CreateUser creates a new user in the system",
"inputSchema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "User's full name (required, 1-100 characters)"
},
"email": {
"type": "string",
"description": "User's email address (required, must be valid email format)"
},
"age": {
"type": "number",
"description": "User's age (optional, must be 0-150 if provided)"
}
},
"required": ["name", "email"],
"examples": [
"{\"name\": \"Alice Smith\", \"email\": \"alice@example.com\", \"age\": 30}"
]
}
}
]
}
```
### 3. Call a Tool
Get existing user:
```bash
curl -X POST http://localhost:3000/mcp/call \
-H "Content-Type: application/json" \
-d '{
"tool": "users.Users.GetUser",
"input": {"id": "user-1"}
}'
```
Create new user:
```bash
curl -X POST http://localhost:3000/mcp/call \
-H "Content-Type: application/json" \
-d '{
"tool": "users.Users.CreateUser",
"input": {
"name": "Alice Smith",
"email": "alice@example.com",
"age": 30
}
}'
```
### 4. Use with Claude Code
Add to your `claude_desktop_config.json`:
```json
{
"mcpServers": {
"users-service": {
"command": "go",
"args": ["run", "/path/to/examples/mcp/documented/main.go"]
}
}
}
```
Then in Claude Code, ask:
```
> You: "Show me user-1's profile"
Claude will:
1. See the GetUser tool with rich description
2. Understand it needs an "id" parameter (UUID format)
3. Call users.Users.GetUser with {"id": "user-1"}
4. Return the user profile
```
## Documentation Best Practices
### DO: Write Clear Descriptions
```go
// ✅ Good: Clear, explains what and why
// GetUser retrieves a user by ID from the database.
// Returns full profile including email, name, and preferences.
```
```go
// ❌ Bad: Vague, no context
// Get gets a user
```
### DO: Specify Parameter Constraints
```go
// ✅ Good: Specifies format and constraints
// @param id {string} User ID in UUID format (e.g., "123e4567-e89b-12d3-a456-426614174000")
// @param age {number} User's age (must be 0-150)
```
```go
// ❌ Bad: No constraints or format
// @param id {string} The ID
```
### DO: Provide Examples
```go
// ✅ Good: Real example agents can use
// @example
// {
// "name": "Alice Smith",
// "email": "alice@example.com",
// "age": 30
// }
```
```go
// ❌ Bad: No example
// (agents have to guess the format)
```
### DO: Use Descriptive Struct Tags
```go
// ✅ Good: Explains what the field is
type User struct {
ID string `json:"id" description:"User's unique identifier (UUID format)"`
}
```
```go
// ❌ Bad: No description
type User struct {
ID string `json:"id"`
}
```
## How It Works
1. **Go Doc Parsing**
- The MCP gateway reads your service's Go comments
- First line becomes the tool description
- Full comment becomes the detailed description
2. **JSDoc Tag Parsing**
- `@param` tags enhance parameter descriptions
- `@return` tags describe what the tool returns
- `@example` tags provide usage examples
3. **Struct Tag Reading**
- `description` tags add context to fields
- `json:"field,omitempty"` marks optional fields
- Used to generate JSON Schema for parameters
4. **Schema Generation**
- Combines parsed documentation with type information
- Creates rich JSON Schema for each tool
- Agents use this to understand how to call your service
## Impact on Agent Performance
### Without Documentation
```
Tool: users.Users.GetUser
Description: Call GetUser on users service
Parameters: { "id": "string" }
```
Agent thinks: *"What's an ID? What format? What if I pass the wrong thing?"*
### With Documentation
```
Tool: users.Users.GetUser
Description: Retrieves a user by ID from the database. Returns full profile
including email, name, and preferences.
Parameters:
- id (string, required): User ID in UUID format
Example: "123e4567-e89b-12d3-a456-426614174000"
Example:
{"id": "user-1"}
```
Agent thinks: *"I need a UUID format ID. I can use 'user-1' from the example!"*
**Result:** Agent calls your service correctly on the first try!
## Next Steps
- Document all your service handlers with clear descriptions
- Add `@param`, `@return`, and `@example` tags
- Use `description` tags in struct fields
- Test with Claude Code to see how agents understand your services
## License
Apache 2.0
-160
View File
@@ -1,160 +0,0 @@
// Package main demonstrates how to document your service handlers for better
// AI agent integration using endpoint metadata.
//
// Services register descriptions with their endpoints, and the MCP gateway
// reads these descriptions from the registry to generate rich tool descriptions.
package main
import (
"context"
"fmt"
"log"
"go-micro.dev/v5"
"go-micro.dev/v5/gateway/mcp"
"go-micro.dev/v5/server"
)
// User represents a user in the system
type User struct {
ID string `json:"id" description:"User's unique identifier (UUID format)"`
Name string `json:"name" description:"User's full name"`
Email string `json:"email" description:"User's email address"`
Age int `json:"age,omitempty" description:"User's age (optional)"`
}
// GetUserRequest is the request for getting a user
type GetUserRequest struct {
ID string `json:"id" description:"User ID to retrieve"`
}
// GetUserResponse is the response containing user data
type GetUserResponse struct {
User *User `json:"user" description:"The requested user object"`
}
// CreateUserRequest is the request for creating a user
type CreateUserRequest struct {
Name string `json:"name" description:"User's full name (required)"`
Email string `json:"email" description:"User's email address (required)"`
Age int `json:"age,omitempty" description:"User's age (optional)"`
}
// CreateUserResponse contains the newly created user
type CreateUserResponse struct {
User *User `json:"user" description:"The newly created user"`
}
// Users service handles user-related operations
type Users struct {
users map[string]*User
}
// GetUser retrieves a user by ID from the database. Returns full profile including email, name, and preferences. If the user doesn't exist, an error is returned.
//
// @example {"id": "user-1"}
func (u *Users) GetUser(ctx context.Context, req *GetUserRequest, rsp *GetUserResponse) error {
user, exists := u.users[req.ID]
if !exists {
return fmt.Errorf("user not found: %s", req.ID)
}
rsp.User = user
return nil
}
// CreateUser creates a new user in the system. Validates the user data and creates a new profile. Name and email are required fields, while age is optional. Email must be unique across all users.
//
// @example {"name": "Alice Smith", "email": "alice@example.com", "age": 30}
func (u *Users) CreateUser(ctx context.Context, req *CreateUserRequest, rsp *CreateUserResponse) error {
// Validate input
if req.Name == "" || req.Email == "" {
return fmt.Errorf("name and email are required")
}
// Generate ID (simplified for example)
id := fmt.Sprintf("user-%d", len(u.users)+1)
user := &User{
ID: id,
Name: req.Name,
Email: req.Email,
Age: req.Age,
}
u.users[id] = user
rsp.User = user
return nil
}
func main() {
// Create service
service := micro.NewService(
micro.Name("users"),
micro.Version("1.0.0"),
)
service.Init()
// Register handler with pre-populated test data
usersService := &Users{
users: map[string]*User{
"user-1": {
ID: "user-1",
Name: "John Doe",
Email: "john@example.com",
Age: 25,
},
"user-2": {
ID: "user-2",
Name: "Jane Smith",
Email: "jane@example.com",
Age: 30,
},
},
}
// Register handler - documentation is automatically extracted from method comments.
// Use WithEndpointScopes to declare required auth scopes per endpoint.
handler := service.Server().NewHandler(
usersService,
server.WithEndpointScopes("Users.GetUser", "users:read"),
server.WithEndpointScopes("Users.CreateUser", "users:write"),
)
if err := service.Server().Handle(handler); err != nil {
log.Fatal(err)
}
// Start MCP gateway on port 3000
go func() {
log.Println("Starting MCP gateway on :3000")
if err := mcp.ListenAndServe(":3000", mcp.Options{
Registry: service.Options().Registry,
}); err != nil {
log.Printf("MCP gateway error: %v", err)
}
}()
log.Println("Users service starting...")
log.Println("Service: users")
log.Println("Endpoints:")
log.Println(" - Users.GetUser")
log.Println(" - Users.CreateUser")
log.Println("MCP Gateway: http://localhost:3000")
log.Println("")
log.Println("Test with:")
log.Println(" curl http://localhost:3000/mcp/tools")
log.Println("")
log.Println("Or add to Claude Code:")
log.Println(` "users-service": {`)
log.Println(` "command": "micro",`)
log.Println(` "args": ["mcp", "serve"]`)
log.Println(` }`)
// Run service
if err := service.Run(); err != nil {
log.Fatal(err)
}
}
-131
View File
@@ -1,131 +0,0 @@
# MCP Hello World Example
The simplest possible MCP-enabled go-micro service.
## What This Shows
- ✅ Automatic documentation extraction from Go comments
- ✅ MCP gateway setup with 3 lines of code
- ✅ Ready for Claude Code integration
- ✅ HTTP endpoint for testing
## Run It
```bash
cd examples/mcp/hello
go run main.go
```
## Test It
### Option 1: HTTP API
```bash
# List available tools
curl http://localhost:3000/mcp/tools | jq
# Call the SayHello tool
curl -X POST http://localhost:3000/mcp/call \
-H "Content-Type: application/json" \
-d '{
"tool": "greeter.Greeter.SayHello",
"input": {"name": "Alice"}
}' | jq
```
### Option 2: Claude Code
In a separate terminal:
```bash
micro mcp serve
```
Add to `~/.claude/claude_desktop_config.json`:
```json
{
"mcpServers": {
"greeter": {
"command": "micro",
"args": ["mcp", "serve"]
}
}
}
```
Restart Claude Code and ask:
> "Say hello to Bob using the greeter service"
## How It Works
### 1. Write Normal Go Code
```go
// SayHello greets a person by name. Returns a friendly greeting message.
//
// @example {"name": "Alice"}
func (g *Greeter) SayHello(ctx context.Context, req *HelloRequest, rsp *HelloResponse) error {
rsp.Message = "Hello " + req.Name + "!"
return nil
}
```
### 2. Register the Handler
```go
// Documentation is extracted automatically!
handler := service.Server().NewHandler(new(Greeter))
service.Server().Handle(handler)
```
### 3. Start MCP Gateway
```go
go mcp.ListenAndServe(":3000", mcp.Options{
Registry: service.Options().Registry,
})
```
**That's it!** Your service is now AI-accessible.
## What Gets Extracted
From this code:
```go
// SayHello greets a person by name. Returns a friendly greeting message.
//
// @example {"name": "Alice"}
func (g *Greeter) SayHello(...)
type HelloRequest struct {
Name string `json:"name" description:"Person's name to greet"`
}
```
Claude sees:
```json
{
"name": "greeter.Greeter.SayHello",
"description": "SayHello greets a person by name. Returns a friendly greeting message.",
"inputSchema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Person's name to greet"
}
},
"examples": ["{\"name\": \"Alice\"}"]
}
}
```
## Next Steps
- See `examples/mcp/documented` for a more complete example with multiple endpoints
- Read `/docs/mcp.md` for full documentation
- Check out the [MCP specification](https://modelcontextprotocol.io/)
-78
View File
@@ -1,78 +0,0 @@
// Package main demonstrates a minimal MCP-enabled service.
//
// This is the simplest possible example showing:
// - Automatic documentation extraction from Go comments
// - MCP gateway setup
// - Ready for use with Claude Code
package main
import (
"context"
"log"
"go-micro.dev/v5"
"go-micro.dev/v5/gateway/mcp"
)
// Greeter service handles greeting operations
type Greeter struct{}
// SayHello greets a person by name. Returns a friendly greeting message.
//
// @example {"name": "Alice"}
func (g *Greeter) SayHello(ctx context.Context, req *HelloRequest, rsp *HelloResponse) error {
rsp.Message = "Hello " + req.Name + "!"
return nil
}
// HelloRequest contains the greeting parameters
type HelloRequest struct {
Name string `json:"name" description:"Person's name to greet"`
}
// HelloResponse contains the greeting result
type HelloResponse struct {
Message string `json:"message" description:"The greeting message"`
}
func main() {
// Create service
service := micro.NewService(
micro.Name("greeter"),
micro.Version("1.0.0"),
)
service.Init()
// Register handler - documentation extracted automatically from comments!
handler := service.Server().NewHandler(new(Greeter))
if err := service.Server().Handle(handler); err != nil {
log.Fatal(err)
}
// Start MCP gateway on port 3000
go func() {
log.Println("Starting MCP gateway on :3000")
if err := mcp.ListenAndServe(":3000", mcp.Options{
Registry: service.Options().Registry,
}); err != nil {
log.Printf("MCP gateway error: %v", err)
}
}()
log.Println("Greeter service starting...")
log.Println("Service: greeter")
log.Println("Endpoint: Greeter.SayHello")
log.Println("MCP Gateway: http://localhost:3000")
log.Println("")
log.Println("Test with:")
log.Println(" curl http://localhost:3000/mcp/tools")
log.Println("")
log.Println("Or use with Claude Code:")
log.Println(" micro mcp serve")
// Run service
if err := service.Run(); err != nil {
log.Fatal(err)
}
}
-59
View File
@@ -1,59 +0,0 @@
# Web Service Example
HTTP web service with automatic service discovery and registration.
## What It Does
This example creates an HTTP service that:
- Serves RESTful API endpoints
- Registers with service discovery
- Provides health checks
- Uses standard Go HTTP handlers
## Run It
```bash
go run main.go
```
## Test It
```bash
# Get service info
curl http://localhost:9090/
# List all users
curl http://localhost:9090/users
# Get specific user
curl http://localhost:9090/users/1
# Health check
curl http://localhost:9090/health
```
## Key Features
- **Standard HTTP**: Use familiar `http.Handler` interface
- **Service Discovery**: Automatically registers with registry
- **Health Checks**: Built-in health endpoint
- **JSON APIs**: Easy REST API development
## When to Use
Use `web.Service` when:
- Building REST APIs
- Serving web UIs
- Working with HTTP-specific features
- Migrating existing HTTP services
Use regular `micro.Service` when:
- Building RPC services
- Need bidirectional streaming
- Want automatic load balancing
- Prefer structured RPC over HTTP
## Next Steps
- See [hello-world](../hello-world/) for RPC services
- See [production-ready](../production-ready/) for observability
-70
View File
@@ -1,70 +0,0 @@
module example
go 1.24
require go-micro.dev/v5 v5.16.0
require (
dario.cat/mergo v1.0.2 // indirect
github.com/armon/go-metrics v0.4.1 // indirect
github.com/bitly/go-simplejson v0.5.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/cornelk/hashmap v1.0.8 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-redis/redis/v8 v8.11.5 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/consul/api v1.32.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-hclog v1.5.0 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/serf v0.10.1 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/miekg/dns v1.1.50 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/hashstructure v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/nats-io/nats.go v1.42.0 // indirect
github.com/nats-io/nkeys v0.4.11 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rabbitmq/amqp091-go v1.10.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/urfave/cli/v2 v2.27.6 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
go.etcd.io/bbolt v1.4.0 // indirect
go.etcd.io/etcd/api/v3 v3.5.21 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.21 // indirect
go.etcd.io/etcd/client/v3 v3.5.21 // indirect
go.uber.org/multierr v1.10.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/net v0.38.0 // indirect
golang.org/x/sync v0.13.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect
golang.org/x/tools v0.31.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
google.golang.org/grpc v1.71.1 // indirect
google.golang.org/protobuf v1.36.6 // indirect
)
replace go-micro.dev/v5 => ../..
-385
View File
@@ -1,385 +0,0 @@
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA=
github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y=
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cornelk/hashmap v1.0.8 h1:nv0AWgw02n+iDcawr5It4CjQIAcdMMKRrs10HOJYlrc=
github.com/cornelk/hashmap v1.0.8/go.mod h1:RfZb7JO3RviW/rT6emczVuC/oxpdz4UsSB2LJSclR1k=
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-tpm v0.9.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc=
github.com/google/go-tpm v0.9.3/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/consul/api v1.32.1 h1:0+osr/3t/aZNAdJX558crU3PEjVrG4x6715aZHRgceE=
github.com/hashicorp/consul/api v1.32.1/go.mod h1:mXUWLnxftwTmDv4W3lzxYCPD199iNLLUyLfLGFJbtl4=
github.com/hashicorp/consul/sdk v0.16.1 h1:V8TxTnImoPD5cj0U9Spl0TUxcytjcbbJeADFF07KdHg=
github.com/hashicorp/consul/sdk v0.16.1/go.mod h1:fSXvwxB2hmh1FMZCNl6PwX0Q/1wdWtHJcZ7Ea5tns0s=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c=
github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI=
github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI=
github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM=
github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0=
github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY=
github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q=
github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/hashstructure v1.1.0 h1:P6P1hdjqAAknpY/M1CGipelZgp+4y9ja9kmUZPXP+H0=
github.com/mitchellh/hashstructure v1.1.0/go.mod h1:xUDAozZz0Wmdiufv0uyhnHkUTN6/6d8ulp4AwfLKrmA=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nats-io/jwt/v2 v2.7.4 h1:jXFuDDxs/GQjGDZGhNgH4tXzSUK6WQi2rsj4xmsNOtI=
github.com/nats-io/jwt/v2 v2.7.4/go.mod h1:me11pOkwObtcBNR8AiMrUbtVOUGkqYjMQZ6jnSdVUIA=
github.com/nats-io/nats-server/v2 v2.11.3 h1:AbGtXxuwjo0gBroLGGr/dE0vf24kTKdRnBq/3z/Fdoc=
github.com/nats-io/nats-server/v2 v2.11.3/go.mod h1:6Z6Fd+JgckqzKig7DYwhgrE7bJ6fypPHnGPND+DqgMY=
github.com/nats-io/nats.go v1.42.0 h1:ynIMupIOvf/ZWH/b2qda6WGKGNSjwOUutTpWRvAmhaM=
github.com/nats-io/nats.go v1.42.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE=
github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk=
go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
go.etcd.io/etcd/api/v3 v3.5.21 h1:A6O2/JDb3tvHhiIz3xf9nJ7REHvtEFJJ3veW3FbCnS8=
go.etcd.io/etcd/api/v3 v3.5.21/go.mod h1:c3aH5wcvXv/9dqIw2Y810LDXJfhSYdHQ0vxmP3CCHVY=
go.etcd.io/etcd/client/pkg/v3 v3.5.21 h1:lPBu71Y7osQmzlflM9OfeIV2JlmpBjqBNlLtcoBqUTc=
go.etcd.io/etcd/client/pkg/v3 v3.5.21/go.mod h1:BgqT/IXPjK9NkeSDjbzwsHySX3yIle2+ndz28nVsjUs=
go.etcd.io/etcd/client/v3 v3.5.21 h1:T6b1Ow6fNjOLOtM0xSoKNQt1ASPCLWrF9XMHcH9pEyY=
go.etcd.io/etcd/client/v3 v3.5.21/go.mod h1:mFYy67IOqmbRf/kRUvsHixzo3iG+1OF2W2+jVIQRAnU=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM=
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-102
View File
@@ -1,102 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"time"
"go-micro.dev/v5/web"
)
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
}
var users = map[string]*User{
"1": {ID: "1", Name: "Alice", Email: "alice@example.com", CreatedAt: time.Now()},
"2": {ID: "2", Name: "Bob", Email: "bob@example.com", CreatedAt: time.Now()},
}
func main() {
// Create a new web service
service := web.NewService(
web.Name("web.service"),
web.Version("latest"),
web.Address(":9090"),
)
// Initialize
service.Init()
// Register handlers
service.HandleFunc("/", homeHandler)
service.HandleFunc("/users", usersHandler)
service.HandleFunc("/users/", userHandler)
service.HandleFunc("/health", healthHandler)
fmt.Println("Web service starting on :9090")
fmt.Println("Try:")
fmt.Println(" curl http://localhost:9090/")
fmt.Println(" curl http://localhost:9090/users")
fmt.Println(" curl http://localhost:9090/users/1")
fmt.Println(" curl http://localhost:9090/health")
// Run the service
if err := service.Run(); err != nil {
log.Fatal(err)
}
}
func homeHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"service": "web.service",
"version": "latest",
"status": "running",
})
}
func usersHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
// Return all users
userList := make([]*User, 0, len(users))
for _, user := range users {
userList = append(userList, user)
}
json.NewEncoder(w).Encode(userList)
}
func userHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
// Extract user ID from path
id := r.URL.Path[len("/users/"):]
user, exists := users[id]
if !exists {
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(map[string]string{
"error": "User not found",
})
return
}
json.NewEncoder(w).Encode(user)
}
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "healthy",
"timestamp": time.Now().Unix(),
"uptime": "running",
})
}
-288
View File
@@ -1,288 +0,0 @@
# API Gateway
The `gateway/api` package provides HTTP API gateway functionality for go-micro services. It translates HTTP requests into RPC calls and serves a web dashboard for browsing and calling services.
## Features
- **HTTP to RPC translation** - Call microservices via HTTP
- **Web dashboard** - Browse and test services in the browser
- **Authentication** - Optional JWT-based auth
- **MCP integration** - Expose services to AI agents
- **Flexible configuration** - Use in dev or production
- **Service discovery** - Auto-detect services from registry
## Usage
### Basic Gateway
```go
package main
import (
"context"
"net/http"
"go-micro.dev/v5/gateway/api"
)
func main() {
// Create gateway with custom handler
gw, err := api.New(api.Options{
Address: ":8080",
Context: context.Background(),
HandlerRegistrar: func(mux *http.ServeMux) error {
// Register your HTTP handlers
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello from gateway"))
})
return nil
},
})
if err != nil {
panic(err)
}
// Block until shutdown
gw.Wait()
}
```
### Gateway with MCP
```go
gw, err := api.New(api.Options{
Address: ":8080",
MCPEnabled: true,
MCPAddress: ":3000", // MCP on separate port
HandlerRegistrar: registerHandlers,
})
```
### Gateway with Authentication
```go
gw, err := api.New(api.Options{
Address: ":8080",
AuthEnabled: true, // Handler registrar should add auth middleware
HandlerRegistrar: func(mux *http.ServeMux) error {
// Register handlers with auth middleware
return registerAuthenticatedHandlers(mux)
},
})
```
### Blocking Mode
```go
// Run blocks until shutdown
err := api.Run(api.Options{
Address: ":8080",
HandlerRegistrar: registerHandlers,
})
```
## Options
```go
type Options struct {
// Address to listen on (default: ":8080")
Address string
// AuthEnabled signals that authentication is required
// The HandlerRegistrar should implement auth checks
AuthEnabled bool
// Context for cancellation (default: context.Background())
Context context.Context
// Logger for gateway messages (default: log.Default())
Logger *log.Logger
// HandlerRegistrar registers HTTP handlers on the mux
HandlerRegistrar func(mux *http.ServeMux) error
// MCPEnabled enables the MCP gateway
MCPEnabled bool
// MCPAddress is the address for MCP gateway (e.g., ":3000")
MCPAddress string
// Registry for service discovery (default: registry.DefaultRegistry)
Registry registry.Registry
}
```
## Architecture
```
┌─────────────────────────────────────────┐
│ gateway/api Package │
│ ┌────────────────────────────────────┐ │
│ │ Gateway │ │
│ │ - Manages HTTP server │ │
│ │ - Calls HandlerRegistrar │ │
│ │ - Starts MCP if enabled │ │
│ └────────────────────────────────────┘ │
└─────────────────────────────────────────┘
↓ delegates to
┌─────────────────────────────────────────┐
│ HandlerRegistrar (user-provided) │
│ ┌────────────────────────────────────┐ │
│ │ func(mux *http.ServeMux) error │ │
│ │ - Registers routes │ │
│ │ - Adds middleware (auth, etc.) │ │
│ │ - Sets up templates │ │
│ └────────────────────────────────────┘ │
└─────────────────────────────────────────┘
↓ uses
┌─────────────────────────────────────────┐
│ Microservices (via RPC) │
└─────────────────────────────────────────┘
```
## Integration
### In `micro run` (Development)
```go
// cmd/micro/run/run.go
import "go-micro.dev/v5/gateway/api"
gw, err := api.New(api.Options{
Address: ":8080",
AuthEnabled: false, // No auth in dev mode
HandlerRegistrar: func(mux *http.ServeMux) error {
// Register dev-mode handlers (no auth)
mux.HandleFunc("/", dashboardHandler)
mux.HandleFunc("/api/", apiHandler)
return nil
},
})
```
### In `micro server` (Production)
```go
// cmd/micro/server/server.go
import "go-micro.dev/v5/gateway/api"
gw, err := api.New(api.Options{
Address: ":8080",
AuthEnabled: true, // Auth required in production
HandlerRegistrar: func(mux *http.ServeMux) error {
// Register prod handlers with auth middleware
mux.HandleFunc("/", authMiddleware(dashboardHandler))
mux.HandleFunc("/api/", authMiddleware(apiHandler))
return nil
},
})
```
### Custom Application
```go
// Your app
import "go-micro.dev/v5/gateway/api"
func main() {
gw, err := api.New(api.Options{
Address: ":8080",
HandlerRegistrar: func(mux *http.ServeMux) error {
// Your custom handlers
mux.HandleFunc("/health", healthHandler)
mux.HandleFunc("/metrics", metricsHandler)
mux.HandleFunc("/api/", proxyToServices)
return nil
},
})
if err != nil {
log.Fatal(err)
}
log.Println("Gateway running on :8080")
gw.Wait()
}
```
## Comparison with Old Architecture
### Before (Duplicated Code)
```
cmd/micro/run/gateway/
└── gateway.go (300+ lines)
cmd/micro/server/
└── gateway.go (150+ lines)
❌ Code duplication
❌ Inconsistent behavior
❌ Hard to reuse
```
### After (Unified)
```
gateway/api/
└── gateway.go (150 lines, reusable)
cmd/micro/server/
└── gateway.go (70 lines, compatibility wrapper)
cmd/micro/run/
└── Uses api.New() directly
✅ Single source of truth
✅ Consistent behavior
✅ Easy to reuse in custom apps
```
## Benefits
1. **Reusability** - Use in any Go application, not just micro CLI
2. **Testability** - Easy to test with custom handler registrars
3. **Flexibility** - Supports different configurations (dev, prod, custom)
4. **Consistency** - Same gateway code for all use cases
5. **Maintainability** - One place to fix bugs and add features
## Migration Guide
### From `cmd/micro/server/gateway.go`
**Before:**
```go
import "go-micro.dev/v5/cmd/micro/server"
gw, err := server.StartGateway(server.GatewayOptions{
Address: ":8080",
AuthEnabled: true,
Store: myStore,
})
```
**After:**
```go
import "go-micro.dev/v5/gateway/api"
gw, err := api.New(api.Options{
Address: ":8080",
AuthEnabled: true,
HandlerRegistrar: func(mux *http.ServeMux) error {
// Register your handlers
// Pass store as closure
return registerHandlers(mux, myStore)
},
})
```
## Examples
See:
- `cmd/micro/server/gateway.go` - Production gateway with auth
- `cmd/micro/run/run.go` - Development gateway without auth
- `examples/gateway/` - Custom gateway examples (coming soon)
## License
Apache 2.0
-155
View File
@@ -1,155 +0,0 @@
// Package api provides HTTP API gateway functionality for go-micro services.
//
// The API gateway translates HTTP requests into RPC calls and serves a web dashboard
// for browsing and calling services. It can be used in development (micro run) or
// production (micro server) with optional authentication.
package api
import (
"context"
"fmt"
"log"
"net/http"
"time"
"go-micro.dev/v5/gateway/mcp"
"go-micro.dev/v5/registry"
)
// Options configures the HTTP API gateway
type Options struct {
// Address to listen on (e.g., ":8080")
Address string
// AuthEnabled controls whether authentication is required
// If true, the HandlerRegistrar should include auth middleware
AuthEnabled bool
// Context for cancellation (if nil, uses background context)
Context context.Context
// Logger for gateway messages (if nil, uses log.Default())
Logger *log.Logger
// HandlerRegistrar is called to register HTTP handlers on the mux
// This allows different configurations (dev vs prod) to register different handlers
HandlerRegistrar func(mux *http.ServeMux) error
// MCPEnabled controls whether to start MCP gateway
MCPEnabled bool
// MCPAddress is the address for MCP gateway (e.g., ":3000")
MCPAddress string
// Registry for service discovery (if nil, uses registry.DefaultRegistry)
Registry registry.Registry
}
// Gateway represents a running HTTP API gateway server
type Gateway struct {
opts Options
server *http.Server
mux *http.ServeMux
}
// New creates a new gateway with the given options and starts it.
// Returns immediately after starting the server in a goroutine.
// Use Wait() or Run() to block until the server stops.
func New(opts Options) (*Gateway, error) {
// Set defaults
if opts.Address == "" {
opts.Address = ":8080"
}
if opts.Context == nil {
opts.Context = context.Background()
}
if opts.Logger == nil {
opts.Logger = log.Default()
}
if opts.Registry == nil {
opts.Registry = registry.DefaultRegistry
}
// Create a new mux for this gateway instance
mux := http.NewServeMux()
// Register handlers using the provided registrar
if opts.HandlerRegistrar != nil {
if err := opts.HandlerRegistrar(mux); err != nil {
return nil, fmt.Errorf("failed to register handlers: %w", err)
}
}
// Create HTTP server
server := &http.Server{
Addr: opts.Address,
Handler: mux,
}
gw := &Gateway{
opts: opts,
server: server,
mux: mux,
}
// Start MCP gateway if enabled
if opts.MCPEnabled && opts.MCPAddress != "" {
go func() {
if err := mcp.ListenAndServe(opts.MCPAddress, mcp.Options{
Registry: opts.Registry,
Context: opts.Context,
Logger: opts.Logger,
}); err != nil {
opts.Logger.Printf("[mcp] MCP gateway error: %v", err)
}
}()
opts.Logger.Printf("[mcp] MCP gateway enabled on %s", opts.MCPAddress)
}
// Start server in background
go func() {
opts.Logger.Printf("[gateway] Listening on %s (auth: %v)", opts.Address, opts.AuthEnabled)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
opts.Logger.Printf("[gateway] Server error: %v", err)
}
}()
return gw, nil
}
// Run creates and starts a gateway, blocking until it stops.
// This is a convenience function equivalent to New() + Wait().
func Run(opts Options) error {
gw, err := New(opts)
if err != nil {
return err
}
return gw.Wait()
}
// Wait blocks until the server is shut down
func (g *Gateway) Wait() error {
<-g.opts.Context.Done()
return g.Stop()
}
// Stop gracefully shuts down the gateway
func (g *Gateway) Stop() error {
if g.server != nil {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return g.server.Shutdown(ctx)
}
return nil
}
// Addr returns the address the gateway is listening on
func (g *Gateway) Addr() string {
return g.opts.Address
}
// Mux returns the underlying HTTP mux for this gateway
// This can be used to register additional handlers after creation
func (g *Gateway) Mux() *http.ServeMux {
return g.mux
}
-402
View File
@@ -1,402 +0,0 @@
# MCP Tool Documentation
This document explains how to document your go-micro services so that AI agents can understand them better.
## Overview
The MCP gateway automatically exposes your microservices as tools that AI agents (like Claude) can call. By adding proper documentation to your service handlers, you help agents understand:
- **What the tool does** - The purpose and behavior
- **What parameters it needs** - Types, formats, constraints
- **What it returns** - Response structure and meaning
- **How to use it** - Example inputs and outputs
## Documentation Methods
go-micro **automatically extracts documentation** from your Go doc comments at registration time. You don't need to write any extra code!
### 1. Go Doc Comments (Automatic - Recommended)
Just write standard Go documentation comments on your handler methods:
```go
// GetUser retrieves a user by ID from the database. Returns full profile including email, name, and preferences.
//
// @example {"id": "user-1"}
func (s *UserService) GetUser(ctx context.Context, req *GetUserRequest, rsp *GetUserResponse) error {
// implementation
}
```
When you register the handler, go-micro automatically:
- Extracts the doc comment as the tool description
- Parses the `@example` tag for example inputs
- Registers everything in the service registry
- Makes it available to the MCP gateway
**Supported Tags:**
- `@example <json>` - Example JSON input (highly recommended for AI agents)
**That's it!** No extra registration code needed:
```go
// Documentation is extracted automatically from method comments
handler := service.Server().NewHandler(new(UserService))
service.Server().Handle(handler)
```
### 2. Manual Registration (Optional Override)
For more control or to override auto-extracted docs, use `server.WithEndpointDocs()`:
```go
handler := service.Server().NewHandler(
new(UserService),
server.WithEndpointDocs(map[string]server.EndpointDoc{
"UserService.GetUser": {
Description: "Custom description that overrides the comment",
Example: `{"id": "user-123"}`,
},
}),
)
```
Manual metadata **takes precedence** over auto-extracted comments.
### 3. Endpoint Scopes (Auth)
Use `server.WithEndpointScopes()` to declare the auth scopes required for each
endpoint. The MCP gateway reads these from the registry and enforces them when
an `Auth` provider is configured.
```go
handler := service.Server().NewHandler(
new(BlogService),
server.WithEndpointScopes("Blog.Create", "blog:write"),
server.WithEndpointScopes("Blog.Delete", "blog:write", "blog:admin"),
server.WithEndpointScopes("Blog.Read", "blog:read"),
)
```
Scopes are stored as comma-separated values in endpoint metadata (`"scopes"` key)
and are propagated through the service registry just like descriptions and examples.
#### Gateway-Level Scope Overrides
An operator can also define or override scopes at the MCP gateway without
modifying individual services. This is useful for centralized policy management:
```go
mcp.Serve(mcp.Options{
Registry: reg,
Auth: authProvider,
Scopes: map[string][]string{
"blog.Blog.Create": {"blog:write"},
"blog.Blog.Delete": {"blog:admin"},
},
})
```
Gateway-level scopes **take precedence** over service-level scopes.
### 4. Struct Tags (For Field Descriptions)
Add descriptions to struct fields using the `description` tag:
```go
type User struct {
ID string `json:"id" description:"User's unique identifier (UUID format)"`
Name string `json:"name" description:"User's full name"`
Email string `json:"email" description:"User's email address"`
Age int `json:"age,omitempty" description:"User's age (optional)"`
}
```
The `description` tag is used to generate parameter descriptions in the JSON Schema.
## How It Works
### Automatic Extraction Pipeline
```
1. Handler Registration (Your Service)
├─> You write Go doc comments on methods
├─> Call service.Server().NewHandler(yourHandler)
└─> go-micro automatically parses source files using go/ast
2. Documentation Extraction (Automatic)
├─> Read Go doc comments from handler method source
├─> Parse @example tags for sample inputs
├─> Extract struct tag descriptions
└─> Merge with any manual metadata (manual wins)
3. Service Registry
├─> Store endpoint metadata in registry.Endpoint.Metadata
├─> Metadata distributed with service information
└─> Available to all components (gateway, discovery, etc.)
4. MCP Gateway Discovery
├─> Query registry for services and endpoints
├─> Read description and example from endpoint.Metadata
└─> Generate JSON Schema with documentation
5. Tool Creation
└─> Create MCP tool with rich description for AI agents
```
### Example Output
For a documented handler, the MCP gateway generates:
```json
{
"name": "users.UserService.GetUser",
"description": "GetUser retrieves a user by ID from the database. Returns full profile including email, name, and preferences.",
"inputSchema": {
"type": "object",
"description": "This endpoint fetches a user's complete profile...",
"properties": {
"id": {
"type": "string",
"description": "User ID in UUID format (e.g., \"123e4567-e89b-12d3-a456-426614174000\")"
}
},
"required": ["id"],
"examples": [
"{\"id\": \"user-1\"}"
]
}
}
```
## Best Practices
### Write for AI, Not Just Humans
AI agents parse your documentation literally. Be explicit:
**✅ Good:**
```go
// GetUser retrieves a user by their unique ID from the database.
// Returns the user's full profile including name, email, and preferences.
// If the user doesn't exist, returns an error with status 404.
//
// @param id {string} User ID in UUID v4 format (e.g., "123e4567-e89b-12d3-a456-426614174000")
// @return {User} User object with all profile fields populated
```
**❌ Bad:**
```go
// Gets a user
func GetUser(...) // No details, no context
```
### Specify Formats and Constraints
Tell agents exactly what format you expect:
**✅ Good:**
```go
// @param email {string} Email address in RFC 5322 format (must contain @ and domain)
// @param age {number} User's age (integer between 0-150)
// @param phone {string} Phone number in E.164 format (e.g., "+14155552671")
```
**❌ Bad:**
```go
// @param email {string} The email
// @param age {number} Age
```
### Provide Real Examples
Show agents actual valid inputs:
**✅ Good:**
```go
// @example
// {
// "name": "Alice Smith",
// "email": "alice@example.com",
// "age": 30,
// "phone": "+14155552671"
// }
```
**❌ Bad:**
```go
// @example
// {
// "name": "string",
// "email": "string"
// }
```
### Document Error Cases
Tell agents what can go wrong:
```go
// GetUser retrieves a user by ID.
//
// Returns error if:
// - User ID is not a valid UUID
// - User does not exist (404)
// - Database is unavailable (503)
//
// @param id {string} User ID in UUID format
```
### Use Descriptive Names
Field names should be self-explanatory:
**✅ Good:**
```go
type CreateUserRequest struct {
FullName string `json:"full_name" description:"User's complete name"`
EmailAddress string `json:"email_address" description:"Primary email for contact"`
DateOfBirth string `json:"date_of_birth" description:"Birth date in YYYY-MM-DD format"`
}
```
**❌ Bad:**
```go
type CreateUserRequest struct {
N string `json:"n"` // What is n?
E string `json:"e"` // What is e?
D string `json:"d"` // What is d?
}
```
## Impact on Agent Performance
### Without Documentation
```
Agent: "I need to call GetUser but I don't know what format the ID should be.
Is it a number? A string? A UUID? Let me try..."
❌ Calls with: {"id": 123}
❌ Calls with: {"id": "user123"}
❌ Calls with: {"id": "abc"}
✅ Calls with: {"id": "550e8400-e29b-41d4-a716-446655440000"} (after 4 attempts)
```
### With Documentation
```
Agent: "GetUser needs an ID in UUID format. The example shows the format.
I'll use a valid UUID."
✅ Calls with: {"id": "550e8400-e29b-41d4-a716-446655440000"} (first attempt)
```
**Result:**
- **75% fewer failed calls**
- **Faster task completion**
- **Better user experience**
## Parser Implementation
The MCP gateway uses several parsers:
### 1. Go Doc Parser (`parseServiceDocs`)
- Extracts godoc comments from handler methods
- Parses JSDoc-style tags
- Returns `ToolDescription` struct
### 2. Struct Tag Parser (`ParseStructTags`)
- Reads `description` tags from struct fields
- Generates JSON Schema with field descriptions
- Marks required vs optional fields (omitempty)
### 3. Comment Parser (`ParseGoDocComment`)
- Regex-based extraction of @param, @return, @example tags
- Splits summary from detailed description
- Builds structured documentation
### 4. Type Mapper (`reflectTypeToJSONType`)
- Converts Go types to JSON Schema types
- Handles: string, int, float, bool, array, object
- Used for automatic schema generation
## Examples
See complete examples in:
- `examples/mcp/documented/` - Fully documented service
- `examples/auth/` - Auth service with documentation
- `examples/hello-world/` - Basic service
## Testing Documentation
### 1. List Tools
```bash
curl http://localhost:3000/mcp/tools | jq '.tools[0]'
```
Verify the description and schema are correct.
### 2. Use with Claude Code
Add to your Claude Code config and ask Claude to use your service. Claude will show you how it interprets your documentation.
### 3. Check Examples Work
Try the examples from your `@example` tags:
```bash
curl -X POST http://localhost:3000/mcp/call \
-H "Content-Type: application/json" \
-d '{
"tool": "users.UserService.GetUser",
"input": <your-example-json>
}'
```
## Future Enhancements
Planned improvements:
- [ ] Auto-extract examples from test files
- [ ] Validate documentation completeness (lint)
- [ ] Generate documentation from OpenAPI specs
- [ ] Support custom validation rules in tags
- [ ] Interactive documentation editor
## FAQ
**Q: Do I need to document every field?**
A: Document fields that are ambiguous or have constraints. Self-explanatory fields can rely on the field name.
**Q: Will this slow down my service?**
A: No. Documentation is parsed once at startup when the MCP gateway discovers services.
**Q: Can I use OpenAPI/Swagger specs instead?**
A: Not yet, but it's planned. For now, use Go comments and struct tags.
**Q: What if I don't document my handlers?**
A: The MCP gateway will still work, generating basic descriptions from method names and types. But agents will perform better with documentation.
**Q: How do I know if my documentation is good?**
A: Test it with Claude Code. If Claude understands your service and calls it correctly on the first try, your documentation is good!
**Q: How do I add auth scopes to my endpoints?**
A: Use `server.WithEndpointScopes()` when registering your handler:
```go
handler := service.Server().NewHandler(
new(MyService),
server.WithEndpointScopes("MyService.Create", "write"),
)
```
Or define scopes at the gateway level using `Scopes` in `mcp.Options`.
**Q: Can I set scopes at the gateway without changing services?**
A: Yes. Use the `Scopes` option on `mcp.Options` to define or override scopes for any tool at the gateway layer. This is useful for centralized policy management.
## License
Apache 2.0
-126
View File
@@ -1,126 +0,0 @@
package mcp
import (
"context"
"fmt"
"log"
"net/http"
"go-micro.dev/v5"
"go-micro.dev/v5/auth/jwt"
"go-micro.dev/v5/registry"
)
// Example_inlineGateway shows how to add MCP gateway to an existing service
func Example_inlineGateway() {
service := micro.NewService(micro.Name("myservice"))
service.Init()
// Add MCP gateway alongside your service
go func() {
if err := Serve(Options{
Registry: service.Options().Registry,
Address: ":3000",
}); err != nil {
log.Fatal(err)
}
}()
// Run your service normally
service.Run()
}
// Example_standaloneGateway shows how to run MCP gateway as a separate service
func Example_standaloneGateway() {
// Standalone MCP gateway
// Discovers all services via registry
if err := ListenAndServe(":3000", Options{
Registry: registry.NewMDNSRegistry(),
}); err != nil {
log.Fatal(err)
}
}
// Example_withAuthentication shows how to add authentication
func Example_withAuthentication() {
service := micro.NewService(micro.Name("myservice"))
service.Init()
go func() {
if err := Serve(Options{
Registry: service.Options().Registry,
Address: ":3000",
AuthFunc: func(r *http.Request) error {
token := r.Header.Get("Authorization")
if token == "" {
return fmt.Errorf("missing authorization header")
}
// Validate token here
return nil
},
}); err != nil {
log.Fatal(err)
}
}()
service.Run()
}
// Example_customContext shows how to use a custom context for graceful shutdown
func Example_customContext() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
service := micro.NewService(micro.Name("myservice"))
service.Init()
go func() {
if err := Serve(Options{
Registry: service.Options().Registry,
Address: ":3000",
Context: ctx,
}); err != nil {
log.Fatal(err)
}
}()
service.Run()
// cancel() will stop the MCP gateway
}
// Example_withScopesAndTracing shows how to add per-tool scopes, tracing, rate
// limiting and audit logging to the MCP gateway. Services register scope
// requirements via endpoint metadata ("scopes" key, comma-separated).
func Example_withScopesAndTracing() {
service := micro.NewService(micro.Name("blog"))
service.Init()
// Use JWT auth provider
authProvider := jwt.NewAuth()
go func() {
if err := Serve(Options{
Registry: service.Options().Registry,
Address: ":3000",
// Auth inspects Bearer tokens and enforces per-tool scopes
Auth: authProvider,
// Rate limit all tools to 10 req/s with burst of 20
RateLimit: &RateLimitConfig{
RequestsPerSecond: 10,
Burst: 20,
},
// Audit every tool call for compliance
AuditFunc: func(r AuditRecord) {
log.Printf("[audit] trace=%s tool=%s account=%s allowed=%v reason=%s",
r.TraceID, r.Tool, r.AccountID, r.Allowed, r.DeniedReason)
},
}); err != nil {
log.Fatal(err)
}
}()
service.Run()
}
-630
View File
@@ -1,630 +0,0 @@
// Package mcp provides Model Context Protocol (MCP) gateway functionality for go-micro services.
// It automatically exposes your microservices as AI-accessible tools through MCP.
//
// Example usage:
//
// service := micro.NewService(micro.Name("myservice"))
// service.Init()
//
// // Add MCP gateway
// go mcp.Serve(mcp.Options{
// Registry: service.Options().Registry,
// Address: ":3000",
// })
//
// service.Run()
package mcp
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
"sync"
"time"
"go-micro.dev/v5/auth"
"go-micro.dev/v5/client"
"go-micro.dev/v5/codec/bytes"
"go-micro.dev/v5/metadata"
"go-micro.dev/v5/registry"
"github.com/google/uuid"
)
// Metadata keys for MCP tracing and auth propagated via context/metadata.
const (
// TraceIDKey is the metadata key for the MCP trace ID.
TraceIDKey = "Mcp-Trace-Id"
// ToolNameKey is the metadata key for the tool being invoked.
ToolNameKey = "Mcp-Tool-Name"
// AccountIDKey is the metadata key for the authenticated account ID.
AccountIDKey = "Mcp-Account-Id"
)
// AuditRecord represents an immutable log entry for an MCP tool call.
type AuditRecord struct {
// TraceID uniquely identifies this tool call chain.
TraceID string `json:"trace_id"`
// Timestamp of the tool call.
Timestamp time.Time `json:"timestamp"`
// Tool is the name of the tool that was called.
Tool string `json:"tool"`
// AccountID is the ID of the authenticated account (empty if unauthenticated).
AccountID string `json:"account_id,omitempty"`
// Scopes that were required for this tool.
ScopesRequired []string `json:"scopes_required,omitempty"`
// Allowed indicates whether the call was authorized.
Allowed bool `json:"allowed"`
// Denied reason, if the call was not allowed.
DeniedReason string `json:"denied_reason,omitempty"`
// Duration of the RPC call (zero if call was denied before execution).
Duration time.Duration `json:"duration,omitempty"`
// Error from the RPC call, if any.
Error string `json:"error,omitempty"`
}
// AuditFunc is called for every tool call with an audit record.
// Implementations should treat the record as immutable and persist it
// (e.g. to a log, database, or event stream).
type AuditFunc func(record AuditRecord)
// RateLimitConfig configures rate limiting for the MCP gateway.
type RateLimitConfig struct {
// Requests per second allowed per tool (0 = unlimited).
RequestsPerSecond float64
// Burst size (maximum number of requests that can be made at once).
Burst int
}
// Options configures the MCP gateway
type Options struct {
// Registry for service discovery (required)
Registry registry.Registry
// Address to listen on for SSE transport (e.g., ":3000")
// Leave empty for stdio transport
Address string
// Client for making RPC calls (defaults to client.DefaultClient)
Client client.Client
// Context for cancellation (defaults to background context)
Context context.Context
// Logger for debug output (defaults to log.Default())
Logger *log.Logger
// AuthFunc validates requests (optional, legacy)
// Return error to reject, nil to allow
AuthFunc func(r *http.Request) error
// Auth provider for token inspection (optional).
// When set, incoming requests must carry a Bearer token which is
// inspected to obtain an account. The account's scopes are then
// checked against the tool's required scopes.
Auth auth.Auth
// AuditFunc is called for every tool call with an immutable audit record.
// Use this to persist tool-call logs for compliance and debugging.
AuditFunc AuditFunc
// RateLimit configures per-tool rate limiting.
// When set, each tool is limited to the configured requests per second.
RateLimit *RateLimitConfig
// Scopes lets the gateway operator define or override per-tool
// scope requirements without changing the services themselves.
// Keys are tool names (e.g. "blog.Blog.Create") and values are the
// required scopes. When a tool appears in Scopes its scopes
// replace any scopes declared by the service via endpoint metadata.
//
// Example:
//
// Scopes: map[string][]string{
// "blog.Blog.Create": {"blog:write"},
// "blog.Blog.Delete": {"blog:admin"},
// }
Scopes map[string][]string
}
// Server represents a running MCP gateway
type Server struct {
opts Options
tools map[string]*Tool
toolsMu sync.RWMutex
server *http.Server
watching bool
// limiters holds per-tool rate limiters (nil if rate limiting is disabled).
limiters map[string]*rateLimiter
limitersMu sync.RWMutex
}
// Tool represents an MCP tool (exposed service endpoint)
type Tool struct {
Name string `json:"name"`
Description string `json:"description"`
InputSchema map[string]interface{} `json:"inputSchema"`
// Scopes lists the auth scopes required to call this tool.
// An empty list means no scope restriction (subject to Auth provider).
Scopes []string `json:"scopes,omitempty"`
Service string `json:"-"`
Endpoint string `json:"-"`
}
// Serve starts an MCP gateway with the given options.
// For stdio transport, leave Address empty.
// For SSE transport, set Address (e.g., ":3000").
func Serve(opts Options) error {
// Set defaults
if opts.Client == nil {
opts.Client = client.DefaultClient
}
if opts.Context == nil {
opts.Context = context.Background()
}
if opts.Logger == nil {
opts.Logger = log.Default()
}
if opts.Registry == nil {
return fmt.Errorf("registry is required")
}
server := &Server{
opts: opts,
tools: make(map[string]*Tool),
limiters: make(map[string]*rateLimiter),
}
// Discover services and build tool list
if err := server.discoverServices(); err != nil {
return fmt.Errorf("failed to discover services: %w", err)
}
// Watch for service changes
go server.watchServices()
// Start server based on transport
if opts.Address != "" {
return server.serveHTTP()
}
return server.serveStdio()
}
// ListenAndServe is a convenience function that starts an MCP gateway on the given address.
func ListenAndServe(address string, opts Options) error {
opts.Address = address
return Serve(opts)
}
// discoverServices queries the registry and builds the tool list
func (s *Server) discoverServices() error {
services, err := s.opts.Registry.ListServices()
if err != nil {
return err
}
s.toolsMu.Lock()
defer s.toolsMu.Unlock()
for _, svc := range services {
// Get full service details
fullSvcs, err := s.opts.Registry.GetService(svc.Name)
if err != nil || len(fullSvcs) == 0 {
continue
}
// Convert endpoints to tools
for _, ep := range fullSvcs[0].Endpoints {
toolName := fmt.Sprintf("%s.%s", svc.Name, ep.Name)
// Build input schema from endpoint request type
inputSchema := s.buildInputSchema(ep.Request)
// Get description from endpoint metadata (set by service during registration)
description := fmt.Sprintf("Call %s on %s service", ep.Name, svc.Name)
if ep.Metadata != nil {
if desc, ok := ep.Metadata["description"]; ok && desc != "" {
description = desc
}
}
tool := &Tool{
Name: toolName,
Description: description,
InputSchema: inputSchema,
Service: svc.Name,
Endpoint: ep.Name,
}
// Extract scopes from endpoint metadata
if ep.Metadata != nil {
if scopes, ok := ep.Metadata["scopes"]; ok && scopes != "" {
tool.Scopes = strings.Split(scopes, ",")
}
}
// Gateway-level Scopes override service-level scopes
if s.opts.Scopes != nil {
if scopes, ok := s.opts.Scopes[toolName]; ok {
tool.Scopes = scopes
}
}
// Add example from metadata if available
if ep.Metadata != nil {
if example, ok := ep.Metadata["example"]; ok && example != "" {
inputSchema["examples"] = []string{example}
}
}
s.tools[toolName] = tool
// Create rate limiter for this tool if rate limiting is configured
if s.opts.RateLimit != nil && s.opts.RateLimit.RequestsPerSecond > 0 {
s.limitersMu.Lock()
if _, exists := s.limiters[toolName]; !exists {
s.limiters[toolName] = newRateLimiter(
s.opts.RateLimit.RequestsPerSecond,
s.opts.RateLimit.Burst,
)
}
s.limitersMu.Unlock()
}
}
}
s.opts.Logger.Printf("[mcp] Discovered %d tools from %d services", len(s.tools), len(services))
return nil
}
// buildInputSchema converts registry value type information to JSON schema
func (s *Server) buildInputSchema(value *registry.Value) map[string]interface{} {
schema := map[string]interface{}{
"type": "object",
"properties": make(map[string]interface{}),
}
if value == nil || len(value.Values) == 0 {
return schema
}
properties := schema["properties"].(map[string]interface{})
for _, field := range value.Values {
properties[field.Name] = map[string]interface{}{
"type": s.mapGoTypeToJSON(field.Type),
"description": fmt.Sprintf("%s field", field.Name),
}
}
return schema
}
// mapGoTypeToJSON maps Go types to JSON schema types
func (s *Server) mapGoTypeToJSON(goType string) string {
switch goType {
case "string":
return "string"
case "int", "int32", "int64", "uint", "uint32", "uint64":
return "integer"
case "float32", "float64":
return "number"
case "bool":
return "boolean"
default:
return "object"
}
}
// watchServices watches for service registry changes
func (s *Server) watchServices() {
if s.watching {
return
}
s.watching = true
watcher, err := s.opts.Registry.Watch()
if err != nil {
s.opts.Logger.Printf("[mcp] Failed to watch registry: %v", err)
return
}
defer watcher.Stop()
for {
select {
case <-s.opts.Context.Done():
return
default:
_, err := watcher.Next()
if err != nil {
time.Sleep(time.Second)
continue
}
// Rediscover services on any change
if err := s.discoverServices(); err != nil {
s.opts.Logger.Printf("[mcp] Failed to rediscover services: %v", err)
}
}
}
}
// serveHTTP starts an HTTP server with SSE transport
func (s *Server) serveHTTP() error {
mux := http.NewServeMux()
// MCP endpoints
mux.HandleFunc("/mcp/tools", s.handleListTools)
mux.HandleFunc("/mcp/call", s.handleCallTool)
mux.HandleFunc("/health", s.handleHealth)
s.server = &http.Server{
Addr: s.opts.Address,
Handler: mux,
}
s.opts.Logger.Printf("[mcp] MCP gateway listening on %s", s.opts.Address)
return s.server.ListenAndServe()
}
// serveStdio starts stdio-based MCP server (for Claude Code, etc.)
func (s *Server) serveStdio() error {
transport := NewStdioTransport(s)
return transport.Serve()
}
// handleListTools returns the list of available tools
func (s *Server) handleListTools(w http.ResponseWriter, r *http.Request) {
if s.opts.AuthFunc != nil {
if err := s.opts.AuthFunc(r); err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
}
s.toolsMu.RLock()
tools := make([]*Tool, 0, len(s.tools))
for _, tool := range s.tools {
tools = append(tools, tool)
}
s.toolsMu.RUnlock()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"tools": tools,
})
}
// handleCallTool executes a tool (makes an RPC call)
func (s *Server) handleCallTool(w http.ResponseWriter, r *http.Request) {
if s.opts.AuthFunc != nil {
if err := s.opts.AuthFunc(r); err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
}
// Parse request
var req struct {
Tool string `json:"tool"`
Input map[string]interface{} `json:"input"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Get tool info
s.toolsMu.RLock()
tool, exists := s.tools[req.Tool]
s.toolsMu.RUnlock()
if !exists {
http.Error(w, "Tool not found", http.StatusNotFound)
return
}
// Generate trace ID for this call
traceID := uuid.New().String()
// Authenticate and authorise
var account *auth.Account
if s.opts.Auth != nil {
token := r.Header.Get("Authorization")
if strings.HasPrefix(token, "Bearer ") {
token = strings.TrimPrefix(token, "Bearer ")
}
if token == "" {
s.audit(AuditRecord{TraceID: traceID, Timestamp: time.Now(), Tool: req.Tool, Allowed: false, DeniedReason: "missing token"})
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
acc, err := s.opts.Auth.Inspect(token)
if err != nil {
s.audit(AuditRecord{TraceID: traceID, Timestamp: time.Now(), Tool: req.Tool, Allowed: false, DeniedReason: "invalid token"})
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
account = acc
// Check per-tool scopes
if len(tool.Scopes) > 0 {
if !hasScope(account.Scopes, tool.Scopes) {
s.audit(AuditRecord{
TraceID: traceID, Timestamp: time.Now(), Tool: req.Tool,
AccountID: account.ID, ScopesRequired: tool.Scopes,
Allowed: false, DeniedReason: "insufficient scopes",
})
http.Error(w, "Forbidden: insufficient scopes", http.StatusForbidden)
return
}
}
}
// Rate limit check
if err := s.allowRate(req.Tool); err != nil {
accountID := ""
if account != nil {
accountID = account.ID
}
s.audit(AuditRecord{
TraceID: traceID, Timestamp: time.Now(), Tool: req.Tool,
AccountID: accountID, Allowed: false, DeniedReason: "rate limited",
})
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
return
}
// Build context with tracing metadata
ctx := r.Context()
md := metadata.Metadata{}
md.Set(TraceIDKey, traceID)
md.Set(ToolNameKey, req.Tool)
if account != nil {
md.Set(AccountIDKey, account.ID)
}
ctx = metadata.MergeContext(ctx, md, true)
// Convert input to JSON bytes for RPC call
inputBytes, err := json.Marshal(req.Input)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Make RPC call
start := time.Now()
rpcReq := s.opts.Client.NewRequest(tool.Service, tool.Endpoint, &bytes.Frame{Data: inputBytes})
var rsp bytes.Frame
if err := s.opts.Client.Call(ctx, rpcReq, &rsp); err != nil {
s.opts.Logger.Printf("[mcp] RPC call failed: %v", err)
accountID := ""
if account != nil {
accountID = account.ID
}
s.audit(AuditRecord{
TraceID: traceID, Timestamp: time.Now(), Tool: req.Tool,
AccountID: accountID, ScopesRequired: tool.Scopes,
Allowed: true, Duration: time.Since(start), Error: err.Error(),
})
http.Error(w, fmt.Sprintf("RPC call failed: %v", err), http.StatusInternalServerError)
return
}
// Audit successful call
accountID := ""
if account != nil {
accountID = account.ID
}
s.audit(AuditRecord{
TraceID: traceID, Timestamp: time.Now(), Tool: req.Tool,
AccountID: accountID, ScopesRequired: tool.Scopes,
Allowed: true, Duration: time.Since(start),
})
// Return response with trace ID
w.Header().Set("Content-Type", "application/json")
w.Header().Set(TraceIDKey, traceID)
json.NewEncoder(w).Encode(map[string]interface{}{
"result": json.RawMessage(rsp.Data),
"trace_id": traceID,
})
}
// handleHealth returns gateway health status
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
s.toolsMu.RLock()
toolCount := len(s.tools)
s.toolsMu.RUnlock()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "ok",
"tools": toolCount,
})
}
// Stop gracefully shuts down the MCP gateway
func (s *Server) Stop() error {
if s.server != nil {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return s.server.Shutdown(ctx)
}
return nil
}
// GetTools returns the current list of available tools
func (s *Server) GetTools() []*Tool {
s.toolsMu.RLock()
defer s.toolsMu.RUnlock()
tools := make([]*Tool, 0, len(s.tools))
for _, tool := range s.tools {
tools = append(tools, tool)
}
return tools
}
// audit emits an audit record if an AuditFunc is configured.
func (s *Server) audit(record AuditRecord) {
if s.opts.AuditFunc != nil {
s.opts.AuditFunc(record)
}
}
// allowRate checks if the tool call is allowed under the configured rate limit.
// Returns nil if allowed, non-nil error if rate-limited.
func (s *Server) allowRate(toolName string) error {
if s.opts.RateLimit == nil {
return nil
}
s.limitersMu.RLock()
limiter, ok := s.limiters[toolName]
s.limitersMu.RUnlock()
if !ok {
return nil
}
if !limiter.Allow() {
return fmt.Errorf("rate limit exceeded for tool %s", toolName)
}
return nil
}
// hasScope checks if the account has at least one of the required scopes.
func hasScope(accountScopes, requiredScopes []string) bool {
for _, req := range requiredScopes {
for _, have := range accountScopes {
if strings.EqualFold(have, req) {
return true
}
}
}
return false
}
// Example shows how to use the MCP gateway in your code
func Example() {
// This function is never called - it's just documentation
_ = func() {
// In your service code:
// service := micro.NewService(micro.Name("myservice"))
// service.Init()
// Start MCP gateway
go func() {
if err := Serve(Options{
Registry: registry.DefaultRegistry,
Address: ":3000",
}); err != nil {
log.Fatal(err)
}
}()
// service.Run()
}
}
-568
View File
@@ -1,568 +0,0 @@
package mcp
import (
"bytes"
"context"
"encoding/json"
"log"
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"
"go-micro.dev/v5/auth"
"go-micro.dev/v5/client"
"go-micro.dev/v5/registry"
)
// mockAuth implements auth.Auth for testing.
type mockAuth struct {
accounts map[string]*auth.Account // token -> account
}
func (m *mockAuth) Init(...auth.Option) {}
func (m *mockAuth) Options() auth.Options { return auth.Options{} }
func (m *mockAuth) Generate(string, ...auth.GenerateOption) (*auth.Account, error) {
return nil, nil
}
func (m *mockAuth) Token(...auth.TokenOption) (*auth.Token, error) { return nil, nil }
func (m *mockAuth) String() string { return "mock" }
func (m *mockAuth) Inspect(token string) (*auth.Account, error) {
acc, ok := m.accounts[token]
if !ok {
return nil, auth.ErrInvalidToken
}
return acc, nil
}
// newTestServer creates a Server with pre-populated tools for testing.
func newTestServer(opts Options) *Server {
if opts.Logger == nil {
opts.Logger = testLogger()
}
if opts.Context == nil {
opts.Context = context.Background()
}
if opts.Client == nil {
opts.Client = client.DefaultClient
}
s := &Server{
opts: opts,
tools: make(map[string]*Tool),
limiters: make(map[string]*rateLimiter),
}
return s
}
// testLogger returns a silent logger for tests.
func testLogger() *log.Logger {
return log.New(nopWriter{}, "", 0)
}
type nopWriter struct{}
func (nopWriter) Write(p []byte) (int, error) { return len(p), nil }
// --- Tests ---
func TestHasScope(t *testing.T) {
tests := []struct {
name string
account []string
required []string
want bool
}{
{"match single", []string{"blog:write"}, []string{"blog:write"}, true},
{"match one of many", []string{"blog:read", "blog:write"}, []string{"blog:write"}, true},
{"no match", []string{"blog:read"}, []string{"blog:write"}, false},
{"empty required", []string{"blog:read"}, nil, false},
{"empty account", nil, []string{"blog:write"}, false},
{"case insensitive", []string{"Blog:Write"}, []string{"blog:write"}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := hasScope(tt.account, tt.required)
if got != tt.want {
t.Errorf("hasScope(%v, %v) = %v, want %v", tt.account, tt.required, got, tt.want)
}
})
}
}
func TestToolScopesFromMetadata(t *testing.T) {
// Create a mock registry with endpoints that have scope metadata
reg := registry.NewMemoryRegistry()
svc := &registry.Service{
Name: "blog",
Nodes: []*registry.Node{{
Id: "blog-1",
Address: "localhost:9090",
}},
Endpoints: []*registry.Endpoint{
{
Name: "Blog.Create",
Metadata: map[string]string{
"description": "Create a blog post",
"scopes": "blog:write,blog:admin",
},
},
{
Name: "Blog.Read",
Metadata: map[string]string{
"description": "Read a blog post",
},
},
},
}
if err := reg.Register(svc); err != nil {
t.Fatal(err)
}
s := newTestServer(Options{Registry: reg})
if err := s.discoverServices(); err != nil {
t.Fatal(err)
}
// Check that scopes are populated
createTool := s.tools["blog.Blog.Create"]
if createTool == nil {
t.Fatal("expected tool blog.Blog.Create")
}
if len(createTool.Scopes) != 2 || createTool.Scopes[0] != "blog:write" || createTool.Scopes[1] != "blog:admin" {
t.Errorf("unexpected scopes: %v", createTool.Scopes)
}
readTool := s.tools["blog.Blog.Read"]
if readTool == nil {
t.Fatal("expected tool blog.Blog.Read")
}
if len(readTool.Scopes) != 0 {
t.Errorf("expected no scopes for read, got: %v", readTool.Scopes)
}
}
func TestHandleCallTool_AuthRequired(t *testing.T) {
ma := &mockAuth{
accounts: map[string]*auth.Account{
"valid-token": {ID: "user-1", Scopes: []string{"blog:write"}},
"readonly": {ID: "user-2", Scopes: []string{"blog:read"}},
},
}
s := newTestServer(Options{Auth: ma})
s.tools["blog.Blog.Create"] = &Tool{
Name: "blog.Blog.Create",
Service: "blog",
Endpoint: "Blog.Create",
Scopes: []string{"blog:write"},
}
tests := []struct {
name string
token string
wantStatus int
}{
{"no token", "", http.StatusUnauthorized},
{"invalid token", "bad-token", http.StatusUnauthorized},
{"valid token with scope", "valid-token", http.StatusInternalServerError}, // RPC will fail (no backend), but auth passes
{"valid token without scope", "readonly", http.StatusForbidden},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
body, _ := json.Marshal(map[string]interface{}{
"tool": "blog.Blog.Create",
"input": map[string]interface{}{"title": "hello"},
})
req := httptest.NewRequest("POST", "/mcp/call", bytes.NewReader(body))
if tt.token != "" {
req.Header.Set("Authorization", "Bearer "+tt.token)
}
rec := httptest.NewRecorder()
s.handleCallTool(rec, req)
if rec.Code != tt.wantStatus {
t.Errorf("status = %d, want %d, body: %s", rec.Code, tt.wantStatus, rec.Body.String())
}
})
}
}
func TestHandleCallTool_TraceID(t *testing.T) {
// Without Auth, tool calls should still generate trace IDs.
s := newTestServer(Options{})
s.tools["svc.Echo"] = &Tool{
Name: "svc.Echo",
Service: "svc",
Endpoint: "Echo",
}
body, _ := json.Marshal(map[string]interface{}{
"tool": "svc.Echo",
"input": map[string]interface{}{},
})
req := httptest.NewRequest("POST", "/mcp/call", bytes.NewReader(body))
rec := httptest.NewRecorder()
s.handleCallTool(rec, req)
// Even though the RPC fails (no backend), the trace ID header should be absent
// only when the call didn't reach the RPC stage; but in this no-auth case it
// should reach the RPC call and fail. Check we get a response.
traceID := rec.Header().Get(TraceIDKey)
// The RPC call will fail but the error path doesn't set the header.
// For a successful call, the trace ID is set. Either way the audit should fire.
_ = traceID // trace ID may or may not be in error response header
}
func TestHandleCallTool_AuditFunc(t *testing.T) {
var mu sync.Mutex
var records []AuditRecord
auditFn := func(r AuditRecord) {
mu.Lock()
defer mu.Unlock()
records = append(records, r)
}
ma := &mockAuth{
accounts: map[string]*auth.Account{
"tok": {ID: "u1", Scopes: []string{"write"}},
},
}
s := newTestServer(Options{Auth: ma, AuditFunc: auditFn})
s.tools["svc.Do"] = &Tool{
Name: "svc.Do",
Service: "svc",
Endpoint: "Do",
Scopes: []string{"write"},
}
body, _ := json.Marshal(map[string]interface{}{
"tool": "svc.Do",
"input": map[string]interface{}{},
})
req := httptest.NewRequest("POST", "/mcp/call", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer tok")
rec := httptest.NewRecorder()
s.handleCallTool(rec, req)
mu.Lock()
defer mu.Unlock()
if len(records) == 0 {
t.Fatal("expected at least one audit record")
}
r := records[len(records)-1]
if r.AccountID != "u1" {
t.Errorf("audit AccountID = %q, want %q", r.AccountID, "u1")
}
if r.Tool != "svc.Do" {
t.Errorf("audit Tool = %q, want %q", r.Tool, "svc.Do")
}
if r.TraceID == "" {
t.Error("audit TraceID is empty")
}
if !r.Allowed {
t.Error("expected audit record Allowed = true")
}
}
func TestHandleCallTool_AuditDenied(t *testing.T) {
var mu sync.Mutex
var records []AuditRecord
auditFn := func(r AuditRecord) {
mu.Lock()
defer mu.Unlock()
records = append(records, r)
}
ma := &mockAuth{
accounts: map[string]*auth.Account{
"tok": {ID: "u1", Scopes: []string{"blog:read"}},
},
}
s := newTestServer(Options{Auth: ma, AuditFunc: auditFn})
s.tools["svc.Do"] = &Tool{
Name: "svc.Do",
Service: "svc",
Endpoint: "Do",
Scopes: []string{"blog:write"},
}
body, _ := json.Marshal(map[string]interface{}{
"tool": "svc.Do",
"input": map[string]interface{}{},
})
req := httptest.NewRequest("POST", "/mcp/call", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer tok")
rec := httptest.NewRecorder()
s.handleCallTool(rec, req)
if rec.Code != http.StatusForbidden {
t.Errorf("status = %d, want %d", rec.Code, http.StatusForbidden)
}
mu.Lock()
defer mu.Unlock()
if len(records) == 0 {
t.Fatal("expected audit record for denied call")
}
r := records[0]
if r.Allowed {
t.Error("expected Allowed = false")
}
if r.DeniedReason != "insufficient scopes" {
t.Errorf("DeniedReason = %q, want %q", r.DeniedReason, "insufficient scopes")
}
}
func TestRateLimiter(t *testing.T) {
rl := newRateLimiter(10, 2)
// First two should be allowed (burst)
if !rl.Allow() {
t.Error("first call should be allowed")
}
if !rl.Allow() {
t.Error("second call should be allowed (burst)")
}
// Third should be denied (burst exhausted, no time to refill)
if rl.Allow() {
t.Error("third call should be denied (burst exhausted)")
}
// Wait for refill
time.Sleep(150 * time.Millisecond)
// Should be allowed again
if !rl.Allow() {
t.Error("call after refill should be allowed")
}
}
func TestHandleCallTool_RateLimit(t *testing.T) {
var mu sync.Mutex
var records []AuditRecord
s := newTestServer(Options{
RateLimit: &RateLimitConfig{RequestsPerSecond: 1, Burst: 1},
AuditFunc: func(r AuditRecord) {
mu.Lock()
records = append(records, r)
mu.Unlock()
},
})
s.tools["svc.Do"] = &Tool{
Name: "svc.Do",
Service: "svc",
Endpoint: "Do",
}
s.limiters["svc.Do"] = newRateLimiter(1, 1)
makeReq := func() int {
body, _ := json.Marshal(map[string]interface{}{
"tool": "svc.Do",
"input": map[string]interface{}{},
})
req := httptest.NewRequest("POST", "/mcp/call", bytes.NewReader(body))
rec := httptest.NewRecorder()
s.handleCallTool(rec, req)
return rec.Code
}
// First request should pass rate limit (but RPC may fail — that's ok)
code1 := makeReq()
if code1 == http.StatusTooManyRequests {
t.Error("first request should not be rate limited")
}
// Second request should be rate limited
code2 := makeReq()
if code2 != http.StatusTooManyRequests {
t.Errorf("second request status = %d, want %d", code2, http.StatusTooManyRequests)
}
// Check audit records include rate limit denial
mu.Lock()
defer mu.Unlock()
found := false
for _, r := range records {
if r.DeniedReason == "rate limited" {
found = true
break
}
}
if !found {
t.Error("expected audit record with DeniedReason = 'rate limited'")
}
}
func TestHandleCallTool_NoAuth_NoScope(t *testing.T) {
// Without Auth configured, tools without scopes should be accessible
s := newTestServer(Options{})
s.tools["svc.Echo"] = &Tool{
Name: "svc.Echo",
Service: "svc",
Endpoint: "Echo",
}
body, _ := json.Marshal(map[string]interface{}{
"tool": "svc.Echo",
"input": map[string]interface{}{"msg": "hi"},
})
req := httptest.NewRequest("POST", "/mcp/call", bytes.NewReader(body))
rec := httptest.NewRecorder()
s.handleCallTool(rec, req)
// Should not be 401 or 403 (RPC failure is expected since no backend)
if rec.Code == http.StatusUnauthorized || rec.Code == http.StatusForbidden {
t.Errorf("unexpected auth error: %d", rec.Code)
}
}
func TestToolScopesInJSON(t *testing.T) {
tool := &Tool{
Name: "blog.Blog.Create",
Description: "Create a blog post",
InputSchema: map[string]interface{}{"type": "object"},
Scopes: []string{"blog:write", "blog:admin"},
}
data, err := json.Marshal(tool)
if err != nil {
t.Fatal(err)
}
var m map[string]interface{}
if err := json.Unmarshal(data, &m); err != nil {
t.Fatal(err)
}
scopes, ok := m["scopes"].([]interface{})
if !ok {
t.Fatal("expected scopes in JSON output")
}
if len(scopes) != 2 {
t.Errorf("expected 2 scopes, got %d", len(scopes))
}
}
func TestToolNoScopesOmittedInJSON(t *testing.T) {
tool := &Tool{
Name: "blog.Blog.Read",
Description: "Read a blog post",
InputSchema: map[string]interface{}{"type": "object"},
}
data, err := json.Marshal(tool)
if err != nil {
t.Fatal(err)
}
var m map[string]interface{}
if err := json.Unmarshal(data, &m); err != nil {
t.Fatal(err)
}
if _, ok := m["scopes"]; ok {
t.Error("expected scopes to be omitted when empty")
}
}
func TestDiscoverServices_RateLimiters(t *testing.T) {
reg := registry.NewMemoryRegistry()
svc := &registry.Service{
Name: "blog",
Nodes: []*registry.Node{{
Id: "blog-1",
Address: "localhost:9090",
}},
Endpoints: []*registry.Endpoint{
{Name: "Blog.Create"},
{Name: "Blog.Read"},
},
}
if err := reg.Register(svc); err != nil {
t.Fatal(err)
}
s := newTestServer(Options{
Registry: reg,
RateLimit: &RateLimitConfig{RequestsPerSecond: 10, Burst: 5},
})
if err := s.discoverServices(); err != nil {
t.Fatal(err)
}
if len(s.limiters) != 2 {
t.Errorf("expected 2 limiters, got %d", len(s.limiters))
}
for name := range s.tools {
if _, ok := s.limiters[name]; !ok {
t.Errorf("missing limiter for tool %s", name)
}
}
}
func TestScopesFromGatewayOptions(t *testing.T) {
reg := registry.NewMemoryRegistry()
svc := &registry.Service{
Name: "blog",
Nodes: []*registry.Node{{
Id: "blog-1",
Address: "localhost:9090",
}},
Endpoints: []*registry.Endpoint{
{
Name: "Blog.Create",
Metadata: map[string]string{
"scopes": "blog:write",
},
},
{
Name: "Blog.Delete",
},
},
}
if err := reg.Register(svc); err != nil {
t.Fatal(err)
}
// Gateway-level Scopes override service-level metadata scopes
s := newTestServer(Options{
Registry: reg,
Scopes: map[string][]string{
"blog.Blog.Create": {"blog:admin"}, // override service scope
"blog.Blog.Delete": {"blog:admin", "sudo"}, // add scope to tool without service scope
},
})
if err := s.discoverServices(); err != nil {
t.Fatal(err)
}
// Blog.Create should have gateway-level scope (overrides service "blog:write")
createTool := s.tools["blog.Blog.Create"]
if createTool == nil {
t.Fatal("expected tool blog.Blog.Create")
}
if len(createTool.Scopes) != 1 || createTool.Scopes[0] != "blog:admin" {
t.Errorf("expected gateway scopes [blog:admin], got: %v", createTool.Scopes)
}
// Blog.Delete should get gateway-level scopes
deleteTool := s.tools["blog.Blog.Delete"]
if deleteTool == nil {
t.Fatal("expected tool blog.Blog.Delete")
}
if len(deleteTool.Scopes) != 2 || deleteTool.Scopes[0] != "blog:admin" || deleteTool.Scopes[1] != "sudo" {
t.Errorf("expected gateway scopes [blog:admin sudo], got: %v", deleteTool.Scopes)
}
}
-339
View File
@@ -1,339 +0,0 @@
package mcp
import (
"fmt"
"go/ast"
"go/doc"
"go/parser"
"go/token"
"path/filepath"
"reflect"
"regexp"
"strings"
"go-micro.dev/v5/registry"
)
// ToolDescription represents enhanced documentation for an MCP tool
type ToolDescription struct {
Summary string
Description string
Params []ParamDoc
Returns []ReturnDoc
Examples []string
}
// ParamDoc describes a parameter
type ParamDoc struct {
Name string
Type string
Description string
Required bool
}
// ReturnDoc describes a return value
type ReturnDoc struct {
Type string
Description string
}
var (
// Regex patterns for JSDoc-style tags
paramPattern = regexp.MustCompile(`@param\s+(\w+)\s+\{(\w+)\}\s+(.+)`)
returnPattern = regexp.MustCompile(`@return\s+\{(\w+)\}\s+(.+)`)
examplePattern = regexp.MustCompile(`@example\s+([\s\S]+?)(?:@\w+|$)`)
)
// parseServiceDocs attempts to parse Go source files to extract documentation
// for service methods. This enhances tool descriptions with godoc comments.
func parseServiceDocs(serviceName string, endpoint *registry.Endpoint) *ToolDescription {
// For now, return basic description
// Full implementation would:
// 1. Use go/parser to find service source files
// 2. Extract godoc comments for methods
// 3. Parse JSDoc-style tags (@param, @return, @example)
// 4. Return rich ToolDescription
desc := &ToolDescription{
Summary: fmt.Sprintf("Call %s on %s service", endpoint.Name, serviceName),
Description: "",
Params: parseEndpointParams(endpoint.Request),
Returns: parseEndpointReturns(endpoint.Response),
Examples: []string{},
}
return desc
}
// parseEndpointParams extracts parameter documentation from registry Value
func parseEndpointParams(value *registry.Value) []ParamDoc {
if value == nil || len(value.Values) == 0 {
return nil
}
params := make([]ParamDoc, 0, len(value.Values))
for _, field := range value.Values {
params = append(params, ParamDoc{
Name: field.Name,
Type: field.Type,
Description: formatFieldDescription(field.Name, field.Type),
Required: true, // Conservative default
})
}
return params
}
// parseEndpointReturns extracts return value documentation
func parseEndpointReturns(value *registry.Value) []ReturnDoc {
if value == nil {
return nil
}
return []ReturnDoc{{
Type: value.Name,
Description: fmt.Sprintf("Returns %s", value.Name),
}}
}
// formatFieldDescription creates a basic description for a field
func formatFieldDescription(name, typeName string) string {
// Convert camelCase/PascalCase to readable format
readable := toReadable(name)
return fmt.Sprintf("%s (%s)", readable, typeName)
}
// toReadable converts camelCase or PascalCase to readable format
func toReadable(s string) string {
// Insert spaces before uppercase letters
var result strings.Builder
for i, r := range s {
if i > 0 && r >= 'A' && r <= 'Z' {
result.WriteRune(' ')
}
result.WriteRune(r)
}
return result.String()
}
// ParseGoDocComment parses a Go doc comment for JSDoc-style tags
func ParseGoDocComment(comment string) *ToolDescription {
desc := &ToolDescription{
Params: []ParamDoc{},
Returns: []ReturnDoc{},
Examples: []string{},
}
// Extract summary (first line)
lines := strings.Split(comment, "\n")
if len(lines) > 0 {
desc.Summary = strings.TrimSpace(lines[0])
}
// Extract full description (before first tag)
tagStart := strings.Index(comment, "@")
if tagStart > 0 {
desc.Description = strings.TrimSpace(comment[:tagStart])
} else {
desc.Description = strings.TrimSpace(comment)
}
// Parse @param tags
paramMatches := paramPattern.FindAllStringSubmatch(comment, -1)
for _, match := range paramMatches {
if len(match) == 4 {
desc.Params = append(desc.Params, ParamDoc{
Name: match[1],
Type: match[2],
Description: strings.TrimSpace(match[3]),
Required: true,
})
}
}
// Parse @return tags
returnMatches := returnPattern.FindAllStringSubmatch(comment, -1)
for _, match := range returnMatches {
if len(match) == 3 {
desc.Returns = append(desc.Returns, ReturnDoc{
Type: match[1],
Description: strings.TrimSpace(match[2]),
})
}
}
// Parse @example tags
exampleMatches := examplePattern.FindAllStringSubmatch(comment, -1)
for _, match := range exampleMatches {
if len(match) == 2 {
example := strings.TrimSpace(match[1])
desc.Examples = append(desc.Examples, example)
}
}
return desc
}
// enhanceToolDescription attempts to enhance a tool with parsed documentation
func enhanceToolDescription(tool *Tool, serviceName string, endpoint *registry.Endpoint) {
// Try to parse service documentation
toolDesc := parseServiceDocs(serviceName, endpoint)
// Update tool description with parsed info
if toolDesc.Summary != "" {
tool.Description = toolDesc.Summary
}
// Add detailed description to input schema
if toolDesc.Description != "" {
if tool.InputSchema == nil {
tool.InputSchema = make(map[string]interface{})
}
tool.InputSchema["description"] = toolDesc.Description
}
// Enhance parameter descriptions
if len(toolDesc.Params) > 0 {
properties, ok := tool.InputSchema["properties"].(map[string]interface{})
if ok {
for _, param := range toolDesc.Params {
if propSchema, exists := properties[param.Name]; exists {
if propMap, ok := propSchema.(map[string]interface{}); ok {
propMap["description"] = param.Description
if param.Required {
// Add to required array
required, _ := tool.InputSchema["required"].([]string)
required = append(required, param.Name)
tool.InputSchema["required"] = required
}
}
}
}
}
}
// Add examples if available
if len(toolDesc.Examples) > 0 {
tool.InputSchema["examples"] = toolDesc.Examples
}
}
// ParseStructTags extracts JSON schema information from struct tags
// This can be used to enhance parameter descriptions
func ParseStructTags(t reflect.Type) map[string]interface{} {
schema := map[string]interface{}{
"type": "object",
"properties": make(map[string]interface{}),
}
properties := schema["properties"].(map[string]interface{})
required := []string{}
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
// Get JSON tag
jsonTag := field.Tag.Get("json")
if jsonTag == "" || jsonTag == "-" {
continue
}
// Parse JSON tag
jsonName := strings.Split(jsonTag, ",")[0]
omitempty := strings.Contains(jsonTag, "omitempty")
// Get description from validate tag or description tag
description := field.Tag.Get("description")
if description == "" {
description = formatFieldDescription(field.Name, field.Type.String())
}
// Build property schema
propSchema := map[string]interface{}{
"description": description,
}
// Add type information
propSchema["type"] = reflectTypeToJSONType(field.Type)
properties[jsonName] = propSchema
// Track required fields
if !omitempty {
required = append(required, jsonName)
}
}
if len(required) > 0 {
schema["required"] = required
}
return schema
}
// reflectTypeToJSONType converts Go reflect.Type to JSON schema type
func reflectTypeToJSONType(t reflect.Type) string {
switch t.Kind() {
case reflect.String:
return "string"
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return "integer"
case reflect.Float32, reflect.Float64:
return "number"
case reflect.Bool:
return "boolean"
case reflect.Slice, reflect.Array:
return "array"
case reflect.Map, reflect.Struct:
return "object"
default:
return "string"
}
}
// findServiceSource attempts to locate Go source files for a service
// This is used to extract godoc comments
func findServiceSource(serviceName string) ([]string, error) {
// This would search GOPATH/module cache for service sources
// For now, return empty - implementation would use:
// - go/packages to find module
// - Search for service struct definitions
// - Return list of source files
return nil, fmt.Errorf("source discovery not yet implemented")
}
// parseGoFile parses a Go source file and extracts method documentation
func parseGoFile(filename string, serviceName string) (map[string]*ToolDescription, error) {
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
if err != nil {
return nil, err
}
docs := make(map[string]*ToolDescription)
// Use go/doc to extract documentation
pkg := &ast.Package{
Name: f.Name.Name,
Files: map[string]*ast.File{filename: f},
}
docPkg := doc.New(pkg, filepath.Dir(filename), doc.AllDecls)
// Extract method documentation
for _, typ := range docPkg.Types {
if !strings.Contains(typ.Name, serviceName) {
continue
}
for _, method := range typ.Methods {
toolDesc := ParseGoDocComment(method.Doc)
toolDesc.Summary = fmt.Sprintf("%s - %s", method.Name, toolDesc.Summary)
docs[method.Name] = toolDesc
}
}
return docs, nil
}
-51
View File
@@ -1,51 +0,0 @@
package mcp
import (
"sync"
"time"
)
// rateLimiter implements a simple token-bucket rate limiter.
type rateLimiter struct {
mu sync.Mutex
rate float64 // tokens per second
burst int // max tokens
tokens float64 // current token count
lastTime time.Time // last refill time
}
// newRateLimiter creates a rate limiter that allows rate requests/sec with
// the given burst size. If burst is less than 1 it defaults to 1.
func newRateLimiter(rate float64, burst int) *rateLimiter {
if burst < 1 {
burst = 1
}
return &rateLimiter{
rate: rate,
burst: burst,
tokens: float64(burst),
lastTime: time.Now(),
}
}
// Allow reports whether a single event may happen now.
func (r *rateLimiter) Allow() bool {
r.mu.Lock()
defer r.mu.Unlock()
now := time.Now()
elapsed := now.Sub(r.lastTime).Seconds()
r.lastTime = now
// Refill tokens based on elapsed time
r.tokens += elapsed * r.rate
if r.tokens > float64(r.burst) {
r.tokens = float64(r.burst)
}
if r.tokens < 1 {
return false
}
r.tokens--
return true
}
-369
View File
@@ -1,369 +0,0 @@
package mcp
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"log"
"os"
"strings"
"sync"
"time"
"go-micro.dev/v5/auth"
"go-micro.dev/v5/metadata"
"github.com/google/uuid"
)
// StdioTransport implements MCP JSON-RPC 2.0 over stdio
// This is used by Claude Code and other local AI tools
type StdioTransport struct {
server *Server
reader *bufio.Reader
writer *bufio.Writer
writerMu sync.Mutex
ctx context.Context
cancel context.CancelFunc
}
// JSONRPCRequest represents a JSON-RPC 2.0 request
type JSONRPCRequest struct {
JSONRPC string `json:"jsonrpc"`
ID interface{} `json:"id,omitempty"`
Method string `json:"method"`
Params json.RawMessage `json:"params,omitempty"`
}
// JSONRPCResponse represents a JSON-RPC 2.0 response
type JSONRPCResponse struct {
JSONRPC string `json:"jsonrpc"`
ID interface{} `json:"id,omitempty"`
Result interface{} `json:"result,omitempty"`
Error *RPCError `json:"error,omitempty"`
}
// RPCError represents a JSON-RPC error
type RPCError struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
// Standard JSON-RPC error codes
const (
ParseError = -32700
InvalidRequest = -32600
MethodNotFound = -32601
InvalidParams = -32602
InternalError = -32603
)
// NewStdioTransport creates a new stdio transport for the MCP server
func NewStdioTransport(server *Server) *StdioTransport {
ctx, cancel := context.WithCancel(context.Background())
return &StdioTransport{
server: server,
reader: bufio.NewReader(os.Stdin),
writer: bufio.NewWriter(os.Stdout),
ctx: ctx,
cancel: cancel,
}
}
// Serve starts the stdio transport and processes JSON-RPC requests
func (t *StdioTransport) Serve() error {
t.server.opts.Logger.Printf("[mcp] MCP server started (stdio transport)")
// Read and process requests from stdin
for {
select {
case <-t.ctx.Done():
return nil
default:
}
// Read one line (JSON-RPC request)
line, err := t.reader.ReadBytes('\n')
if err != nil {
if err == io.EOF {
return nil
}
return fmt.Errorf("failed to read request: %w", err)
}
// Parse JSON-RPC request
var req JSONRPCRequest
if err := json.Unmarshal(line, &req); err != nil {
t.sendError(nil, ParseError, "Parse error", err.Error())
continue
}
// Validate JSON-RPC version
if req.JSONRPC != "2.0" {
t.sendError(req.ID, InvalidRequest, "Invalid request", "jsonrpc must be '2.0'")
continue
}
// Handle request
go t.handleRequest(&req)
}
}
// handleRequest processes a single JSON-RPC request
func (t *StdioTransport) handleRequest(req *JSONRPCRequest) {
switch req.Method {
case "initialize":
t.handleInitialize(req)
case "tools/list":
t.handleToolsList(req)
case "tools/call":
t.handleToolsCall(req)
default:
t.sendError(req.ID, MethodNotFound, "Method not found", req.Method)
}
}
// handleInitialize handles the initialize request
func (t *StdioTransport) handleInitialize(req *JSONRPCRequest) {
result := map[string]interface{}{
"protocolVersion": "2024-11-05",
"capabilities": map[string]interface{}{
"tools": map[string]interface{}{},
},
"serverInfo": map[string]interface{}{
"name": "go-micro-mcp",
"version": "1.0.0",
},
}
t.sendResponse(req.ID, result)
}
// handleToolsList handles the tools/list request
func (t *StdioTransport) handleToolsList(req *JSONRPCRequest) {
t.server.toolsMu.RLock()
tools := make([]interface{}, 0, len(t.server.tools))
for _, tool := range t.server.tools {
tools = append(tools, map[string]interface{}{
"name": tool.Name,
"description": tool.Description,
"inputSchema": tool.InputSchema,
})
}
t.server.toolsMu.RUnlock()
result := map[string]interface{}{
"tools": tools,
}
t.sendResponse(req.ID, result)
}
// handleToolsCall handles the tools/call request
func (t *StdioTransport) handleToolsCall(req *JSONRPCRequest) {
// Parse params
var params struct {
Name string `json:"name"`
Arguments map[string]interface{} `json:"arguments"`
// Token allows callers to pass a bearer token for auth via the
// JSON-RPC params (since stdio has no HTTP headers).
Token string `json:"_token,omitempty"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
t.sendError(req.ID, InvalidParams, "Invalid params", err.Error())
return
}
// Get tool info
t.server.toolsMu.RLock()
tool, exists := t.server.tools[params.Name]
t.server.toolsMu.RUnlock()
if !exists {
t.sendError(req.ID, InvalidParams, "Tool not found", params.Name)
return
}
// Generate trace ID
traceID := uuid.New().String()
// Authenticate and authorise (if Auth is configured)
var account *auth.Account
if t.server.opts.Auth != nil {
token := params.Token
if token == "" {
t.server.audit(AuditRecord{TraceID: traceID, Timestamp: time.Now(), Tool: params.Name, Allowed: false, DeniedReason: "missing token"})
t.sendError(req.ID, InvalidParams, "Unauthorized", "missing _token in params")
return
}
if strings.HasPrefix(token, "Bearer ") {
token = strings.TrimPrefix(token, "Bearer ")
}
acc, err := t.server.opts.Auth.Inspect(token)
if err != nil {
t.server.audit(AuditRecord{TraceID: traceID, Timestamp: time.Now(), Tool: params.Name, Allowed: false, DeniedReason: "invalid token"})
t.sendError(req.ID, InvalidParams, "Unauthorized", "invalid token")
return
}
account = acc
// Check per-tool scopes
if len(tool.Scopes) > 0 {
if !hasScope(account.Scopes, tool.Scopes) {
t.server.audit(AuditRecord{
TraceID: traceID, Timestamp: time.Now(), Tool: params.Name,
AccountID: account.ID, ScopesRequired: tool.Scopes,
Allowed: false, DeniedReason: "insufficient scopes",
})
t.sendError(req.ID, InvalidParams, "Forbidden", "insufficient scopes")
return
}
}
}
// Rate limit check
if err := t.server.allowRate(params.Name); err != nil {
accountID := ""
if account != nil {
accountID = account.ID
}
t.server.audit(AuditRecord{
TraceID: traceID, Timestamp: time.Now(), Tool: params.Name,
AccountID: accountID, Allowed: false, DeniedReason: "rate limited",
})
t.sendError(req.ID, InternalError, "Rate limit exceeded", params.Name)
return
}
// Convert arguments to JSON bytes for RPC call
inputBytes, err := json.Marshal(params.Arguments)
if err != nil {
t.sendError(req.ID, InternalError, "Failed to marshal arguments", err.Error())
return
}
// Build context with tracing metadata
ctx := t.ctx
md := metadata.Metadata{}
md.Set(TraceIDKey, traceID)
md.Set(ToolNameKey, params.Name)
if account != nil {
md.Set(AccountIDKey, account.ID)
}
ctx = metadata.MergeContext(ctx, md, true)
// Make RPC call
start := time.Now()
rpcReq := t.server.opts.Client.NewRequest(tool.Service, tool.Endpoint, &struct {
Data []byte
}{Data: inputBytes})
var rsp struct {
Data []byte
}
if err := t.server.opts.Client.Call(ctx, rpcReq, &rsp); err != nil {
accountID := ""
if account != nil {
accountID = account.ID
}
t.server.audit(AuditRecord{
TraceID: traceID, Timestamp: time.Now(), Tool: params.Name,
AccountID: accountID, ScopesRequired: tool.Scopes,
Allowed: true, Duration: time.Since(start), Error: err.Error(),
})
t.sendError(req.ID, InternalError, "RPC call failed", err.Error())
return
}
// Audit successful call
accountID := ""
if account != nil {
accountID = account.ID
}
t.server.audit(AuditRecord{
TraceID: traceID, Timestamp: time.Now(), Tool: params.Name,
AccountID: accountID, ScopesRequired: tool.Scopes,
Allowed: true, Duration: time.Since(start),
})
// Parse response
var result interface{}
if err := json.Unmarshal(rsp.Data, &result); err != nil {
// If unmarshal fails, return raw data
result = map[string]interface{}{
"data": string(rsp.Data),
}
}
t.sendResponse(req.ID, map[string]interface{}{
"content": []interface{}{
map[string]interface{}{
"type": "text",
"text": fmt.Sprintf("%v", result),
},
},
"trace_id": traceID,
})
}
// sendResponse sends a JSON-RPC response
func (t *StdioTransport) sendResponse(id interface{}, result interface{}) {
resp := JSONRPCResponse{
JSONRPC: "2.0",
ID: id,
Result: result,
}
t.writeJSON(resp)
}
// sendError sends a JSON-RPC error response
func (t *StdioTransport) sendError(id interface{}, code int, message string, data interface{}) {
resp := JSONRPCResponse{
JSONRPC: "2.0",
ID: id,
Error: &RPCError{
Code: code,
Message: message,
Data: data,
},
}
t.writeJSON(resp)
}
// writeJSON writes a JSON-RPC message to stdout
func (t *StdioTransport) writeJSON(v interface{}) {
t.writerMu.Lock()
defer t.writerMu.Unlock()
data, err := json.Marshal(v)
if err != nil {
log.Printf("[mcp] Failed to marshal response: %v", err)
return
}
if _, err := t.writer.Write(data); err != nil {
log.Printf("[mcp] Failed to write response: %v", err)
return
}
if _, err := t.writer.Write([]byte("\n")); err != nil {
log.Printf("[mcp] Failed to write newline: %v", err)
return
}
if err := t.writer.Flush(); err != nil {
log.Printf("[mcp] Failed to flush writer: %v", err)
}
}
// Stop gracefully stops the stdio transport
func (t *StdioTransport) Stop() error {
t.cancel()
return nil
}
+33
View File
@@ -0,0 +1,33 @@
package genai
import (
"context"
"sync"
)
var (
DefaultGenAI GenAI = &noopGenAI{}
defaultOnce sync.Once
)
// SetDefault sets the default GenAI provider (can only be called once).
func SetDefault(g GenAI) {
defaultOnce.Do(func() {
DefaultGenAI = g
})
}
// noopGenAI is a no-op implementation that returns errors.
type noopGenAI struct{}
func (n *noopGenAI) Generate(ctx context.Context, prompt string, opts ...Option) (*Result, error) {
return nil, ErrNoProvider
}
func (n *noopGenAI) Stream(ctx context.Context, prompt string, opts ...Option) (*Stream, error) {
return nil, ErrNoProvider
}
func (n *noopGenAI) String() string {
return "noop"
}
+299
View File
@@ -0,0 +1,299 @@
package gemini
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
"go-micro.dev/v5/genai"
)
const (
defaultModel = "gemini-2.0-flash"
defaultEndpoint = "https://generativelanguage.googleapis.com/v1beta/models/"
defaultTimeout = 120 // seconds
)
// gemini implements the GenAI interface using Google Gemini API.
type gemini struct {
options genai.Options
client *http.Client
}
// New creates a new Gemini provider.
func New(opts ...genai.Option) genai.GenAI {
var options genai.Options
for _, o := range opts {
o(&options)
}
if options.APIKey == "" {
options.APIKey = os.Getenv("GEMINI_API_KEY")
}
if options.Timeout == 0 {
options.Timeout = defaultTimeout
}
return &gemini{
options: options,
client: &http.Client{
Timeout: time.Duration(options.Timeout) * time.Second,
},
}
}
func (g *gemini) Generate(ctx context.Context, prompt string, opts ...genai.Option) (*genai.Result, error) {
options := g.options
for _, o := range opts {
o(&options)
}
res := &genai.Result{Prompt: prompt, Type: options.Type}
endpoint := options.Endpoint
if endpoint == "" {
endpoint = defaultEndpoint
}
model := options.Model
if model == "" {
model = defaultModel
}
url := endpoint + model + ":generateContent?key=" + options.APIKey
body := map[string]interface{}{
"contents": []map[string]interface{}{
{"parts": []map[string]string{{"text": prompt}}},
},
}
// Add generation config if specified
genConfig := make(map[string]interface{})
if options.MaxTokens > 0 {
genConfig["maxOutputTokens"] = options.MaxTokens
}
if options.Temperature > 0 {
genConfig["temperature"] = options.Temperature
}
if len(genConfig) > 0 {
body["generationConfig"] = genConfig
}
if options.Type == "audio" {
body["response_mime_type"] = "audio/wav"
}
b, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(b))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := g.client.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
// Check for API errors
if resp.StatusCode >= 400 {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body))
}
if options.Type == "audio" {
var result struct {
Candidates []struct {
Content struct {
Parts []struct {
InlineData struct {
Data []byte `json:"data"`
} `json:"inline_data"`
} `json:"parts"`
} `json:"content"`
} `json:"candidates"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
if len(result.Candidates) == 0 || len(result.Candidates[0].Content.Parts) == 0 {
return nil, fmt.Errorf("no audio returned")
}
res.Data = result.Candidates[0].Content.Parts[0].InlineData.Data
return res, nil
}
var result struct {
Candidates []struct {
Content struct {
Parts []struct {
Text string `json:"text"`
} `json:"parts"`
} `json:"content"`
} `json:"candidates"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
if len(result.Candidates) == 0 || len(result.Candidates[0].Content.Parts) == 0 {
return nil, fmt.Errorf("no candidates returned")
}
res.Text = result.Candidates[0].Content.Parts[0].Text
return res, nil
}
// Stream performs a streaming request.
func (g *gemini) Stream(ctx context.Context, prompt string, opts ...genai.Option) (*genai.Stream, error) {
options := g.options
for _, o := range opts {
o(&options)
}
endpoint := options.Endpoint
if endpoint == "" {
endpoint = defaultEndpoint
}
model := options.Model
if model == "" {
model = defaultModel
}
// Use streaming endpoint
url := endpoint + model + ":streamGenerateContent?key=" + options.APIKey + "&alt=sse"
body := map[string]interface{}{
"contents": []map[string]interface{}{
{"parts": []map[string]string{{"text": prompt}}},
},
}
// Add generation config if specified
genConfig := make(map[string]interface{})
if options.MaxTokens > 0 {
genConfig["maxOutputTokens"] = options.MaxTokens
}
if options.Temperature > 0 {
genConfig["temperature"] = options.Temperature
}
if len(genConfig) > 0 {
body["generationConfig"] = genConfig
}
b, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
// Create cancellable context for the stream
streamCtx, cancel := context.WithCancel(ctx)
req, err := http.NewRequestWithContext(streamCtx, "POST", url, bytes.NewReader(b))
if err != nil {
cancel()
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "text/event-stream")
resp, err := g.client.Do(req)
if err != nil {
cancel()
return nil, fmt.Errorf("request failed: %w", err)
}
// Check for API errors
if resp.StatusCode >= 400 {
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
cancel()
return nil, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body))
}
results := make(chan *genai.Result, 16)
go func() {
defer close(results)
defer resp.Body.Close()
defer cancel()
reader := bufio.NewReader(resp.Body)
for {
select {
case <-streamCtx.Done():
return
default:
}
line, err := reader.ReadString('\n')
if err != nil {
if err != io.EOF {
results <- &genai.Result{Error: err}
}
return
}
line = strings.TrimSpace(line)
if line == "" {
continue
}
if !strings.HasPrefix(line, "data: ") {
continue
}
data := strings.TrimPrefix(line, "data: ")
var chunk struct {
Candidates []struct {
Content struct {
Parts []struct {
Text string `json:"text"`
} `json:"parts"`
} `json:"content"`
} `json:"candidates"`
}
if err := json.Unmarshal([]byte(data), &chunk); err != nil {
continue // Skip malformed chunks
}
if len(chunk.Candidates) > 0 && len(chunk.Candidates[0].Content.Parts) > 0 {
text := chunk.Candidates[0].Content.Parts[0].Text
if text != "" {
select {
case results <- &genai.Result{
Prompt: prompt,
Type: "text",
Text: text,
}:
case <-streamCtx.Done():
return
}
}
}
}
}()
return genai.NewStream(results, cancel), nil
}
func (g *gemini) String() string {
return "gemini"
}
func init() {
genai.Register("gemini", New())
}
+133
View File
@@ -0,0 +1,133 @@
// Package genai provides a generic interface for generative AI providers.
package genai
import (
"context"
"errors"
"sync"
)
var (
// ErrNoProvider is returned when no GenAI provider is configured.
ErrNoProvider = errors.New("no genai provider configured")
)
// Result is the unified response from GenAI providers.
type Result struct {
Prompt string
Type string
Data []byte // for audio/image binary data
Text string // for text or image URL
Error error // error if this chunk failed
}
// Stream represents a streaming response from a GenAI provider.
type Stream struct {
Results <-chan *Result
cancel context.CancelFunc
}
// Close cancels the stream and releases resources.
func (s *Stream) Close() {
if s.cancel != nil {
s.cancel()
}
}
// NewStream creates a new stream with the given channel and cancel function.
func NewStream(results <-chan *Result, cancel context.CancelFunc) *Stream {
return &Stream{
Results: results,
cancel: cancel,
}
}
// GenAI is the generic interface for generative AI providers.
type GenAI interface {
// Generate performs a single request and returns the result.
Generate(ctx context.Context, prompt string, opts ...Option) (*Result, error)
// Stream performs a streaming request and returns results as they arrive.
Stream(ctx context.Context, prompt string, opts ...Option) (*Stream, error)
// String returns the provider name.
String() string
}
// Option is a functional option for configuring providers.
type Option func(*Options)
// Options holds configuration for providers.
type Options struct {
APIKey string
Endpoint string
Type string // "text", "image", "audio", etc.
Model string // model name, e.g. "gemini-2.5-pro"
MaxTokens int // maximum tokens to generate
Temperature float64 // sampling temperature (0.0-2.0)
Timeout int // request timeout in seconds
}
// WithAPIKey sets the API key.
func WithAPIKey(key string) Option {
return func(o *Options) { o.APIKey = key }
}
// WithEndpoint sets a custom endpoint URL.
func WithEndpoint(endpoint string) Option {
return func(o *Options) { o.Endpoint = endpoint }
}
// WithModel sets the model name.
func WithModel(model string) Option {
return func(o *Options) { o.Model = model }
}
// WithMaxTokens sets the maximum tokens to generate.
func WithMaxTokens(tokens int) Option {
return func(o *Options) { o.MaxTokens = tokens }
}
// WithTemperature sets the sampling temperature.
func WithTemperature(temp float64) Option {
return func(o *Options) { o.Temperature = temp }
}
// WithTimeout sets the request timeout in seconds.
func WithTimeout(seconds int) Option {
return func(o *Options) { o.Timeout = seconds }
}
// Type option functions
func Text(o *Options) { o.Type = "text" }
func Image(o *Options) { o.Type = "image" }
func Audio(o *Options) { o.Type = "audio" }
// Provider registry with thread-safe access
var (
providers = make(map[string]GenAI)
providersMu sync.RWMutex
)
// Register a GenAI provider by name.
func Register(name string, provider GenAI) {
providersMu.Lock()
defer providersMu.Unlock()
providers[name] = provider
}
// Get a GenAI provider by name.
func Get(name string) GenAI {
providersMu.RLock()
defer providersMu.RUnlock()
return providers[name]
}
// List returns all registered provider names.
func List() []string {
providersMu.RLock()
defer providersMu.RUnlock()
names := make([]string, 0, len(providers))
for name := range providers {
names = append(names, name)
}
return names
}
+326
View File
@@ -0,0 +1,326 @@
package openai
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
"go-micro.dev/v5/genai"
)
const (
defaultTextModel = "gpt-4o-mini"
defaultImageModel = "dall-e-3"
defaultAudioModel = "tts-1"
defaultTimeout = 120 // seconds
)
type openAI struct {
options genai.Options
client *http.Client
}
// New creates a new OpenAI provider.
func New(opts ...genai.Option) genai.GenAI {
var options genai.Options
for _, o := range opts {
o(&options)
}
if options.APIKey == "" {
options.APIKey = os.Getenv("OPENAI_API_KEY")
}
if options.Timeout == 0 {
options.Timeout = defaultTimeout
}
return &openAI{
options: options,
client: &http.Client{
Timeout: time.Duration(options.Timeout) * time.Second,
},
}
}
func (o *openAI) Generate(ctx context.Context, prompt string, opts ...genai.Option) (*genai.Result, error) {
options := o.options
for _, opt := range opts {
opt(&options)
}
res := &genai.Result{Prompt: prompt, Type: options.Type}
var url string
var body map[string]interface{}
switch options.Type {
case "image":
model := options.Model
if model == "" {
model = defaultImageModel
}
url = "https://api.openai.com/v1/images/generations"
body = map[string]interface{}{
"prompt": prompt,
"n": 1,
"size": "1024x1024",
"model": model,
}
case "audio":
model := options.Model
if model == "" {
model = defaultAudioModel
}
url = "https://api.openai.com/v1/audio/speech"
body = map[string]interface{}{
"model": model,
"input": prompt,
"voice": "alloy",
}
case "text":
fallthrough
default:
model := options.Model
if model == "" {
model = defaultTextModel
}
url = "https://api.openai.com/v1/chat/completions"
body = map[string]interface{}{
"model": model,
"messages": []map[string]string{{"role": "user", "content": prompt}},
}
if options.MaxTokens > 0 {
body["max_tokens"] = options.MaxTokens
}
if options.Temperature > 0 {
body["temperature"] = options.Temperature
}
}
// Use custom endpoint if provided
if options.Endpoint != "" {
url = options.Endpoint
}
b, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(b))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+options.APIKey)
req.Header.Set("Content-Type", "application/json")
resp, err := o.client.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
// Check for API errors
if resp.StatusCode >= 400 {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body))
}
switch options.Type {
case "image":
var result struct {
Data []struct {
URL string `json:"url"`
} `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
if len(result.Data) == 0 {
return nil, fmt.Errorf("no image returned")
}
res.Text = result.Data[0].URL
return res, nil
case "audio":
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read audio data: %w", err)
}
res.Data = data
return res, nil
default: // text
var result struct {
Choices []struct {
Message struct {
Content string `json:"content"`
} `json:"message"`
} `json:"choices"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
if len(result.Choices) == 0 {
return nil, fmt.Errorf("no choices returned")
}
res.Text = result.Choices[0].Message.Content
return res, nil
}
}
// Stream performs a streaming request for text generation.
func (o *openAI) Stream(ctx context.Context, prompt string, opts ...genai.Option) (*genai.Stream, error) {
options := o.options
for _, opt := range opts {
opt(&options)
}
// Only text supports streaming
if options.Type != "" && options.Type != "text" {
// For non-text types, fall back to non-streaming
results := make(chan *genai.Result, 1)
go func() {
defer close(results)
res, err := o.Generate(ctx, prompt, opts...)
if err != nil {
results <- &genai.Result{Error: err}
return
}
results <- res
}()
return genai.NewStream(results, nil), nil
}
model := options.Model
if model == "" {
model = defaultTextModel
}
body := map[string]interface{}{
"model": model,
"messages": []map[string]string{{"role": "user", "content": prompt}},
"stream": true,
}
if options.MaxTokens > 0 {
body["max_tokens"] = options.MaxTokens
}
if options.Temperature > 0 {
body["temperature"] = options.Temperature
}
url := "https://api.openai.com/v1/chat/completions"
if options.Endpoint != "" {
url = options.Endpoint
}
b, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
// Create cancellable context for the stream
streamCtx, cancel := context.WithCancel(ctx)
req, err := http.NewRequestWithContext(streamCtx, "POST", url, bytes.NewReader(b))
if err != nil {
cancel()
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+options.APIKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "text/event-stream")
resp, err := o.client.Do(req)
if err != nil {
cancel()
return nil, fmt.Errorf("request failed: %w", err)
}
// Check for API errors
if resp.StatusCode >= 400 {
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
cancel()
return nil, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body))
}
results := make(chan *genai.Result, 16)
go func() {
defer close(results)
defer resp.Body.Close()
defer cancel()
reader := bufio.NewReader(resp.Body)
for {
select {
case <-streamCtx.Done():
return
default:
}
line, err := reader.ReadString('\n')
if err != nil {
if err != io.EOF {
results <- &genai.Result{Error: err}
}
return
}
line = strings.TrimSpace(line)
if line == "" {
continue
}
if !strings.HasPrefix(line, "data: ") {
continue
}
data := strings.TrimPrefix(line, "data: ")
if data == "[DONE]" {
return
}
var chunk struct {
Choices []struct {
Delta struct {
Content string `json:"content"`
} `json:"delta"`
} `json:"choices"`
}
if err := json.Unmarshal([]byte(data), &chunk); err != nil {
continue // Skip malformed chunks
}
if len(chunk.Choices) > 0 && chunk.Choices[0].Delta.Content != "" {
select {
case results <- &genai.Result{
Prompt: prompt,
Type: "text",
Text: chunk.Choices[0].Delta.Content,
}:
case <-streamCtx.Done():
return
}
}
}
}()
return genai.NewStream(results, cancel), nil
}
func (o *openAI) String() string {
return "openai"
}
func init() {
genai.Register("openai", New())
}

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