Compare commits

..

1 Commits

Author SHA1 Message Date
Shelley c09695d4e2 feat: add micro build and micro deploy commands
Run Tests / Unit Tests (push) Waiting to run
Run Tests / Etcd Integration Tests (push) Waiting to run
micro build:
  - Generates Dockerfiles for services (if not present)
  - Builds container images for all services in micro.mu
  - Supports --tag, --registry, --push flags
  - --compose flag generates docker-compose.yml

micro deploy:
  - Default: deploys with docker-compose
  - --ssh user@host: deploys via SSH (rsync + build on remote)
  - --build: rebuild images before deploying

Complete workflow:
  micro run          # Develop locally
  micro build        # Build images
  micro deploy       # Deploy

Or for simple SSH deploys:
  micro deploy --ssh user@host

Co-authored-by: Shelley <shelley@exe.dev>
2026-01-27 12:39:43 +00:00
414 changed files with 3165 additions and 48053 deletions
-31
View File
@@ -1,31 +0,0 @@
# EditorConfig for go-micro
# https://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.go]
indent_style = tab
indent_size = 4
[*.{yml,yaml}]
indent_style = space
indent_size = 2
[*.{json,proto}]
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false
indent_style = space
indent_size = 2
[Makefile]
indent_style = tab
+7 -14
View File
@@ -26,26 +26,19 @@ A clear and concise description of what you expected to happen.
## Environment
- Go Micro version: [e.g. v5.3.0]
- Go version: [run `go version`]
- OS/Platform: [e.g. Ubuntu 22.04, macOS 14, Docker]
- Plugins/Integrations: [e.g. consul registry, nats broker, redis cache]
- Go version: [e.g. 1.21.0]
- OS: [e.g. Ubuntu 22.04]
- Plugins used: [e.g. consul registry, nats broker]
## Logs
```
Paste relevant logs here (use -v flag for verbose output)
Paste relevant logs here
```
## Checklist
- [ ] I've searched existing issues and this is not a duplicate
- [ ] I've provided a minimal code sample that reproduces the issue
- [ ] I've included my environment details
- [ ] I've checked the documentation
## Additional context
Add any other context about the problem here.
## Helpful Resources
- [Troubleshooting Guide](https://github.com/micro/go-micro/tree/master/internal/website/docs/getting-started.md)
- [Examples](https://github.com/micro/go-micro/tree/master/examples)
## Resources
- [Documentation](https://github.com/micro/go-micro/tree/master/internal/website/docs)
- [Examples](https://github.com/micro/go-micro/tree/master/internal/website/docs/examples)
- [API Reference](https://pkg.go.dev/go-micro.dev/v5)
- [Discord Community](https://discord.gg/jwTYuUVAGh)
+5 -17
View File
@@ -18,25 +18,13 @@ A clear and concise description of any alternative solutions or features you've
## Use case
Describe how this feature would be used in practice. What problem does it solve?
**Example:**
```go
// Show how the feature would be used
```
## Implementation ideas (optional)
If you have thoughts on how this could be implemented, share them here.
## Additional context
Add any other context, code examples, or screenshots about the feature request here.
## Checklist
- [ ] I've searched existing issues and this is not a duplicate
- [ ] I've checked the roadmap and this isn't already planned
- [ ] I've provided a clear use case
- [ ] I'd be willing to submit a PR for this feature (optional)
## Willing to contribute?
- [ ] I'd be willing to submit a PR for this feature
## Helpful Resources
## Resources
- [Documentation](https://github.com/micro/go-micro/tree/master/internal/website/docs)
- [Plugins](https://github.com/micro/go-micro/tree/master/internal/website/docs/plugins.md)
- [Roadmap](https://github.com/micro/go-micro/blob/master/ROADMAP.md)
- [Contributing Guide](https://github.com/micro/go-micro/blob/master/CONTRIBUTING.md)
- [Architecture Docs](https://github.com/micro/go-micro/tree/master/internal/website/docs/architecture.md)
- [Discord Community](https://discord.gg/jwTYuUVAGh)
-61
View File
@@ -1,61 +0,0 @@
---
name: Performance issue
about: Report a performance problem or regression
title: '[PERFORMANCE] '
labels: performance
assignees: ''
---
## Performance Issue
**Symptom:**
Describe the performance problem (e.g., high latency, memory leak, CPU usage)
**Expected Performance:**
What performance did you expect?
## Benchmarks
Please provide benchmarks or profiling data:
```bash
# CPU profiling
go test -cpuprofile=cpu.prof -bench=.
# Memory profiling
go test -memprofile=mem.prof -bench=.
# Results
```
**Before/After comparison (if applicable):**
- Before: X req/sec, Y ms latency
- After: X req/sec, Y ms latency
## Code Sample
```go
// Minimal code that demonstrates the performance issue
```
## Environment
- Go Micro version: [e.g. v5.3.0]
- Go version: [run `go version`]
- Hardware: [e.g. 4 CPU, 8GB RAM]
- OS: [e.g. Ubuntu 22.04]
- Load: [e.g. 1000 req/sec, 100 concurrent connections]
## Profiling Data
Attach pprof profiles if available:
- CPU profile
- Memory profile
- Goroutine dump
## Additional Context
Add any other context about the performance issue.
## Resources
- [Performance Guide](https://github.com/micro/go-micro/tree/master/internal/website/docs/performance.md)
- [Benchmarking](https://pkg.go.dev/testing#hdr-Benchmarks)
-51
View File
@@ -1,51 +0,0 @@
name: goreleaser
on:
push:
tags:
- 'v*.*.*'
permissions:
contents: write
id-token: write
packages: write
attestations: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
-
name: Set up Go
uses: actions/setup-go@v5
with:
go-version: stable
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
-
name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
-
name: Run GoReleaser
uses: goreleaser/goreleaser-action@v7
with:
distribution: goreleaser
version: '~> v2'
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+1 -19
View File
@@ -1,11 +1,8 @@
# Develop tools
/.vscode/
/.idea/
/.trunk
# VS Code workspace files (keep settings for consistency)
/.vscode/*
!/.vscode/settings.json
# Binaries for programs and plugins
*.exe
*.exe~
@@ -33,7 +30,6 @@ _cgo_export.*
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
coverage.html
# vim temp files
*~
@@ -43,17 +39,3 @@ coverage.html
# go work files
go.work
go.work.sum
# Build artifacts
dist/
bin/
# Example binaries (go build in examples/)
examples/**/server/server
examples/**/client/client
examples/mcp/documented/documented
examples/mcp/hello/hello
# IDE-specific files
.DS_Store
/micro
-136
View File
@@ -1,136 +0,0 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
version: 2
before:
hooks:
- go mod tidy
builds:
- main: ./cmd/micro
id: micro
binary: micro
env:
- CGO_ENABLED=0
- >-
{{- if eq .Os "darwin" }}
{{- if eq .Arch "amd64"}}CC=o64-clang{{- end }}
{{- if eq .Arch "arm64"}}CC=aarch64-apple-darwin20.2-clang{{- end }}
{{- end }}
{{- if eq .Os "windows" }}
{{- if eq .Arch "amd64" }}CC=x86_64-w64-mingw32-gcc{{- end }}
{{- end }}
goos:
- linux
- windows
- darwin
goarch:
- amd64
- arm
- arm64
goarm:
- 7
ignore:
- goos: windows
goarch: arm
- main: ./cmd/protoc-gen-micro
id: protoc-gen-micro
binary: protoc-gen-micro
env:
- CGO_ENABLED=0
- >-
{{- if eq .Os "darwin" }}
{{- if eq .Arch "amd64"}}CC=o64-clang{{- end }}
{{- if eq .Arch "arm64"}}CC=aarch64-apple-darwin20.2-clang{{- end }}
{{- end }}
{{- if eq .Os "windows" }}
{{- if eq .Arch "amd64" }}CC=x86_64-w64-mingw32-gcc{{- end }}
{{- end }}
goos:
- linux
- windows
- darwin
goarch:
- amd64
- arm
- arm64
goarm:
- 7
ignore:
- goos: windows
goarch: arm
archives:
- id: micro
ids:
- micro
formats: [tar.gz]
name_template: >-
{{ .Binary }}_
{{- .Os }}_
{{- .Arch }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
files:
- none*
format_overrides:
- goos: windows
formats: [zip]
- id: protoc-gen-micro
ids:
- protoc-gen-micro
formats: [tar.gz]
name_template: >-
{{ .Binary }}_
{{- .Os }}_
{{- .Arch }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
files:
- none*
format_overrides:
- goos: windows
formats: [zip]
report_sizes: true
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
dockers_v2:
-
ids:
- micro
- protoc-gen-micro
images:
- "micro/micro"
- "ghcr.io/micro/go-micro"
tags:
- "v{{ .Version }}"
- "{{ if .IsNightly }}nightly{{ end }}"
- "{{ if not .IsNightly }}latest{{ end }}"
labels:
"io.artifacthub.package.readme-url": "https://raw.githubusercontent.com/micro/go-micro/refs/heads/master/README.md"
"io.artifacthub.package.logo-url": "https://www.gravatar.com/avatar/09d1da3ea9ee61753219a19016d6a672?s=120&r=g&d=404"
"org.opencontainers.image.description": "A Go Platform built for Developers"
"org.opencontainers.image.created": "{{.Date}}"
"org.opencontainers.image.title": "{{.ProjectName}}"
"org.opencontainers.image.revision": "{{.FullCommit}}"
"org.opencontainers.image.version": "{{.Version}}"
"org.opencontainers.image.source": "{{.GitURL}}"
"org.opencontainers.image.url": "{{.GitURL}}"
"org.opencontainers.image.licenses": "MIT"
platforms:
- linux/amd64
- linux/arm64
retry:
attempts: 5
delay: 5s
max_delay: 2m
+29
View File
@@ -0,0 +1,29 @@
labelType: long
coverThreshold: 70
buildStyle:
bold: true
foreground: yellow
startStyle:
foreground: lightBlack
passStyle:
foreground: green
failStyle:
bold: true
foreground: "#821515"
skipStyle:
foreground: lightBlack
passPackageStyle:
foreground: green
hide: false
failPackageStyle:
bold: true
foreground: "#821515"
coveredStyle:
foreground: green
uncoveredStyle:
bold: true
foreground: yellow
fileStyle:
foreground: cyan
lineStyle:
foreground: magenta
-137
View File
@@ -1,137 +0,0 @@
{
"folders": [
{
"path": "."
}
],
"settings": {
"go.toolsManagement.autoUpdate": true,
"go.useLanguageServer": true,
"go.lintOnSave": "workspace",
"go.lintTool": "golangci-lint",
"go.lintFlags": [
"--fast"
],
"go.formatTool": "goimports",
"go.formatFlags": [],
"go.buildOnSave": "workspace",
"go.testOnSave": false,
"go.coverOnSave": false,
"go.testFlags": ["-v", "-race"],
"go.testTimeout": "60s",
"go.gopath": "",
"go.goroot": "",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
},
"files.exclude": {
"**/.git": true,
"**/.DS_Store": true,
"**/node_modules": true,
"**/*.test": true,
"**/coverage.out": true,
"**/coverage.html": true
},
"files.watcherExclude": {
"**/.git/objects/**": true,
"**/.git/subtree-cache/**": true,
"**/node_modules/**": true,
"**/.vscode/**": true
},
"search.exclude": {
"**/node_modules": true,
"**/bower_components": true,
"**/*.code-search": true,
"**/vendor": true,
"**/.git": true
},
"[go]": {
"editor.tabSize": 4,
"editor.insertSpaces": false,
"editor.formatOnSave": true,
"editor.defaultFormatter": "golang.go"
},
"[go.mod]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "golang.go"
},
"[markdown]": {
"editor.formatOnSave": false,
"editor.wordWrap": "on"
},
"gopls": {
"ui.semanticTokens": true,
"ui.completion.usePlaceholders": true,
"formatting.gofumpt": false,
"analyses": {
"unusedparams": true,
"shadow": true,
"fieldalignment": false
}
}
},
"extensions": {
"recommendations": [
"golang.go",
"editorconfig.editorconfig",
"redhat.vscode-yaml",
"ms-vscode.makefile-tools"
]
},
"tasks": {
"version": "2.0.0",
"tasks": [
{
"label": "Run Tests",
"type": "shell",
"command": "make test",
"group": {
"kind": "test",
"isDefault": true
},
"presentation": {
"reveal": "always",
"panel": "new"
}
},
{
"label": "Run Tests with Coverage",
"type": "shell",
"command": "make test-coverage",
"group": "test"
},
{
"label": "Run Linter",
"type": "shell",
"command": "make lint",
"group": "build"
},
{
"label": "Format Code",
"type": "shell",
"command": "make fmt",
"group": "build"
}
]
},
"launch": {
"version": "0.2.0",
"configurations": [
{
"name": "Debug Current File",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${file}"
},
{
"name": "Debug Test",
"type": "go",
"request": "launch",
"mode": "test",
"program": "${workspaceFolder}"
}
]
}
}
-105
View File
@@ -1,105 +0,0 @@
# Changelog
All notable changes to Go Micro are documented here.
Format follows [Keep a Changelog](https://keepachangelog.com/). Go Micro uses
calendar-based versions (YYYY.MM) for the AI-native era.
---
## [Unreleased]
### Added
- **Agent platform showcase** — full platform example (Users, Posts, Comments, Mail) mirroring [micro/blog](https://github.com/micro/blog), demonstrating how existing microservices become agent-accessible with zero code changes (`examples/mcp/platform/`).
- **Blog post: "Your Microservices Are Already an AI Platform"** — walkthrough of agent-service interaction patterns using real-world services (`internal/website/blog/7.md`).
- **Circuit breakers for MCP gateway** — per-tool circuit breakers protect downstream services from cascading failures. Configurable max failures, open-state timeout, and half-open probing. Available via `Options.CircuitBreaker` and `--circuit-breaker` CLI flag (`gateway/mcp/circuitbreaker.go`).
- **Helm chart for MCP gateway** — official Helm chart at `deploy/helm/mcp-gateway/` with Deployment, Service, ServiceAccount, HPA, and Ingress templates. Supports Consul/etcd/mDNS registries, JWT auth, rate limiting, audit logging, per-tool scopes, TLS ingress, and auto-scaling.
- **MCP gateway benchmarks** — comprehensive benchmark suite for tool listing, lookup, auth, rate limiting, and JSON serialization (`gateway/mcp/benchmark_test.go`)
- **Workflow example** — cross-service orchestration demo with Inventory, Orders, and Notifications services showing agents chaining multi-step workflows from natural language (`examples/mcp/workflow/`)
- **Docker Compose deployment** — production-like setup with Consul registry, standalone MCP gateway, and Jaeger tracing in one `docker-compose up` (`examples/deployment/`)
---
## [2026.03] - March 2026
### Added
#### Developer Experience
- **`micro new` MCP templates** — `micro new myservice` generates MCP-enabled services with doc comments, `@example` tags, and `WithMCP()` wired in. Use `--no-mcp` to opt out.
- **`micro.New("name")` unified API** — single way to create services: `micro.New("greeter")` or `micro.New("greeter", micro.Address(":8080"))`. Replaces `micro.NewService()` + `service.New()` dual API.
- **`service.Handle()` simplified registration** — register handlers with `service.Handle(new(Greeter))` instead of manual `server.NewHandler` + `server.Handle`.
- **`micro.NewGroup()` modular monoliths** — run multiple services in one binary with shared lifecycle: `micro.NewGroup(users, orders).Run()`.
- **`mcp.WithMCP()` one-liner** — add MCP to any service with a single option: `micro.New("name", mcp.WithMCP(":3001"))`.
- **CRUD example** — contact book service with 6 operations, rich agent docs, and validation patterns (`examples/mcp/crud/`).
#### MCP Gateway
- **WebSocket transport** — bidirectional JSON-RPC 2.0 streaming over WebSocket for real-time agent communication (`gateway/mcp/websocket.go`).
- **OpenTelemetry integration** — full span instrumentation across HTTP, stdio, and WebSocket transports with W3C trace context propagation (`gateway/mcp/otel.go`).
- **Standalone gateway binary** — `micro-mcp-gateway` with Docker support for running the MCP gateway independently of services.
- **Per-tool auth scopes** — service-level (`server.WithEndpointScopes()`) and gateway-level (`Options.Scopes`) scope enforcement with bearer token auth.
- **Rate limiting** — per-tool token bucket rate limiting (`Options.RateLimit`).
- **Audit logging** — immutable audit records per tool call with trace ID, account, scopes, duration, and errors (`Options.AuditFunc`).
#### AI Model Package
- **`model.Model` interface** — unified AI provider abstraction with `Generate()` and `Stream()` methods.
- **Anthropic Claude provider** — `model/anthropic` with tool execution and auto-calling.
- **OpenAI GPT provider** — `model/openai` with provider auto-detection from base URL.
#### Agent SDKs
- **LangChain SDK** — `contrib/langchain-go-micro/` Python package with auto-discovery, tool generation, and multi-agent workflow examples.
- **LlamaIndex SDK** — `contrib/go-micro-llamaindex/` Python package with RAG integration examples.
#### Documentation
- **AI-native services guide** — building services for AI agents from scratch
- **MCP security guide** — auth, scopes, and audit logging
- **Tool descriptions guide** — writing doc comments that improve agent performance
- **Agent patterns guide** — architecture patterns for agent integration
- **Error handling guide** — writing agent-friendly error responses with typed errors
- **Troubleshooting guide** — common MCP issues and solutions
- **Migration guide** — add MCP to existing services in 5 minutes
#### CLI
- **`micro mcp serve`** — start MCP server (stdio for Claude Code, HTTP for web agents)
- **`micro mcp list`** — list available tools (human-readable or JSON)
- **`micro mcp test`** — test tools with JSON input
- **`micro mcp docs`** — generate tool documentation
- **`micro mcp export`** — export to LangChain, OpenAPI, or JSON formats
#### Agent Playground
- **Chat-focused UI** — redesigned playground with collapsible tool calls, real-time status, and thinking indicators
- **Provider settings** — configurable OpenAI/Anthropic provider, model, and API key
### Changed
- Service interface moved to `service.Service` with `micro.Service` as a type alias for backward compatibility.
- `service.New()` returns `service.Service` interface (was `*ServiceImpl`).
- `service.NewGroup()` accepts `service.Service` interface (was `*ServiceImpl`).
- `go.mod` template in `micro new` updated to Go 1.22.
### Fixed
- Handler `Handle()` method accepts variadic `server.HandlerOption` for scopes and metadata.
- Store initialization uses service name as table automatically.
- Service `Stop()` properly aggregates errors from lifecycle hooks.
---
## [2026.02] - February 2026
### Added
- **MCP gateway library** — `gateway/mcp/` with HTTP/SSE and stdio transports, service discovery, tool generation, and JSON schema generation from Go types (2,500+ lines).
- **CLI integration** — `micro run --mcp-address` flag to start MCP alongside services.
- **Documentation extraction** — auto-extract tool descriptions from Go doc comments with `@example` tag and struct tag parsing.
- **Blog post** — "Making Microservices AI-Native with MCP"
- **MCP examples** — `examples/mcp/hello/` and `examples/mcp/documented/`
---
## [2026.01] - January 2026
### Added
- **`micro deploy`** — deploy services to any Linux server via SSH + systemd with `micro deploy user@server`.
- **`micro build`** — build Go binaries and Docker images with `micro build --docker`.
- **Blog post** — "Introducing micro deploy"
---
_For earlier changes, see the [git log](https://github.com/micro/go-micro/commits/master)._
-145
View File
@@ -1,145 +0,0 @@
# CLAUDE.md - Go Micro Project Guide
## Project Overview
Go Micro is a framework for distributed systems development in Go. It provides pluggable abstractions for service discovery, RPC, pub/sub, config, auth, storage, and more.
The framework is evolving into an **AI-native platform** where every microservice is automatically accessible to AI agents via the Model Context Protocol (MCP).
## Build & Test
```bash
# Run all tests
make test
# Run tests for a specific package
go test ./gateway/mcp/...
go test ./ai/...
go test ./model/...
# Lint
make lint
# Format
make fmt
# Build CLI
go build -o micro ./cmd/micro
# Run locally with hot reload
micro run
```
## Project Structure
```
go-micro/
├── ai/ # AI model providers (Anthropic, OpenAI)
├── auth/ # Authentication (JWT, no-op)
├── broker/ # Message broker (NATS, RabbitMQ)
├── cache/ # Caching (Redis)
├── client/ # RPC client (gRPC)
├── cmd/micro/ # CLI tool (run, deploy, mcp, build, server)
├── codec/ # Message codecs (JSON, Proto)
├── config/ # Dynamic config (env, file, etcd, NATS)
├── errors/ # Error handling
├── events/ # Event system (NATS JetStream)
├── gateway/
│ ├── api/ # REST API gateway
│ └── mcp/ # MCP gateway (core AI integration)
│ └── deploy/ # Helm charts for MCP gateway
├── health/ # Health checking
├── logger/ # Logging
├── metadata/ # Context metadata
├── model/ # Typed data models (CRUD, queries, schemas)
├── registry/ # Service discovery (mDNS, Consul, etcd)
├── selector/ # Client-side load balancing
├── server/ # RPC server
├── service/ # Service interface + profiles
├── store/ # Data persistence (Postgres, NATS KV)
├── transport/ # Network transport
├── wrapper/ # Middleware (auth, trace, metrics)
├── examples/ # Working examples
└── internal/ # Non-public: docs, utils, test harness
```
## Key Architectural Decisions
- **Plugin architecture**: All abstractions use Go interfaces. Defaults work out of the box, everything is swappable.
- **Progressive complexity**: Zero-config for development, full control for production.
- **AI-native by default**: Every service is automatically an MCP tool. No extra code needed.
- **In-repo plugins**: Plugins live in the main repo to avoid version compatibility issues.
- **Reflection-based registration**: Handlers are registered via reflection for minimal boilerplate.
## Code Conventions
- Standard Go conventions (gofmt, golint)
- Functional options pattern for configuration (`WithX()` functions)
- Interface-first design: define the interface, then implement
- Tests alongside code (not in separate test directories)
- Commit messages: imperative mood, concise summary line
## Current Focus & Priorities (March 2026)
### Status
- **Q1 2026 (MCP Foundation):** COMPLETE
- **Q2 2026 (Agent DX):** COMPLETE (100%)
- **Q3 2026 (Production):** 50% complete (ahead of schedule)
### Priority 1: Agent Showcase & Examples
Build compelling demos showing agents interacting with go-micro services in realistic scenarios.
### Priority 2: Additional Protocol Support
- gRPC reflection-based MCP
- HTTP/3 support
### Priority 3: Kubernetes & Deployment
- Helm Charts for MCP gateway
- Kubernetes Operator with CRDs
### Recently Completed
- **`micro new` MCP Templates** - Scaffolds MCP-enabled services with doc comments, `@example` tags, `WithMCP()`. `--no-mcp` to opt out.
- **CRUD Example** - Contact book service with 6 operations, rich agent docs (`examples/mcp/crud/`)
- **Migration Guide** - "Add MCP to Existing Services" guide with 3 approaches
- **Troubleshooting Guide** - Common MCP issues and solutions
- **Error Handling Guide** - Patterns for agent-friendly error responses
- **Documentation Guides** - Six guides: AI-native services, MCP security, tool descriptions, agent patterns, error handling, troubleshooting
- **WithMCP Option** - One-line MCP setup (`gateway/mcp/option.go`)
- **Agent Playground Redesign** - Chat-focused UI with collapsible tool calls
- **Standalone Gateway Binary** - `micro-mcp-gateway` with Docker support
- **WebSocket Transport** - Bidirectional JSON-RPC 2.0 streaming (`gateway/mcp/websocket.go`)
- **OpenTelemetry Integration** - Full span instrumentation with W3C trace context (`gateway/mcp/otel.go`)
- **LlamaIndex SDK** - Python package with RAG examples (`contrib/go-micro-llamaindex/`)
## Key Files
| Purpose | File |
|---------|------|
| MCP Gateway | `gateway/mcp/mcp.go` |
| MCP Docs | `gateway/mcp/DOCUMENTATION.md` |
| AI Interface | `ai/model.go` |
| Model Layer | `model/model.go` |
| CLI Entry | `cmd/micro/main.go` |
| MCP CLI | `cmd/micro/mcp/` |
| Server (run/server) | `cmd/micro/server/server.go` |
| Roadmap | `internal/docs/ROADMAP_2026.md` |
| Status | `internal/docs/CURRENT_STATUS_SUMMARY.md` |
| Changelog | `CHANGELOG.md` |
| Docs Site | `internal/website/docs/` |
## Roadmap & Status Documents
- **[ROADMAP.md](ROADMAP.md)** - General framework roadmap
- **[internal/docs/ROADMAP_2026.md](internal/docs/ROADMAP_2026.md)** - AI-native era roadmap with business model
- **[internal/docs/CURRENT_STATUS_SUMMARY.md](internal/docs/CURRENT_STATUS_SUMMARY.md)** - Quick status overview
- **[internal/docs/PROJECT_STATUS_2026.md](internal/docs/PROJECT_STATUS_2026.md)** - Detailed technical status
- **[internal/docs/IMPLEMENTATION_SUMMARY.md](internal/docs/IMPLEMENTATION_SUMMARY.md)** - Implementation notes
- **[CHANGELOG.md](CHANGELOG.md)** - What changed and when
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) for full guidelines. Key points:
- Open an issue before large changes
- Include tests for new features
- Run `make test` and `make lint` before submitting
- Follow commit message format: `type: description` (e.g., `feat: add WebSocket transport`)
+5 -17
View File
@@ -19,24 +19,16 @@ Be respectful, inclusive, and collaborative. We're all here to build great softw
# Install dependencies
go mod download
# Install development tools
make install-tools
# Run tests
make test
go test ./...
# Run tests with race detector and coverage
make test-coverage
# Run tests with coverage
go test -race -coverprofile=coverage.out ./...
# Run linter
make lint
# Format code
make fmt
# Run linter (install golangci-lint first)
golangci-lint run
```
See `make help` for all available commands.
## Making Changes
### Code Guidelines
@@ -91,10 +83,6 @@ go test -v ./...
# Run specific test
go test -run TestMyFunction ./pkg/...
# Optional: Use richgo for colored output
go install github.com/kyoh86/richgo@latest
richgo test -v ./...
```
### Documentation
-26
View File
@@ -1,26 +0,0 @@
FROM alpine:latest
ARG TARGETPLATFORM
ENV USER=micro
ENV GROUPNAME=$USER
ARG UID=1001
ARG GID=1001
RUN addgroup --gid "$GID" "$GROUPNAME" \
&& adduser \
--disabled-password \
--gecos "" \
--home "/micro" \
--ingroup "$GROUPNAME" \
--no-create-home \
--uid "$UID" "$USER"
ENV PATH=/usr/local/go/bin:$PATH
RUN apk --no-cache add git make curl
COPY --from=golang:1.26.0-alpine /usr/local/go /usr/local/go
COPY $TARGETPLATFORM/micro /usr/local/go/bin/
COPY $TARGETPLATFORM/protoc-gen-micro /usr/local/go/bin/
WORKDIR /micro
EXPOSE 8080
ENTRYPOINT ["/usr/local/go/bin/micro"]
CMD ["server"]
-82
View File
@@ -1,82 +0,0 @@
NAME = micro
GIT_COMMIT = $(shell git rev-parse --short HEAD)
GIT_TAG = $(shell git describe --abbrev=0 --tags --always --match "v*")
GIT_IMPORT = go-micro.dev/v5/cmd/micro
BUILD_DATE = $(shell date +%s)
LDFLAGS = -X $(GIT_IMPORT).BuildDate=$(BUILD_DATE) -X $(GIT_IMPORT).GitCommit=$(GIT_COMMIT) -X $(GIT_IMPORT).GitTag=$(GIT_TAG)
# GORELEASER_DOCKER_IMAGE = ghcr.io/goreleaser/goreleaser-cross:v1.25.7
GORELEASER_DOCKER_IMAGE = ghcr.io/goreleaser/goreleaser:latest
.PHONY: test test-race test-coverage lint fmt install-tools proto clean help gorelease-dry-run gorelease-dry-run-docker
# Default target
help:
@echo "Go Micro Development Tasks"
@echo ""
@echo " make test - Run tests"
@echo " make test-race - Run tests with race detector"
@echo " make test-coverage - Run tests with coverage"
@echo " make lint - Run linter"
@echo " make fmt - Format code"
@echo " make install-tools - Install development tools"
@echo " make proto - Generate protobuf code"
@echo " make clean - Clean build artifacts"
$(NAME):
CGO_ENABLED=0 go build -ldflags "-s -w ${LDFLAGS}" -o $(NAME) cmd/micro/main.go
# Run tests
test:
go test -v ./...
# Run tests with race detector
test-race:
go test -v -race ./...
# Run tests with coverage
test-coverage:
go test -v -race -coverprofile=coverage.out -covermode=atomic ./...
go tool cover -html=coverage.out -o coverage.html
@echo "Coverage report: coverage.html"
# Run linter
lint:
golangci-lint run
# Format code
fmt:
gofmt -s -w .
goimports -w .
# Install development tools
install-tools:
@echo "Installing development tools..."
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
go install golang.org/x/tools/cmd/goimports@latest
go install github.com/kyoh86/richgo@latest
go install go-micro.dev/v5/cmd/protoc-gen-micro@latest
@echo "Tools installed successfully"
# Generate protobuf code
proto:
@echo "Generating protobuf code..."
find . -name "*.proto" -not -path "./vendor/*" -exec protoc --proto_path=. --micro_out=. --go_out=. {} \;
# Clean build artifacts
clean:
rm -f coverage.out coverage.html
find . -name "*.test" -type f -delete
go clean -cache -testcache
# Try binary release
gorelease-dry-run:
docker run \
--rm \
-e CGO_ENABLED=0 \
-v $(CURDIR):/$(NAME) \
-v /var/run/docker.sock:/var/run/docker.sock \
-w /$(NAME) \
$(GORELEASER_DOCKER_IMAGE) \
--clean --verbose --skip=publish,validate --snapshot
+22 -312
View File
@@ -1,18 +1,11 @@
# Go Micro [![Go.Dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/go-micro.dev/v5?tab=doc) [![Go Report Card](https://goreportcard.com/badge/github.com/go-micro/go-micro)](https://goreportcard.com/report/github.com/go-micro/go-micro)
# Go Micro [![Go.Dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/go-micro.dev/v5?tab=doc) [![Go Report Card](https://goreportcard.com/badge/github.com/go-micro/go-micro)](https://goreportcard.com/report/github.com/go-micro/go-micro)
Go Micro is a framework for distributed systems development.
## Sponsors
<a href="https://go-micro.dev/blog/3"><img src="https://upload.wikimedia.org/wikipedia/commons/7/78/Anthropic_logo.svg" height="26" /></a>
<br>
<a href="https://go-micro.dev/blog/8"><img src="https://www.atlascloud.ai/logo.svg" height="26" /></a>
**[📖 Documentation](https://go-micro.dev/docs/)** | [Sponsor the project](https://github.com/sponsors/micro)
## Overview
<img src="internal/website/images/generated/hero.png" alt="Go Micro microservices architecture" width="100%" />
Go Micro provides the core requirements for distributed systems development including RPC and Event driven communication.
The Go Micro philosophy is sane defaults with a pluggable architecture. We provide defaults to get you started quickly
but everything can be easily swapped out.
@@ -31,10 +24,6 @@ Go Micro abstracts away the details of distributed systems. Here are the main fe
- **Data Storage** - A simple data store interface to read, write and delete records. It includes support for many storage backends
in the plugins repo. State and persistence becomes a core requirement beyond prototyping and Micro looks to build that into the framework.
- **Data Model** - A typed data model layer with CRUD operations, queries, and multiple backends (memory, SQLite, Postgres). Define Go
structs with tags and get type-safe Create/Read/Update/Delete/List/Count operations. Accessible via `service.Model()` alongside
`service.Client()` and `service.Server()` for a complete service experience: call services, handle requests, save and query data.
- **Service Discovery** - Automatic service registration and name resolution. Service discovery is at the core of micro service
development. When service A needs to speak to service B it needs the location of that service. The default discovery mechanism is
multicast DNS (mdns), a zeroconf system.
@@ -53,13 +42,6 @@ in the plugins repo. State and persistence becomes a core requirement beyond pro
- **Async Messaging** - PubSub is built in as a first class citizen for asynchronous communication and event driven architectures.
Event notifications are a core pattern in micro service development. The default messaging system is a HTTP event message broker.
- **MCP Integration** - An MCP gateway you can integrate as a library, server or CLI command which automatically exposes services
as tools for agents or other AI applications. Every service/endpoint get's converted into a callable tool.
- **Multi-Service Binaries** - Run multiple services in a single process with isolated state per service. Start as a modular monolith,
split into separate deployments when you need independent scaling. Each service gets its own server, client, and store while sharing
the registry and broker for inter-service communication.
- **Pluggable Interfaces** - Go Micro makes use of Go interfaces for each distributed system abstraction. Because of this these interfaces
are pluggable and allows Go Micro to be runtime agnostic. You can plugin any underlying technology.
@@ -68,7 +50,7 @@ in the plugins repo. State and persistence becomes a core requirement beyond pro
To make use of Go Micro
```bash
go get go-micro.dev/v5@v5.16.0
go get go-micro.dev/v5@latest
```
Create a service and register a handler
@@ -110,7 +92,10 @@ func main() {
Set a fixed address
```go
service := micro.New("helloworld", micro.Address(":8080"))
service := micro.NewService(
micro.Name("helloworld"),
micro.Address(":8080"),
)
```
Call it via curl
@@ -123,181 +108,18 @@ curl -XPOST \
http://localhost:8080
```
## MCP & AI Agents
## Experimental
<img src="internal/website/images/generated/mcp-agent.png" alt="AI agent calling microservices via MCP" width="100%" />
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 `/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/mcp/tools
```
Use `micro mcp serve` for local AI tools like Claude Code, or connect any MCP-compatible agent to the HTTP endpoint.
### micro chat
For an interactive terminal session that lets you talk to your services through an LLM:
```bash
ANTHROPIC_API_KEY=sk-ant-... micro chat --provider anthropic
> list all users
> create an order for product 42
```
`micro chat` discovers every service in the registry, exposes each endpoint as a tool, and lets the model orchestrate calls. The same building blocks (`ai.Tools`) work from your own services:
```go
tools := ai.NewTools(service.Registry())
discovered, _ := tools.Discover()
m := ai.New("anthropic",
ai.WithAPIKey(key),
ai.WithTools(tools),
)
resp, _ := m.Generate(ctx, &ai.Request{
Prompt: userInput,
Tools: discovered,
})
```
See the [MCP guide](https://go-micro.dev/docs/mcp.html) for authentication, scopes, and advanced usage.
## Multi-Service Binaries
Run multiple services in a single binary — start as a modular monolith, split into separate deployments later when you actually need to.
```go
users := micro.New("users", micro.Address(":9001"))
orders := micro.New("orders", micro.Address(":9002"))
users.Handle(new(Users))
orders.Handle(new(Orders))
// Run all services together with shared lifecycle
g := micro.NewGroup(users, orders)
g.Run()
```
Each service gets its own server, client, store, and cache while sharing the registry, broker, and transport — so they can discover and call each other within the same process.
See the [multi-service example](examples/multi-service/) for a working demo.
## Data Model
Go Micro includes a typed data model layer for persistence. Define a struct, tag a key field, and get type-safe CRUD and query operations backed by memory, SQLite, or Postgres.
```go
import (
"go-micro.dev/v5/model"
"go-micro.dev/v5/model/sqlite"
)
// Define your data type
type User struct {
ID string `json:"id" model:"key"`
Name string `json:"name"`
Email string `json:"email" model:"index"`
Age int `json:"age"`
}
```
Register your types and use the model:
```go
service := micro.New("users")
// Register and use the service's model backend
db := service.Model()
db.Register(&User{})
// CRUD operations
db.Create(ctx, &User{ID: "1", Name: "Alice", Email: "alice@example.com", Age: 30})
user := &User{}
db.Read(ctx, "1", user)
user.Name = "Alice Smith"
db.Update(ctx, user)
db.Delete(ctx, "1", &User{})
```
Query with filters, ordering, and pagination:
```go
var results []*User
// Find users by field
db.List(ctx, &results, model.Where("email", "alice@example.com"))
// Complex queries
db.List(ctx, &results,
model.WhereOp("age", ">=", 18),
model.OrderDesc("name"),
model.Limit(10),
model.Offset(20),
)
count, _ := users.Count(ctx, model.Where("age", 30))
```
Swap backends with an option:
```go
// Development: in-memory (default)
service := micro.New("users")
// Production: SQLite or Postgres
db, _ := sqlite.New(model.WithDSN("file:app.db"))
service := micro.New("users", micro.Model(db))
```
Every service gets `Client()`, `Server()`, and `Model()` — call services, handle requests, and save data all from the same interface.
## Examples
Check out [/examples](examples/) for runnable code:
- [hello-world](examples/hello-world/) - Basic RPC service
- [grpc-interop](examples/grpc-interop/) - Call go-micro from any gRPC client
- [web-service](examples/web-service/) - HTTP REST API
- [multi-service](examples/multi-service/) - Multiple services in one binary
- [mcp](examples/mcp/) - MCP integration with AI agents
See [all examples](examples/README.md) for more.
There's a new `genai` package for generative AI capabilities.
## Protobuf
Install the code generator and see usage in the docs:
```bash
go install go-micro.dev/v5/cmd/protoc-gen-micro@v5.16.0
go install go-micro.dev/v5/cmd/protoc-gen-micro@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
@@ -305,79 +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
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.
### Generate From a Prompt
Describe what you need in plain English. The AI designs services, writes handlers with real business logic, compiles them, and starts them:
```bash
micro run --prompt "a task management system with categories" --provider anthropic
```
Then talk to your services through an agent:
```bash
micro chat --provider anthropic
> Create a Work category, then add a task called 'Finish report' to it
```
The agent orchestrates across services automatically. When you need a capability that doesn't exist, the agent generates a new service mid-conversation. [Read more](https://go-micro.dev/blog/13).
### Development Workflow
| Stage | Command | Purpose |
|-------|---------|---------|
| **Create** | `micro new myservice` | Scaffold a service (`--template crud/pubsub/api`) |
| **Develop** | `micro run` | Dev mode with hot reload and API gateway |
| **Test** | `micro call` | Call a service endpoint from the CLI |
| **Chat** | `micro chat` | Talk to your services through an LLM |
| **Gateway** | `micro api` | Standalone HTTP-to-RPC gateway |
| **Build** | `micro build` | Compile production binaries |
| **Deploy** | `micro deploy` | Push to a remote Linux server via SSH + systemd |
| **Dashboard** | `micro server` | Production web UI with auth |
### Inspecting the Framework
Every core interface has a matching command — inspect the registry, broker, store, and config from the terminal:
```bash
micro registry list # list services
micro broker subscribe events # stream a topic
micro broker publish events 'hello' # publish a message
micro store write greeting hello # write a record
micro store read greeting # read it back
micro config get database.host # read config (from DATABASE_HOST)
```
These mirror the `registry`, `broker`, `store`, and `config` packages.
### 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 `/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
@@ -407,83 +177,23 @@ The gateway runs on :8080 by default, so services should use other ports.
### Deployment
Deploy to any Linux server with systemd:
```bash
# On your server (one-time setup)
curl -fsSL https://go-micro.dev/install.sh | sh
sudo micro init --server
# From your laptop
micro deploy user@your-server
micro build # Build container images
micro build --compose # Generate docker-compose.yml
micro deploy # Deploy with docker-compose
micro deploy --ssh user@host # Deploy via SSH
```
The deploy command:
1. Builds binaries for Linux
2. Copies via SSH to the server
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
micro logs --remote user@server # View logs
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 [cmd/micro/README.md](cmd/micro/README.md) for full CLI documentation.
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)
- [AI Integration](internal/website/docs/ai-integration.md) — how services, MCP, tools, and LLMs fit together
- [MCP & AI Agents](internal/website/docs/mcp.md)
- [Data Model](internal/website/docs/model.md)
- [Plugins Overview](internal/website/docs/plugins.md)
- [Deployment Guide](internal/website/docs/deployment.md)
- [Learn by Example](internal/website/docs/examples/index.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)
## Supported AI Providers
Go Micros `ai` package gives every provider the same interface: `Init`, `Generate`, `Stream`, and functional options. Swap providers with a single import.
| Provider | Import | Default Model |
|----------|--------|---------------|
| **Anthropic** | `go-micro.dev/v5/ai/anthropic` | `claude-sonnet-4-20250514` |
| **Google Gemini** | `go-micro.dev/v5/ai/gemini` | `gemini-2.5-flash` |
| **Groq** | `go-micro.dev/v5/ai/groq` | `llama-3.3-70b-versatile` |
| **Mistral** | `go-micro.dev/v5/ai/mistral` | `mistral-large-latest` |
| **OpenAI** | `go-micro.dev/v5/ai/openai` | `gpt-4o` |
| **Together AI** | `go-micro.dev/v5/ai/together` | `Llama-3.3-70B-Instruct-Turbo` |
| **Atlas Cloud** | `go-micro.dev/v5/ai/atlascloud` | `llama-3.3-70b` |
Any provider that exposes an OpenAI-compatible API can also be used directly:
```go
m := ai.New("openai",
ai.WithAPIKey("your-key"),
ai.WithBaseURL("https://api.yourprovider.com"),
)
```
**Want to add your platform?** See the [AI Provider Integration Guide](internal/website/docs/guides/ai-provider-guide.md) for how to implement `ai.Model` and submit a PR. We welcome both code contributions and sponsorships from AI infrastructure companies — reach out via a [GitHub issue](https://github.com/micro/go-micro/issues).
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
+13 -32
View File
@@ -2,27 +2,16 @@
This roadmap outlines the planned features and improvements for Go Micro. Community feedback and contributions are welcome!
> **See [internal/docs/ROADMAP_2026.md](internal/docs/ROADMAP_2026.md) for the AI-Native Era roadmap** focused on MCP integration, agent-first development, and business sustainability. This document covers general framework improvements.
## Current Focus (Q1 2026) - COMPLETE
## Current Focus (Q1 2026)
### Documentation & Developer Experience
- [x] Modernize documentation structure
- [x] Add learn-by-example guides
- [x] Update issue templates
- [x] MCP integration documentation
- [x] Agent playground and MCP tools registry
- [ ] Create video tutorials
- [ ] Interactive documentation site
- [ ] Plugin discovery dashboard
### AI & Model Integration
- [x] AI package with provider abstraction (`ai.Model` interface)
- [x] Anthropic Claude provider (`ai/anthropic`)
- [x] OpenAI GPT provider (`ai/openai`)
- [x] Tool execution with auto-calling support
- [x] Streaming support via `ai.Stream`
### Observability
- [ ] OpenTelemetry native support
- [ ] Auto-instrumentation for handlers
@@ -31,10 +20,7 @@ This roadmap outlines the planned features and improvements for Go Micro. Commun
- [ ] Integration with popular observability platforms
### Developer Tools
- [x] `micro run` with hot reload and unified gateway
- [x] `micro deploy` with SSH + systemd deployment
- [x] `micro mcp` command suite (serve, list, test, docs, export)
- [ ] `micro dev` with enhanced hot reload
- [ ] `micro dev` with hot reload
- [ ] Service templates (`micro new --template`)
- [ ] Better error messages with suggestions
- [ ] Debug tooling improvements
@@ -43,8 +29,8 @@ This roadmap outlines the planned features and improvements for Go Micro. Commun
## Q2 2026
### Production Readiness
- [x] Health check standardization
- [x] Graceful shutdown improvements
- [ ] Health check standardization
- [ ] Graceful shutdown improvements
- [ ] Resource cleanup best practices
- [ ] Load testing framework integration
- [ ] Performance benchmarking suite
@@ -57,10 +43,6 @@ This roadmap outlines the planned features and improvements for Go Micro. Commun
- [ ] Multi-cluster patterns
### Security
- [x] Bearer token authentication for MCP
- [x] Per-tool scope enforcement
- [x] Audit logging
- [x] Rate limiting
- [ ] mTLS by default option
- [ ] Secret management integration (Vault, AWS Secrets Manager)
- [ ] RBAC improvements
@@ -78,7 +60,7 @@ This roadmap outlines the planned features and improvements for Go Micro. Commun
### Streaming & Async
- [ ] Improved streaming support
- [x] Server-sent events (SSE) support (via MCP gateway)
- [ ] Server-sent events (SSE) support
- [ ] WebSocket plugin
- [ ] Event sourcing patterns
- [ ] CQRS examples
@@ -132,7 +114,6 @@ This roadmap outlines the planned features and improvements for Go Micro. Commun
### Differentiation
- **Batteries included, fully swappable** - Start simple, scale complex
- **Zero-config local development** - No infrastructure required to start
- **AI-native by default** - Every service is an MCP tool automatically
- **Plugin ecosystem in-repo** - No version compatibility hell
- **Progressive complexity** - Learn as you grow
- **Cloud-native first** - Built for Kubernetes and containers
@@ -142,11 +123,11 @@ This roadmap outlines the planned features and improvements for Go Micro. Commun
We welcome contributions to any roadmap items! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
### High Priority Areas
1. Documentation improvements (guides, tutorials)
2. Multi-protocol MCP support (WebSocket, gRPC)
3. Agent SDK integrations (LlamaIndex, AutoGPT)
4. OpenTelemetry integration
5. Kubernetes operator and Helm charts
1. Documentation improvements
2. Real-world examples
3. Plugin development
4. Performance optimizations
5. Testing infrastructure
### How to Contribute
- Pick an item from the roadmap
@@ -156,7 +137,7 @@ We welcome contributions to any roadmap items! See [CONTRIBUTING.md](CONTRIBUTIN
## Feedback
Have suggestions for the roadmap?
Have suggestions for the roadmap?
- Open a [feature request](.github/ISSUE_TEMPLATE/feature_request.md)
- Start a discussion in GitHub Discussions
@@ -177,6 +158,6 @@ We follow semantic versioning:
---
Last updated: March 2026
Last updated: November 2025
This roadmap is subject to change based on community needs and priorities.
This roadmap is subject to change based on community needs and priorities. Star the repo to stay updated! ⭐
-179
View File
@@ -1,179 +0,0 @@
# Security Policy
## Supported Versions
We actively support the following versions of go-micro:
| Version | Supported |
| ------- | ------------------ |
| 5.x | :white_check_mark: |
| 4.x | :x: |
| 3.x | :x: |
| < 3.0 | :x: |
## Reporting a Vulnerability
**Please do not report security vulnerabilities through public GitHub issues.**
### How to Report
Send security vulnerability reports to: **security@go-micro.dev**
Or use GitHub's private security advisory feature:
https://github.com/micro/go-micro/security/advisories/new
### What to Include
Please include as much of the following information as possible:
- Type of vulnerability (e.g., RCE, XSS, SQL injection, etc.)
- Full paths of source file(s) related to the vulnerability
- Location of the affected source code (tag/branch/commit or direct URL)
- Step-by-step instructions to reproduce the issue
- Proof-of-concept or exploit code (if possible)
- Impact of the issue, including how an attacker might exploit it
### Response Timeline
- **Acknowledgment**: Within 48 hours
- **Initial Assessment**: Within 5 business days
- **Fix Timeline**: Depends on severity
- Critical: 7 days
- High: 14 days
- Medium: 30 days
- Low: Next release cycle
### Disclosure Policy
- We follow **coordinated disclosure**
- We'll work with you to understand and fix the issue
- We'll credit you in the security advisory (unless you prefer to remain anonymous)
- Please give us reasonable time to fix before public disclosure
- We'll publish a security advisory on GitHub when the fix is released
## Security Best Practices
When using go-micro in production:
### TLS/Transport Security
```go
import "go-micro.dev/v5/transport"
// Enable TLS verification (recommended)
os.Setenv("MICRO_TLS_SECURE", "true")
// Or use SecureConfig explicitly
tlsConfig := transport.SecureConfig()
```
See [TLS Security Update](internal/website/docs/TLS_SECURITY_UPDATE.md) for details.
### Authentication
```go
import "go-micro.dev/v5/auth"
// Use JWT authentication
service := micro.NewService(
micro.Auth(auth.NewAuth()),
)
```
### Input Validation
Always validate and sanitize inputs in your handlers:
```go
func (h *Handler) Create(ctx context.Context, req *Request, rsp *Response) error {
// Validate input
if req.Name == "" {
return errors.BadRequest("handler.create", "name is required")
}
// Sanitize and process
// ...
}
```
### Rate Limiting
Implement rate limiting for public-facing services:
```go
import "go-micro.dev/v5/client"
// Client-side rate limiting
client.NewClient(
client.RequestTimeout(time.Second * 5),
client.Retries(3),
)
```
### Secrets Management
Never commit secrets to version control:
```go
// Good: Use environment variables
apiKey := os.Getenv("API_KEY")
// Better: Use a secrets manager
import "github.com/hashicorp/vault/api"
```
### Dependency Security
Regularly update dependencies:
```bash
# Check for vulnerabilities
go list -json -m all | nancy sleuth
# Update dependencies
go get -u ./...
go mod tidy
```
## Known Security Considerations
### Reflection Usage
go-micro uses reflection for automatic handler registration. While this is a deliberate design choice for developer productivity, be aware:
- Type safety is enforced at runtime, not compile time
- Malformed requests won't crash services (errors are returned)
- See [Performance Considerations](internal/website/docs/performance.md)
### TLS Certificate Verification
**Default behavior in v5**: TLS certificate verification is **disabled** for backward compatibility.
**Production recommendation**: Enable secure mode:
```bash
export MICRO_TLS_SECURE=true
```
This will be the default in v6.
## Security Updates
Security updates are published as:
- GitHub Security Advisories
- Release notes with `[SECURITY]` prefix
- CVE entries for critical issues
Subscribe to releases: https://github.com/micro/go-micro/releases
## Bug Bounty
We currently do not offer a bug bounty program, but we greatly appreciate responsible disclosure and will publicly credit researchers who report valid security issues.
## Questions?
For security questions that are not vulnerabilities, please:
- Open a discussion: https://github.com/micro/go-micro/discussions
- Join Discord: https://discord.gg/jwTYuUVAGh
- Email: support@go-micro.dev
-350
View File
@@ -1,350 +0,0 @@
# AI Package
The `ai` package provides simple, high-level interfaces for AI model providers. It supports text generation (`Model`), image generation (`ImageModel`), and video generation (`VideoModel`).
## Interfaces
### Text Generation (Model)
The Model interface follows the same patterns as other go-micro packages (Registry, Client, Broker):
```go
type Model interface {
Init(...Option) error
Options() Options
Generate(ctx context.Context, req *Request, opts ...GenerateOption) (*Response, error)
Stream(ctx context.Context, req *Request, opts ...GenerateOption) (Stream, error)
String() string
}
```
## Quick Start
```go
import (
"context"
"go-micro.dev/v5/ai"
_ "go-micro.dev/v5/ai/anthropic"
_ "go-micro.dev/v5/ai/openai"
)
// Create a model
m := ai.New("openai",
ai.WithAPIKey("your-api-key"),
ai.WithModel("gpt-4o"),
)
// Generate a response
req := &ai.Request{
Prompt: "What is Go?",
SystemPrompt: "You are a helpful programming assistant",
}
resp, err := m.Generate(context.Background(), req)
if err != nil {
log.Fatal(err)
}
fmt.Println(resp.Reply)
```
### Image Generation (ImageModel)
```go
type ImageModel interface {
GenerateImage(ctx context.Context, req *ImageRequest, opts ...GenerateOption) (*ImageResponse, error)
String() string
}
```
```go
import (
"go-micro.dev/v5/ai"
_ "go-micro.dev/v5/ai/atlascloud"
)
ig := ai.NewImage("atlascloud",
ai.WithAPIKey("your-api-key"),
)
resp, err := ig.GenerateImage(context.Background(), &ai.ImageRequest{
Prompt: "A Go gopher in space",
Size: "1024x1024",
})
fmt.Println(resp.Images[0].URL)
```
Providers that support image generation: **Atlas Cloud**, **OpenAI**.
### Video Generation (VideoModel)
```go
type VideoModel interface {
GenerateVideo(ctx context.Context, req *VideoRequest, opts ...GenerateOption) (*VideoResponse, error)
String() string
}
```
```go
import (
"go-micro.dev/v5/ai"
_ "go-micro.dev/v5/ai/atlascloud"
)
vg := ai.NewVideo("atlascloud",
ai.WithAPIKey("your-api-key"),
)
resp, err := vg.GenerateVideo(context.Background(), &ai.VideoRequest{
Prompt: "Microservices nodes animating with data flowing between them",
Images: []string{"https://example.com/diagram.png"}, // optional: image-to-video
Duration: 6,
})
fmt.Println(resp.URL)
```
Providers that support video generation: **Atlas Cloud**.
## Options
Configure the model using functional options:
```go
m := ai.New("anthropic",
ai.WithAPIKey("your-key"), // Required
ai.WithModel("claude-sonnet-4-20250514"), // Optional, uses provider default
ai.WithBaseURL("https://api.anthropic.com"), // Optional, uses provider default
)
```
You can also update options after creation:
```go
m.Init(
ai.WithModel("gpt-4o-mini"),
ai.WithAPIKey("new-key"),
)
```
## Using Tools
The model can automatically execute tool calls when provided with a tool handler:
```go
// Define a tool handler
toolHandler := func(name string, input map[string]any) (result any, content string) {
// Execute the tool and return results
switch name {
case "get_weather":
return map[string]string{"temp": "72F"}, `{"temp": "72F"}`
default:
return nil, `{"error": "unknown tool"}`
}
}
// Create model with tool handler
m := ai.New("openai",
ai.WithAPIKey("your-key"),
ai.WithToolHandler(toolHandler),
)
// Provide tools in the request
req := &ai.Request{
Prompt: "What's the weather?",
SystemPrompt: "You are a helpful assistant",
Tools: []ai.Tool{
{
Name: "get_weather",
Description: "Get current weather",
Properties: map[string]any{
"location": map[string]any{
"type": "string",
"description": "City name",
},
},
},
},
}
// Generate will automatically call tools and return final answer
resp, err := m.Generate(context.Background(), req)
fmt.Println(resp.Answer) // Final answer after tool execution
```
## Response Structure
```go
type Response struct {
Reply string // Initial reply from model
ToolCalls []ToolCall // Tools the model wants to call
Answer string // Final answer (after tool execution if handler provided)
}
```
- `Reply`: The model's first response
- `ToolCalls`: List of tools the model requested (if any)
- `Answer`: The final answer after tools are executed (only set if ToolHandler is provided)
## Supported Providers
### Anthropic Claude
```go
m := ai.New("anthropic",
ai.WithAPIKey("sk-ant-..."),
ai.WithModel("claude-sonnet-4-20250514"), // default
)
```
Default model: `claude-sonnet-4-20250514`
Default base URL: `https://api.anthropic.com`
### OpenAI GPT
```go
m := ai.New("openai",
ai.WithAPIKey("sk-..."),
ai.WithModel("gpt-4o"), // default
)
```
Default model: `gpt-4o`
Default base URL: `https://api.openai.com`
### Google Gemini
```go
m := ai.New("gemini",
ai.WithAPIKey("your-key"),
ai.WithModel("gemini-2.5-flash"), // default
)
```
Default model: `gemini-2.5-flash`
Default base URL: `https://generativelanguage.googleapis.com`
Google Gemini uses its own API format with `system_instruction`, `contents` (not `messages`), and `functionDeclarations` for tool calling. The provider handles the translation automatically.
### Groq
```go
m := ai.New("groq",
ai.WithAPIKey("your-key"),
ai.WithModel("llama-3.3-70b-versatile"), // default
)
```
Default model: `llama-3.3-70b-versatile`
Default base URL: `https://api.groq.com/openai`
Groq provides ultra-fast inference for open-weight models via an OpenAI-compatible endpoint.
### Mistral
```go
m := ai.New("mistral",
ai.WithAPIKey("your-key"),
ai.WithModel("mistral-large-latest"), // default
)
```
Default model: `mistral-large-latest`
Default base URL: `https://api.mistral.ai`
Mistral AI is a European AI company offering high-performance models via an OpenAI-compatible endpoint.
### Together AI
```go
m := ai.New("together",
ai.WithAPIKey("your-key"),
ai.WithModel("meta-llama/Llama-3.3-70B-Instruct-Turbo"), // default
)
```
Default model: `meta-llama/Llama-3.3-70B-Instruct-Turbo`
Default base URL: `https://api.together.xyz`
Together AI provides fast inference for open-weight models via an OpenAI-compatible endpoint.
### Atlas Cloud
```go
m := ai.New("atlascloud",
ai.WithAPIKey("your-key"),
ai.WithModel("llama-3.3-70b"), // default
)
```
Default model: `llama-3.3-70b`
Default base URL: `https://api.atlascloud.ai`
Atlas Cloud is an enterprise AI infrastructure platform offering high-performance LLM APIs. It exposes an OpenAI-compatible chat completions endpoint with tool calling support.
## Auto-Detection
Use `AutoDetectProvider()` to detect the provider from a base URL:
```go
provider := ai.AutoDetectProvider("https://api.anthropic.com")
// Returns "anthropic"
m := ai.New(provider, ai.WithAPIKey("..."))
```
## Adding a New Provider
See the full **[AI Provider Integration Guide](../internal/website/docs/guides/ai-provider-guide.md)** for a step-by-step walkthrough, checklist, and design notes.
Quick summary:
1. Create `ai/yourprovider/yourprovider.go` implementing `ai.Model`.
2. Call `ai.Register("yourprovider", ...)` in `init()`.
3. Add tests in `ai/yourprovider/yourprovider_test.go`.
4. Users enable the provider with a blank import:
```go
import _ "go-micro.dev/v5/ai/yourprovider"
```
We welcome contributions and sponsorships from AI infrastructure companies — see the guide for details.
## Comparison with Other Packages
The ai package follows the same patterns as other go-micro packages:
**Registry:**
```go
r := registry.NewRegistry(registry.Addrs("..."))
r.Register(service)
```
**Client:**
```go
c := client.NewClient(client.Retries(3))
c.Call(ctx, req, rsp)
```
**AI:**
```go
m := ai.New("openai", ai.WithAPIKey("..."))
m.Generate(ctx, req)
```
All use:
- `Init()` to update options
- `Options()` to get current options
- `String()` to get the implementation name
- Functional options pattern
## Testing
```bash
go test ./ai/...
```
## Examples
See the [server implementation](../cmd/micro/server/server.go) for a complete example of using the ai package with tool execution.
-272
View File
@@ -1,272 +0,0 @@
// Package anthropic implements the Anthropic Claude model provider
package anthropic
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"go-micro.dev/v5/ai"
)
func init() {
ai.Register("anthropic", func(opts ...ai.Option) ai.Model {
return NewProvider(opts...)
})
}
// Provider implements the ai.Model interface for Anthropic Claude
type Provider struct {
opts ai.Options
}
// NewProvider creates a new Anthropic provider
func NewProvider(opts ...ai.Option) *Provider {
options := ai.NewOptions(opts...)
// Set defaults if not provided
if options.Model == "" {
options.Model = "claude-sonnet-4-20250514"
}
if options.BaseURL == "" {
options.BaseURL = "https://api.anthropic.com"
}
return &Provider{
opts: options,
}
}
// Init initializes the provider with options
func (p *Provider) Init(opts ...ai.Option) error {
for _, o := range opts {
o(&p.opts)
}
return nil
}
// Options returns the provider options
func (p *Provider) Options() ai.Options {
return p.opts
}
// String returns the provider name
func (p *Provider) String() string {
return "anthropic"
}
// Generate generates a response from the model
func (p *Provider) Generate(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (*ai.Response, error) {
// Build tools for Anthropic format
var anthropicTools []map[string]any
for _, t := range req.Tools {
anthropicTools = append(anthropicTools, map[string]any{
"name": t.Name,
"description": t.Description,
"input_schema": map[string]any{
"type": "object",
"properties": t.Properties,
},
})
}
// Build initial request
apiReq := map[string]any{
"model": p.opts.Model,
"max_tokens": 8192,
"system": req.SystemPrompt,
"messages": []map[string]any{
{"role": "user", "content": req.Prompt},
},
}
if len(anthropicTools) > 0 {
apiReq["tools"] = anthropicTools
}
// Make API call
resp, rawContent, err := p.callAPI(ctx, apiReq)
if err != nil {
return nil, err
}
// If no tool calls or no handler, return as-is
if len(resp.ToolCalls) == 0 || p.opts.ToolHandler == nil {
return resp, nil
}
// Tool execution loop: execute tools, send results back, repeat
// until the model responds with text only (no more tool calls)
messages := []map[string]any{
{"role": "user", "content": req.Prompt},
{"role": "assistant", "content": cleanContent(rawContent)},
}
pendingCalls := resp.ToolCalls
for rounds := 0; rounds < 10; rounds++ {
var toolResultBlocks []map[string]any
for i := range pendingCalls {
_, content := p.opts.ToolHandler(pendingCalls[i].Name, pendingCalls[i].Input)
pendingCalls[i].Result = content
toolResultBlocks = append(toolResultBlocks, map[string]any{
"type": "tool_result",
"tool_use_id": pendingCalls[i].ID,
"content": content,
})
}
messages = append(messages, map[string]any{
"role": "user",
"content": toolResultBlocks,
})
followUpReq := map[string]any{
"model": p.opts.Model,
"max_tokens": 8192,
"system": req.SystemPrompt,
"messages": messages,
}
if len(anthropicTools) > 0 {
followUpReq["tools"] = anthropicTools
}
followUpResp, followUpRaw, err := p.callAPI(ctx, followUpReq)
if err != nil {
break
}
if len(followUpResp.ToolCalls) > 0 {
resp.ToolCalls = append(resp.ToolCalls, followUpResp.ToolCalls...)
pendingCalls = followUpResp.ToolCalls
messages = append(messages, map[string]any{
"role": "assistant",
"content": cleanContent(followUpRaw),
})
continue
}
if followUpResp.Reply != "" {
resp.Answer = followUpResp.Reply
}
break
}
return resp, nil
}
// Stream generates a streaming response (not yet implemented)
func (p *Provider) Stream(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (ai.Stream, error) {
return nil, fmt.Errorf("streaming not yet implemented for anthropic provider")
}
// callAPI makes an HTTP request to the Anthropic API
func (p *Provider) callAPI(ctx context.Context, req map[string]any) (*ai.Response, any, error) {
// Marshal request
reqBody, err := json.Marshal(req)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal request: %w", err)
}
// Build HTTP request
apiURL := strings.TrimRight(p.opts.BaseURL, "/") + "/v1/messages"
httpReq, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewReader(reqBody))
if err != nil {
return nil, nil, fmt.Errorf("failed to create request: %w", err)
}
// Set headers
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("x-api-key", p.opts.APIKey)
httpReq.Header.Set("anthropic-version", "2023-06-01")
// Make request
httpResp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, nil, fmt.Errorf("API request failed: %w", err)
}
defer httpResp.Body.Close()
// Read response
respBody, _ := io.ReadAll(httpResp.Body)
if httpResp.StatusCode != 200 {
return nil, nil, fmt.Errorf("API error (%s): %s", httpResp.Status, string(respBody))
}
// Parse response
var anthropicResp struct {
Content []struct {
Type string `json:"type"`
Text string `json:"text"`
ID string `json:"id"`
Name string `json:"name"`
Input json.RawMessage `json:"input"`
} `json:"content"`
StopReason string `json:"stop_reason"`
}
if err := json.Unmarshal(respBody, &anthropicResp); err != nil {
return nil, nil, fmt.Errorf("failed to parse response: %w", err)
}
response := &ai.Response{}
// Extract text reply
var replyParts []string
for _, block := range anthropicResp.Content {
if block.Type == "text" && block.Text != "" {
replyParts = append(replyParts, block.Text)
}
}
if len(replyParts) > 0 {
response.Reply = strings.Join(replyParts, "\n")
}
// Extract tool calls
for _, block := range anthropicResp.Content {
if block.Type == "tool_use" {
var input map[string]any
if err := json.Unmarshal(block.Input, &input); err != nil {
input = map[string]any{}
}
response.ToolCalls = append(response.ToolCalls, ai.ToolCall{
ID: block.ID,
Name: block.Name,
Input: input,
})
}
}
return response, anthropicResp.Content, nil
}
// cleanContent strips fields from response content blocks that Anthropic
// rejects when sent back as assistant message content (e.g. "id" on text blocks).
func cleanContent(raw any) any {
blocks, ok := raw.([]struct {
Type string `json:"type"`
Text string `json:"text"`
ID string `json:"id"`
Name string `json:"name"`
Input json.RawMessage `json:"input"`
})
if !ok {
return raw
}
var cleaned []map[string]any
for _, b := range blocks {
switch b.Type {
case "text":
cleaned = append(cleaned, map[string]any{"type": "text", "text": b.Text})
case "tool_use":
var input any
json.Unmarshal(b.Input, &input)
cleaned = append(cleaned, map[string]any{"type": "tool_use", "id": b.ID, "name": b.Name, "input": input})
}
}
return cleaned
}
-94
View File
@@ -1,94 +0,0 @@
package anthropic
import (
"context"
"testing"
"go-micro.dev/v5/ai"
)
func TestProvider_String(t *testing.T) {
p := NewProvider()
if p.String() != "anthropic" {
t.Errorf("Expected provider name 'anthropic', got '%s'", p.String())
}
}
func TestProvider_Init(t *testing.T) {
p := NewProvider()
err := p.Init(
ai.WithModel("test-model"),
ai.WithAPIKey("test-key"),
ai.WithBaseURL("https://test.com"),
)
if err != nil {
t.Fatalf("Init failed: %v", err)
}
opts := p.Options()
if opts.Model != "test-model" {
t.Errorf("Expected model 'test-model', got '%s'", opts.Model)
}
if opts.APIKey != "test-key" {
t.Errorf("Expected API key 'test-key', got '%s'", opts.APIKey)
}
if opts.BaseURL != "https://test.com" {
t.Errorf("Expected base URL 'https://test.com', got '%s'", opts.BaseURL)
}
}
func TestProvider_Options(t *testing.T) {
p := NewProvider(
ai.WithModel("custom-model"),
ai.WithAPIKey("my-key"),
)
opts := p.Options()
if opts.Model != "custom-model" {
t.Errorf("Expected model 'custom-model', got '%s'", opts.Model)
}
if opts.APIKey != "my-key" {
t.Errorf("Expected API key 'my-key', got '%s'", opts.APIKey)
}
}
func TestProvider_Defaults(t *testing.T) {
p := NewProvider()
opts := p.Options()
if opts.Model != "claude-sonnet-4-20250514" {
t.Errorf("Expected default model 'claude-sonnet-4-20250514', got '%s'", opts.Model)
}
if opts.BaseURL != "https://api.anthropic.com" {
t.Errorf("Expected default base URL 'https://api.anthropic.com', got '%s'", opts.BaseURL)
}
}
func TestProvider_Generate_NoAPIKey(t *testing.T) {
p := NewProvider()
req := &ai.Request{
Prompt: "Hello",
SystemPrompt: "You are helpful",
}
_, err := p.Generate(context.Background(), req)
if err == nil {
t.Error("Expected error when API key is missing, got nil")
}
}
func TestProvider_Stream_NotImplemented(t *testing.T) {
p := NewProvider()
req := &ai.Request{
Prompt: "Hello",
}
_, err := p.Stream(context.Background(), req)
if err == nil {
t.Error("Expected error for unimplemented streaming, got nil")
}
}
-489
View File
@@ -1,489 +0,0 @@
// Package atlascloud implements the Atlas Cloud model provider.
//
// Atlas Cloud is an enterprise AI infrastructure platform offering
// high-performance LLM, image, and video APIs. It exposes
// OpenAI-compatible endpoints for chat completions and image
// generation.
//
// Usage:
//
// import _ "go-micro.dev/v5/ai/atlascloud"
//
// m := ai.New("atlascloud",
// ai.WithAPIKey("your-api-key"),
// )
//
// // Image generation
// ig := ai.NewImage("atlascloud",
// ai.WithAPIKey("your-api-key"),
// )
package atlascloud
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"go-micro.dev/v5/ai"
)
func init() {
ai.Register("atlascloud", func(opts ...ai.Option) ai.Model {
return NewProvider(opts...)
})
ai.RegisterImage("atlascloud", func(opts ...ai.Option) ai.ImageModel {
return NewProvider(opts...)
})
ai.RegisterVideo("atlascloud", func(opts ...ai.Option) ai.VideoModel {
return NewProvider(opts...)
})
}
// Provider implements the ai.Model interface for Atlas Cloud.
type Provider struct {
opts ai.Options
}
// NewProvider creates a new Atlas Cloud provider.
func NewProvider(opts ...ai.Option) *Provider {
options := ai.NewOptions(opts...)
if options.Model == "" {
options.Model = "deepseek-ai/DeepSeek-V3-0324"
}
if options.BaseURL == "" {
options.BaseURL = "https://api.atlascloud.ai"
}
return &Provider{opts: options}
}
func (p *Provider) Init(opts ...ai.Option) error {
for _, o := range opts {
o(&p.opts)
}
return nil
}
func (p *Provider) Options() ai.Options { return p.opts }
func (p *Provider) String() string { return "atlascloud" }
func (p *Provider) Generate(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (*ai.Response, error) {
var tools []map[string]any
for _, t := range req.Tools {
tools = append(tools, map[string]any{
"type": "function",
"function": map[string]any{
"name": t.Name,
"description": t.Description,
"parameters": map[string]any{
"type": "object",
"properties": t.Properties,
},
},
})
}
messages := []map[string]any{
{"role": "system", "content": req.SystemPrompt},
{"role": "user", "content": req.Prompt},
}
apiReq := map[string]any{
"model": p.opts.Model,
"messages": messages,
}
if len(tools) > 0 {
apiReq["tools"] = tools
}
resp, rawMessage, err := p.callAPI(ctx, apiReq)
if err != nil {
return nil, err
}
if len(resp.ToolCalls) == 0 {
return resp, nil
}
if p.opts.ToolHandler != nil {
followUpMessages := append(messages, map[string]any{
"role": "assistant",
"content": rawMessage["content"],
"tool_calls": rawMessage["tool_calls"],
})
for _, tc := range resp.ToolCalls {
_, content := p.opts.ToolHandler(tc.Name, tc.Input)
followUpMessages = append(followUpMessages, map[string]any{
"role": "tool",
"tool_call_id": tc.ID,
"content": content,
})
}
followUpReq := map[string]any{
"model": p.opts.Model,
"messages": followUpMessages,
}
followUpResp, _, err := p.callAPI(ctx, followUpReq)
if err == nil && followUpResp.Reply != "" {
resp.Answer = followUpResp.Reply
}
}
return resp, nil
}
func (p *Provider) Stream(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (ai.Stream, error) {
return nil, fmt.Errorf("streaming not yet implemented for atlascloud provider")
}
func (p *Provider) callAPI(ctx context.Context, req map[string]any) (*ai.Response, map[string]any, error) {
reqBody, err := json.Marshal(req)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal request: %w", err)
}
apiURL := strings.TrimRight(p.opts.BaseURL, "/") + "/v1/chat/completions"
httpReq, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewReader(reqBody))
if err != nil {
return nil, nil, fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+p.opts.APIKey)
httpResp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, nil, fmt.Errorf("API request failed: %w", err)
}
defer httpResp.Body.Close()
respBody, _ := io.ReadAll(httpResp.Body)
if httpResp.StatusCode != 200 {
return nil, nil, fmt.Errorf("API error (%s): %s", httpResp.Status, string(respBody))
}
var chatResp struct {
Choices []struct {
Message struct {
Content string `json:"content"`
ToolCalls []struct {
ID string `json:"id"`
Function struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
} `json:"function"`
} `json:"tool_calls"`
} `json:"message"`
} `json:"choices"`
}
if err := json.Unmarshal(respBody, &chatResp); err != nil {
return nil, nil, fmt.Errorf("failed to parse response: %w", err)
}
if len(chatResp.Choices) == 0 {
return nil, nil, fmt.Errorf("no response from API")
}
choice := chatResp.Choices[0]
response := &ai.Response{
Reply: choice.Message.Content,
}
for _, tc := range choice.Message.ToolCalls {
var input map[string]any
if err := json.Unmarshal([]byte(tc.Function.Arguments), &input); err != nil {
input = map[string]any{}
}
response.ToolCalls = append(response.ToolCalls, ai.ToolCall{
ID: tc.ID,
Name: tc.Function.Name,
Input: input,
})
}
rawMessage := map[string]any{
"content": choice.Message.Content,
"tool_calls": choice.Message.ToolCalls,
}
return response, rawMessage, nil
}
const defaultImageModel = "openai/gpt-image-2/text-to-image"
// GenerateImage creates an image using Atlas Cloud's async image API.
// It submits the job and polls until completion or context cancellation.
func (p *Provider) GenerateImage(ctx context.Context, req *ai.ImageRequest, opts ...ai.GenerateOption) (*ai.ImageResponse, error) {
model := req.Model
if model == "" {
model = defaultImageModel
}
quality := req.Quality
if quality == "" {
quality = "medium"
}
outputFmt := req.OutputFormat
if outputFmt == "" {
outputFmt = "png"
}
size := req.Size
if size == "" {
size = "1024x1024"
}
apiReq := map[string]any{
"model": model,
"prompt": req.Prompt,
"quality": quality,
"output_format": outputFmt,
"size": size,
"enable_sync_mode": false,
"enable_base64_output": false,
"moderation": "low",
}
reqBody, err := json.Marshal(apiReq)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
apiURL := strings.TrimRight(p.opts.BaseURL, "/") + "/api/v1/model/generateImage"
httpReq, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewReader(reqBody))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+p.opts.APIKey)
httpResp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("API request failed: %w", err)
}
defer httpResp.Body.Close()
respBody, _ := io.ReadAll(httpResp.Body)
if httpResp.StatusCode != 200 {
return nil, fmt.Errorf("API error (%s): %s", httpResp.Status, string(respBody))
}
var submitResp struct {
Code int `json:"code"`
Msg string `json:"message"`
Data struct {
ID string `json:"id"`
Status string `json:"status"`
} `json:"data"`
}
if err := json.Unmarshal(respBody, &submitResp); err != nil {
return nil, fmt.Errorf("failed to parse submit response: %w", err)
}
if submitResp.Code != 200 {
return nil, fmt.Errorf("API error: %s", submitResp.Msg)
}
predictionID := submitResp.Data.ID
pollURL := strings.TrimRight(p.opts.BaseURL, "/") + "/api/v1/model/prediction/" + predictionID
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-ticker.C:
result, err := p.pollPrediction(ctx, pollURL)
if err != nil {
return nil, err
}
if result != nil {
return result, nil
}
}
}
}
func (p *Provider) pollPrediction(ctx context.Context, url string) (*ai.ImageResponse, error) {
httpReq, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
httpReq.Header.Set("Authorization", "Bearer "+p.opts.APIKey)
httpResp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("poll request failed: %w", err)
}
defer httpResp.Body.Close()
body, _ := io.ReadAll(httpResp.Body)
var pollResp struct {
Data struct {
Status string `json:"status"`
Outputs []string `json:"outputs"`
Error string `json:"error"`
} `json:"data"`
}
if err := json.Unmarshal(body, &pollResp); err != nil {
return nil, fmt.Errorf("failed to parse poll response: %w", err)
}
switch pollResp.Data.Status {
case "completed":
resp := &ai.ImageResponse{}
for _, output := range pollResp.Data.Outputs {
resp.Images = append(resp.Images, ai.Image{URL: output})
}
return resp, nil
case "failed":
return nil, fmt.Errorf("image generation failed: %s", pollResp.Data.Error)
default:
return nil, nil
}
}
const defaultVideoModel = "google/gemini-omni-flash/image-to-video-developer"
// GenerateVideo creates a video using Atlas Cloud's async video API.
// Supports text-to-video and image-to-video depending on whether
// Images are provided in the request.
func (p *Provider) GenerateVideo(ctx context.Context, req *ai.VideoRequest, opts ...ai.GenerateOption) (*ai.VideoResponse, error) {
model := req.Model
if model == "" {
model = defaultVideoModel
}
duration := req.Duration
if duration <= 0 {
duration = 6
}
aspect := req.AspectRatio
if aspect == "" {
aspect = "16:9"
}
resolution := req.Resolution
if resolution == "" {
resolution = "720p"
}
apiReq := map[string]any{
"model": model,
"prompt": req.Prompt,
"duration": duration,
"aspect_ratio": aspect,
"resolution": resolution,
"seed": -1,
}
if len(req.Images) > 0 {
apiReq["images"] = req.Images
}
reqBody, err := json.Marshal(apiReq)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
apiURL := strings.TrimRight(p.opts.BaseURL, "/") + "/api/v1/model/generateVideo"
httpReq, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewReader(reqBody))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+p.opts.APIKey)
httpResp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("API request failed: %w", err)
}
defer httpResp.Body.Close()
respBody, _ := io.ReadAll(httpResp.Body)
if httpResp.StatusCode != 200 {
return nil, fmt.Errorf("API error (%s): %s", httpResp.Status, string(respBody))
}
var submitResp struct {
Code int `json:"code"`
Msg string `json:"message"`
Data struct {
ID string `json:"id"`
Status string `json:"status"`
} `json:"data"`
}
if err := json.Unmarshal(respBody, &submitResp); err != nil {
return nil, fmt.Errorf("failed to parse submit response: %w", err)
}
if submitResp.Code != 200 {
return nil, fmt.Errorf("API error: %s", submitResp.Msg)
}
pollURL := strings.TrimRight(p.opts.BaseURL, "/") + "/api/v1/model/prediction/" + submitResp.Data.ID
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-ticker.C:
result, err := p.pollVideo(ctx, pollURL)
if err != nil {
return nil, err
}
if result != nil {
return result, nil
}
}
}
}
func (p *Provider) pollVideo(ctx context.Context, url string) (*ai.VideoResponse, error) {
httpReq, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
httpReq.Header.Set("Authorization", "Bearer "+p.opts.APIKey)
httpResp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("poll request failed: %w", err)
}
defer httpResp.Body.Close()
body, _ := io.ReadAll(httpResp.Body)
var pollResp struct {
Data struct {
Status string `json:"status"`
Outputs []string `json:"outputs"`
Error string `json:"error"`
} `json:"data"`
}
if err := json.Unmarshal(body, &pollResp); err != nil {
return nil, fmt.Errorf("failed to parse poll response: %w", err)
}
switch pollResp.Data.Status {
case "completed", "succeeded":
if len(pollResp.Data.Outputs) == 0 {
return nil, fmt.Errorf("video completed but no outputs returned")
}
return &ai.VideoResponse{URL: pollResp.Data.Outputs[0]}, nil
case "failed":
return nil, fmt.Errorf("video generation failed: %s", pollResp.Data.Error)
default:
return nil, nil
}
}
-148
View File
@@ -1,148 +0,0 @@
package atlascloud
import (
"context"
"testing"
"go-micro.dev/v5/ai"
)
func TestProvider_String(t *testing.T) {
p := NewProvider()
if p.String() != "atlascloud" {
t.Errorf("Expected provider name 'atlascloud', got '%s'", p.String())
}
}
func TestProvider_Init(t *testing.T) {
p := NewProvider()
err := p.Init(
ai.WithModel("test-model"),
ai.WithAPIKey("test-key"),
ai.WithBaseURL("https://test.com"),
)
if err != nil {
t.Fatalf("Init failed: %v", err)
}
opts := p.Options()
if opts.Model != "test-model" {
t.Errorf("Expected model 'test-model', got '%s'", opts.Model)
}
if opts.APIKey != "test-key" {
t.Errorf("Expected API key 'test-key', got '%s'", opts.APIKey)
}
if opts.BaseURL != "https://test.com" {
t.Errorf("Expected base URL 'https://test.com', got '%s'", opts.BaseURL)
}
}
func TestProvider_Options(t *testing.T) {
p := NewProvider(
ai.WithModel("custom-model"),
ai.WithAPIKey("my-key"),
)
opts := p.Options()
if opts.Model != "custom-model" {
t.Errorf("Expected model 'custom-model', got '%s'", opts.Model)
}
if opts.APIKey != "my-key" {
t.Errorf("Expected API key 'my-key', got '%s'", opts.APIKey)
}
}
func TestProvider_Defaults(t *testing.T) {
p := NewProvider()
opts := p.Options()
if opts.Model != "deepseek-ai/DeepSeek-V3-0324" {
t.Errorf("Expected default model 'deepseek-ai/DeepSeek-V3-0324', got '%s'", opts.Model)
}
if opts.BaseURL != "https://api.atlascloud.ai" {
t.Errorf("Expected default base URL 'https://api.atlascloud.ai', got '%s'", opts.BaseURL)
}
}
func TestProvider_Generate_NoAPIKey(t *testing.T) {
p := NewProvider()
req := &ai.Request{
Prompt: "Hello",
SystemPrompt: "You are helpful",
}
_, err := p.Generate(context.Background(), req)
if err == nil {
t.Error("Expected error when API key is missing, got nil")
}
}
func TestProvider_Stream_NotImplemented(t *testing.T) {
p := NewProvider()
req := &ai.Request{
Prompt: "Hello",
}
_, err := p.Stream(context.Background(), req)
if err == nil {
t.Error("Expected error for unimplemented streaming, got nil")
}
}
func TestProvider_Registration(t *testing.T) {
m := ai.New("atlascloud", ai.WithAPIKey("test"))
if m == nil {
t.Fatal("ai.New('atlascloud') returned nil — provider not registered")
}
if m.String() != "atlascloud" {
t.Errorf("Expected 'atlascloud', got '%s'", m.String())
}
}
func TestProvider_ImageRegistration(t *testing.T) {
ig := ai.NewImage("atlascloud", ai.WithAPIKey("test"))
if ig == nil {
t.Fatal("ai.NewImage('atlascloud') returned nil — image provider not registered")
}
if ig.String() != "atlascloud" {
t.Errorf("Expected 'atlascloud', got '%s'", ig.String())
}
}
func TestProvider_GenerateImage_NoAPIKey(t *testing.T) {
p := NewProvider()
_, err := p.GenerateImage(context.Background(), &ai.ImageRequest{Prompt: "a cat"})
if err == nil {
t.Error("Expected error when API key is missing, got nil")
}
}
func TestProvider_ImplementsImageModel(t *testing.T) {
var _ ai.ImageModel = (*Provider)(nil)
}
func TestProvider_VideoRegistration(t *testing.T) {
vg := ai.NewVideo("atlascloud", ai.WithAPIKey("test"))
if vg == nil {
t.Fatal("ai.NewVideo('atlascloud') returned nil — video provider not registered")
}
if vg.String() != "atlascloud" {
t.Errorf("Expected 'atlascloud', got '%s'", vg.String())
}
}
func TestProvider_GenerateVideo_NoAPIKey(t *testing.T) {
p := NewProvider()
_, err := p.GenerateVideo(context.Background(), &ai.VideoRequest{Prompt: "a cat"})
if err == nil {
t.Error("Expected error when API key is missing, got nil")
}
}
func TestProvider_ImplementsVideoModel(t *testing.T) {
var _ ai.VideoModel = (*Provider)(nil)
}
-213
View File
@@ -1,213 +0,0 @@
// Package flow provides event-driven LLM orchestration for go-micro
// services. A Flow subscribes to a broker topic, feeds each event
// into an LLM with all registered services as tools, and lets the
// model decide which RPCs to call.
//
// Usage:
//
// f := flow.New("onboard-user",
// flow.Trigger("events.user.created"),
// flow.Prompt("New user created: {{.Data}}. Send welcome email and create workspace."),
// flow.Provider("anthropic"),
// flow.APIKey(key),
// )
// f.Register(service)
// service.Run()
package flow
import (
"bytes"
"context"
"encoding/json"
"fmt"
"sync"
"text/template"
"time"
"go-micro.dev/v5/ai"
"go-micro.dev/v5/broker"
"go-micro.dev/v5/client"
"go-micro.dev/v5/logger"
"go-micro.dev/v5/registry"
// Register default providers.
_ "go-micro.dev/v5/ai/anthropic"
_ "go-micro.dev/v5/ai/atlascloud"
_ "go-micro.dev/v5/ai/gemini"
_ "go-micro.dev/v5/ai/groq"
_ "go-micro.dev/v5/ai/mistral"
_ "go-micro.dev/v5/ai/openai"
_ "go-micro.dev/v5/ai/together"
)
// Flow is an event-driven LLM orchestration unit. It subscribes to
// a broker topic, discovers services as tools, and feeds each event
// into an LLM that decides which RPCs to call.
type Flow struct {
name string
opts Options
model ai.Model
toolSet *ai.Tools
tmpl *template.Template
log logger.Logger
mu sync.Mutex
results []Result
}
// Result records one flow execution.
type Result struct {
FlowName string `json:"flow"`
Trigger string `json:"trigger"`
Prompt string `json:"prompt"`
Reply string `json:"reply,omitempty"`
Answer string `json:"answer,omitempty"`
ToolCalls []string `json:"tool_calls,omitempty"`
Error string `json:"error,omitempty"`
Timestamp time.Time `json:"timestamp"`
Duration float64 `json:"duration_seconds"`
}
// New creates a Flow with the given name and options.
func New(name string, opts ...Option) *Flow {
o := Options{
Provider: "openai",
SystemPrompt: "You are a service orchestrator. Use the available tools to fulfill the request. Explain what you do.",
HistoryLimit: 20,
}
for _, opt := range opts {
opt(&o)
}
var tmpl *template.Template
if o.Prompt != "" {
var err error
tmpl, err = template.New(name).Parse(o.Prompt)
if err != nil {
tmpl = template.Must(template.New(name).Parse("{{.Data}}"))
}
}
return &Flow{
name: name,
opts: o,
tmpl: tmpl,
log: logger.DefaultLogger,
}
}
// Register wires the flow into a running service. It sets up the
// model, discovers tools from the registry, and subscribes to the
// trigger topic on the broker. Call this before service.Run().
func (f *Flow) Register(reg registry.Registry, br broker.Broker, cl client.Client) error {
f.toolSet = ai.NewTools(reg, ai.ToolClient(cl))
var modelOpts []ai.Option
if f.opts.APIKey != "" {
modelOpts = append(modelOpts, ai.WithAPIKey(f.opts.APIKey))
}
if f.opts.Model != "" {
modelOpts = append(modelOpts, ai.WithModel(f.opts.Model))
}
if f.opts.BaseURL != "" {
modelOpts = append(modelOpts, ai.WithBaseURL(f.opts.BaseURL))
}
modelOpts = append(modelOpts, ai.WithTools(f.toolSet))
f.model = ai.New(f.opts.Provider, modelOpts...)
if f.model == nil {
return fmt.Errorf("unknown provider: %s", f.opts.Provider)
}
if f.opts.TriggerTopic != "" {
_, err := br.Subscribe(f.opts.TriggerTopic, func(p broker.Event) error {
data := string(p.Message().Body)
if err := f.Execute(context.Background(), data); err != nil {
f.log.Logf(logger.ErrorLevel, "Flow %s failed: %v", f.name, err)
}
return nil
})
if err != nil {
return fmt.Errorf("subscribe to %s: %w", f.opts.TriggerTopic, err)
}
f.log.Logf(logger.InfoLevel, "Flow %s subscribed to %s", f.name, f.opts.TriggerTopic)
}
return nil
}
// Execute runs the flow once with the given input data. This is
// called automatically on each broker event, but can also be
// invoked directly for testing or one-shot use.
func (f *Flow) Execute(ctx context.Context, data string) error {
start := time.Now()
discovered, err := f.toolSet.Discover()
if err != nil {
return fmt.Errorf("discover tools: %w", err)
}
prompt := data
if f.tmpl != nil {
var buf bytes.Buffer
f.tmpl.Execute(&buf, map[string]string{"Data": data})
prompt = buf.String()
}
resp, err := f.model.Generate(ctx, &ai.Request{
Prompt: prompt,
SystemPrompt: f.opts.SystemPrompt,
Tools: discovered,
})
result := Result{
FlowName: f.name,
Trigger: f.opts.TriggerTopic,
Prompt: prompt,
Timestamp: start,
Duration: time.Since(start).Seconds(),
}
if err != nil {
result.Error = err.Error()
f.record(result)
return err
}
result.Reply = resp.Reply
result.Answer = resp.Answer
for _, tc := range resp.ToolCalls {
args, _ := json.Marshal(tc.Input)
result.ToolCalls = append(result.ToolCalls, fmt.Sprintf("%s(%s)", tc.Name, args))
}
f.record(result)
f.log.Logf(logger.InfoLevel, "Flow %s completed in %.1fs: %d tool calls",
f.name, result.Duration, len(result.ToolCalls))
return nil
}
// Results returns a copy of all recorded execution results.
func (f *Flow) Results() []Result {
f.mu.Lock()
defer f.mu.Unlock()
out := make([]Result, len(f.results))
copy(out, f.results)
return out
}
// Name returns the flow name.
func (f *Flow) Name() string {
return f.name
}
func (f *Flow) record(r Result) {
f.mu.Lock()
f.results = append(f.results, r)
f.mu.Unlock()
if f.opts.OnResult != nil {
f.opts.OnResult(r)
}
}
-85
View File
@@ -1,85 +0,0 @@
package flow
import (
"testing"
)
func TestNew(t *testing.T) {
f := New("test-flow",
Trigger("events.test"),
Prompt("Handle this: {{.Data}}"),
Provider("anthropic"),
APIKey("test-key"),
HistoryLimit(10),
)
if f.Name() != "test-flow" {
t.Errorf("name = %q, want test-flow", f.Name())
}
if f.opts.TriggerTopic != "events.test" {
t.Errorf("trigger = %q", f.opts.TriggerTopic)
}
if f.opts.Provider != "anthropic" {
t.Errorf("provider = %q", f.opts.Provider)
}
if f.opts.HistoryLimit != 10 {
t.Errorf("history limit = %d", f.opts.HistoryLimit)
}
if f.tmpl == nil {
t.Fatal("template not parsed")
}
}
func TestPromptTemplate(t *testing.T) {
f := New("tmpl-test",
Prompt("User created: {{.Data}}. Send welcome email."),
)
// Test that the template renders
if f.tmpl == nil {
t.Fatal("template not parsed")
}
}
func TestResultsEmpty(t *testing.T) {
f := New("empty")
results := f.Results()
if len(results) != 0 {
t.Errorf("expected 0 results, got %d", len(results))
}
}
func TestOnResultCallback(t *testing.T) {
var called bool
f := New("callback",
OnResult(func(r Result) {
called = true
if r.FlowName != "callback" {
t.Errorf("flow name = %q", r.FlowName)
}
}),
)
f.record(Result{FlowName: "callback"})
if !called {
t.Error("OnResult not called")
}
if len(f.Results()) != 1 {
t.Errorf("results = %d, want 1", len(f.Results()))
}
}
func TestDefaultOptions(t *testing.T) {
f := New("defaults")
if f.opts.Provider != "openai" {
t.Errorf("default provider = %q, want openai", f.opts.Provider)
}
if f.opts.HistoryLimit != 20 {
t.Errorf("default history limit = %d, want 20", f.opts.HistoryLimit)
}
if f.opts.SystemPrompt == "" {
t.Error("default system prompt is empty")
}
}
-71
View File
@@ -1,71 +0,0 @@
package flow
// Options configures a Flow.
type Options struct {
// TriggerTopic is the broker topic that triggers this flow.
TriggerTopic string
// Prompt is a Go template string. {{.Data}} is the event payload.
Prompt string
// SystemPrompt is the system instruction for the LLM.
SystemPrompt string
// Provider is the AI provider name (e.g. "anthropic", "openai").
Provider string
// APIKey for the AI provider.
APIKey string
// Model overrides the provider's default model.
Model string
// BaseURL overrides the provider's default base URL.
BaseURL string
// HistoryLimit is the max messages per flow execution.
HistoryLimit int
// OnResult is called after each execution with the result.
OnResult func(Result)
}
// Option applies a configuration to Options.
type Option func(*Options)
// Trigger sets the broker topic that triggers this flow.
func Trigger(topic string) Option {
return func(o *Options) { o.TriggerTopic = topic }
}
// Prompt sets the prompt template. Use {{.Data}} for the event payload.
func Prompt(p string) Option {
return func(o *Options) { o.Prompt = p }
}
// SystemPrompt sets the system instruction for the LLM.
func SystemPrompt(p string) Option {
return func(o *Options) { o.SystemPrompt = p }
}
// Provider sets the AI provider name.
func Provider(name string) Option {
return func(o *Options) { o.Provider = name }
}
// APIKey sets the API key for the AI provider.
func APIKey(key string) Option {
return func(o *Options) { o.APIKey = key }
}
// Model sets the model name.
func Model(name string) Option {
return func(o *Options) { o.Model = name }
}
// BaseURL sets the provider base URL.
func BaseURL(url string) Option {
return func(o *Options) { o.BaseURL = url }
}
// HistoryLimit sets the max messages per execution.
func HistoryLimit(n int) Option {
return func(o *Options) { o.HistoryLimit = n }
}
// OnResult sets a callback for each execution result.
func OnResult(fn func(Result)) Option {
return func(o *Options) { o.OnResult = fn }
}
-226
View File
@@ -1,226 +0,0 @@
// Package gemini implements the Google Gemini model provider.
//
// Usage:
//
// import _ "go-micro.dev/v5/ai/gemini"
//
// m := ai.New("gemini",
// ai.WithAPIKey("your-api-key"),
// )
package gemini
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"go-micro.dev/v5/ai"
)
func init() {
ai.Register("gemini", func(opts ...ai.Option) ai.Model {
return NewProvider(opts...)
})
}
// Provider implements the ai.Model interface for Google Gemini.
type Provider struct {
opts ai.Options
}
// NewProvider creates a new Gemini provider.
func NewProvider(opts ...ai.Option) *Provider {
options := ai.NewOptions(opts...)
if options.Model == "" {
options.Model = "gemini-2.5-flash"
}
if options.BaseURL == "" {
options.BaseURL = "https://generativelanguage.googleapis.com"
}
return &Provider{opts: options}
}
func (p *Provider) Init(opts ...ai.Option) error {
for _, o := range opts {
o(&p.opts)
}
return nil
}
func (p *Provider) Options() ai.Options { return p.opts }
func (p *Provider) String() string { return "gemini" }
func (p *Provider) Generate(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (*ai.Response, error) {
var tools []map[string]any
for _, t := range req.Tools {
tools = append(tools, map[string]any{
"name": t.Name,
"description": t.Description,
"parameters": map[string]any{
"type": "object",
"properties": t.Properties,
},
})
}
contents := []map[string]any{
{"role": "user", "parts": []map[string]any{{"text": req.Prompt}}},
}
apiReq := map[string]any{
"contents": contents,
}
if req.SystemPrompt != "" {
apiReq["system_instruction"] = map[string]any{
"parts": []map[string]any{{"text": req.SystemPrompt}},
}
}
if len(tools) > 0 {
apiReq["tools"] = []map[string]any{
{"functionDeclarations": tools},
}
}
resp, rawParts, err := p.callAPI(ctx, apiReq)
if err != nil {
return nil, err
}
if len(resp.ToolCalls) == 0 {
return resp, nil
}
if p.opts.ToolHandler != nil {
var resultParts []map[string]any
for _, tc := range resp.ToolCalls {
result, _ := p.opts.ToolHandler(tc.Name, tc.Input)
resultParts = append(resultParts, map[string]any{
"functionResponse": map[string]any{
"name": tc.Name,
"id": tc.ID,
"response": result,
},
})
}
followUpContents := append(contents,
map[string]any{"role": "model", "parts": rawParts},
map[string]any{"role": "user", "parts": resultParts},
)
followUpReq := map[string]any{
"contents": followUpContents,
}
if req.SystemPrompt != "" {
followUpReq["system_instruction"] = map[string]any{
"parts": []map[string]any{{"text": req.SystemPrompt}},
}
}
followUpResp, _, err := p.callAPI(ctx, followUpReq)
if err == nil && followUpResp.Reply != "" {
resp.Answer = followUpResp.Reply
}
}
return resp, nil
}
func (p *Provider) Stream(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (ai.Stream, error) {
return nil, fmt.Errorf("streaming not yet implemented for gemini provider")
}
func (p *Provider) callAPI(ctx context.Context, req map[string]any) (*ai.Response, []map[string]any, error) {
reqBody, err := json.Marshal(req)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal request: %w", err)
}
apiURL := strings.TrimRight(p.opts.BaseURL, "/") +
"/v1beta/models/" + p.opts.Model + ":generateContent"
httpReq, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewReader(reqBody))
if err != nil {
return nil, nil, fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("x-goog-api-key", p.opts.APIKey)
httpResp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, nil, fmt.Errorf("API request failed: %w", err)
}
defer httpResp.Body.Close()
respBody, _ := io.ReadAll(httpResp.Body)
if httpResp.StatusCode != 200 {
return nil, nil, fmt.Errorf("API error (%s): %s", httpResp.Status, string(respBody))
}
var geminiResp struct {
Candidates []struct {
Content struct {
Parts []struct {
Text string `json:"text"`
FunctionCall *functionCallPB `json:"functionCall"`
} `json:"parts"`
} `json:"content"`
} `json:"candidates"`
}
if err := json.Unmarshal(respBody, &geminiResp); err != nil {
return nil, nil, fmt.Errorf("failed to parse response: %w", err)
}
if len(geminiResp.Candidates) == 0 {
return nil, nil, fmt.Errorf("no response from API")
}
parts := geminiResp.Candidates[0].Content.Parts
response := &ai.Response{}
var replyParts []string
var rawParts []map[string]any
for _, part := range parts {
if part.Text != "" {
replyParts = append(replyParts, part.Text)
rawParts = append(rawParts, map[string]any{"text": part.Text})
}
if part.FunctionCall != nil {
response.ToolCalls = append(response.ToolCalls, ai.ToolCall{
ID: part.FunctionCall.ID,
Name: part.FunctionCall.Name,
Input: part.FunctionCall.Args,
})
rawParts = append(rawParts, map[string]any{
"functionCall": map[string]any{
"id": part.FunctionCall.ID,
"name": part.FunctionCall.Name,
"args": part.FunctionCall.Args,
},
})
}
}
if len(replyParts) > 0 {
response.Reply = strings.Join(replyParts, "\n")
}
return response, rawParts, nil
}
type functionCallPB struct {
ID string `json:"id"`
Name string `json:"name"`
Args map[string]any `json:"args"`
}
-104
View File
@@ -1,104 +0,0 @@
package gemini
import (
"context"
"testing"
"go-micro.dev/v5/ai"
)
func TestProvider_String(t *testing.T) {
p := NewProvider()
if p.String() != "gemini" {
t.Errorf("Expected provider name 'gemini', got '%s'", p.String())
}
}
func TestProvider_Init(t *testing.T) {
p := NewProvider()
err := p.Init(
ai.WithModel("gemini-2.0-flash"),
ai.WithAPIKey("test-key"),
ai.WithBaseURL("https://test.com"),
)
if err != nil {
t.Fatalf("Init failed: %v", err)
}
opts := p.Options()
if opts.Model != "gemini-2.0-flash" {
t.Errorf("Expected model 'gemini-2.0-flash', got '%s'", opts.Model)
}
if opts.APIKey != "test-key" {
t.Errorf("Expected API key 'test-key', got '%s'", opts.APIKey)
}
if opts.BaseURL != "https://test.com" {
t.Errorf("Expected base URL 'https://test.com', got '%s'", opts.BaseURL)
}
}
func TestProvider_Options(t *testing.T) {
p := NewProvider(
ai.WithModel("custom-model"),
ai.WithAPIKey("my-key"),
)
opts := p.Options()
if opts.Model != "custom-model" {
t.Errorf("Expected model 'custom-model', got '%s'", opts.Model)
}
if opts.APIKey != "my-key" {
t.Errorf("Expected API key 'my-key', got '%s'", opts.APIKey)
}
}
func TestProvider_Defaults(t *testing.T) {
p := NewProvider()
opts := p.Options()
if opts.Model != "gemini-2.5-flash" {
t.Errorf("Expected default model 'gemini-2.5-flash', got '%s'", opts.Model)
}
if opts.BaseURL != "https://generativelanguage.googleapis.com" {
t.Errorf("Expected default base URL 'https://generativelanguage.googleapis.com', got '%s'", opts.BaseURL)
}
}
func TestProvider_Generate_NoAPIKey(t *testing.T) {
p := NewProvider()
req := &ai.Request{
Prompt: "Hello",
SystemPrompt: "You are helpful",
}
_, err := p.Generate(context.Background(), req)
if err == nil {
t.Error("Expected error when API key is missing, got nil")
}
}
func TestProvider_Stream_NotImplemented(t *testing.T) {
p := NewProvider()
req := &ai.Request{
Prompt: "Hello",
}
_, err := p.Stream(context.Background(), req)
if err == nil {
t.Error("Expected error for unimplemented streaming, got nil")
}
}
func TestProvider_Registration(t *testing.T) {
m := ai.New("gemini", ai.WithAPIKey("test"))
if m == nil {
t.Fatal("ai.New('gemini') returned nil — provider not registered")
}
if m.String() != "gemini" {
t.Errorf("Expected 'gemini', got '%s'", m.String())
}
}
-194
View File
@@ -1,194 +0,0 @@
// Package groq implements the Groq model provider.
//
// Groq provides ultra-fast inference for open-weight models via an
// OpenAI-compatible chat completions endpoint.
//
// Usage:
//
// import _ "go-micro.dev/v5/ai/groq"
//
// m := ai.New("groq",
// ai.WithAPIKey("your-api-key"),
// )
package groq
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"go-micro.dev/v5/ai"
)
func init() {
ai.Register("groq", func(opts ...ai.Option) ai.Model {
return NewProvider(opts...)
})
}
type Provider struct {
opts ai.Options
}
func NewProvider(opts ...ai.Option) *Provider {
options := ai.NewOptions(opts...)
if options.Model == "" {
options.Model = "llama-3.3-70b-versatile"
}
if options.BaseURL == "" {
options.BaseURL = "https://api.groq.com/openai"
}
return &Provider{opts: options}
}
func (p *Provider) Init(opts ...ai.Option) error {
for _, o := range opts {
o(&p.opts)
}
return nil
}
func (p *Provider) Options() ai.Options { return p.opts }
func (p *Provider) String() string { return "groq" }
func (p *Provider) Generate(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (*ai.Response, error) {
var tools []map[string]any
for _, t := range req.Tools {
tools = append(tools, map[string]any{
"type": "function",
"function": map[string]any{
"name": t.Name,
"description": t.Description,
"parameters": map[string]any{
"type": "object",
"properties": t.Properties,
},
},
})
}
messages := []map[string]any{
{"role": "system", "content": req.SystemPrompt},
{"role": "user", "content": req.Prompt},
}
apiReq := map[string]any{
"model": p.opts.Model,
"messages": messages,
}
if len(tools) > 0 {
apiReq["tools"] = tools
}
resp, rawMessage, err := p.callAPI(ctx, apiReq)
if err != nil {
return nil, err
}
if len(resp.ToolCalls) == 0 {
return resp, nil
}
if p.opts.ToolHandler != nil {
followUpMessages := append(messages, map[string]any{
"role": "assistant",
"content": rawMessage["content"],
"tool_calls": rawMessage["tool_calls"],
})
for _, tc := range resp.ToolCalls {
_, content := p.opts.ToolHandler(tc.Name, tc.Input)
followUpMessages = append(followUpMessages, map[string]any{
"role": "tool",
"tool_call_id": tc.ID,
"content": content,
})
}
followUpResp, _, err := p.callAPI(ctx, map[string]any{
"model": p.opts.Model,
"messages": followUpMessages,
})
if err == nil && followUpResp.Reply != "" {
resp.Answer = followUpResp.Reply
}
}
return resp, nil
}
func (p *Provider) Stream(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (ai.Stream, error) {
return nil, fmt.Errorf("streaming not yet implemented for groq provider")
}
func (p *Provider) callAPI(ctx context.Context, req map[string]any) (*ai.Response, map[string]any, error) {
reqBody, err := json.Marshal(req)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal request: %w", err)
}
apiURL := strings.TrimRight(p.opts.BaseURL, "/") + "/v1/chat/completions"
httpReq, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewReader(reqBody))
if err != nil {
return nil, nil, fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+p.opts.APIKey)
httpResp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, nil, fmt.Errorf("API request failed: %w", err)
}
defer httpResp.Body.Close()
respBody, _ := io.ReadAll(httpResp.Body)
if httpResp.StatusCode != 200 {
return nil, nil, fmt.Errorf("API error (%s): %s", httpResp.Status, string(respBody))
}
var chatResp struct {
Choices []struct {
Message struct {
Content string `json:"content"`
ToolCalls []struct {
ID string `json:"id"`
Function struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
} `json:"function"`
} `json:"tool_calls"`
} `json:"message"`
} `json:"choices"`
}
if err := json.Unmarshal(respBody, &chatResp); err != nil {
return nil, nil, fmt.Errorf("failed to parse response: %w", err)
}
if len(chatResp.Choices) == 0 {
return nil, nil, fmt.Errorf("no response from API")
}
choice := chatResp.Choices[0]
response := &ai.Response{Reply: choice.Message.Content}
for _, tc := range choice.Message.ToolCalls {
var input map[string]any
if err := json.Unmarshal([]byte(tc.Function.Arguments), &input); err != nil {
input = map[string]any{}
}
response.ToolCalls = append(response.ToolCalls, ai.ToolCall{
ID: tc.ID,
Name: tc.Function.Name,
Input: input,
})
}
rawMessage := map[string]any{
"content": choice.Message.Content,
"tool_calls": choice.Message.ToolCalls,
}
return response, rawMessage, nil
}
-56
View File
@@ -1,56 +0,0 @@
package groq
import (
"context"
"testing"
"go-micro.dev/v5/ai"
)
func TestProvider_String(t *testing.T) {
if NewProvider().String() != "groq" {
t.Errorf("got %q", NewProvider().String())
}
}
func TestProvider_Defaults(t *testing.T) {
opts := NewProvider().Options()
if opts.Model != "llama-3.3-70b-versatile" {
t.Errorf("default model = %q", opts.Model)
}
if opts.BaseURL != "https://api.groq.com/openai" {
t.Errorf("default base URL = %q", opts.BaseURL)
}
}
func TestProvider_Init(t *testing.T) {
p := NewProvider()
if err := p.Init(ai.WithModel("m"), ai.WithAPIKey("k")); err != nil {
t.Fatal(err)
}
if p.Options().Model != "m" || p.Options().APIKey != "k" {
t.Error("Init did not apply options")
}
}
func TestProvider_Generate_NoAPIKey(t *testing.T) {
if _, err := NewProvider().Generate(context.Background(), &ai.Request{Prompt: "hi"}); err == nil {
t.Error("expected error without API key")
}
}
func TestProvider_Stream_NotImplemented(t *testing.T) {
if _, err := NewProvider().Stream(context.Background(), &ai.Request{Prompt: "hi"}); err == nil {
t.Error("expected error")
}
}
func TestProvider_Registration(t *testing.T) {
m := ai.New("groq", ai.WithAPIKey("test"))
if m == nil {
t.Fatal("provider not registered")
}
if m.String() != "groq" {
t.Errorf("got %q", m.String())
}
}
-45
View File
@@ -1,45 +0,0 @@
package ai
// History is a convenience for accumulating conversation messages
// with automatic truncation. Use it to build Request.Messages for
// multi-turn conversations.
//
// hist := ai.NewHistory(50)
// hist.Add("user", "hello")
// resp, _ := m.Generate(ctx, &ai.Request{Messages: hist.Messages(), Prompt: "next"})
// hist.Add("assistant", resp.Reply)
type History struct {
messages []Message
limit int
}
// NewHistory creates an empty History. limit controls the maximum
// number of messages retained (0 = unlimited).
func NewHistory(limit int) *History {
return &History{limit: limit}
}
// Add appends a message and truncates if over limit.
func (h *History) Add(role string, content any) {
h.messages = append(h.messages, Message{Role: role, Content: content})
if h.limit > 0 && len(h.messages) > h.limit {
h.messages = h.messages[len(h.messages)-h.limit:]
}
}
// Messages returns a copy of the accumulated messages.
func (h *History) Messages() []Message {
out := make([]Message, len(h.messages))
copy(out, h.messages)
return out
}
// Len returns the number of messages.
func (h *History) Len() int {
return len(h.messages)
}
// Reset clears all messages.
func (h *History) Reset() {
h.messages = nil
}
-62
View File
@@ -1,62 +0,0 @@
package ai
import "testing"
func TestHistory_Add(t *testing.T) {
h := NewHistory(0)
h.Add("user", "hello")
h.Add("assistant", "hi")
if h.Len() != 2 {
t.Errorf("len = %d, want 2", h.Len())
}
msgs := h.Messages()
if msgs[0].Role != "user" || msgs[0].Content != "hello" {
t.Errorf("first = %+v", msgs[0])
}
if msgs[1].Role != "assistant" || msgs[1].Content != "hi" {
t.Errorf("second = %+v", msgs[1])
}
}
func TestHistory_Truncation(t *testing.T) {
h := NewHistory(3)
for _, m := range []string{"a", "b", "c", "d", "e"} {
h.Add("user", m)
}
if h.Len() != 3 {
t.Errorf("len = %d, want 3", h.Len())
}
if h.Messages()[0].Content != "c" {
t.Errorf("first retained = %+v", h.Messages()[0])
}
}
func TestHistory_Reset(t *testing.T) {
h := NewHistory(0)
h.Add("user", "hello")
h.Reset()
if h.Len() != 0 {
t.Errorf("len after reset = %d", h.Len())
}
}
func TestHistory_SnapshotIsCopy(t *testing.T) {
h := NewHistory(0)
h.Add("user", "hello")
msgs := h.Messages()
msgs[0].Content = "mutated"
if h.Messages()[0].Content == "mutated" {
t.Error("snapshot returned reference, not copy")
}
}
func TestHistory_Unlimited(t *testing.T) {
h := NewHistory(0)
for i := 0; i < 100; i++ {
h.Add("user", "msg")
}
if h.Len() != 100 {
t.Errorf("len = %d, want 100", h.Len())
}
}
-65
View File
@@ -1,65 +0,0 @@
package ai
import "context"
// ImageModel provides an interface for image generation providers.
// Providers that support image generation implement this alongside
// or instead of Model. Use NewImage to construct, or type-assert
// a provider that implements both:
//
// p := atlascloud.NewProvider(ai.WithAPIKey(key))
// if ig, ok := p.(ai.ImageModel); ok {
// resp, _ := ig.GenerateImage(ctx, req)
// }
type ImageModel interface {
GenerateImage(ctx context.Context, req *ImageRequest, opts ...GenerateOption) (*ImageResponse, error)
String() string
}
// ImageRequest describes what image to generate.
type ImageRequest struct {
// Prompt is the text description of the image to generate.
Prompt string
// Model overrides the provider's default image model.
Model string
// Size of the generated image (e.g. "1024x1024"). Provider-specific.
Size string
// N is the number of images to generate. Defaults to 1.
N int
// Quality controls generation quality. Provider-specific (e.g. "low", "medium", "high").
Quality string
// OutputFormat sets the image format (e.g. "png", "jpeg"). Provider-specific.
OutputFormat string
}
// ImageResponse holds the generated images.
type ImageResponse struct {
Images []Image
}
// Image is a single generated image, returned as a URL, base64 data, or both
// depending on the provider and request options.
type Image struct {
// URL is a remote URL where the image can be fetched.
URL string
// Base64 is the base64-encoded image data.
Base64 string
}
// NewImageFunc creates a new ImageModel instance.
type NewImageFunc func(...Option) ImageModel
var imageProviders = make(map[string]NewImageFunc)
// RegisterImage registers an image generation provider.
func RegisterImage(name string, fn NewImageFunc) {
imageProviders[name] = fn
}
// NewImage creates a new ImageModel instance based on the provider name.
func NewImage(provider string, opts ...Option) ImageModel {
if fn, ok := imageProviders[provider]; ok {
return fn(opts...)
}
return nil
}
-194
View File
@@ -1,194 +0,0 @@
// Package mistral implements the Mistral AI model provider.
//
// Mistral AI is a European AI company offering high-performance models
// via an OpenAI-compatible chat completions endpoint.
//
// Usage:
//
// import _ "go-micro.dev/v5/ai/mistral"
//
// m := ai.New("mistral",
// ai.WithAPIKey("your-api-key"),
// )
package mistral
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"go-micro.dev/v5/ai"
)
func init() {
ai.Register("mistral", func(opts ...ai.Option) ai.Model {
return NewProvider(opts...)
})
}
type Provider struct {
opts ai.Options
}
func NewProvider(opts ...ai.Option) *Provider {
options := ai.NewOptions(opts...)
if options.Model == "" {
options.Model = "mistral-large-latest"
}
if options.BaseURL == "" {
options.BaseURL = "https://api.mistral.ai"
}
return &Provider{opts: options}
}
func (p *Provider) Init(opts ...ai.Option) error {
for _, o := range opts {
o(&p.opts)
}
return nil
}
func (p *Provider) Options() ai.Options { return p.opts }
func (p *Provider) String() string { return "mistral" }
func (p *Provider) Generate(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (*ai.Response, error) {
var tools []map[string]any
for _, t := range req.Tools {
tools = append(tools, map[string]any{
"type": "function",
"function": map[string]any{
"name": t.Name,
"description": t.Description,
"parameters": map[string]any{
"type": "object",
"properties": t.Properties,
},
},
})
}
messages := []map[string]any{
{"role": "system", "content": req.SystemPrompt},
{"role": "user", "content": req.Prompt},
}
apiReq := map[string]any{
"model": p.opts.Model,
"messages": messages,
}
if len(tools) > 0 {
apiReq["tools"] = tools
}
resp, rawMessage, err := p.callAPI(ctx, apiReq)
if err != nil {
return nil, err
}
if len(resp.ToolCalls) == 0 {
return resp, nil
}
if p.opts.ToolHandler != nil {
followUpMessages := append(messages, map[string]any{
"role": "assistant",
"content": rawMessage["content"],
"tool_calls": rawMessage["tool_calls"],
})
for _, tc := range resp.ToolCalls {
_, content := p.opts.ToolHandler(tc.Name, tc.Input)
followUpMessages = append(followUpMessages, map[string]any{
"role": "tool",
"tool_call_id": tc.ID,
"content": content,
})
}
followUpResp, _, err := p.callAPI(ctx, map[string]any{
"model": p.opts.Model,
"messages": followUpMessages,
})
if err == nil && followUpResp.Reply != "" {
resp.Answer = followUpResp.Reply
}
}
return resp, nil
}
func (p *Provider) Stream(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (ai.Stream, error) {
return nil, fmt.Errorf("streaming not yet implemented for mistral provider")
}
func (p *Provider) callAPI(ctx context.Context, req map[string]any) (*ai.Response, map[string]any, error) {
reqBody, err := json.Marshal(req)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal request: %w", err)
}
apiURL := strings.TrimRight(p.opts.BaseURL, "/") + "/v1/chat/completions"
httpReq, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewReader(reqBody))
if err != nil {
return nil, nil, fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+p.opts.APIKey)
httpResp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, nil, fmt.Errorf("API request failed: %w", err)
}
defer httpResp.Body.Close()
respBody, _ := io.ReadAll(httpResp.Body)
if httpResp.StatusCode != 200 {
return nil, nil, fmt.Errorf("API error (%s): %s", httpResp.Status, string(respBody))
}
var chatResp struct {
Choices []struct {
Message struct {
Content string `json:"content"`
ToolCalls []struct {
ID string `json:"id"`
Function struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
} `json:"function"`
} `json:"tool_calls"`
} `json:"message"`
} `json:"choices"`
}
if err := json.Unmarshal(respBody, &chatResp); err != nil {
return nil, nil, fmt.Errorf("failed to parse response: %w", err)
}
if len(chatResp.Choices) == 0 {
return nil, nil, fmt.Errorf("no response from API")
}
choice := chatResp.Choices[0]
response := &ai.Response{Reply: choice.Message.Content}
for _, tc := range choice.Message.ToolCalls {
var input map[string]any
if err := json.Unmarshal([]byte(tc.Function.Arguments), &input); err != nil {
input = map[string]any{}
}
response.ToolCalls = append(response.ToolCalls, ai.ToolCall{
ID: tc.ID,
Name: tc.Function.Name,
Input: input,
})
}
rawMessage := map[string]any{
"content": choice.Message.Content,
"tool_calls": choice.Message.ToolCalls,
}
return response, rawMessage, nil
}
-56
View File
@@ -1,56 +0,0 @@
package mistral
import (
"context"
"testing"
"go-micro.dev/v5/ai"
)
func TestProvider_String(t *testing.T) {
if NewProvider().String() != "mistral" {
t.Errorf("got %q", NewProvider().String())
}
}
func TestProvider_Defaults(t *testing.T) {
opts := NewProvider().Options()
if opts.Model != "mistral-large-latest" {
t.Errorf("default model = %q", opts.Model)
}
if opts.BaseURL != "https://api.mistral.ai" {
t.Errorf("default base URL = %q", opts.BaseURL)
}
}
func TestProvider_Init(t *testing.T) {
p := NewProvider()
if err := p.Init(ai.WithModel("m"), ai.WithAPIKey("k")); err != nil {
t.Fatal(err)
}
if p.Options().Model != "m" || p.Options().APIKey != "k" {
t.Error("Init did not apply options")
}
}
func TestProvider_Generate_NoAPIKey(t *testing.T) {
if _, err := NewProvider().Generate(context.Background(), &ai.Request{Prompt: "hi"}); err == nil {
t.Error("expected error without API key")
}
}
func TestProvider_Stream_NotImplemented(t *testing.T) {
if _, err := NewProvider().Stream(context.Background(), &ai.Request{Prompt: "hi"}); err == nil {
t.Error("expected error")
}
}
func TestProvider_Registration(t *testing.T) {
m := ai.New("mistral", ai.WithAPIKey("test"))
if m == nil {
t.Fatal("provider not registered")
}
if m.String() != "mistral" {
t.Errorf("got %q", m.String())
}
}
-144
View File
@@ -1,144 +0,0 @@
// Package ai provides abstraction for AI model providers
package ai
import (
"context"
"strings"
)
// Model provides an interface for interacting with AI model providers
type Model interface {
// Init initializes the model with options
Init(...Option) error
// Options returns the model options
Options() Options
// Generate generates a response from the model
Generate(ctx context.Context, req *Request, opts ...GenerateOption) (*Response, error)
// Stream generates a streaming response (for future implementation)
Stream(ctx context.Context, req *Request, opts ...GenerateOption) (Stream, error)
// String returns the name of the provider
String() string
}
// Tool represents a tool/function that can be called by the model
type Tool struct {
Name string // LLM-safe name (e.g., "greeter_Greeter_Hello")
OriginalName string // Original name (e.g., "greeter.Greeter.Hello")
Description string
Properties map[string]any // JSON schema for tool parameters
}
// Request represents a request to generate content from a model
type Request struct {
// Prompt is the user's message/prompt
Prompt string
// SystemPrompt is the system instruction for the model
SystemPrompt string
// Tools available for the model to use
Tools []Tool
// Messages for continuing a conversation (optional).
// Use ai.History to accumulate these across turns.
Messages []Message
}
// Message represents a conversation message
type Message struct {
Role string // "user", "assistant", "system", "tool"
Content any // Can be string or structured content
}
// Response represents the response from a model
type Response struct {
// Reply is the text response from the model
Reply string
// ToolCalls are tool calls requested by the model
ToolCalls []ToolCall
// Answer is the final answer after tool execution (if tools were used)
Answer string
}
// ToolCall represents a request to call a tool and its result
type ToolCall struct {
ID string // Tool call ID (for correlation)
Name string // Tool name
Input map[string]any // Tool input arguments
Result string // Tool execution result (populated after execution)
Error string // Tool execution error (populated after execution)
}
// ToolResult represents the result of a tool execution
type ToolResult struct {
ID string // Tool call ID (for correlation)
Content string // Tool execution result (JSON string)
}
// Stream is the interface for streaming responses (future implementation)
type Stream interface {
// Recv receives the next chunk of the response
Recv() (*Response, error)
// Close closes the stream
Close() error
}
// ToolHandler is a function that handles tool calls
type ToolHandler func(name string, input map[string]any) (result any, content string)
// NewFunc creates a new Model instance
type NewFunc func(...Option) Model
var providers = make(map[string]NewFunc)
// Register registers a model provider
func Register(name string, fn NewFunc) {
providers[name] = fn
}
// New creates a new Model instance based on the provider name
func New(provider string, opts ...Option) Model {
if fn, ok := providers[provider]; ok {
return fn(opts...)
}
// Default to first registered provider
if len(providers) > 0 {
for _, fn := range providers {
return fn(opts...)
}
}
return nil
}
// AutoDetectProvider attempts to detect the provider from the base URL
func AutoDetectProvider(baseURL string) string {
if baseURL == "" {
return "openai"
}
switch {
case strings.Contains(baseURL, "anthropic"):
return "anthropic"
case strings.Contains(baseURL, "atlascloud"):
return "atlascloud"
case strings.Contains(baseURL, "googleapis.com"), strings.Contains(baseURL, "google"):
return "gemini"
case strings.Contains(baseURL, "groq"):
return "groq"
case strings.Contains(baseURL, "mistral"):
return "mistral"
case strings.Contains(baseURL, "together"):
return "together"
default:
return "openai"
}
}
// DefaultModel is a default model instance
var DefaultModel Model
// Generate generates a response using the default model.
func Generate(ctx context.Context, req *Request, opts ...GenerateOption) (*Response, error) {
if DefaultModel == nil {
return nil, nil
}
return DefaultModel.Generate(ctx, req, opts...)
}
-297
View File
@@ -1,297 +0,0 @@
// Package openai implements the OpenAI model provider
package openai
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"go-micro.dev/v5/ai"
)
func init() {
ai.Register("openai", func(opts ...ai.Option) ai.Model {
return NewProvider(opts...)
})
ai.RegisterImage("openai", func(opts ...ai.Option) ai.ImageModel {
return NewProvider(opts...)
})
}
// Provider implements the ai.Model interface for OpenAI
type Provider struct {
opts ai.Options
}
// NewProvider creates a new OpenAI provider
func NewProvider(opts ...ai.Option) *Provider {
options := ai.NewOptions(opts...)
// Set defaults if not provided
if options.Model == "" {
options.Model = "gpt-4o"
}
if options.BaseURL == "" {
options.BaseURL = "https://api.openai.com"
}
return &Provider{
opts: options,
}
}
// Init initializes the provider with options
func (p *Provider) Init(opts ...ai.Option) error {
for _, o := range opts {
o(&p.opts)
}
return nil
}
// Options returns the provider options
func (p *Provider) Options() ai.Options {
return p.opts
}
// String returns the provider name
func (p *Provider) String() string {
return "openai"
}
// Generate generates a response from the model
func (p *Provider) Generate(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (*ai.Response, error) {
// Build tools for OpenAI format
var openaiTools []map[string]any
for _, t := range req.Tools {
openaiTools = append(openaiTools, map[string]any{
"type": "function",
"function": map[string]any{
"name": t.Name,
"description": t.Description,
"parameters": map[string]any{
"type": "object",
"properties": t.Properties,
},
},
})
}
// Build messages
messages := []map[string]any{
{"role": "system", "content": req.SystemPrompt},
{"role": "user", "content": req.Prompt},
}
// Build initial request
apiReq := map[string]any{
"model": p.opts.Model,
"messages": messages,
}
if len(openaiTools) > 0 {
apiReq["tools"] = openaiTools
}
// Make API call
resp, rawMessage, err := p.callAPI(ctx, apiReq)
if err != nil {
return nil, err
}
// If no tool calls, return response
if len(resp.ToolCalls) == 0 {
return resp, nil
}
// If tool handler is provided, execute tools and get final answer
if p.opts.ToolHandler != nil {
// Build follow-up messages
followUpMessages := append(messages, map[string]any{
"role": "assistant",
"content": rawMessage["content"],
"tool_calls": rawMessage["tool_calls"],
})
for _, tc := range resp.ToolCalls {
_, content := p.opts.ToolHandler(tc.Name, tc.Input)
followUpMessages = append(followUpMessages, map[string]any{
"role": "tool",
"tool_call_id": tc.ID,
"content": content,
})
}
followUpReq := map[string]any{
"model": p.opts.Model,
"messages": followUpMessages,
}
// Make follow-up API call
followUpResp, _, err := p.callAPI(ctx, followUpReq)
if err == nil && followUpResp.Reply != "" {
resp.Answer = followUpResp.Reply
}
}
return resp, nil
}
// Stream generates a streaming response (not yet implemented)
func (p *Provider) Stream(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (ai.Stream, error) {
return nil, fmt.Errorf("streaming not yet implemented for openai provider")
}
// callAPI makes an HTTP request to the OpenAI API
func (p *Provider) callAPI(ctx context.Context, req map[string]any) (*ai.Response, map[string]any, error) {
// Marshal request
reqBody, err := json.Marshal(req)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal request: %w", err)
}
// Build HTTP request
apiURL := strings.TrimRight(p.opts.BaseURL, "/") + "/v1/chat/completions"
httpReq, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewReader(reqBody))
if err != nil {
return nil, nil, fmt.Errorf("failed to create request: %w", err)
}
// Set headers
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+p.opts.APIKey)
// Make request
httpResp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, nil, fmt.Errorf("API request failed: %w", err)
}
defer httpResp.Body.Close()
// Read response
respBody, _ := io.ReadAll(httpResp.Body)
if httpResp.StatusCode != 200 {
return nil, nil, fmt.Errorf("API error (%s): %s", httpResp.Status, string(respBody))
}
// Parse response
var chatResp struct {
Choices []struct {
Message struct {
Content string `json:"content"`
ToolCalls []struct {
ID string `json:"id"`
Function struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
} `json:"function"`
} `json:"tool_calls"`
} `json:"message"`
} `json:"choices"`
}
if err := json.Unmarshal(respBody, &chatResp); err != nil {
return nil, nil, fmt.Errorf("failed to parse response: %w", err)
}
if len(chatResp.Choices) == 0 {
return nil, nil, fmt.Errorf("no response from API")
}
choice := chatResp.Choices[0]
response := &ai.Response{
Reply: choice.Message.Content,
}
// Extract tool calls
for _, tc := range choice.Message.ToolCalls {
var input map[string]any
if err := json.Unmarshal([]byte(tc.Function.Arguments), &input); err != nil {
input = map[string]any{}
}
response.ToolCalls = append(response.ToolCalls, ai.ToolCall{
ID: tc.ID,
Name: tc.Function.Name,
Input: input,
})
}
// Return raw message for potential follow-up
rawMessage := map[string]any{
"content": choice.Message.Content,
"tool_calls": choice.Message.ToolCalls,
}
return response, rawMessage, nil
}
const defaultImageModel = "gpt-image-1"
func (p *Provider) GenerateImage(ctx context.Context, req *ai.ImageRequest, opts ...ai.GenerateOption) (*ai.ImageResponse, error) {
model := req.Model
if model == "" {
model = defaultImageModel
}
n := req.N
if n <= 0 {
n = 1
}
apiReq := map[string]any{
"model": model,
"prompt": req.Prompt,
"n": n,
}
if req.Size != "" {
apiReq["size"] = req.Size
}
reqBody, err := json.Marshal(apiReq)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
apiURL := strings.TrimRight(p.opts.BaseURL, "/") + "/v1/images/generations"
httpReq, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewReader(reqBody))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+p.opts.APIKey)
httpResp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("API request failed: %w", err)
}
defer httpResp.Body.Close()
respBody, _ := io.ReadAll(httpResp.Body)
if httpResp.StatusCode != 200 {
return nil, fmt.Errorf("API error (%s): %s", httpResp.Status, string(respBody))
}
var imgResp struct {
Data []struct {
URL string `json:"url"`
B64JSON string `json:"b64_json"`
} `json:"data"`
}
if err := json.Unmarshal(respBody, &imgResp); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
response := &ai.ImageResponse{}
for _, d := range imgResp.Data {
response.Images = append(response.Images, ai.Image{
URL: d.URL,
Base64: d.B64JSON,
})
}
return response, nil
}
-116
View File
@@ -1,116 +0,0 @@
package openai
import (
"context"
"testing"
"go-micro.dev/v5/ai"
)
func TestProvider_String(t *testing.T) {
p := NewProvider()
if p.String() != "openai" {
t.Errorf("Expected provider name 'openai', got '%s'", p.String())
}
}
func TestProvider_Init(t *testing.T) {
p := NewProvider()
err := p.Init(
ai.WithModel("test-model"),
ai.WithAPIKey("test-key"),
ai.WithBaseURL("https://test.com"),
)
if err != nil {
t.Fatalf("Init failed: %v", err)
}
opts := p.Options()
if opts.Model != "test-model" {
t.Errorf("Expected model 'test-model', got '%s'", opts.Model)
}
if opts.APIKey != "test-key" {
t.Errorf("Expected API key 'test-key', got '%s'", opts.APIKey)
}
if opts.BaseURL != "https://test.com" {
t.Errorf("Expected base URL 'https://test.com', got '%s'", opts.BaseURL)
}
}
func TestProvider_Options(t *testing.T) {
p := NewProvider(
ai.WithModel("custom-model"),
ai.WithAPIKey("my-key"),
)
opts := p.Options()
if opts.Model != "custom-model" {
t.Errorf("Expected model 'custom-model', got '%s'", opts.Model)
}
if opts.APIKey != "my-key" {
t.Errorf("Expected API key 'my-key', got '%s'", opts.APIKey)
}
}
func TestProvider_Defaults(t *testing.T) {
p := NewProvider()
opts := p.Options()
if opts.Model != "gpt-4o" {
t.Errorf("Expected default model 'gpt-4o', got '%s'", opts.Model)
}
if opts.BaseURL != "https://api.openai.com" {
t.Errorf("Expected default base URL 'https://api.openai.com', got '%s'", opts.BaseURL)
}
}
func TestProvider_Generate_NoAPIKey(t *testing.T) {
p := NewProvider()
req := &ai.Request{
Prompt: "Hello",
SystemPrompt: "You are helpful",
}
_, err := p.Generate(context.Background(), req)
if err == nil {
t.Error("Expected error when API key is missing, got nil")
}
}
func TestProvider_Stream_NotImplemented(t *testing.T) {
p := NewProvider()
req := &ai.Request{
Prompt: "Hello",
}
_, err := p.Stream(context.Background(), req)
if err == nil {
t.Error("Expected error for unimplemented streaming, got nil")
}
}
func TestProvider_ImageRegistration(t *testing.T) {
ig := ai.NewImage("openai", ai.WithAPIKey("test"))
if ig == nil {
t.Fatal("ai.NewImage('openai') returned nil — image provider not registered")
}
if ig.String() != "openai" {
t.Errorf("Expected 'openai', got '%s'", ig.String())
}
}
func TestProvider_GenerateImage_NoAPIKey(t *testing.T) {
p := NewProvider()
_, err := p.GenerateImage(context.Background(), &ai.ImageRequest{Prompt: "a cat"})
if err == nil {
t.Error("Expected error when API key is missing, got nil")
}
}
func TestProvider_ImplementsImageModel(t *testing.T) {
var _ ai.ImageModel = (*Provider)(nil)
}
-93
View File
@@ -1,93 +0,0 @@
package ai
import (
"context"
)
// Options for model configuration
type Options struct {
// Context for the model
Context context.Context
// Model name (e.g., "gpt-4o", "claude-sonnet-4-20250514")
Model string
// APIKey for authentication
APIKey string
// BaseURL for the API endpoint
BaseURL string
// ToolHandler handles tool calls (optional, for automatic tool execution)
ToolHandler ToolHandler
}
// GenerateOptions for generate call
type GenerateOptions struct {
// Context for this specific generate call
Context context.Context
}
// Option is a function that modifies Options
type Option func(*Options)
// GenerateOption is a function that modifies GenerateOptions
type GenerateOption func(*GenerateOptions)
// NewOptions creates new Options with defaults
func NewOptions(opts ...Option) Options {
options := Options{
Context: context.Background(),
}
for _, o := range opts {
o(&options)
}
return options
}
// WithModel sets the model name
func WithModel(m string) Option {
return func(o *Options) {
o.Model = m
}
}
// WithAPIKey sets the API key
func WithAPIKey(key string) Option {
return func(o *Options) {
o.APIKey = key
}
}
// WithBaseURL sets the base URL
func WithBaseURL(url string) Option {
return func(o *Options) {
o.BaseURL = url
}
}
// WithContext sets the context
func WithContext(ctx context.Context) Option {
return func(o *Options) {
o.Context = ctx
}
}
// WithToolHandler sets the tool handler
func WithToolHandler(handler ToolHandler) Option {
return func(o *Options) {
o.ToolHandler = handler
}
}
// WithTools wires a Tools instance into the model, setting the tool
// handler so the model can execute discovered service endpoints. The
// tool list itself is passed per-request via Request.Tools.
//
// tools := ai.NewTools(service.Registry())
// list, _ := tools.Discover()
// m := ai.New("anthropic", ai.WithAPIKey(key), ai.WithTools(tools))
// resp, _ := m.Generate(ctx, &ai.Request{Prompt: input, Tools: list})
func WithTools(t *Tools) Option {
return func(o *Options) {
if t != nil {
o.ToolHandler = t.Handler()
}
}
}
-194
View File
@@ -1,194 +0,0 @@
// Package together implements the Together AI model provider.
//
// Together AI provides fast inference for open-weight models via an
// OpenAI-compatible chat completions endpoint.
//
// Usage:
//
// import _ "go-micro.dev/v5/ai/together"
//
// m := ai.New("together",
// ai.WithAPIKey("your-api-key"),
// )
package together
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"go-micro.dev/v5/ai"
)
func init() {
ai.Register("together", func(opts ...ai.Option) ai.Model {
return NewProvider(opts...)
})
}
type Provider struct {
opts ai.Options
}
func NewProvider(opts ...ai.Option) *Provider {
options := ai.NewOptions(opts...)
if options.Model == "" {
options.Model = "meta-llama/Llama-3.3-70B-Instruct-Turbo"
}
if options.BaseURL == "" {
options.BaseURL = "https://api.together.xyz"
}
return &Provider{opts: options}
}
func (p *Provider) Init(opts ...ai.Option) error {
for _, o := range opts {
o(&p.opts)
}
return nil
}
func (p *Provider) Options() ai.Options { return p.opts }
func (p *Provider) String() string { return "together" }
func (p *Provider) Generate(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (*ai.Response, error) {
var tools []map[string]any
for _, t := range req.Tools {
tools = append(tools, map[string]any{
"type": "function",
"function": map[string]any{
"name": t.Name,
"description": t.Description,
"parameters": map[string]any{
"type": "object",
"properties": t.Properties,
},
},
})
}
messages := []map[string]any{
{"role": "system", "content": req.SystemPrompt},
{"role": "user", "content": req.Prompt},
}
apiReq := map[string]any{
"model": p.opts.Model,
"messages": messages,
}
if len(tools) > 0 {
apiReq["tools"] = tools
}
resp, rawMessage, err := p.callAPI(ctx, apiReq)
if err != nil {
return nil, err
}
if len(resp.ToolCalls) == 0 {
return resp, nil
}
if p.opts.ToolHandler != nil {
followUpMessages := append(messages, map[string]any{
"role": "assistant",
"content": rawMessage["content"],
"tool_calls": rawMessage["tool_calls"],
})
for _, tc := range resp.ToolCalls {
_, content := p.opts.ToolHandler(tc.Name, tc.Input)
followUpMessages = append(followUpMessages, map[string]any{
"role": "tool",
"tool_call_id": tc.ID,
"content": content,
})
}
followUpResp, _, err := p.callAPI(ctx, map[string]any{
"model": p.opts.Model,
"messages": followUpMessages,
})
if err == nil && followUpResp.Reply != "" {
resp.Answer = followUpResp.Reply
}
}
return resp, nil
}
func (p *Provider) Stream(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (ai.Stream, error) {
return nil, fmt.Errorf("streaming not yet implemented for together provider")
}
func (p *Provider) callAPI(ctx context.Context, req map[string]any) (*ai.Response, map[string]any, error) {
reqBody, err := json.Marshal(req)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal request: %w", err)
}
apiURL := strings.TrimRight(p.opts.BaseURL, "/") + "/v1/chat/completions"
httpReq, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewReader(reqBody))
if err != nil {
return nil, nil, fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+p.opts.APIKey)
httpResp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, nil, fmt.Errorf("API request failed: %w", err)
}
defer httpResp.Body.Close()
respBody, _ := io.ReadAll(httpResp.Body)
if httpResp.StatusCode != 200 {
return nil, nil, fmt.Errorf("API error (%s): %s", httpResp.Status, string(respBody))
}
var chatResp struct {
Choices []struct {
Message struct {
Content string `json:"content"`
ToolCalls []struct {
ID string `json:"id"`
Function struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
} `json:"function"`
} `json:"tool_calls"`
} `json:"message"`
} `json:"choices"`
}
if err := json.Unmarshal(respBody, &chatResp); err != nil {
return nil, nil, fmt.Errorf("failed to parse response: %w", err)
}
if len(chatResp.Choices) == 0 {
return nil, nil, fmt.Errorf("no response from API")
}
choice := chatResp.Choices[0]
response := &ai.Response{Reply: choice.Message.Content}
for _, tc := range choice.Message.ToolCalls {
var input map[string]any
if err := json.Unmarshal([]byte(tc.Function.Arguments), &input); err != nil {
input = map[string]any{}
}
response.ToolCalls = append(response.ToolCalls, ai.ToolCall{
ID: tc.ID,
Name: tc.Function.Name,
Input: input,
})
}
rawMessage := map[string]any{
"content": choice.Message.Content,
"tool_calls": choice.Message.ToolCalls,
}
return response, rawMessage, nil
}
-56
View File
@@ -1,56 +0,0 @@
package together
import (
"context"
"testing"
"go-micro.dev/v5/ai"
)
func TestProvider_String(t *testing.T) {
if NewProvider().String() != "together" {
t.Errorf("got %q", NewProvider().String())
}
}
func TestProvider_Defaults(t *testing.T) {
opts := NewProvider().Options()
if opts.Model != "meta-llama/Llama-3.3-70B-Instruct-Turbo" {
t.Errorf("default model = %q", opts.Model)
}
if opts.BaseURL != "https://api.together.xyz" {
t.Errorf("default base URL = %q", opts.BaseURL)
}
}
func TestProvider_Init(t *testing.T) {
p := NewProvider()
if err := p.Init(ai.WithModel("m"), ai.WithAPIKey("k")); err != nil {
t.Fatal(err)
}
if p.Options().Model != "m" || p.Options().APIKey != "k" {
t.Error("Init did not apply options")
}
}
func TestProvider_Generate_NoAPIKey(t *testing.T) {
if _, err := NewProvider().Generate(context.Background(), &ai.Request{Prompt: "hi"}); err == nil {
t.Error("expected error without API key")
}
}
func TestProvider_Stream_NotImplemented(t *testing.T) {
if _, err := NewProvider().Stream(context.Background(), &ai.Request{Prompt: "hi"}); err == nil {
t.Error("expected error")
}
}
func TestProvider_Registration(t *testing.T) {
m := ai.New("together", ai.WithAPIKey("test"))
if m == nil {
t.Fatal("provider not registered")
}
if m.String() != "together" {
t.Errorf("got %q", m.String())
}
}
-184
View File
@@ -1,184 +0,0 @@
package ai
import (
"context"
"encoding/json"
"fmt"
"strings"
"sync"
"go-micro.dev/v5/client"
codecBytes "go-micro.dev/v5/codec/bytes"
"go-micro.dev/v5/registry"
)
type toolNameMap struct {
mu sync.RWMutex
m map[string]string
}
func (n *toolNameMap) put(safe, original string) {
n.mu.Lock()
n.m[safe] = original
n.mu.Unlock()
}
func (n *toolNameMap) get(safe string) (string, bool) {
n.mu.RLock()
v, ok := n.m[safe]
n.mu.RUnlock()
return v, ok
}
// Tools discovers go-micro services from a registry and converts their
// endpoints into Tool definitions. It also executes tool calls via RPC.
//
// Create with NewTools, discover the tool list with Discover, and wire
// execution into a model with WithTools:
//
// tools := ai.NewTools(service.Registry())
// list, _ := tools.Discover()
// m := ai.New("anthropic", ai.WithAPIKey(key), ai.WithTools(tools))
// resp, _ := m.Generate(ctx, &ai.Request{Prompt: input, Tools: list})
type Tools struct {
registry registry.Registry
client client.Client
names *toolNameMap
}
// ToolOption configures a Tools instance.
type ToolOption func(*Tools)
// ToolClient sets the client used to execute tool calls. Defaults to
// client.DefaultClient.
func ToolClient(c client.Client) ToolOption {
return func(t *Tools) {
if c != nil {
t.client = c
}
}
}
// NewTools creates a Tools bound to the given registry.
func NewTools(reg registry.Registry, opts ...ToolOption) *Tools {
t := &Tools{
registry: reg,
client: client.DefaultClient,
names: &toolNameMap{m: map[string]string{}},
}
for _, o := range opts {
o(t)
}
return t
}
// Discover walks the registry and returns one Tool per service
// endpoint. Tool names are LLM-safe (dots replaced with underscores).
func (t *Tools) Discover() ([]Tool, error) {
services, err := t.registry.ListServices()
if err != nil {
return nil, err
}
var out []Tool
for _, svc := range services {
full, err := t.registry.GetService(svc.Name)
if err != nil || len(full) == 0 {
continue
}
for _, ep := range full[0].Endpoints {
original := fmt.Sprintf("%s.%s", svc.Name, ep.Name)
safe := strings.ReplaceAll(original, ".", "_")
t.names.put(safe, original)
desc := fmt.Sprintf("Call %s on %s service", ep.Name, svc.Name)
if ep.Metadata != nil {
if d, ok := ep.Metadata["description"]; ok && d != "" {
desc = d
}
}
props := map[string]any{}
if ep.Request != nil {
for _, field := range ep.Request.Values {
props[field.Name] = map[string]any{
"type": toolJSONType(field.Type),
"description": fmt.Sprintf("%s (%s)", field.Name, field.Type),
}
}
}
out = append(out, Tool{
Name: safe,
OriginalName: original,
Description: desc,
Properties: props,
})
}
}
return out, nil
}
// Handler returns a ToolHandler that executes tool calls via RPC using
// the configured client. Tool names may be LLM-safe (underscored) or
// original (dotted). WithTools uses this internally.
func (t *Tools) Handler() ToolHandler {
c := t.client
if c == nil {
c = client.DefaultClient
}
return func(name string, input map[string]any) (any, string) {
if orig, ok := t.names.get(name); ok {
name = orig
}
parts := strings.SplitN(name, ".", 2)
if len(parts) != 2 {
return toolErrResult("invalid tool name: " + name)
}
inputBytes, err := json.Marshal(input)
if err != nil {
return toolErrResult("failed to marshal input: " + err.Error())
}
req := c.NewRequest(parts[0], parts[1], &codecBytes.Frame{Data: inputBytes})
var rsp codecBytes.Frame
if err := c.Call(context.Background(), req, &rsp); err != nil {
return toolErrResult(err.Error())
}
var result any
if err := json.Unmarshal(rsp.Data, &result); err != nil {
result = string(rsp.Data)
}
return result, string(rsp.Data)
}
}
// DiscoverTools is a convenience that discovers tools from a registry
// without creating a Tools instance. For paired discovery + execution,
// create a Tools with NewTools instead.
func DiscoverTools(reg registry.Registry) ([]Tool, error) {
return NewTools(reg).Discover()
}
func toolErrResult(msg string) (any, string) {
encoded, _ := json.Marshal(map[string]string{"error": msg})
return map[string]string{"error": msg}, string(encoded)
}
func toolJSONType(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"
}
}
-115
View File
@@ -1,115 +0,0 @@
package ai
import (
"testing"
"go-micro.dev/v5/registry"
)
func TestToolJSONType(t *testing.T) {
cases := map[string]string{
"string": "string",
"int": "integer",
"int64": "integer",
"float64": "number",
"bool": "boolean",
"User": "object",
"": "object",
}
for in, want := range cases {
if got := toolJSONType(in); got != want {
t.Errorf("toolJSONType(%q) = %q, want %q", in, got, want)
}
}
}
func TestDiscoverTools_Empty(t *testing.T) {
reg := registry.NewMemoryRegistry()
tools, err := DiscoverTools(reg)
if err != nil {
t.Fatalf("DiscoverTools: %v", err)
}
if len(tools) != 0 {
t.Errorf("expected 0 tools, got %d", len(tools))
}
}
func TestDiscoverTools_DiscoversEndpoints(t *testing.T) {
reg := registry.NewMemoryRegistry()
svc := &registry.Service{
Name: "users",
Version: "1.0.0",
Nodes: []*registry.Node{
{Id: "users-1", Address: "127.0.0.1:9000"},
},
Endpoints: []*registry.Endpoint{
{
Name: "Users.Get",
Metadata: map[string]string{
"description": "Fetch a user by ID",
},
Request: &registry.Value{
Name: "GetRequest",
Type: "GetRequest",
Values: []*registry.Value{
{Name: "id", Type: "string"},
{Name: "expand", Type: "bool"},
},
},
},
},
}
if err := reg.Register(svc); err != nil {
t.Fatalf("Register: %v", err)
}
tools, err := DiscoverTools(reg)
if err != nil {
t.Fatalf("DiscoverTools: %v", err)
}
if len(tools) != 1 {
t.Fatalf("expected 1 tool, got %d", len(tools))
}
tool := tools[0]
if tool.Name != "users_Users_Get" {
t.Errorf("safe name = %q", tool.Name)
}
if tool.OriginalName != "users.Users.Get" {
t.Errorf("original = %q", tool.OriginalName)
}
if tool.Description != "Fetch a user by ID" {
t.Errorf("description = %q", tool.Description)
}
}
func TestTools_HandlerResolvesSafeName(t *testing.T) {
tools := NewTools(registry.NewMemoryRegistry())
tools.names.put("users_Users_Get", "users.Users.Get")
resolved, ok := tools.names.get("users_Users_Get")
if !ok || resolved != "users.Users.Get" {
t.Errorf("name map lookup = (%q, %v)", resolved, ok)
}
}
func TestTools_HandlerInvalidName(t *testing.T) {
tools := NewTools(registry.NewMemoryRegistry())
h := tools.Handler()
result, content := h("foo", map[string]any{})
if result == nil {
t.Fatal("expected error result")
}
if content == "" {
t.Error("expected non-empty content")
}
}
func TestWithTools(t *testing.T) {
tools := NewTools(registry.NewMemoryRegistry())
opts := NewOptions(WithTools(tools))
if opts.ToolHandler == nil {
t.Error("WithTools did not set a ToolHandler")
}
}
-51
View File
@@ -1,51 +0,0 @@
package ai
import "context"
// VideoModel provides an interface for video generation providers.
// Providers that support video generation implement this alongside
// Model and/or ImageModel.
type VideoModel interface {
GenerateVideo(ctx context.Context, req *VideoRequest, opts ...GenerateOption) (*VideoResponse, error)
String() string
}
// VideoRequest describes what video to generate.
type VideoRequest struct {
// Prompt is the text description or instructions for the video.
Prompt string
// Model overrides the provider's default video model.
Model string
// Images are reference image URLs for image-to-video generation.
Images []string
// Duration in seconds. Provider-specific defaults apply.
Duration int
// AspectRatio (e.g. "16:9", "9:16"). Provider-specific.
AspectRatio string
// Resolution (e.g. "720p", "1080p"). Provider-specific.
Resolution string
}
// VideoResponse holds the generated video.
type VideoResponse struct {
// URL is the remote URL where the video can be fetched.
URL string
}
// NewVideoFunc creates a new VideoModel instance.
type NewVideoFunc func(...Option) VideoModel
var videoProviders = make(map[string]NewVideoFunc)
// RegisterVideo registers a video generation provider.
func RegisterVideo(name string, fn NewVideoFunc) {
videoProviders[name] = fn
}
// NewVideo creates a new VideoModel instance based on the provider name.
func NewVideo(provider string, opts ...Option) VideoModel {
if fn, ok := videoProviders[provider]; ok {
return fn(opts...)
}
return nil
}
-345
View File
@@ -1,345 +0,0 @@
# Auth Package Analysis
## Current Status: ✅ Fully Functional
The auth package is now **production-ready** with complete server/client wrappers and integration examples.
---
## ✅ What Exists
### 1. Core Interfaces (`auth.go`)
```go
type Auth interface {
Generate(id string, opts ...GenerateOption) (*Account, error)
Inspect(token string) (*Account, error)
Token(opts ...TokenOption) (*Token, error)
}
type Rules interface {
Verify(acc *Account, res *Resource, opts ...VerifyOption) error
Grant(rule *Rule) error
Revoke(rule *Rule) error
List(...ListOption) ([]*Rule, error)
}
```
**Status:** ✅ Well-designed, complete
### 2. Data Types
- `Account` - represents authenticated user/service
- `Token` - access/refresh token pair
- `Resource` - service endpoint to protect
- `Rule` - access control rule
- `Access` - grant/deny enum
**Status:** ✅ Complete
### 3. Implementations
**Noop Auth** (`noop.go`):
- For development/testing
- Always grants access
- No actual authentication
**Status:** ✅ Works for dev
**JWT Auth** (`jwt/jwt.go`):
- Uses RSA keys for signing
- Generates and verifies JWT tokens
- **⚠️ Problem:** Depends on external plugin `github.com/micro/plugins/v5/auth/jwt/token`
**Status:** ⚠️ External dependency
### 4. Authorization Logic (`rules.go`)
- Rule-based access control (RBAC)
- Supports wildcards (`*`)
- Priority-based rule evaluation
- Scope-based permissions
**Status:** ✅ Complete and tested
---
## ✅ Recently Completed
### 1. **Service Integration Wrapper** ✅
**Status:** IMPLEMENTED in `wrapper/auth/server.go`
```go
// AuthHandler wraps a service to enforce authentication
func AuthHandler(opts HandlerOptions) server.HandlerWrapper
func PublicEndpoints(...) HandlerOptions
func AuthRequired(...) HandlerOptions
func AuthOptional(authProvider auth.Auth) server.HandlerWrapper
```
Features:
- Token extraction from metadata
- Token verification with auth.Inspect()
- Authorization checks with rules.Verify()
- Account injection into context
- Skip endpoints support
- Comprehensive error handling (401/403)
### 2. **Client Wrapper** ✅
**Status:** IMPLEMENTED in `wrapper/auth/client.go`
```go
// AuthClient adds authentication tokens to client requests
func AuthClient(opts ClientOptions) client.Wrapper
func FromToken(token string) client.Wrapper
func FromContext(authProvider auth.Auth) client.Wrapper
```
Features:
- Automatic token injection
- Static token support
- Dynamic token generation from context
- Works with Call, Stream, and Publish
### 3. **Metadata Helpers** ✅
**Status:** IMPLEMENTED in `wrapper/auth/metadata.go`
```go
// Standard token extraction and injection
func TokenFromMetadata(md metadata.Metadata) (string, error)
func TokenToMetadata(md metadata.Metadata, token string) metadata.Metadata
func AccountFromMetadata(md metadata.Metadata, a auth.Auth) (*auth.Account, error)
```
Features:
- Bearer token extraction
- Case-insensitive header lookup
- Token format validation
- Direct account extraction
### 6. **Standalone JWT Implementation** ⚠️
**Status:** Partially complete (low priority)
Current JWT auth in `auth/jwt/jwt.go` depends on external plugin:
```go
jwtToken "github.com/micro/plugins/v5/auth/jwt/token"
```
**Note:** This is NOT a blocker. The wrappers work with any auth.Auth implementation including:
- JWT auth (with plugin dependency)
- Noop auth (for development)
- Custom auth implementations
**Future improvement:** Create self-contained JWT implementation to remove plugin dependency.
### 4. **Examples** ✅
**Status:** IMPLEMENTED in `examples/auth/`
Complete working example with:
- Protected Greeter service (server/)
- Client with authentication (client/)
- Proto definitions (proto/)
- Comprehensive README with:
- Architecture diagrams
- Code walkthrough
- Auth strategies
- Authorization rules
- Testing guide
- Production considerations
- Troubleshooting guide
### 5. **Documentation** ✅
**Status:** IMPLEMENTED
Complete documentation:
- `wrapper/auth/README.md` - Full API reference (200+ lines)
- `examples/auth/README.md` - Integration tutorial (400+ lines)
- Server wrapper documentation with examples
- Client wrapper documentation with examples
- Metadata helpers API reference
- Best practices guide
- Troubleshooting guide
- Production considerations
---
## 🔍 Detailed Analysis
### JWT Implementation Dependency Issue
File: `auth/jwt/jwt.go:7`
```go
jwtToken "github.com/micro/plugins/v5/auth/jwt/token"
```
This depends on:
- `github.com/micro/plugins` repository
- Must be separately installed
- May not be maintained
- Breaks self-contained promise
**Recommendation:** Create standalone JWT implementation in `auth/jwt/token/`
### Rules Verification Works Well
The `Verify()` function in `rules.go` is well-implemented:
- ✅ Handles wildcards correctly
- ✅ Priority-based evaluation
- ✅ Supports resource hierarchies (e.g., `/foo/*` matches `/foo/bar`)
- ✅ Public vs authenticated vs scoped access
- ✅ Tested (see `rules_test.go`)
### Context Integration Exists
```go
// From auth.go
func AccountFromContext(ctx context.Context) (*Account, bool)
func ContextWithAccount(ctx context.Context, account *Account) context.Context
```
This is ready to use once wrappers are implemented.
---
## 🛠️ Implementation Status
### Phase 1: Critical ✅ COMPLETE
1.**Server Wrapper** - `wrapper/auth/server.go`
- Token extraction from metadata
- Verification with auth.Inspect()
- Authorization with rules.Verify()
- Skip endpoints support
- Helper functions (AuthRequired, PublicEndpoints, AuthOptional)
2.**Client Wrapper** - `wrapper/auth/client.go`
- Adds Authorization header/metadata
- Static token support (FromToken)
- Dynamic token generation (FromContext)
- Works with Call, Stream, Publish
3.**Metadata Helpers** - `wrapper/auth/metadata.go`
- TokenFromMetadata - extract Bearer token
- TokenToMetadata - inject Bearer token
- AccountFromMetadata - extract and verify in one step
### Phase 2: Important ✅ COMPLETE
4. ⚠️ **Standalone JWT Implementation** - Deferred (not critical)
- Current JWT works with plugin
- Can use noop auth for development
- Future enhancement to remove plugin dependency
5. ⚠️ **Key Generation Utilities** - Deferred (not critical)
- JWT auth handles key management
- Future enhancement for convenience
6.**Examples** - `examples/auth/`
- Complete server/client example
- Protected and public endpoints
- Comprehensive README (400+ lines)
- Code walkthrough and best practices
### Phase 3: Production Ready ✅ COMPLETE
7. ⚠️ **Advanced Examples** - Future enhancement
- Basic example covers most use cases
- Can be added based on demand
8.**Documentation**
- `wrapper/auth/README.md` - Full API reference
- `examples/auth/README.md` - Integration guide
- Best practices and troubleshooting
9.**Testing Utilities**
- Noop auth for tests
- Token generation examples in docs
---
## 📋 Integration Checklist
To use auth with services, users need:
- [x] Auth interface and implementations
- [x] **Server wrapper to enforce auth**
- [x] **Client wrapper to send auth**
- [x] Metadata helpers ✅
- [x] Examples showing integration ✅
- [x] Documentation ✅
- [~] Working JWT implementation (has plugin dependency, not critical)
**Current completeness: ~95%** 🎉
The auth system is now fully functional and production-ready!
---
## 💡 Recommendations
### ✅ Completed
1.**Created wrapper/auth package** with server and client wrappers
2.**Wrote comprehensive examples** showing protected service
3.**Documented** integration patterns with 600+ lines of docs
### Optional Future Enhancements
4. **Remove plugin dependency** - create standalone JWT
- Current solution works fine with plugin
- Would reduce external dependencies
- Priority: Low
5. **Add to CLI** - `micro auth` commands for token management
- Generate tokens from CLI
- Inspect tokens
- Manage accounts
- Priority: Medium
6. **OAuth2 provider** - for enterprise SSO
- Integration with external identity providers
- Priority: Low (can use custom auth provider)
7. **API key auth** - simpler alternative to JWT
- For machine-to-machine auth
- Priority: Low
8. **Audit logging** - track auth events
- Who accessed what and when
- Priority: Medium
9. **Rate limiting** - per account/scope
- Prevent abuse
- Priority: Medium
---
## 🎉 Status: Auth System Complete
The auth system is now **fully functional and production-ready**!
**What's available:**
- ✅ Server wrapper for enforcing auth
- ✅ Client wrapper for adding auth
- ✅ Metadata helpers for token handling
- ✅ Complete working example
- ✅ Comprehensive documentation
- ✅ Best practices guide
- ✅ Troubleshooting guide
**Usage:**
```go
// Server
micro.WrapHandler(authWrapper.AuthHandler(...))
// Client
micro.WrapClient(authWrapper.FromToken(...))
```
See `examples/auth/` for complete working code!
-45
View File
@@ -1,45 +0,0 @@
// Package noop provides a no-op auth implementation for testing and development.
//
// The noop auth provider:
// - Accepts any token (always returns a valid account)
// - Grants all permissions (no actual authorization)
// - Generates tokens (but doesn't verify them)
//
// This is useful for:
// - Local development
// - Testing
// - Prototyping
//
// DO NOT use in production. Use JWT auth or implement a custom auth provider instead.
package noop
import (
"go-micro.dev/v5/auth"
)
// NewAuth returns a new noop auth provider.
//
// The noop provider accepts all tokens and grants all permissions.
// This is for development and testing only - DO NOT use in production.
//
// Example:
//
// authProvider := noop.NewAuth()
// account, _ := authProvider.Generate("user123")
// token, _ := authProvider.Token(auth.WithCredentials(account.ID, account.Secret))
func NewAuth(opts ...auth.Option) auth.Auth {
return auth.NewAuth(opts...)
}
// NewRules returns a new noop rules implementation.
//
// The noop rules implementation grants all access and doesn't enforce any rules.
// This is for development and testing only.
//
// Example:
//
// rules := noop.NewRules()
// err := rules.Verify(account, resource) // Always returns nil
func NewRules() auth.Rules {
return auth.NewRules()
}
+3 -3
View File
@@ -20,9 +20,9 @@ import (
"go-micro.dev/v5/registry"
"go-micro.dev/v5/registry/cache"
"go-micro.dev/v5/transport/headers"
maddr "go-micro.dev/v5/internal/util/addr"
mnet "go-micro.dev/v5/internal/util/net"
mls "go-micro.dev/v5/internal/util/tls"
maddr "go-micro.dev/v5/util/addr"
mnet "go-micro.dev/v5/util/net"
mls "go-micro.dev/v5/util/tls"
"golang.org/x/net/http2"
)
+3 -2
View File
@@ -8,8 +8,8 @@ import (
"github.com/google/uuid"
log "go-micro.dev/v5/logger"
maddr "go-micro.dev/v5/internal/util/addr"
mnet "go-micro.dev/v5/internal/util/net"
maddr "go-micro.dev/v5/util/addr"
mnet "go-micro.dev/v5/util/net"
)
type memoryBroker struct {
@@ -222,6 +222,7 @@ func (m *memorySubscriber) Unsubscribe() error {
func NewMemoryBroker(opts ...Option) Broker {
options := NewOptions(opts...)
return &memoryBroker{
opts: options,
Subscribers: make(map[string][]*memorySubscriber),
+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)
}
}
+1 -1
View File
@@ -12,7 +12,7 @@ import (
amqp "github.com/rabbitmq/amqp091-go"
"go-micro.dev/v5/logger"
mtls "go-micro.dev/v5/internal/util/tls"
mtls "go-micro.dev/v5/util/tls"
)
type MQExchangeType string
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"context"
"time"
"go-micro.dev/v5/internal/util/backoff"
"go-micro.dev/v5/util/backoff"
)
type BackoffFunc func(ctx context.Context, req Request, attempts int) (time.Duration, error)
+1 -1
View File
@@ -19,7 +19,7 @@ import (
"go-micro.dev/v5/metadata"
"go-micro.dev/v5/registry"
"go-micro.dev/v5/selector"
pnet "go-micro.dev/v5/internal/util/net"
pnet "go-micro.dev/v5/util/net"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/encoding"
-6
View File
@@ -401,12 +401,6 @@ func WithMessageContentType(ct string) MessageOption {
}
}
func WithConnectionTimeout(d time.Duration) CallOption {
return func(o *CallOptions) {
o.ConnectionTimeout = d
}
}
// Request Options
func WithContentType(ct string) RequestOption {
+3 -3
View File
@@ -20,9 +20,9 @@ import (
"go-micro.dev/v5/selector"
"go-micro.dev/v5/transport"
"go-micro.dev/v5/transport/headers"
"go-micro.dev/v5/internal/util/buf"
"go-micro.dev/v5/internal/util/net"
"go-micro.dev/v5/internal/util/pool"
"go-micro.dev/v5/util/buf"
"go-micro.dev/v5/util/net"
"go-micro.dev/v5/util/pool"
)
const (
+77 -31
View File
@@ -22,8 +22,11 @@ import (
"go-micro.dev/v5/debug/profile/pprof"
"go-micro.dev/v5/debug/trace"
"go-micro.dev/v5/events"
"go-micro.dev/v5/genai"
"go-micro.dev/v5/genai/gemini"
"go-micro.dev/v5/genai/openai"
"go-micro.dev/v5/logger"
mprofile "go-micro.dev/v5/service/profile"
mprofile "go-micro.dev/v5/profile"
"go-micro.dev/v5/registry"
"go-micro.dev/v5/registry/consul"
"go-micro.dev/v5/registry/etcd"
@@ -244,6 +247,21 @@ var (
EnvVars: []string{"MICRO_CONFIG"},
Usage: "The source of the config to be used to get configuration",
},
&cli.StringFlag{
Name: "genai",
EnvVars: []string{"MICRO_GENAI"},
Usage: "GenAI provider to use (e.g. openai, gemini, noop)",
},
&cli.StringFlag{
Name: "genai_key",
EnvVars: []string{"MICRO_GENAI_KEY"},
Usage: "GenAI API key",
},
&cli.StringFlag{
Name: "genai_model",
EnvVars: []string{"MICRO_GENAI_MODEL"},
Usage: "GenAI model to use (optional)",
},
}
DefaultBrokers = map[string]func(...broker.Option) broker.Broker{
@@ -293,43 +311,31 @@ var (
"redis": redis.NewRedisCache,
}
DefaultStreams = map[string]func(...events.Option) (events.Stream, error){}
DefaultGenAI = map[string]func(...genai.Option) genai.GenAI{
"openai": openai.New,
"gemini": gemini.New,
}
)
func init() {
}
func newCmd(opts ...Option) Cmd {
// Create local copies so each cmd instance is isolated.
// This allows multiple services in a single binary without
// conflicting through shared global pointers.
localAuth := auth.DefaultAuth
localBroker := broker.DefaultBroker
localClient := client.DefaultClient
localRegistry := registry.DefaultRegistry
localServer := server.DefaultServer
localSelector := selector.DefaultSelector
localTransport := transport.DefaultTransport
localStore := store.DefaultStore
localTracer := trace.DefaultTracer
localProfile := profile.DefaultProfile
localConfig := config.DefaultConfig
localCache := cache.DefaultCache
localStream := events.DefaultStream
options := Options{
Auth: &localAuth,
Broker: &localBroker,
Client: &localClient,
Registry: &localRegistry,
Server: &localServer,
Selector: &localSelector,
Transport: &localTransport,
Store: &localStore,
Tracer: &localTracer,
DebugProfile: &localProfile,
Config: &localConfig,
Cache: &localCache,
Stream: &localStream,
Auth: &auth.DefaultAuth,
Broker: &broker.DefaultBroker,
Client: &client.DefaultClient,
Registry: &registry.DefaultRegistry,
Server: &server.DefaultServer,
Selector: &selector.DefaultSelector,
Transport: &transport.DefaultTransport,
Store: &store.DefaultStore,
Tracer: &trace.DefaultTracer,
DebugProfile: &profile.DefaultProfile,
Config: &config.DefaultConfig,
Cache: &cache.DefaultCache,
Stream: &events.DefaultStream,
Brokers: DefaultBrokers,
Clients: DefaultClients,
@@ -381,6 +387,8 @@ func (c *cmd) Options() Options {
}
func (c *cmd) Before(ctx *cli.Context) error {
// Set GenAI provider from flags/env
setGenAIFromFlags(ctx)
// If flags are set then use them otherwise do nothing
var serverOpts []server.Option
var clientOpts []client.Option
@@ -398,9 +406,13 @@ func (c *cmd) Before(ctx *cli.Context) error {
return fmt.Errorf("failed to load local profile: %v", ierr)
}
*c.opts.Registry = imported.Registry
registry.DefaultRegistry = imported.Registry
*c.opts.Broker = imported.Broker
broker.DefaultBroker = imported.Broker
*c.opts.Store = imported.Store
store.DefaultStore = imported.Store
*c.opts.Transport = imported.Transport
transport.DefaultTransport = imported.Transport
case "nats":
imported, ierr := mprofile.NatsProfile()
if ierr != nil {
@@ -441,6 +453,7 @@ func (c *cmd) Before(ctx *cli.Context) error {
// only change if we have the client and type differs
if cl, ok := c.opts.Clients[name]; ok && (*c.opts.Client).String() != name {
*c.opts.Client = cl()
client.DefaultClient = *c.opts.Client
}
}
@@ -449,6 +462,7 @@ func (c *cmd) Before(ctx *cli.Context) error {
// only change if we have the server and type differs
if s, ok := c.opts.Servers[name]; ok && (*c.opts.Server).String() != name {
*c.opts.Server = s()
server.DefaultServer = *c.opts.Server
}
}
@@ -460,6 +474,7 @@ func (c *cmd) Before(ctx *cli.Context) error {
}
*c.opts.Store = s(store.WithClient(*c.opts.Client))
store.DefaultStore = *c.opts.Store
}
// Set the tracer
@@ -470,6 +485,7 @@ func (c *cmd) Before(ctx *cli.Context) error {
}
*c.opts.Tracer = r()
trace.DefaultTracer = *c.opts.Tracer
}
// Setup auth
@@ -496,6 +512,7 @@ func (c *cmd) Before(ctx *cli.Context) error {
}
*c.opts.Auth = r(authOpts...)
auth.DefaultAuth = *c.opts.Auth
}
// Set the registry
@@ -517,6 +534,7 @@ func (c *cmd) Before(ctx *cli.Context) error {
return fmt.Errorf("unsupported profile: %s", name)
}
*c.opts.DebugProfile = p()
profile.DefaultProfile = *c.opts.DebugProfile
}
// Set the broker
@@ -541,6 +559,7 @@ func (c *cmd) Before(ctx *cli.Context) error {
// No server option here. Should there be?
clientOpts = append(clientOpts, client.Selector(*c.opts.Selector))
selector.DefaultSelector = *c.opts.Selector
}
// Set the transport
@@ -693,6 +712,7 @@ func (c *cmd) Before(ctx *cli.Context) error {
logger.Fatalf("Error configuring config: %v", err)
}
*c.opts.Config = rc
config.DefaultConfig = *c.opts.Config
}
}
return nil
@@ -714,6 +734,7 @@ func (c *cmd) setRegistry(r registry.Registry) ([]server.Option, []client.Option
if err := (*c.opts.Broker).Init(broker.Registry(*c.opts.Registry)); err != nil {
logger.Fatalf("Error configuring broker: %v", err)
}
registry.DefaultRegistry = *c.opts.Registry
return serverOpts, clientOpts
}
func (c *cmd) setStream(s events.Stream) ([]server.Option, []client.Option) {
@@ -724,6 +745,7 @@ func (c *cmd) setStream(s events.Stream) ([]server.Option, []client.Option) {
// serverOpts = append(serverOpts, server.Registry(*c.opts.Registry))
// clientOpts = append(clientOpts, client.Registry(*c.opts.Registry))
events.DefaultStream = *c.opts.Stream
return serverOpts, clientOpts
}
@@ -733,6 +755,7 @@ func (c *cmd) setBroker(b broker.Broker) ([]server.Option, []client.Option) {
*c.opts.Broker = b
serverOpts = append(serverOpts, server.Broker(*c.opts.Broker))
clientOpts = append(clientOpts, client.Broker(*c.opts.Broker))
broker.DefaultBroker = *c.opts.Broker
return serverOpts, clientOpts
}
@@ -740,6 +763,7 @@ func (c *cmd) setStore(s store.Store) ([]server.Option, []client.Option) {
var serverOpts []server.Option
var clientOpts []client.Option
*c.opts.Store = s
store.DefaultStore = *c.opts.Store
return serverOpts, clientOpts
}
@@ -749,6 +773,7 @@ func (c *cmd) setTransport(t transport.Transport) ([]server.Option, []client.Opt
*c.opts.Transport = t
serverOpts = append(serverOpts, server.Transport(*c.opts.Transport))
clientOpts = append(clientOpts, client.Transport(*c.opts.Transport))
transport.DefaultTransport = *c.opts.Transport
return serverOpts, clientOpts
}
@@ -796,3 +821,24 @@ func Register(cmds ...*cli.Command) {
return app.Commands[i].Name < app.Commands[j].Name
})
}
func setGenAIFromFlags(ctx *cli.Context) {
provider := ctx.String("genai")
key := ctx.String("genai_key")
model := ctx.String("genai_model")
switch provider {
case "openai":
if key == "" {
key = os.Getenv("OPENAI_API_KEY")
}
genai.DefaultGenAI = openai.New(genai.WithAPIKey(key), genai.WithModel(model))
case "gemini":
if key == "" {
key = os.Getenv("GEMINI_API_KEY")
}
genai.DefaultGenAI = gemini.New(genai.WithAPIKey(key), genai.WithModel(model))
default:
// No GenAI provider configured - using default noop
}
}
-19
View File
@@ -1,19 +0,0 @@
FROM golang:1.23-alpine AS builder
RUN apk add --no-cache git
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /micro-mcp-gateway ./cmd/micro-mcp-gateway
FROM alpine:3.20
RUN apk add --no-cache ca-certificates
COPY --from=builder /micro-mcp-gateway /usr/local/bin/micro-mcp-gateway
EXPOSE 3000
ENTRYPOINT ["micro-mcp-gateway"]
CMD ["--address", ":3000"]
-242
View File
@@ -1,242 +0,0 @@
// Command micro-mcp-gateway runs a standalone MCP gateway that discovers
// go-micro services via a registry and exposes them as AI-accessible tools
// through the Model Context Protocol.
//
// This is the production deployment binary for the MCP gateway, intended
// to run independently of your services.
//
// Usage:
//
// # mDNS (development default)
// micro-mcp-gateway --address :3000
//
// # Consul
// micro-mcp-gateway --address :3000 --registry consul --registry-address consul:8500
//
// # etcd
// micro-mcp-gateway --address :3000 --registry etcd --registry-address etcd:2379
//
// # With auth and rate limiting
// micro-mcp-gateway --address :3000 --registry consul \
// --rate-limit 100 --rate-burst 200 --audit
package main
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"strings"
"syscall"
"time"
"go-micro.dev/v5/auth"
"go-micro.dev/v5/auth/jwt"
"go-micro.dev/v5/gateway/mcp"
"go-micro.dev/v5/registry"
"go-micro.dev/v5/registry/consul"
"go-micro.dev/v5/registry/etcd"
"github.com/urfave/cli/v2"
)
var version = "0.1.0"
func main() {
app := &cli.App{
Name: "micro-mcp-gateway",
Usage: "Standalone MCP gateway for go-micro services",
Version: version,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "address",
Usage: "Address to listen on",
Value: ":3000",
EnvVars: []string{"MCP_ADDRESS"},
},
&cli.StringFlag{
Name: "registry",
Usage: "Service registry (mdns, consul, etcd)",
Value: "mdns",
EnvVars: []string{"MICRO_REGISTRY"},
},
&cli.StringFlag{
Name: "registry-address",
Usage: "Registry address (e.g., consul:8500, etcd:2379)",
EnvVars: []string{"MICRO_REGISTRY_ADDRESS"},
},
&cli.Float64Flag{
Name: "rate-limit",
Usage: "Requests per second per tool (0 = unlimited)",
EnvVars: []string{"MCP_RATE_LIMIT"},
},
&cli.IntFlag{
Name: "rate-burst",
Usage: "Rate limit burst size",
Value: 20,
EnvVars: []string{"MCP_RATE_BURST"},
},
&cli.BoolFlag{
Name: "auth",
Usage: "Enable JWT authentication",
EnvVars: []string{"MCP_AUTH"},
},
&cli.BoolFlag{
Name: "audit",
Usage: "Enable audit logging to stdout",
EnvVars: []string{"MCP_AUDIT"},
},
&cli.StringSliceFlag{
Name: "scope",
Usage: "Tool scope requirement (format: tool=scope1,scope2)",
},
&cli.IntFlag{
Name: "circuit-breaker",
Usage: "Circuit breaker max failures before opening (0 = disabled)",
EnvVars: []string{"MCP_CIRCUIT_BREAKER"},
},
&cli.DurationFlag{
Name: "circuit-breaker-timeout",
Usage: "Circuit breaker open-state timeout before half-open probe",
Value: 30 * time.Second,
EnvVars: []string{"MCP_CIRCUIT_BREAKER_TIMEOUT"},
},
},
Action: run,
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}
func run(c *cli.Context) error {
logger := log.New(os.Stdout, "[mcp-gateway] ", log.LstdFlags)
// Configure registry
reg, err := newRegistry(c.String("registry"), c.String("registry-address"))
if err != nil {
return fmt.Errorf("registry: %w", err)
}
// Build MCP options
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
opts := mcp.Options{
Registry: reg,
Address: c.String("address"),
Context: ctx,
Logger: logger,
}
// Rate limiting
if rps := c.Float64("rate-limit"); rps > 0 {
opts.RateLimit = &mcp.RateLimitConfig{
RequestsPerSecond: rps,
Burst: c.Int("rate-burst"),
}
logger.Printf("Rate limit: %.0f req/s, burst %d", rps, c.Int("rate-burst"))
}
// Auth
if c.Bool("auth") {
opts.Auth = jwt.NewAuth()
logger.Printf("JWT authentication enabled")
}
// Scopes
if scopes := c.StringSlice("scope"); len(scopes) > 0 {
opts.Scopes = parseScopes(scopes)
for tool, s := range opts.Scopes {
logger.Printf("Scope: %s requires [%s]", tool, strings.Join(s, ", "))
}
}
// Circuit breaker
if maxFail := c.Int("circuit-breaker"); maxFail > 0 {
opts.CircuitBreaker = &mcp.CircuitBreakerConfig{
MaxFailures: maxFail,
Timeout: c.Duration("circuit-breaker-timeout"),
}
logger.Printf("Circuit breaker: max %d failures, timeout %s", maxFail, c.Duration("circuit-breaker-timeout"))
}
// Audit
if c.Bool("audit") {
opts.AuditFunc = func(r mcp.AuditRecord) {
status := "ALLOWED"
if !r.Allowed {
status = "DENIED:" + r.DeniedReason
}
logger.Printf("[audit] %s tool=%s account=%s status=%s duration=%s",
r.TraceID, r.Tool, r.AccountID, status, r.Duration)
}
logger.Printf("Audit logging enabled")
}
// Print startup info
logger.Printf("Starting MCP gateway on %s", c.String("address"))
logger.Printf("Registry: %s", c.String("registry"))
if addr := c.String("registry-address"); addr != "" {
logger.Printf("Registry address: %s", addr)
}
// Start gateway in background
errCh := make(chan error, 1)
go func() {
errCh <- mcp.ListenAndServe(opts.Address, opts)
}()
// Wait for signal or error
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
select {
case sig := <-sigCh:
logger.Printf("Received %s, shutting down...", sig)
cancel()
return nil
case err := <-errCh:
return fmt.Errorf("gateway error: %w", err)
}
}
func newRegistry(name, address string) (registry.Registry, error) {
var opts []registry.Option
if address != "" {
opts = append(opts, registry.Addrs(strings.Split(address, ",")...))
}
switch name {
case "mdns", "":
return registry.NewMDNSRegistry(opts...), nil
case "consul":
return consul.NewConsulRegistry(opts...), nil
case "etcd":
return etcd.NewEtcdRegistry(opts...), nil
default:
return nil, fmt.Errorf("unknown registry %q (supported: mdns, consul, etcd)", name)
}
}
func parseScopes(raw []string) map[string][]string {
scopes := make(map[string][]string)
for _, s := range raw {
parts := strings.SplitN(s, "=", 2)
if len(parts) != 2 {
continue
}
tool := strings.TrimSpace(parts[0])
scopeList := strings.Split(parts[1], ",")
for i := range scopeList {
scopeList[i] = strings.TrimSpace(scopeList[i])
}
scopes[tool] = scopeList
}
return scopes
}
// Ensure auth.Auth interface is satisfied at compile time.
var _ auth.Auth = jwt.NewAuth()
+3 -309
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
```
@@ -19,14 +19,6 @@ Create your service (all setup is now automatic!):
micro new helloworld
```
Or use a template for common service patterns:
```
micro new contacts --template crud # CRUD with Create/Read/Update/Delete/List
micro new events --template pubsub # Pub/sub with broker integration
micro new gateway --template api # API gateway with health check
```
This will:
- Create a new service in the `helloworld` directory
- Automatically run `go mod tidy` and `make proto` for you
@@ -44,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/mcp/tools
- **Hot Reload** watching for file changes
- **Services** in dependency order
@@ -283,178 +272,13 @@ func main() {
}
```
## Building and Deployment
### Build Binaries
Build Go binaries for deployment:
```bash
micro build # Build for current OS
micro build --os linux # Cross-compile for Linux
micro build --os linux --arch arm64 # For ARM64
micro build --output ./dist # Custom output directory
```
### Deploy to Server
Deploy to any Linux server with systemd:
```bash
# First time: set up the server
ssh user@server
curl -fsSL https://go-micro.dev/install.sh | sh
sudo micro init --server
exit
# Deploy from your laptop
micro deploy user@server
```
The deploy command:
1. Builds binaries for linux/amd64
2. Copies via SSH to `/opt/micro/bin/`
3. Sets up systemd services (`micro@<service>`)
4. Restarts and verifies services are running
### Named Deploy Targets
Add deploy targets to `micro.mu`:
```
deploy prod
ssh deploy@prod.example.com
deploy staging
ssh deploy@staging.example.com
```
Then:
```bash
micro deploy prod # Deploy to production
micro deploy staging # Deploy to staging
```
### Managing Deployed Services
```bash
# Check status
micro status --remote user@server
# View logs
micro logs --remote user@server
micro logs myservice --remote user@server -f
# Stop a service
micro stop myservice --remote user@server
```
See [internal/website/docs/deployment.md](../../internal/website/docs/deployment.md) for the full deployment guide.
## API Gateway
Run a standalone HTTP-to-RPC gateway (no dashboard, no auth, no hot reload):
```bash
micro api # listen on :8080
micro api --address :3000 # custom port
```
Routes:
- `POST /{service}/{endpoint}` — proxies to an RPC call
- `GET /` — lists all services and endpoints
- `GET /{service}` — describes a service
- `GET /health` — health check
```bash
curl -XPOST -d '{"name":"Alice"}' http://localhost:8080/greeter/Greeter.Hello
```
## Inspecting the Framework
Every core interface has a matching CLI command:
### Registry
```bash
micro registry list # list all registered services (JSON)
micro registry get <name> # show nodes and endpoints for a service
micro registry watch # stream registration events
```
### Broker
```bash
micro broker publish <topic> <message> # publish a message
micro broker subscribe <topic> # stream messages from a topic
```
### Store
```bash
micro store list [prefix] # list keys (optionally by prefix)
micro store read <key> # read a record
micro store write <key> <value> # write a record
micro store delete <key> # delete a record
```
### Config
```bash
micro config get <key> # read a config value (dot notation → env var)
micro config dump # print all configuration
```
Keys use dot notation: `database.host` reads from `DATABASE_HOST`.
## AI & Agents
### micro chat
Interactive LLM agent that discovers services and orchestrates them through natural language:
```bash
ANTHROPIC_API_KEY=sk-ant-... micro chat --provider anthropic
> list all users
> send a welcome email to Alice
```
Supports: `--provider` (anthropic, openai, gemini, atlascloud, groq, mistral, together), `--prompt` for single-shot mode, `--model` and `--base_url` for overrides.
Environment variables: `MICRO_AI_PROVIDER`, `MICRO_AI_API_KEY`, or provider-specific keys like `ANTHROPIC_API_KEY`.
### micro flow
Event-driven LLM orchestration:
```bash
# Subscribe to events and react
micro flow run --trigger events.user.created \
--prompt "New user: {{.Data}}. Send welcome email." \
--provider anthropic
# One-shot execution
micro flow exec --prompt "List all users" --provider anthropic
```
### micro mcp
Expose services as MCP tools for AI agents:
```bash
micro mcp serve # stdio transport (for Claude Code)
micro mcp serve --address :3000 # HTTP/SSE transport
micro mcp list # list available tools
micro mcp test <tool> # test a tool
```
## Protobuf
Use protobuf for code generation with [protoc-gen-micro](https://github.com/micro/go-micro/tree/master/cmd/protoc-gen-micro)
## 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
@@ -462,7 +286,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
@@ -495,133 +319,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 `/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 /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.
-325
View File
@@ -1,325 +0,0 @@
// Package api implements the 'micro api' command — a lightweight
// HTTP-to-RPC gateway that proxies JSON requests to go-micro services.
//
// Usage:
//
// micro api # listen on :8080
// micro api --address :3000 # custom port
//
// Requests:
//
// POST /service/endpoint → RPC call to service.endpoint
// GET /health → {"status":"ok"}
//
// The request body is forwarded as-is (JSON). The Micro-Endpoint
// header can also be used to specify the endpoint.
package api
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"os/signal"
"sort"
"strings"
"syscall"
"github.com/urfave/cli/v2"
"go-micro.dev/v5/broker"
"go-micro.dev/v5/client"
"go-micro.dev/v5/cmd"
codecBytes "go-micro.dev/v5/codec/bytes"
"go-micro.dev/v5/registry"
"go-micro.dev/v5/store"
)
func init() {
cmd.Register(&cli.Command{
Name: "api",
Usage: "Run a lightweight HTTP-to-RPC API gateway",
Description: `Start an HTTP gateway that proxies JSON requests to go-micro services.
Requests are routed by URL path:
POST /service/endpoint → calls service.endpoint via RPC
GET / → lists available services and endpoints
Examples:
# Start on default port
micro api
# Custom port
micro api --address :3000
# Call a service through the gateway
curl -XPOST -d '{"name":"Alice"}' http://localhost:8080/greeter/Greeter.Hello
# Or use the Micro-Endpoint header
curl -XPOST -H 'Micro-Endpoint: Greeter.Hello' \
-d '{"name":"Alice"}' http://localhost:8080/greeter`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "address",
Usage: "Address to listen on",
Value: ":8080",
EnvVars: []string{"MICRO_API_ADDRESS"},
},
},
Action: run,
})
}
func run(c *cli.Context) error {
addr := c.String("address")
mux := http.NewServeMux()
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
})
// Framework primitives under /micro/
registerFrameworkRoutes(mux)
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
path := strings.TrimPrefix(r.URL.Path, "/")
path = strings.TrimSuffix(path, "/")
// Root: list services
if path == "" {
listServices(w)
return
}
// Parse service/endpoint from path
parts := strings.SplitN(path, "/", 2)
serviceName := parts[0]
endpoint := ""
if len(parts) > 1 {
endpoint = parts[1]
}
// Allow Micro-Endpoint header to override
if h := r.Header.Get("Micro-Endpoint"); h != "" {
endpoint = h
}
if endpoint == "" {
describeService(w, serviceName)
return
}
// Proxy RPC call
body, err := io.ReadAll(r.Body)
if err != nil {
writeError(w, http.StatusBadRequest, "failed to read body: "+err.Error())
return
}
if len(body) == 0 {
body = []byte("{}")
}
req := client.DefaultClient.NewRequest(serviceName, endpoint, &codecBytes.Frame{Data: body})
var rsp codecBytes.Frame
if err := client.DefaultClient.Call(r.Context(), req, &rsp); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(rsp.Data)
})
fmt.Println()
fmt.Println(" \033[1mmicro api\033[0m")
fmt.Println()
fmt.Printf(" Listening \033[36m%s\033[0m\n", addr)
fmt.Println()
fmt.Println(" Routes:")
fmt.Println(" \033[32mGET\033[0m / List services")
fmt.Println(" \033[32mGET\033[0m /{service} Describe a service")
fmt.Println(" \033[33mPOST\033[0m /{service}/{endpoint} Call an endpoint")
fmt.Println(" \033[32mGET\033[0m /health Health check")
fmt.Println()
fmt.Println(" Framework:")
fmt.Println(" \033[32mGET\033[0m /micro/registry List registered services")
fmt.Println(" \033[32mGET\033[0m /micro/registry/{name} Describe a service")
fmt.Println(" \033[32mGET\033[0m /micro/store List store keys")
fmt.Println(" \033[32mGET\033[0m /micro/store/{key} Read a record")
fmt.Println(" \033[33mPOST\033[0m /micro/store/{key} Write a record")
fmt.Println(" \033[33mPOST\033[0m /micro/broker/{topic} Publish a message")
fmt.Println()
server := &http.Server{Addr: addr, Handler: mux}
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
fmt.Fprintf(os.Stderr, "server error: %v\n", err)
os.Exit(1)
}
}()
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig
fmt.Println("\nShutting down...")
return server.Close()
}
func listServices(w http.ResponseWriter) {
services, err := registry.ListServices()
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
sort.Slice(services, func(i, j int) bool {
return services[i].Name < services[j].Name
})
type svcInfo struct {
Name string `json:"name"`
Endpoints []string `json:"endpoints,omitempty"`
}
var result []svcInfo
for _, svc := range services {
info := svcInfo{Name: svc.Name}
full, err := registry.GetService(svc.Name)
if err == nil && len(full) > 0 {
for _, ep := range full[0].Endpoints {
info.Endpoints = append(info.Endpoints, ep.Name)
}
}
result = append(result, info)
}
json.NewEncoder(w).Encode(result)
}
func describeService(w http.ResponseWriter, name string) {
services, err := registry.GetService(name)
if err != nil || len(services) == 0 {
writeError(w, http.StatusNotFound, "service not found: "+name)
return
}
type epInfo struct {
Name string `json:"name"`
Metadata map[string]string `json:"metadata,omitempty"`
}
svc := services[0]
var endpoints []epInfo
for _, ep := range svc.Endpoints {
endpoints = append(endpoints, epInfo{
Name: ep.Name,
Metadata: ep.Metadata,
})
}
json.NewEncoder(w).Encode(map[string]any{
"name": svc.Name,
"version": svc.Version,
"endpoints": endpoints,
"nodes": len(svc.Nodes),
})
}
func writeError(w http.ResponseWriter, code int, msg string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
json.NewEncoder(w).Encode(map[string]string{"error": msg})
}
// registerFrameworkRoutes adds /micro/* routes for registry, broker, and store.
func registerFrameworkRoutes(mux *http.ServeMux) {
// Registry
mux.HandleFunc("/micro/registry", func(w http.ResponseWriter, r *http.Request) {
listServices(w)
})
mux.HandleFunc("/micro/registry/", func(w http.ResponseWriter, r *http.Request) {
name := strings.TrimPrefix(r.URL.Path, "/micro/registry/")
if name == "" {
listServices(w)
return
}
describeService(w, name)
})
// Store
mux.HandleFunc("/micro/store", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
keys, err := store.List()
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
json.NewEncoder(w).Encode(keys)
})
mux.HandleFunc("/micro/store/", func(w http.ResponseWriter, r *http.Request) {
key := strings.TrimPrefix(r.URL.Path, "/micro/store/")
if key == "" {
w.Header().Set("Content-Type", "application/json")
keys, _ := store.List()
json.NewEncoder(w).Encode(keys)
return
}
w.Header().Set("Content-Type", "application/json")
switch r.Method {
case http.MethodGet:
records, err := store.Read(key)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if len(records) == 0 {
writeError(w, http.StatusNotFound, "key not found")
return
}
w.Write(records[0].Value)
case http.MethodPost:
body, _ := io.ReadAll(r.Body)
if err := store.Write(&store.Record{Key: key, Value: body}); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
json.NewEncoder(w).Encode(map[string]string{"status": "ok", "key": key})
default:
writeError(w, http.StatusMethodNotAllowed, "use GET or POST")
}
})
// Broker
mux.HandleFunc("/micro/broker/", func(w http.ResponseWriter, r *http.Request) {
topic := strings.TrimPrefix(r.URL.Path, "/micro/broker/")
if topic == "" {
writeError(w, http.StatusBadRequest, "topic required: /micro/broker/{topic}")
return
}
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "use POST to publish")
return
}
body, _ := io.ReadAll(r.Body)
b := broker.DefaultBroker
if err := b.Connect(); err != nil {
writeError(w, http.StatusInternalServerError, "broker connect: "+err.Error())
return
}
if err := b.Publish(topic, &broker.Message{Body: body}); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok", "topic": topic})
})
}
-388
View File
@@ -1,388 +0,0 @@
// Package chat implements the 'micro chat' interactive agent command.
//
// micro chat opens a terminal REPL where you can talk to your services
// through an LLM. It discovers all services from the registry, exposes
// each endpoint as a tool, and lets the model orchestrate calls in
// response to natural-language prompts.
package chat
import (
"bufio"
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/urfave/cli/v2"
"go-micro.dev/v5/ai"
clt "go-micro.dev/v5/client"
"go-micro.dev/v5/cmd"
"go-micro.dev/v5/cmd/micro/cli/generate"
"go-micro.dev/v5/registry"
_ "go-micro.dev/v5/ai/anthropic"
_ "go-micro.dev/v5/ai/atlascloud"
_ "go-micro.dev/v5/ai/gemini"
_ "go-micro.dev/v5/ai/groq"
_ "go-micro.dev/v5/ai/mistral"
_ "go-micro.dev/v5/ai/openai"
_ "go-micro.dev/v5/ai/together"
)
const systemPromptTmpl = `You are an agent that orchestrates microservices. Use the available tools to fulfill user requests. When you call a tool, explain what you are doing.
Available services: %s
If a user asks for something that no existing service can handle, use the micro_generate_service tool to create it. Pass a short description of what the service should do. After it's created, the new service's endpoints will be available as tools and you can use them immediately.
Do NOT make up capabilities. Only use the tools that are available. If generation fails, tell the user.`
var generateTool = ai.Tool{
Name: "micro_generate_service",
OriginalName: "micro.generate_service",
Description: "Generate a new microservice from a description. Use when the user needs a capability that no existing service provides. The service will be created, compiled, and started automatically.",
Properties: map[string]any{
"description": map[string]any{
"type": "string",
"description": "What the service should do, e.g. 'a shipping service that tracks parcels and calculates rates'",
},
},
}
func init() {
cmd.Register(&cli.Command{
Name: "chat",
Usage: "Interactive AI chat that orchestrates your services",
Description: `Start an interactive chat session that uses an LLM to call your services.
micro chat discovers every service in the registry, exposes each endpoint as a
tool, and lets you ask natural-language questions like "list all users" or
"create an order for product 42". The model decides which tool to call and
issues RPCs to the right service.
If you ask for something no existing service handles, the agent will generate
a new service automatically and start using it.
Examples:
ANTHROPIC_API_KEY=sk-ant-... micro chat --provider anthropic
micro chat --provider openai --prompt "list all users"`,
Flags: []cli.Flag{
&cli.StringFlag{Name: "provider", Usage: "AI provider (anthropic, openai, gemini, groq, mistral, together, atlascloud)", EnvVars: []string{"MICRO_AI_PROVIDER"}},
&cli.StringFlag{Name: "api_key", Usage: "API key for the provider", EnvVars: []string{"MICRO_AI_API_KEY"}},
&cli.StringFlag{Name: "model", Usage: "Model name (uses provider default if unset)", EnvVars: []string{"MICRO_AI_MODEL"}},
&cli.StringFlag{Name: "base_url", Usage: "Override the provider's base URL", EnvVars: []string{"MICRO_AI_BASE_URL"}},
&cli.StringFlag{Name: "prompt", Usage: "Send a single prompt and exit (non-interactive)"},
},
Action: run,
})
}
type session struct {
provider string
apiKey string
model ai.Model
tools *ai.Tools
reg registry.Registry
hist *ai.History
toolList []ai.Tool
sysPrompt string
procs []*exec.Cmd
}
func (s *session) refreshTools() {
discovered, err := s.tools.Discover()
if err != nil {
return
}
s.toolList = append(discovered, generateTool)
serviceNames := make(map[string]bool)
for _, t := range discovered {
parts := strings.SplitN(t.OriginalName, ".", 2)
if len(parts) == 2 {
serviceNames[parts[0]] = true
}
}
var svcList []string
for name := range serviceNames {
svcList = append(svcList, name)
}
if len(svcList) == 0 {
s.sysPrompt = fmt.Sprintf(systemPromptTmpl, "(none yet)")
} else {
s.sysPrompt = fmt.Sprintf(systemPromptTmpl, strings.Join(svcList, ", "))
}
}
func (s *session) handleGenerate(input map[string]any) (any, string) {
desc, _ := input["description"].(string)
if desc == "" {
return map[string]string{"error": "description is required"}, `{"error":"description is required"}`
}
fmt.Printf("\n \033[36m⚡\033[0m generating service: %s\n", desc)
design, err := generate.Design(context.Background(), s.provider, s.apiKey, "", ".", desc)
if err != nil {
msg := fmt.Sprintf(`{"error":"design failed: %s"}`, err)
return map[string]string{"error": err.Error()}, msg
}
if err := generate.Generate(context.Background(), ".", design, s.provider, s.apiKey, ""); err != nil {
msg := fmt.Sprintf(`{"error":"generate failed: %s"}`, err)
return map[string]string{"error": err.Error()}, msg
}
// Find which services are new (not already in registry)
existing := make(map[string]bool)
if svcs, err := s.reg.ListServices(); err == nil {
for _, svc := range svcs {
existing[svc.Name] = true
}
}
var created []string
for _, svc := range design.Services {
name := strings.TrimSuffix(svc.Name, "-service")
if existing[name] {
continue
}
created = append(created, svc.Name)
// Build and start the new service
svcDir, _ := filepath.Abs(svc.Name)
fmt.Printf(" \033[36m⚡\033[0m starting %s...\n", svc.Name)
buildCmd := exec.Command("go", "build", "-o", svc.Name, ".")
buildCmd.Dir = svcDir
if out, err := buildCmd.CombinedOutput(); err != nil {
fmt.Printf(" \033[33m⚠\033[0m build failed: %s\n", string(out))
continue
}
runCmd := exec.Command(filepath.Join(svcDir, svc.Name))
runCmd.Dir = svcDir
if err := runCmd.Start(); err != nil {
fmt.Printf(" \033[33m⚠\033[0m start failed: %v\n", err)
continue
}
s.procs = append(s.procs, runCmd)
}
if len(created) == 0 {
result := map[string]any{"message": "No new services needed — all already exist."}
b, _ := json.Marshal(result)
return result, string(b)
}
// Wait for services to register
fmt.Printf(" \033[36m⚡\033[0m waiting for services to register...\n")
time.Sleep(5 * time.Second)
s.refreshTools()
fmt.Printf(" \033[32m✓\033[0m %d tools available\n\n", len(s.toolList)-1)
result := map[string]any{
"created": created,
"message": fmt.Sprintf("Created and started: %s. Their endpoints are now available as tools.", strings.Join(created, ", ")),
}
b, _ := json.Marshal(result)
return result, string(b)
}
func run(c *cli.Context) error {
provider := c.String("provider")
apiKey := c.String("api_key")
modelName := c.String("model")
baseURL := c.String("base_url")
singlePrompt := c.String("prompt")
if provider == "" {
provider = ai.AutoDetectProvider(baseURL)
}
if apiKey == "" {
apiKey = fallbackAPIKey(provider)
}
if apiKey == "" {
return fmt.Errorf("no API key configured; set --api_key or %s", envVarForProvider(provider))
}
reg := registry.DefaultRegistry
cl := clt.DefaultClient
tools := ai.NewTools(reg, ai.ToolClient(cl))
s := &session{
provider: provider,
apiKey: apiKey,
tools: tools,
reg: reg,
hist: ai.NewHistory(50),
}
s.refreshTools()
// Wrap the tool handler to intercept generate calls
baseHandler := tools.Handler()
wrappedHandler := func(name string, input map[string]any) (any, string) {
if name == "micro_generate_service" {
return s.handleGenerate(input)
}
return baseHandler(name, input)
}
opts := []ai.Option{
ai.WithAPIKey(apiKey),
ai.WithToolHandler(wrappedHandler),
}
if modelName != "" {
opts = append(opts, ai.WithModel(modelName))
}
if baseURL != "" {
opts = append(opts, ai.WithBaseURL(baseURL))
}
s.model = ai.New(provider, opts...)
if s.model == nil {
return fmt.Errorf("unknown provider: %s", provider)
}
defer s.cleanup()
if singlePrompt != "" {
return s.ask(c.Context, singlePrompt)
}
fmt.Println()
fmt.Println(" \033[1mmicro chat\033[0m")
fmt.Println()
fmt.Printf(" Provider \033[36m%s\033[0m\n", provider)
fmt.Printf(" Model \033[36m%s\033[0m\n", s.model.Options().Model)
fmt.Println()
fmt.Println(" Tools:")
for _, t := range s.toolList {
fmt.Printf(" \033[32m●\033[0m %s\n", t.OriginalName)
}
if len(s.toolList) == 0 {
fmt.Println(" \033[33m(no services found)\033[0m")
}
fmt.Println()
fmt.Println(" Type a prompt and press enter. \033[2mCtrl-D or 'exit' to quit.\033[0m")
fmt.Println()
scanner := bufio.NewScanner(os.Stdin)
scanner.Buffer(make([]byte, 0, 4096), 1024*1024)
for {
fmt.Print("\033[1;36m>\033[0m ")
if !scanner.Scan() {
fmt.Println()
return nil
}
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
if line == "exit" || line == "quit" {
return nil
}
if line == "reset" {
s.hist.Reset()
fmt.Println("\033[2m(history cleared)\033[0m")
fmt.Println()
continue
}
if err := s.ask(c.Context, line); err != nil {
fmt.Printf("\033[31merror:\033[0m %v\n", err)
}
fmt.Println()
}
}
func (s *session) ask(ctx context.Context, prompt string) error {
s.hist.Add("user", prompt)
resp, err := s.model.Generate(ctx, &ai.Request{
Prompt: prompt,
SystemPrompt: s.sysPrompt,
Tools: s.toolList,
Messages: s.hist.Messages(),
})
if err != nil {
return err
}
if resp.Reply != "" {
s.hist.Add("assistant", resp.Reply)
}
if resp.Answer != "" {
s.hist.Add("assistant", resp.Answer)
}
if resp.Reply != "" {
fmt.Println(resp.Reply)
}
for _, tc := range resp.ToolCalls {
if tc.Name == "micro_generate_service" {
continue // output handled by handleGenerate
}
args, _ := json.Marshal(tc.Input)
fmt.Printf(" \033[33m→\033[0m \033[2m%s\033[0m(%s)\n", tc.Name, args)
if tc.Result != "" {
fmt.Printf(" \033[32m←\033[0m \033[2m%s\033[0m\n", truncateResult(tc.Result))
}
if tc.Error != "" {
fmt.Printf(" \033[31m✗\033[0m %s\n", tc.Error)
}
}
if resp.Answer != "" {
fmt.Println()
fmt.Println(resp.Answer)
}
return nil
}
func (s *session) cleanup() {
for _, p := range s.procs {
if p.Process != nil {
p.Process.Kill()
}
}
}
func fallbackAPIKey(provider string) string {
if v := os.Getenv(envVarForProvider(provider)); v != "" {
return v
}
return ""
}
func envVarForProvider(provider string) string {
switch provider {
case "anthropic":
return "ANTHROPIC_API_KEY"
case "openai":
return "OPENAI_API_KEY"
case "gemini":
return "GEMINI_API_KEY"
case "groq":
return "GROQ_API_KEY"
case "mistral":
return "MISTRAL_API_KEY"
case "together":
return "TOGETHER_API_KEY"
case "atlascloud":
return "ATLASCLOUD_API_KEY"
default:
return "MICRO_AI_API_KEY"
}
}
func truncateResult(s string) string {
if len(s) <= 200 {
return s
}
return s[:200] + "..."
}
+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
```
+71 -163
View File
@@ -1,4 +1,4 @@
// Package build provides the micro build command for building service binaries
// Package build provides the micro build command for building container images
package build
import (
@@ -6,7 +6,6 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/urfave/cli/v2"
@@ -14,7 +13,24 @@ import (
"go-micro.dev/v5/cmd/micro/run/config"
)
// Build builds Go binaries for services
const dockerfileTemplate = `# Auto-generated by micro build
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /service %s
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /service /app/service
EXPOSE %d
CMD ["/app/service"]
`
// Build builds container images for services
func Build(c *cli.Context) error {
dir := c.Args().Get(0)
if dir == "" {
@@ -32,112 +48,26 @@ func Build(c *cli.Context) error {
return fmt.Errorf("failed to load config: %w", err)
}
// Output directory
outDir := c.String("output")
if outDir == "" {
outDir = filepath.Join(absDir, "bin")
}
if err := os.MkdirAll(outDir, 0755); err != nil {
return fmt.Errorf("failed to create output dir: %w", err)
tag := c.String("tag")
if tag == "" {
tag = "latest"
}
// Target OS/ARCH
targetOS := c.String("os")
targetArch := c.String("arch")
if targetOS == "" {
targetOS = runtime.GOOS
}
if targetArch == "" {
targetArch = runtime.GOARCH
}
registry := c.String("registry")
push := c.Bool("push")
if cfg != nil && len(cfg.Services) > 0 {
// Build each service from config
sorted, err := cfg.TopologicalSort()
if err != nil {
return err
}
for _, svc := range sorted {
for name, svc := range cfg.Services {
svcDir := filepath.Join(absDir, svc.Path)
if err := buildService(svc.Name, svcDir, outDir, targetOS, targetArch); err != nil {
return fmt.Errorf("failed to build %s: %w", svc.Name, err)
if err := buildService(name, svcDir, svc.Port, tag, registry, push); err != nil {
return fmt.Errorf("failed to build %s: %w", name, err)
}
}
} else {
// Build single service from current directory
name := filepath.Base(absDir)
if err := buildService(name, absDir, outDir, targetOS, targetArch); err != nil {
return err
}
}
fmt.Printf("\n \033[32m✓\033[0m Built to \033[36m%s\033[0m\n", outDir)
return nil
}
func buildService(name, dir, outDir, targetOS, targetArch string) error {
binName := name
if targetOS == "windows" {
binName += ".exe"
}
outPath := filepath.Join(outDir, binName)
fmt.Printf(" Building \033[36m%s (%s/%s)...\n", name, targetOS, targetArch)
// Build command
buildCmd := exec.Command("go", "build", "-o", outPath, ".")
buildCmd.Dir = dir
buildCmd.Env = append(os.Environ(),
"GOOS="+targetOS,
"GOARCH="+targetArch,
"CGO_ENABLED=0",
)
buildCmd.Stdout = os.Stdout
buildCmd.Stderr = os.Stderr
if err := buildCmd.Run(); err != nil {
return fmt.Errorf("go build failed: %w", err)
}
fmt.Printf(" \033[32m✓\033[0m %s\n", outPath)
return nil
}
// Docker builds container images (optional)
func Docker(c *cli.Context) error {
dir := c.Args().Get(0)
if dir == "" {
dir = "."
}
absDir, err := filepath.Abs(dir)
if err != nil {
return fmt.Errorf("failed to get absolute path: %w", err)
}
cfg, err := config.Load(absDir)
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
tag := c.String("tag")
if tag == "" {
tag = "latest"
}
registry := c.String("registry")
push := c.Bool("push")
if cfg != nil && len(cfg.Services) > 0 {
for name, svc := range cfg.Services {
svcDir := filepath.Join(absDir, svc.Path)
if err := buildDockerImage(name, svcDir, svc.Port, tag, registry, push); err != nil {
return fmt.Errorf("failed to build %s: %w", name, err)
}
}
} else {
name := filepath.Base(absDir)
if err := buildDockerImage(name, absDir, 8080, tag, registry, push); err != nil {
if err := buildService(name, absDir, 8080, tag, registry, push); err != nil {
return err
}
}
@@ -145,21 +75,7 @@ func Docker(c *cli.Context) error {
return nil
}
const dockerfileTemplate = `FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /service .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
COPY --from=builder /service /service
EXPOSE %d
CMD ["/service"]
`
func buildDockerImage(name, dir string, port int, tag, registry string, push bool) error {
func buildService(name, dir string, port int, tag, registry string, push bool) error {
if port == 0 {
port = 8080
}
@@ -168,19 +84,31 @@ func buildDockerImage(name, dir string, port int, tag, registry string, push boo
dockerfilePath := filepath.Join(dir, "Dockerfile")
if _, err := os.Stat(dockerfilePath); os.IsNotExist(err) {
fmt.Printf("Generating Dockerfile for %s...\n", name)
dockerfile := fmt.Sprintf(dockerfileTemplate, port)
// Find the main package path
mainPath := "."
if _, err := os.Stat(filepath.Join(dir, "main.go")); os.IsNotExist(err) {
// Look for cmd/main.go or similar
if _, err := os.Stat(filepath.Join(dir, "cmd", "main.go")); err == nil {
mainPath = "./cmd"
}
}
dockerfile := fmt.Sprintf(dockerfileTemplate, mainPath, port)
if err := os.WriteFile(dockerfilePath, []byte(dockerfile), 0644); err != nil {
return fmt.Errorf("failed to write Dockerfile: %w", err)
}
}
// Build image name
imageName := name + ":" + tag
if registry != "" {
imageName = registry + "/" + imageName
}
fmt.Printf(" Building \033[36m%s...\n", imageName)
fmt.Printf("Building %s...\n", imageName)
// Run docker build
buildCmd := exec.Command("docker", "build", "-t", imageName, dir)
buildCmd.Stdout = os.Stdout
buildCmd.Stderr = os.Stderr
@@ -188,8 +116,9 @@ func buildDockerImage(name, dir string, port int, tag, registry string, push boo
return fmt.Errorf("docker build failed: %w", err)
}
fmt.Printf(" \033[32m✓\033[0m Built %s\n", imageName)
fmt.Printf(" Built %s\n", imageName)
// Push if requested
if push {
fmt.Printf("Pushing %s...\n", imageName)
pushCmd := exec.Command("docker", "push", imageName)
@@ -198,14 +127,14 @@ func buildDockerImage(name, dir string, port int, tag, registry string, push boo
if err := pushCmd.Run(); err != nil {
return fmt.Errorf("docker push failed: %w", err)
}
fmt.Printf(" \033[32m✓\033[0m Pushed %s\n", imageName)
fmt.Printf(" Pushed %s\n", imageName)
}
return nil
}
// Compose generates docker-compose.yml (optional)
func Compose(c *cli.Context) error {
// GenerateDockerCompose generates a docker-compose.yml from micro.mu config
func GenerateDockerCompose(c *cli.Context) error {
dir := c.Args().Get(0)
if dir == "" {
dir = "."
@@ -231,10 +160,13 @@ func Compose(c *cli.Context) error {
tag = "latest"
}
// Generate docker-compose.yml
var sb strings.Builder
sb.WriteString("# Generated by micro build --compose\n")
sb.WriteString("version: '3.8'\n\nservices:\n")
sb.WriteString("# Auto-generated by micro build --compose\n")
sb.WriteString("version: '3.8'\n\n")
sb.WriteString("services:\n")
// Sort by dependencies
sorted, err := cfg.TopologicalSort()
if err != nil {
return err
@@ -248,9 +180,10 @@ func Compose(c *cli.Context) error {
sb.WriteString(fmt.Sprintf(" %s:\n", svc.Name))
sb.WriteString(fmt.Sprintf(" image: %s\n", imageName))
if svc.Port > 0 {
sb.WriteString(fmt.Sprintf(" ports:\n - \"%d:%d\"\n", svc.Port, svc.Port))
sb.WriteString(fmt.Sprintf(" ports:\n"))
sb.WriteString(fmt.Sprintf(" - \"%d:%d\"\n", svc.Port, svc.Port))
}
if len(svc.Depends) > 0 {
@@ -260,7 +193,9 @@ func Compose(c *cli.Context) error {
}
}
sb.WriteString(" environment:\n - MICRO_REGISTRY=mdns\n\n")
sb.WriteString(" environment:\n")
sb.WriteString(" - MICRO_REGISTRY=mdns\n")
sb.WriteString("\n")
}
output := filepath.Join(absDir, "docker-compose.yml")
@@ -268,75 +203,48 @@ func Compose(c *cli.Context) error {
return fmt.Errorf("failed to write docker-compose.yml: %w", err)
}
fmt.Printf(" \033[32m✓\033[0m Generated %s\n", output)
fmt.Printf(" Generated %s\n", output)
return nil
}
func init() {
cmd.Register(&cli.Command{
Name: "build",
Usage: "Build Go binaries for services",
Description: `Build compiles Go binaries for your services.
Usage: "Build container images for services",
Description: `Build creates Docker container images for your services.
With a micro.mu config, builds all services. Without, builds the current directory.
Output goes to ./bin/ by default.
Examples:
micro build # Build for current OS/arch
micro build --os linux # Cross-compile for Linux
micro build --os linux --arch arm64 # For ARM64
micro build --output ./dist # Custom output directory
Docker (optional):
micro build --docker # Build container images
micro build --docker --push # Build and push
micro build --compose # Generate docker-compose.yml`,
micro build # Build all services
micro build --tag v1.0.0 # Build with specific tag
micro build --push # Build and push to registry
micro build --compose # Generate docker-compose.yml`,
Action: func(c *cli.Context) error {
if c.Bool("docker") {
return Docker(c)
}
if c.Bool("compose") {
return Compose(c)
return GenerateDockerCompose(c)
}
return Build(c)
},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "output",
Aliases: []string{"o"},
Usage: "Output directory (default: ./bin)",
},
&cli.StringFlag{
Name: "os",
Usage: "Target OS (linux, darwin, windows)",
},
&cli.StringFlag{
Name: "arch",
Usage: "Target architecture (amd64, arm64)",
},
// Docker options (optional)
&cli.BoolFlag{
Name: "docker",
Usage: "Build Docker container images instead",
},
&cli.StringFlag{
Name: "tag",
Aliases: []string{"t"},
Usage: "Docker image tag (default: latest)",
Usage: "Image tag (default: latest)",
Value: "latest",
},
&cli.StringFlag{
Name: "registry",
Aliases: []string{"r"},
Usage: "Docker registry (e.g., docker.io/myuser)",
Usage: "Container registry (e.g., docker.io/myuser)",
},
&cli.BoolFlag{
Name: "push",
Usage: "Push Docker images after building",
Usage: "Push images after building",
},
&cli.BoolFlag{
Name: "compose",
Usage: "Generate docker-compose.yml",
Usage: "Generate docker-compose.yml instead of building",
},
},
})
+227 -62
View File
@@ -1,26 +1,26 @@
package microcli
import (
"bufio"
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strings"
"syscall"
"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/genai"
"go-micro.dev/v5/registry"
"go-micro.dev/v5/cmd/micro/cli/new"
"go-micro.dev/v5/cmd/micro/cli/util"
// Import packages that register commands via init()
_ "go-micro.dev/v5/cmd/micro/cli/build"
_ "go-micro.dev/v5/cmd/micro/cli/deploy"
_ "go-micro.dev/v5/cmd/micro/cli/init"
_ "go-micro.dev/v5/cmd/micro/cli/remote"
)
var (
@@ -36,46 +36,99 @@ 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 lastNonEmptyLine(s string) string {
lines := strings.Split(s, "\n")
for i := len(lines) - 1; i >= 0; i-- {
if strings.TrimSpace(lines[i]) != "" {
return lines[i]
}
}
return ""
}
func lastLogLine(path string) string {
f, err := os.Open(path)
if err != nil {
return ""
}
defer f.Close()
var last string
scan := bufio.NewScanner(f)
for scan.Scan() {
if strings.TrimSpace(scan.Text()) != "" {
last = scan.Text()
}
}
return last
}
func waitAndCleanup(procs []*exec.Cmd, pidFiles []string) {
ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Interrupt)
go func() {
<-ch
for _, proc := range procs {
if proc.Process != nil {
_ = proc.Process.Kill()
}
}
for _, pf := range pidFiles {
_ = os.Remove(pf)
}
os.Exit(1)
}()
for i, proc := range procs {
_ = proc.Wait()
if proc.Process != nil {
_ = os.Remove(pidFiles[i])
}
}
}
func init() {
cmd.Register([]*cli.Command{
{
Name: "new",
Usage: "Create a new service",
ArgsUsage: "[name]",
UsageText: ` micro new helloworld # scaffold a single service
micro new --prompt "a todo list with tasks" # AI-design multiple services
micro new --prompt "add tags to the task service" # extend existing services`,
Name: "new",
Usage: "Create a new service",
Action: new.Run,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "no-mcp",
Usage: "Disable MCP gateway integration in generated code",
},
&cli.StringFlag{
Name: "template",
Usage: "Service template: default, crud, pubsub, api",
},
&cli.StringFlag{
Name: "prompt",
Usage: "Describe the system to generate (uses AI to design & build services with real business logic)",
EnvVars: []string{"MICRO_NEW_PROMPT"},
},
&cli.StringFlag{
Name: "provider",
Usage: "AI provider for --prompt (anthropic, openai, gemini, atlascloud, groq, mistral, together)",
EnvVars: []string{"MICRO_AI_PROVIDER"},
},
&cli.StringFlag{
Name: "api_key",
Usage: "API key for --prompt (or set ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)",
EnvVars: []string{"MICRO_AI_API_KEY"},
},
},
},
{
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",
@@ -100,18 +153,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()
@@ -127,16 +168,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
}
@@ -168,11 +202,142 @@ func init() {
return nil
},
},
// Note: The following commands are registered in their respective packages:
// - status, logs, stop: remote/remote.go
// - build: build/build.go
// - deploy: deploy/deploy.go
// - init: init/init.go
{
Name: "status",
Usage: "Check status of running services",
Action: func(ctx *cli.Context) error {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home dir: %w", err)
}
runDir := filepath.Join(homeDir, "micro", "run")
files, err := os.ReadDir(runDir)
if err != nil {
return fmt.Errorf("failed to read run dir: %w", err)
}
fmt.Printf("%-20s %-8s %-8s %s\n", "SERVICE", "PID", "STATUS", "DIRECTORY")
for _, f := range files {
if f.IsDir() || !strings.HasSuffix(f.Name(), ".pid") {
continue
}
service := f.Name()[:len(f.Name())-4]
pidFilePath := filepath.Join(runDir, f.Name())
pidFile, err := os.Open(pidFilePath)
if err != nil {
continue
}
var pid int
var dir string
scanner := bufio.NewScanner(pidFile)
if scanner.Scan() {
fmt.Sscanf(scanner.Text(), "%d", &pid)
}
if scanner.Scan() {
dir = scanner.Text()
}
pidFile.Close()
status := "stopped"
if pid > 0 {
proc, err := os.FindProcess(pid)
if err == nil {
if err := proc.Signal(syscall.Signal(0)); err == nil {
status = "running"
}
}
}
fmt.Printf("%-20s %-8d %-8s %-40s %s\n", service, pid, status, "", dir)
}
return nil
},
},
{
Name: "stop",
Usage: "Stop a running service",
Action: func(ctx *cli.Context) error {
if ctx.Args().Len() != 1 {
return fmt.Errorf("Usage: micro stop [service]")
}
service := ctx.Args().Get(0)
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home dir: %w", err)
}
runDir := filepath.Join(homeDir, "micro", "run")
pidFilePath := filepath.Join(runDir, service+".pid")
pidFile, err := os.Open(pidFilePath)
if err != nil {
return fmt.Errorf("no pid file for service %s", service)
}
var pid int
var dir string
scanner := bufio.NewScanner(pidFile)
if scanner.Scan() {
fmt.Sscanf(scanner.Text(), "%d", &pid)
}
if scanner.Scan() {
dir = scanner.Text()
}
pidFile.Close()
if pid <= 0 {
_ = os.Remove(pidFilePath)
return fmt.Errorf("service %s is not running", service)
}
proc, err := os.FindProcess(pid)
if err != nil {
_ = os.Remove(pidFilePath)
return fmt.Errorf("could not find process for %s", service)
}
if err := proc.Signal(syscall.SIGTERM); err != nil {
_ = os.Remove(pidFilePath)
return fmt.Errorf("failed to stop service %s: %v", service, err)
}
_ = os.Remove(pidFilePath)
fmt.Printf("Stopped service %s (pid %d) in directory %s\n", service, pid, dir)
return nil
},
},
{
Name: "logs",
Usage: "Show logs for a service, or list available logs if no service is specified",
Action: func(ctx *cli.Context) error {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home dir: %w", err)
}
logsDir := filepath.Join(homeDir, "micro", "logs")
if ctx.Args().Len() == 0 {
// List available logs
dirEntries, err := os.ReadDir(logsDir)
if err != nil {
return fmt.Errorf("could not list logs directory: %v", err)
}
fmt.Println("Available logs:")
found := false
for _, entry := range dirEntries {
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".log") {
fmt.Println(" ", strings.TrimSuffix(entry.Name(), ".log"))
found = true
}
}
if !found {
fmt.Println(" (no logs found)")
}
return nil
}
service := ctx.Args().Get(0)
logFilePath := filepath.Join(logsDir, service+".log")
f, err := os.Open(logFilePath)
if err != nil {
return fmt.Errorf("could not open log file for service %s: %v", service, err)
}
defer f.Close()
scan := bufio.NewScanner(f)
for scan.Scan() {
fmt.Println(scan.Text())
}
return scan.Err()
},
},
}...)
cmd.App().Action = func(c *cli.Context) error {
+139 -418
View File
@@ -6,84 +6,26 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/urfave/cli/v2"
"go-micro.dev/v5/cmd"
"go-micro.dev/v5/cmd/micro/run/config"
)
const (
defaultRemotePath = "/opt/micro"
)
// Deploy deploys services to a target
func Deploy(c *cli.Context) error {
// Get target from args or flag
target := c.Args().First()
if target == "" {
target = c.String("ssh")
sshTarget := c.String("ssh")
if sshTarget != "" {
return deploySSH(c, sshTarget)
}
// Load config to check for deploy targets
dir := "."
absDir, _ := filepath.Abs(dir)
cfg, _ := config.Load(absDir)
// If still no target, check config for named targets
if target == "" && cfg != nil && len(cfg.Deploy) > 0 {
// Show available targets
return showDeployTargets(cfg)
}
if target == "" {
return showDeployHelp()
}
// Check if target is a named target from config
if cfg != nil {
if dt, ok := cfg.Deploy[target]; ok {
target = dt.SSH
}
}
return deploySSH(c, target, cfg)
// Default: docker-compose up
return deployCompose(c)
}
func showDeployHelp() error {
return fmt.Errorf(`No deployment target specified.
To deploy, you need a server running micro. Quick setup:
1. On your server (Ubuntu/Debian):
ssh user@your-server
curl -fsSL https://go-micro.dev/install.sh | sh
sudo micro init --server
2. Then deploy from here:
micro deploy user@your-server
Or add to micro.mu:
deploy prod
ssh user@your-server
Run 'micro deploy --help' for more options.`)
}
func showDeployTargets(cfg *config.Config) error {
var sb strings.Builder
sb.WriteString("Available deploy targets:\n\n")
for name, dt := range cfg.Deploy {
sb.WriteString(fmt.Sprintf(" %s -> %s\n", name, dt.SSH))
}
sb.WriteString("\nDeploy with: micro deploy <target>")
return fmt.Errorf("%s", sb.String())
}
func deploySSH(c *cli.Context, target string, cfg *config.Config) error {
dir := c.Args().Get(1)
func deployCompose(c *cli.Context) error {
dir := c.Args().Get(0)
if dir == "" {
dir = "."
}
@@ -93,400 +35,179 @@ func deploySSH(c *cli.Context, target string, cfg *config.Config) error {
return fmt.Errorf("failed to get absolute path: %w", err)
}
// Load config if not passed
if cfg == nil {
cfg, _ = config.Load(absDir)
composePath := filepath.Join(absDir, "docker-compose.yml")
if _, err := os.Stat(composePath); os.IsNotExist(err) {
return fmt.Errorf("docker-compose.yml not found. Run 'micro build --compose' first")
}
fmt.Println("Deploying with docker-compose...")
args := []string{"compose", "-f", composePath, "up", "-d"}
if c.Bool("build") {
args = append(args, "--build")
}
cmd := exec.Command("docker", args...)
cmd.Dir = absDir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("docker compose failed: %w", err)
}
fmt.Println("\n✓ Deployed successfully")
fmt.Println("\nView logs: docker compose logs -f")
fmt.Println("Stop: docker compose down")
return nil
}
func deploySSH(c *cli.Context, target string) error {
dir := c.Args().Get(0)
if dir == "" {
dir = "."
}
absDir, err := filepath.Abs(dir)
if err != nil {
return fmt.Errorf("failed to get absolute path: %w", err)
}
// Load config to get service info
cfg, err := config.Load(absDir)
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
remotePath := c.String("path")
if remotePath == "" {
remotePath = defaultRemotePath
remotePath = "~/micro"
}
fmt.Println()
fmt.Println(" \033[1mmicro deploy\033[0m")
fmt.Println()
fmt.Printf(" Target \033[36m%s\033[0m\n\n", target)
fmt.Printf("Deploying to %s...\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 {
fmt.Println("\u2717")
return err
}
fmt.Println("\u2713")
// Step 2: Check server is initialized
fmt.Print(" Checking server setup... ")
if err := checkServerInit(target, remotePath); err != nil {
fmt.Println("\u2717")
return err
}
fmt.Println("\u2713")
// Step 3: Build binaries
var services []string
if cfg != nil && len(cfg.Services) > 0 {
sorted, err := cfg.TopologicalSort()
if err != nil {
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)
}
}
// Parse target: user@host or just host
var sshHost string
if strings.Contains(target, "@") {
sshHost = target
} 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])
}
sshHost = target
}
fmt.Printf(" Building binaries... ")
if err := buildBinaries(absDir, cfg, c.Bool("build"), services); err != nil {
fmt.Println("\u2717")
return err
}
fmt.Printf("\u2713 %s\n", strings.Join(services, ", "))
// Step 4: Copy binaries
fmt.Printf(" Copying binaries... ")
if err := copyBinaries(target, filepath.Join(absDir, "bin"), remotePath); err != nil {
fmt.Println("\u2717")
return err
}
fmt.Printf("\u2713 %d services\n", len(services))
// Step 5: Setup and restart services via systemd
fmt.Printf(" Updating systemd... ")
if err := setupSystemdServices(target, remotePath, services); err != nil {
fmt.Println("\u2717")
return err
}
fmt.Printf("\u2713 %s\n", strings.Join(prefixServices(services), ", "))
// Step 6: Restart services
fmt.Printf(" Restarting services... ")
if err := restartServices(target, services); err != nil {
fmt.Println("\u2717")
return err
}
fmt.Println("\u2713")
// Step 7: Check health
fmt.Printf(" Checking health... ")
time.Sleep(2 * time.Second) // Give services time to start
healthy, unhealthy := checkServicesHealth(target, services)
if len(unhealthy) > 0 {
fmt.Printf("\u26a0 %d/%d healthy\n", len(healthy), len(services))
} else {
fmt.Println("\u2713 all healthy")
}
fmt.Println()
fmt.Printf("\u2713 Deployed to %s\n", target)
fmt.Println()
fmt.Printf(" Status: micro status --remote %s\n", target)
fmt.Printf(" Logs: micro logs --remote %s\n", target)
if len(unhealthy) > 0 {
fmt.Println()
fmt.Printf("\u26a0 Some services may have issues: %s\n", strings.Join(unhealthy, ", "))
fmt.Printf(" Check logs: micro logs %s --remote %s\n", unhealthy[0], target)
}
return nil
}
func prefixServices(services []string) []string {
result := make([]string, len(services))
for i, s := range services {
result[i] = "micro@" + s
}
return result
}
func checkSSH(host string) error {
testCmd := exec.Command("ssh", "-o", "ConnectTimeout=5", "-o", "BatchMode=yes", host, "echo ok")
output, err := testCmd.CombinedOutput()
if err != nil {
return fmt.Errorf(`
\u2717 Cannot connect to %s
SSH connection failed. Check that:
\u2022 The server is reachable: ping %s
\u2022 SSH is configured: ssh %s
\u2022 Your key is added: ssh-add -l
Common fixes:
\u2022 Add SSH key: ssh-copy-id %s
\u2022 Check hostname in ~/.ssh/config
Error: %s`, host, host, host, host, strings.TrimSpace(string(output)))
}
return nil
}
func checkServerInit(host, remotePath string) error {
checkCmd := fmt.Sprintf("test -f %s/.micro-initialized", remotePath)
sshCmd := exec.Command("ssh", host, checkCmd)
if err := sshCmd.Run(); err != nil {
return fmt.Errorf(`
\u2717 Server not initialized
micro is not set up on %s.
Run this on the server:
ssh %s
curl -fsSL https://go-micro.dev/install.sh | sh
sudo micro init --server
Or initialize remotely (requires sudo):
micro init --server --remote %s`, host, host, host)
}
return nil
}
func buildBinaries(absDir string, cfg *config.Config, forceBuild bool, servicesToBuild []string) error {
binDir := filepath.Join(absDir, "bin")
// Check if we already have binaries and don't need to rebuild
if !forceBuild {
if _, err := os.Stat(binDir); err == nil {
// Check if binaries are for linux
// For now, just rebuild to be safe
}
}
// Always build for linux/amd64
targetOS := "linux"
targetArch := "amd64"
if err := os.MkdirAll(binDir, 0755); err != nil {
// Create remote directory
fmt.Println("Creating remote directory...")
if err := runSSH(sshHost, fmt.Sprintf("mkdir -p %s", remotePath)); err != nil {
return err
}
if cfg != nil && len(cfg.Services) > 0 {
sorted, err := cfg.TopologicalSort()
if err != nil {
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)
buildCmd := exec.Command("go", "build", "-o", outPath, ".")
buildCmd.Dir = svcDir
buildCmd.Env = append(os.Environ(),
"GOOS="+targetOS,
"GOARCH="+targetArch,
"CGO_ENABLED=0",
)
if output, err := buildCmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to build %s:\n%s", svc.Name, string(output))
}
}
} else {
name := filepath.Base(absDir)
outPath := filepath.Join(binDir, name)
buildCmd := exec.Command("go", "build", "-o", outPath, ".")
buildCmd.Dir = absDir
buildCmd.Env = append(os.Environ(),
"GOOS="+targetOS,
"GOARCH="+targetArch,
"CGO_ENABLED=0",
)
if output, err := buildCmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to build:\n%s", string(output))
}
}
return nil
}
func copyBinaries(target, binDir, remotePath string) error {
// Ensure remote bin directory exists
mkdirCmd := exec.Command("ssh", target, fmt.Sprintf("mkdir -p %s/bin", remotePath))
if err := mkdirCmd.Run(); err != nil {
return fmt.Errorf("failed to create remote directory: %w", err)
}
// Use rsync for efficient copy
// --omit-dir-times avoids permission errors on directory timestamps
// Sync files using rsync
fmt.Println("Syncing files...")
rsyncArgs := []string{
"-avz", "--delete", "--omit-dir-times",
binDir + "/",
fmt.Sprintf("%s:%s/bin/", target, remotePath),
"-avz", "--delete",
"--exclude", ".git",
"--exclude", "node_modules",
"--exclude", "vendor",
absDir + "/",
fmt.Sprintf("%s:%s/", sshHost, remotePath),
}
rsyncCmd := exec.Command("rsync", rsyncArgs...)
output, err := rsyncCmd.CombinedOutput()
if err != nil {
outputStr := string(output)
// Fall back to scp if rsync not available
if strings.Contains(outputStr, "command not found") {
scpCmd := exec.Command("scp", "-r", binDir+"/", fmt.Sprintf("%s:%s/bin/", target, remotePath))
if scpOutput, scpErr := scpCmd.CombinedOutput(); scpErr != nil {
return fmt.Errorf("copy failed: %s", string(scpOutput))
rsyncCmd.Stdout = os.Stdout
rsyncCmd.Stderr = os.Stderr
if err := rsyncCmd.Run(); err != nil {
return fmt.Errorf("rsync failed: %w", err)
}
// Build and run on remote
fmt.Println("Building on remote...")
if cfg != nil && len(cfg.Services) > 0 {
// Build and run each service
sorted, err := cfg.TopologicalSort()
if err != nil {
return err
}
for _, svc := range sorted {
svcPath := filepath.Join(remotePath, svc.Path)
binPath := filepath.Join(remotePath, "bin", svc.Name)
// Build
buildCmd := fmt.Sprintf("cd %s && go build -o %s .", svcPath, binPath)
if err := runSSH(sshHost, buildCmd); err != nil {
return fmt.Errorf("failed to build %s: %w", svc.Name, err)
}
return nil
}
// rsync exit code 23 means some files failed to transfer, but if we see our files listed, it's ok
// rsync exit code 24 means some files vanished during transfer (harmless)
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") {
// These are acceptable warnings
return nil
// Stop existing if running
stopCmd := fmt.Sprintf("pkill -f '%s' || true", binPath)
runSSH(sshHost, stopCmd)
// Start in background
startCmd := fmt.Sprintf("nohup %s > %s/%s.log 2>&1 &", binPath, remotePath, svc.Name)
if err := runSSH(sshHost, startCmd); err != nil {
return fmt.Errorf("failed to start %s: %w", svc.Name, err)
}
fmt.Printf("✓ Deployed %s\n", svc.Name)
}
return fmt.Errorf("copy failed: %s", outputStr)
}
} else {
// Single service
name := filepath.Base(absDir)
binPath := filepath.Join(remotePath, "bin", name)
return nil
}
func setupSystemdServices(target, remotePath string, services []string) error {
for _, svc := range services {
// Enable the service using the template
enableCmd := fmt.Sprintf("sudo systemctl enable micro@%s 2>/dev/null || true", svc)
sshCmd := exec.Command("ssh", target, enableCmd)
sshCmd.Run() // Ignore errors, service might already be enabled
}
// Reload systemd
reloadCmd := exec.Command("ssh", target, "sudo systemctl daemon-reload")
if err := reloadCmd.Run(); err != nil {
return fmt.Errorf("failed to reload systemd: %w", err)
}
return nil
}
func restartServices(target string, services []string) error {
for _, svc := range services {
restartCmd := fmt.Sprintf("sudo systemctl restart micro@%s", svc)
sshCmd := exec.Command("ssh", target, restartCmd)
if output, err := sshCmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to restart %s: %s", svc, string(output))
buildCmd := fmt.Sprintf("cd %s && mkdir -p bin && go build -o %s .", remotePath, binPath)
if err := runSSH(sshHost, buildCmd); err != nil {
return fmt.Errorf("build failed: %w", err)
}
}
return nil
}
func checkServicesHealth(target string, services []string) (healthy, unhealthy []string) {
for _, svc := range services {
checkCmd := fmt.Sprintf("systemctl is-active micro@%s", svc)
sshCmd := exec.Command("ssh", target, checkCmd)
if err := sshCmd.Run(); err != nil {
unhealthy = append(unhealthy, svc)
} else {
healthy = append(healthy, svc)
stopCmd := fmt.Sprintf("pkill -f '%s' || true", binPath)
runSSH(sshHost, stopCmd)
startCmd := fmt.Sprintf("nohup %s > %s/%s.log 2>&1 &", binPath, remotePath, name)
if err := runSSH(sshHost, startCmd); err != nil {
return fmt.Errorf("start failed: %w", err)
}
fmt.Printf("✓ Deployed %s\n", name)
}
return
fmt.Printf("\n✓ Deployed to %s\n", target)
fmt.Printf("\nView logs: ssh %s 'tail -f %s/*.log'\n", sshHost, remotePath)
return nil
}
// Ensure we're not on Windows for deploy
func checkPlatform() error {
if runtime.GOOS == "windows" {
return fmt.Errorf("micro deploy requires SSH and rsync, which work best on Linux/macOS.\nConsider using WSL on Windows.")
}
return nil
func runSSH(host, command string) error {
cmd := exec.Command("ssh", host, command)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
func init() {
cmd.Register(&cli.Command{
Name: "deploy",
Usage: "Deploy services to a remote server",
Description: `Deploy copies binaries to a remote server and manages them with systemd.
Usage: "Deploy services to a target",
Description: `Deploy services using docker-compose or SSH.
Before deploying, initialize the server:
ssh user@server 'curl -fsSL https://go-micro.dev/install.sh | sh && sudo micro init --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
deploy staging
ssh user@staging.example.com
Then: micro deploy prod
The deploy process:
1. Builds binaries for linux/amd64
2. Copies to /opt/micro/bin/ via rsync
3. Enables and restarts systemd services
4. Verifies services are healthy`,
Action: func(c *cli.Context) error {
if err := checkPlatform(); err != nil {
return err
}
return Deploy(c)
},
Examples:
micro deploy # Deploy with docker-compose
micro deploy --ssh user@host # Deploy via SSH
micro deploy --build # Rebuild before deploying`,
Action: Deploy,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "ssh",
Usage: "Deploy target as user@host (can also be positional arg)",
Usage: "Deploy via SSH to user@host",
},
&cli.StringFlag{
Name: "path",
Usage: "Remote path (default: /opt/micro)",
Value: "/opt/micro",
Usage: "Remote path for SSH deploy (default: ~/micro)",
Value: "~/micro",
},
&cli.BoolFlag{
Name: "build",
Usage: "Force rebuild of binaries",
},
&cli.StringFlag{
Name: "service",
Usage: "Deploy only a specific service (for multi-service projects)",
Usage: "Rebuild before deploying",
},
},
})
+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,
},
},
})
}
-794
View File
@@ -1,794 +0,0 @@
// Package generate implements AI-powered service generation for go-micro.
// It uses an LLM to design service architecture and generate handler code
// with real business logic, then compiles and fixes errors iteratively.
package generate
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"time"
"go-micro.dev/v5/ai"
_ "go-micro.dev/v5/ai/anthropic"
_ "go-micro.dev/v5/ai/atlascloud"
_ "go-micro.dev/v5/ai/gemini"
_ "go-micro.dev/v5/ai/groq"
_ "go-micro.dev/v5/ai/mistral"
_ "go-micro.dev/v5/ai/openai"
_ "go-micro.dev/v5/ai/together"
)
const designPrompt = `You are a Go microservices architect using the go-micro framework.
Given a system description, design the services needed.
Return ONLY valid JSON:
{
"services": [
{
"name": "service-name",
"description": "What this service does",
"fields": [
{"name": "field_name", "type": "string", "description": "What this field is"}
],
"endpoints": [
{"name": "EndpointName", "description": "What this endpoint does", "example": "{\"key\": \"value\"}"}
]
}
]
}
Rules:
- Service names are lowercase, hyphenated, WITHOUT a "-service" suffix (e.g. "task" not "task-service", "shipping" not "shipping-service")
- Each service MUST have CRUD endpoints: Create, Read, Update, Delete, List
- Add 1-3 custom endpoints for real business logic (e.g. PlaceOrder, CheckInventory)
- Field types: string, int64, bool, float64
- Every service needs id (string), created (int64), updated (int64) fields
- Endpoint names are PascalCase
- Examples should be realistic JSON
- 2-4 services max, focused on the domain
- Keep services small and focused — one concern per service, max 5-8 fields
- Services don't call each other; an AI agent orchestrates across them`
const designPromptWithExisting = `You are a Go microservices architect using the go-micro framework.
The user has an EXISTING system with services already running. They want to extend or modify it.
Existing services:
%s
Given the user's request, return the COMPLETE set of services (existing + new/modified).
For existing services the user hasn't asked to change, return them as-is.
For new or modified services, include the full specification.
Return ONLY valid JSON:
{
"services": [
{
"name": "service-name",
"description": "What this service does",
"fields": [
{"name": "field_name", "type": "string", "description": "What this field is"}
],
"endpoints": [
{"name": "EndpointName", "description": "What this endpoint does", "example": "{\"key\": \"value\"}"}
]
}
]
}
Rules:
- Service names are lowercase, hyphenated, WITHOUT a "-service" suffix (e.g. "task" not "task-service", "shipping" not "shipping-service")
- Each service MUST have CRUD endpoints: Create, Read, Update, Delete, List
- Add custom endpoints for real business logic
- Field types: string, int64, bool, float64
- Every service needs id (string), created (int64), updated (int64) fields
- Endpoint names are PascalCase
- Examples should be realistic JSON
- Keep existing services unless the user explicitly asks to change them`
const handlerPrompt = `You are a Go developer writing a handler for a go-micro service.
Generate a COMPLETE, COMPILABLE Go handler file.
The handler must:
1. Use package "handler"
2. Import the proto package as: pb "%s/proto"
3. Import go-micro logger as: log "go-micro.dev/v5/logger"
4. Import "github.com/google/uuid" for ID generation
5. Use "go-micro.dev/v5/store" for persistent storage (NOT in-memory maps)
6. Include REAL business logic — not just CRUD store operations
7. Every exported method must have a doc comment explaining what it does
8. Every method must have an @example tag with realistic JSON input
9. Handle edge cases, validation, and return meaningful errors
10. Keep the file under 200 lines — be concise, no boilerplate
For storage, use the go-micro store package:
import "go-micro.dev/v5/store"
import "encoding/json"
// In the struct:
store store.Store
// In the constructor:
func New() *%s { return &%s{store: store.DefaultStore} }
// Write a record:
data, _ := json.Marshal(record)
store.Write(&store.Record{Key: "prefix/" + id, Value: data})
// Read a record:
recs, err := store.Read("prefix/" + id)
json.Unmarshal(recs[0].Value, &record)
// List keys:
keys, _ := store.List(store.ListPrefix("prefix/"))
// Delete:
store.Delete("prefix/" + id)
Do NOT use sync.Mutex or in-memory maps. Use store for all data.
The struct name is %s.
The constructor is func New() *%s.
Here is the proto definition:
%s
Here is what each endpoint should do:
%s
Return ONLY the Go code. No markdown, no explanation. Just the .go file content starting with "package handler".`
// ServiceDesign is the LLM's output.
type ServiceDesign struct {
Services []ServiceSpec `json:"services"`
}
type ServiceSpec struct {
Name string `json:"name"`
Description string `json:"description"`
Fields []FieldSpec `json:"fields"`
Endpoints []EndpointSpec `json:"endpoints"`
}
type FieldSpec struct {
Name string `json:"name"`
Type string `json:"type"`
Description string `json:"description"`
}
type EndpointSpec struct {
Name string `json:"name"`
Description string `json:"description"`
Example string `json:"example"`
}
// Design calls an LLM to design services from a prompt.
// If baseDir contains existing services, they are included as context
// so the LLM extends the system rather than redesigning from scratch.
func Design(ctx context.Context, provider, apiKey, model, baseDir, prompt string) (*ServiceDesign, error) {
m := newModel(provider, apiKey, model)
if m == nil {
return nil, fmt.Errorf("unknown provider: %s", provider)
}
existing := discoverExisting(baseDir)
var sysPrompt, userPrompt string
if len(existing) > 0 {
sysPrompt = fmt.Sprintf(designPromptWithExisting, existing)
userPrompt = fmt.Sprintf("Extend or modify the system: %s", prompt)
} else {
sysPrompt = designPrompt
userPrompt = fmt.Sprintf("Design a microservices system for: %s", prompt)
}
sp := startSpinner("designing services...")
designCtx, designCancel := context.WithTimeout(ctx, 60*time.Second)
defer designCancel()
resp, err := m.Generate(designCtx, &ai.Request{
Prompt: userPrompt,
SystemPrompt: sysPrompt,
})
sp.Stop()
if err != nil {
return nil, fmt.Errorf("design failed: %w", err)
}
reply := firstNonEmpty(resp.Answer, resp.Reply)
reply = extractJSON(reply)
var design ServiceDesign
if err := json.Unmarshal([]byte(reply), &design); err != nil {
return nil, fmt.Errorf("failed to parse design: %w\nResponse: %s", err, reply)
}
if len(design.Services) == 0 {
return nil, fmt.Errorf("no services designed")
}
return &design, nil
}
// discoverExisting scans a directory for existing go-micro services
// and returns a summary string for inclusion in the design prompt.
func discoverExisting(baseDir string) string {
entries, err := os.ReadDir(baseDir)
if err != nil {
return ""
}
var summaries []string
for _, e := range entries {
if !e.IsDir() {
continue
}
svcDir := filepath.Join(baseDir, e.Name())
// Look for proto files as indicator of a go-micro service
protoDir := filepath.Join(svcDir, "proto")
protos, err := filepath.Glob(filepath.Join(protoDir, "*.proto"))
if err != nil || len(protos) == 0 {
continue
}
proto := readFile(protos[0])
if proto == "" {
continue
}
summaries = append(summaries, fmt.Sprintf("### %s\nProto:\n```\n%s\n```", e.Name(), proto))
}
return strings.Join(summaries, "\n\n")
}
// Generate creates go-micro service directories from a design.
// If a service directory already exists, it skips structure generation
// but regenerates the handler (allowing iterative improvement).
func Generate(ctx context.Context, baseDir string, design *ServiceDesign, provider, apiKey, model string) error {
m := newModel(provider, apiKey, model)
for i, svc := range design.Services {
if ctx.Err() != nil {
return ctx.Err()
}
svcDir := filepath.Join(baseDir, svc.Name)
handlerFile := filepath.Join(svcDir, "handler", svc.Name+".go")
protoFile := filepath.Join(svcDir, "proto", svc.Name+".proto")
// Snapshot proto hash before structure generation
protoBefore := fileHash(protoFile)
fmt.Printf(" \033[2m[%d/%d]\033[0m generating \033[36m%s\033[0m...\n", i+1, len(design.Services), svc.Name)
// Step 1: Generate proto (deterministic — from design spec)
if err := generateStructure(svcDir, svc); err != nil {
return fmt.Errorf("structure %s: %w", svc.Name, err)
}
protoAfter := fileHash(protoFile)
protoChanged := protoBefore != protoAfter
// If proto unchanged and handler unmodified, nothing to do
if !protoChanged && protoBefore != "" && !handlerModified(svcDir, handlerFile) {
fmt.Printf(" \033[32m✓\033[0m %s \033[2m(unchanged)\033[0m\n", svc.Name)
continue
}
// Step 2: Run go mod tidy + make proto to get compiled proto
runIn(svcDir, "go", "mod", "tidy")
runIn(svcDir, "make", "proto")
// Step 3: Generate handler with business logic (LLM)
proto := readFile(protoFile)
if err := generateHandler(ctx, m, svcDir, svc, proto); err != nil {
return fmt.Errorf("handler %s: %w", svc.Name, err)
}
// Step 4: Compile-fix loop
if err := compileFix(ctx, m, svcDir, svc.Name, 3); err != nil {
fmt.Printf(" \033[33m⚠\033[0m %s has compile errors (may need manual fix)\n", svc.Name)
} else {
fmt.Printf(" \033[32m✓\033[0m %s\n", svc.Name)
}
// Record final handler hash (after any compile fixes)
recordHandlerHash(svcDir, handlerFile)
}
return nil
}
// generateStructure creates the proto, main.go, go.mod, Makefile.
// If the directory already exists, only regenerates the proto
// (handler will be regenerated separately by the LLM).
func generateStructure(dir string, svc ServiceSpec) error {
exists := false
if _, err := os.Stat(dir); err == nil {
exists = true
}
os.MkdirAll(filepath.Join(dir, "handler"), 0755)
os.MkdirAll(filepath.Join(dir, "proto"), 0755)
name := svc.Name
titleName := toTitle(name)
dehyphen := strings.ReplaceAll(name, "-", "")
// Regenerate proto unless user has modified it
protoPath := filepath.Join(dir, "proto", name+".proto")
if !fileModified(dir, "proto_hash", protoPath) {
writeFile(protoPath, buildProto(dehyphen, titleName, svc))
recordFileHash(dir, "proto_hash", protoPath)
} else {
fmt.Printf(" \033[2mkeeping %s proto (modified)\033[0m\n", name)
}
// Only write structural files if directory is new
if !exists {
writeFile(filepath.Join(dir, "main.go"), buildMain(name, titleName))
writeFile(filepath.Join(dir, "Makefile"),
"GOPATH:=$(shell go env GOPATH)\n\n.PHONY: proto\nproto:\n\tprotoc --proto_path=. --micro_out=. --go_out=. proto/*.proto\n")
writeFile(filepath.Join(dir, "go.mod"),
fmt.Sprintf("module %s\n\ngo 1.24\n\nrequire go-micro.dev/v5 v5.24.0\n", name))
writeFile(filepath.Join(dir, ".gitignore"),
fmt.Sprintf("%s\n.micro\n", name))
}
// Placeholder handler so go mod tidy works (will be overwritten by LLM)
handlerPath := filepath.Join(dir, "handler", name+".go")
if _, err := os.Stat(handlerPath); os.IsNotExist(err) {
writeFile(handlerPath,
fmt.Sprintf("package handler\n\ntype %s struct{}\n\nfunc New() *%s { return &%s{} }\n", titleName, titleName, titleName))
recordHandlerHash(dir, handlerPath)
}
return nil
}
// generateHandler asks the LLM to write the handler with business logic.
// If the handler exists and the user has modified it since generation,
// it is left untouched.
func generateHandler(ctx context.Context, m ai.Model, dir string, svc ServiceSpec, proto string) error {
if m == nil {
return nil // no LLM — keep the placeholder
}
handlerFile := filepath.Join(dir, "handler", svc.Name+".go")
if handlerModified(dir, handlerFile) {
fmt.Printf(" \033[2mkeeping %s handler (modified)\033[0m\n", svc.Name)
return nil
}
titleName := toTitle(svc.Name)
// Build endpoint descriptions
var epDescs []string
for _, ep := range svc.Endpoints {
epDescs = append(epDescs, fmt.Sprintf("- %s: %s (example input: %s)", ep.Name, ep.Description, ep.Example))
}
prompt := fmt.Sprintf(handlerPrompt,
svc.Name, titleName, titleName, titleName, titleName, proto, strings.Join(epDescs, "\n"))
sp := startSpinner(fmt.Sprintf("writing %s handler...", svc.Name))
genCtx, genCancel := context.WithTimeout(ctx, 90*time.Second)
defer genCancel()
resp, err := m.Generate(genCtx, &ai.Request{
Prompt: fmt.Sprintf("Generate the handler for the %s service with real business logic.", svc.Name),
SystemPrompt: prompt,
})
sp.Stop()
if err != nil {
return err
}
code := firstNonEmpty(resp.Answer, resp.Reply)
code = extractCode(code)
if !strings.HasPrefix(strings.TrimSpace(code), "package") {
return fmt.Errorf("LLM did not return valid Go code")
}
if isTruncated(code) {
fmt.Printf(" \033[33m→\033[0m response truncated, retrying...\n")
sp = startSpinner(fmt.Sprintf("rewriting %s handler...", svc.Name))
retryCtx, retryCancel := context.WithTimeout(ctx, 90*time.Second)
defer retryCancel()
resp, err = m.Generate(retryCtx, &ai.Request{
Prompt: fmt.Sprintf("Generate the handler for the %s service with real business logic. Keep it concise — no more than 200 lines.", svc.Name),
SystemPrompt: prompt,
})
sp.Stop()
if err != nil {
return err
}
code = firstNonEmpty(resp.Answer, resp.Reply)
code = extractCode(code)
}
if !strings.HasPrefix(strings.TrimSpace(code), "package") {
return fmt.Errorf("LLM did not return valid Go code")
}
writeFile(handlerFile, code)
recordHandlerHash(dir, handlerFile)
return nil
}
// compileFix tries to compile, and if it fails, sends the error to
// the LLM to fix. Up to maxAttempts iterations.
func compileFix(ctx context.Context, m ai.Model, dir, name string, maxAttempts int) error {
for attempt := 0; attempt < maxAttempts; attempt++ {
cmd := exec.Command("go", "build", "./...")
cmd.Dir = dir
out, err := cmd.CombinedOutput()
if err == nil {
return nil // compiles!
}
if m == nil {
return fmt.Errorf("compile failed: %s", string(out))
}
// Read current handler
handlerPath := filepath.Join(dir, "handler", name+".go")
currentCode := readFile(handlerPath)
sp := startSpinner(fmt.Sprintf("fixing compile errors (attempt %d/%d)...", attempt+1, maxAttempts))
fixCtx, fixCancel := context.WithTimeout(ctx, 60*time.Second)
resp, fixErr := m.Generate(fixCtx, &ai.Request{
Prompt: fmt.Sprintf("This Go code has compile errors. Fix ALL of them and return the COMPLETE corrected file.\n\nErrors:\n%s\n\nCode:\n%s",
string(out), currentCode),
SystemPrompt: "You are a Go expert. Return ONLY the corrected Go code. No markdown, no explanation. Start with 'package handler'.",
})
fixCancel()
sp.Stop()
if fixErr != nil {
return fmt.Errorf("fix attempt failed: %w", fixErr)
}
fixed := firstNonEmpty(resp.Answer, resp.Reply)
fixed = extractCode(fixed)
if strings.HasPrefix(strings.TrimSpace(fixed), "package") && !isTruncated(fixed) {
writeFile(handlerPath, fixed)
}
}
// Final check
cmd := exec.Command("go", "build", "./...")
cmd.Dir = dir
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("still fails after %d attempts: %s", maxAttempts, string(out))
}
return nil
}
func newModel(provider, apiKey, model string) ai.Model {
if provider == "" {
provider = ai.AutoDetectProvider("")
}
var opts []ai.Option
opts = append(opts, ai.WithAPIKey(apiKey))
if model != "" {
opts = append(opts, ai.WithModel(model))
}
return ai.New(provider, opts...)
}
func buildProto(dehyphen, titleName string, svc ServiceSpec) string {
var b strings.Builder
b.WriteString(fmt.Sprintf("syntax = \"proto3\";\n\npackage %s;\n\noption go_package = \"./proto;%s\";\n\n", dehyphen, dehyphen))
b.WriteString(fmt.Sprintf("service %s {\n", titleName))
for _, ep := range svc.Endpoints {
b.WriteString(fmt.Sprintf("\trpc %s(%sRequest) returns (%sResponse) {}\n", ep.Name, ep.Name, ep.Name))
}
b.WriteString("}\n\n")
// Record message
b.WriteString(fmt.Sprintf("message %sRecord {\n", titleName))
for i, f := range svc.Fields {
b.WriteString(fmt.Sprintf("\t%s %s = %d; // %s\n", protoType(f.Type), f.Name, i+1, f.Description))
}
b.WriteString("}\n\n")
// Request/response for each endpoint
for _, ep := range svc.Endpoints {
switch ep.Name {
case "Create":
b.WriteString(fmt.Sprintf("message CreateRequest {\n"))
n := 1
for _, f := range svc.Fields {
if f.Name == "id" || f.Name == "created" || f.Name == "updated" {
continue
}
b.WriteString(fmt.Sprintf("\t%s %s = %d;\n", protoType(f.Type), f.Name, n))
n++
}
b.WriteString(fmt.Sprintf("}\n\nmessage CreateResponse {\n\t%sRecord record = 1;\n}\n\n", titleName))
case "Read":
b.WriteString(fmt.Sprintf("message ReadRequest {\n\tstring id = 1;\n}\n\nmessage ReadResponse {\n\t%sRecord record = 1;\n}\n\n", titleName))
case "Update":
b.WriteString("message UpdateRequest {\n\tstring id = 1;\n")
n := 2
for _, f := range svc.Fields {
if f.Name == "id" || f.Name == "created" || f.Name == "updated" {
continue
}
b.WriteString(fmt.Sprintf("\t%s %s = %d;\n", protoType(f.Type), f.Name, n))
n++
}
b.WriteString(fmt.Sprintf("}\n\nmessage UpdateResponse {\n\t%sRecord record = 1;\n}\n\n", titleName))
case "Delete":
b.WriteString(fmt.Sprintf("message DeleteRequest {\n\tstring id = 1;\n}\n\nmessage DeleteResponse {\n\tbool deleted = 1;\n}\n\n"))
case "List":
b.WriteString(fmt.Sprintf("message ListRequest {\n\tint64 limit = 1;\n\tint64 offset = 2;\n\tstring query = 3;\n}\n\nmessage ListResponse {\n\trepeated %sRecord records = 1;\n\tint64 total = 2;\n}\n\n", titleName))
default:
// Custom endpoint — use all fields as input, record as output
b.WriteString(fmt.Sprintf("message %sRequest {\n", ep.Name))
n := 1
for _, f := range svc.Fields {
if f.Name == "created" || f.Name == "updated" {
continue
}
b.WriteString(fmt.Sprintf("\t%s %s = %d;\n", protoType(f.Type), f.Name, n))
n++
}
b.WriteString(fmt.Sprintf("}\n\nmessage %sResponse {\n\t%sRecord record = 1;\n\tstring message = 2;\n\tbool success = 3;\n}\n\n", ep.Name, titleName))
}
}
return b.String()
}
func buildMain(name, titleName string) string {
svcName := strings.TrimSuffix(name, "-service")
return fmt.Sprintf(`package main
import (
"%s/handler"
pb "%s/proto"
"go-micro.dev/v5"
"go-micro.dev/v5/gateway/mcp"
)
func main() {
service := micro.New("%s",
mcp.WithMCP(":0"),
)
service.Init()
pb.Register%sHandler(service.Server(), handler.New())
service.Run()
}
`, name, name, svcName, titleName)
}
func extractJSON(s string) string {
if i := strings.Index(s, "```json"); i >= 0 {
s = s[i+7:]
if j := strings.Index(s, "```"); j >= 0 {
return strings.TrimSpace(s[:j])
}
}
if i := strings.Index(s, "```"); i >= 0 {
s = s[i+3:]
if j := strings.Index(s, "```"); j >= 0 {
return strings.TrimSpace(s[:j])
}
}
if i := strings.Index(s, "{"); i >= 0 {
depth := 0
for j := i; j < len(s); j++ {
switch s[j] {
case '{':
depth++
case '}':
depth--
if depth == 0 {
return s[i : j+1]
}
}
}
}
return s
}
func extractCode(s string) string {
if i := strings.Index(s, "```go"); i >= 0 {
s = s[i+5:]
if j := strings.Index(s, "```"); j >= 0 {
return strings.TrimSpace(s[:j])
}
}
if i := strings.Index(s, "```"); i >= 0 {
s = s[i+3:]
if j := strings.Index(s, "```"); j >= 0 {
return strings.TrimSpace(s[:j])
}
}
// Try to find raw package declaration
if i := strings.Index(s, "package "); i >= 0 {
return strings.TrimSpace(s[i:])
}
return strings.TrimSpace(s)
}
func isTruncated(code string) bool {
trimmed := strings.TrimSpace(code)
if len(trimmed) == 0 {
return true
}
// Valid Go files end with a closing brace
if trimmed[len(trimmed)-1] != '}' {
return true
}
// Check balanced braces
depth := 0
for _, c := range trimmed {
switch c {
case '{':
depth++
case '}':
depth--
}
}
return depth != 0
}
func protoType(t string) string {
switch t {
case "int64":
return "int64"
case "int32":
return "int32"
case "bool":
return "bool"
case "float64":
return "double"
default:
return "string"
}
}
func toTitle(s string) string {
words := strings.FieldsFunc(s, func(r rune) bool { return r == '-' || r == '_' || r == ' ' })
for i, w := range words {
if len(w) > 0 {
words[i] = strings.ToUpper(w[:1]) + w[1:]
}
}
return strings.Join(words, "")
}
func firstNonEmpty(vals ...string) string {
for _, v := range vals {
if v != "" {
return v
}
}
return ""
}
func readFile(path string) string {
b, _ := os.ReadFile(path)
return string(b)
}
func writeFile(path, content string) {
os.WriteFile(path, []byte(content), 0644)
}
func runIn(dir string, name string, args ...string) error {
cmd := exec.Command(name, args...)
cmd.Dir = dir
cmd.Env = append(os.Environ(), "PATH="+os.Getenv("PATH")+":"+os.Getenv("GOPATH")+"/bin:"+os.Getenv("HOME")+"/go/bin")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
type spinner struct {
msg string
stop chan struct{}
done sync.WaitGroup
}
func isTTY() bool {
fi, err := os.Stdout.Stat()
if err != nil {
return false
}
return fi.Mode()&os.ModeCharDevice != 0
}
func startSpinner(msg string) *spinner {
s := &spinner{msg: msg, stop: make(chan struct{})}
if !isTTY() {
fmt.Printf(" %s\n", msg)
return s
}
s.done.Add(1)
go func() {
defer s.done.Done()
frames := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
i := 0
t := time.NewTicker(100 * time.Millisecond)
defer t.Stop()
for {
select {
case <-s.stop:
fmt.Printf("\r\033[K")
return
case <-t.C:
fmt.Printf("\r %s %s", frames[i%len(frames)], msg)
i++
}
}
}()
return s
}
func (s *spinner) Stop() {
close(s.stop)
s.done.Wait()
}
func fileHash(path string) string {
b, err := os.ReadFile(path)
if err != nil {
return ""
}
h := sha256.Sum256(b)
return hex.EncodeToString(h[:])
}
func metaPath(svcDir string) string {
return filepath.Join(svcDir, ".micro")
}
func readMeta(svcDir string) map[string]string {
m := make(map[string]string)
b, err := os.ReadFile(metaPath(svcDir))
if err != nil {
return m
}
json.Unmarshal(b, &m)
return m
}
func writeMeta(svcDir string, m map[string]string) {
b, _ := json.MarshalIndent(m, "", " ")
os.WriteFile(metaPath(svcDir), b, 0644)
}
func fileModified(svcDir, key, path string) bool {
meta := readMeta(svcDir)
savedHash, ok := meta[key]
if !ok {
return false
}
return fileHash(path) != savedHash
}
func recordFileHash(svcDir, key, path string) {
meta := readMeta(svcDir)
meta[key] = fileHash(path)
writeMeta(svcDir, meta)
}
func handlerModified(svcDir, handlerFile string) bool {
return fileModified(svcDir, "handler_hash", handlerFile)
}
func recordHandlerHash(svcDir, handlerFile string) {
recordFileHash(svcDir, "handler_hash", handlerFile)
}
-421
View File
@@ -1,421 +0,0 @@
package generate
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestToTitle(t *testing.T) {
tests := []struct {
in, want string
}{
{"order-service", "OrderService"},
{"task", "Task"},
{"inventory_item", "InventoryItem"},
{"hello world", "HelloWorld"},
{"a-b-c", "ABC"},
{"already", "Already"},
}
for _, tt := range tests {
if got := toTitle(tt.in); got != tt.want {
t.Errorf("toTitle(%q) = %q, want %q", tt.in, got, tt.want)
}
}
}
func TestProtoType(t *testing.T) {
tests := []struct {
in, want string
}{
{"string", "string"},
{"int64", "int64"},
{"int32", "int32"},
{"bool", "bool"},
{"float64", "double"},
{"unknown", "string"},
{"", "string"},
}
for _, tt := range tests {
if got := protoType(tt.in); got != tt.want {
t.Errorf("protoType(%q) = %q, want %q", tt.in, got, tt.want)
}
}
}
func TestFirstNonEmpty(t *testing.T) {
if got := firstNonEmpty("", "", "c"); got != "c" {
t.Errorf("got %q, want %q", got, "c")
}
if got := firstNonEmpty("a", "b"); got != "a" {
t.Errorf("got %q, want %q", got, "a")
}
if got := firstNonEmpty("", ""); got != "" {
t.Errorf("got %q, want %q", got, "")
}
}
func TestExtractJSON(t *testing.T) {
tests := []struct {
name, in, want string
}{
{
"fenced json",
"Here's the design:\n```json\n{\"services\": []}\n```\nDone.",
`{"services": []}`,
},
{
"fenced no lang",
"```\n{\"a\": 1}\n```",
`{"a": 1}`,
},
{
"raw json",
`some text {"key": "val"} trailing`,
`{"key": "val"}`,
},
{
"nested braces",
`{"a": {"b": 1}}`,
`{"a": {"b": 1}}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := extractJSON(tt.in)
if got != tt.want {
t.Errorf("extractJSON() = %q, want %q", got, tt.want)
}
})
}
}
func TestExtractCode(t *testing.T) {
tests := []struct {
name, in string
wantPrefix string
}{
{
"go fence",
"Here:\n```go\npackage handler\n\nfunc Foo() {}\n```\nDone.",
"package handler",
},
{
"generic fence",
"```\npackage main\n```",
"package main",
},
{
"raw code",
"Sure, here's the code:\npackage handler\n\ntype X struct{}",
"package handler",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := extractCode(tt.in)
if !strings.HasPrefix(got, tt.wantPrefix) {
t.Errorf("extractCode() = %q, want prefix %q", got, tt.wantPrefix)
}
})
}
}
func TestBuildProto(t *testing.T) {
svc := ServiceSpec{
Name: "task-service",
Description: "Manages tasks",
Fields: []FieldSpec{
{Name: "id", Type: "string", Description: "Task ID"},
{Name: "title", Type: "string", Description: "Task title"},
{Name: "done", Type: "bool", Description: "Completion status"},
{Name: "created", Type: "int64", Description: "Created timestamp"},
{Name: "updated", Type: "int64", Description: "Updated timestamp"},
},
Endpoints: []EndpointSpec{
{Name: "Create", Description: "Create a task"},
{Name: "Read", Description: "Get a task"},
{Name: "Update", Description: "Update a task"},
{Name: "Delete", Description: "Delete a task"},
{Name: "List", Description: "List tasks"},
{Name: "ToggleComplete", Description: "Toggle completion"},
},
}
proto := buildProto("taskservice", "TaskService", svc)
checks := []string{
`syntax = "proto3"`,
`package taskservice`,
`service TaskService`,
`rpc Create(CreateRequest) returns (CreateResponse)`,
`rpc ToggleComplete(ToggleCompleteRequest) returns (ToggleCompleteResponse)`,
`message TaskServiceRecord`,
`string title = 2`,
`bool done = 3`,
`message CreateRequest`,
`message ReadRequest`,
`message DeleteRequest`,
`message ListRequest`,
`message ToggleCompleteRequest`,
}
for _, c := range checks {
if !strings.Contains(proto, c) {
t.Errorf("buildProto() missing %q", c)
}
}
// Create should not include id, created, updated
createIdx := strings.Index(proto, "message CreateRequest")
createEnd := strings.Index(proto[createIdx:], "}")
createBlock := proto[createIdx : createIdx+createEnd]
for _, skip := range []string{"string id", "int64 created", "int64 updated"} {
if strings.Contains(createBlock, skip) {
t.Errorf("CreateRequest should not contain %q", skip)
}
}
}
func TestBuildMain(t *testing.T) {
// New naming: no -service suffix
main := buildMain("order", "Order")
checks := []string{
`"order/handler"`,
`pb "order/proto"`,
`micro.New("order"`,
`pb.RegisterOrderHandler`,
`handler.New()`,
}
for _, c := range checks {
if !strings.Contains(main, c) {
t.Errorf("buildMain(order) missing %q", c)
}
}
// Legacy naming: -service suffix stripped
main = buildMain("order-service", "OrderService")
checks = []string{
`"order-service/handler"`,
`pb "order-service/proto"`,
`micro.New("order"`,
`pb.RegisterOrderServiceHandler`,
`handler.New()`,
}
for _, c := range checks {
if !strings.Contains(main, c) {
t.Errorf("buildMain() missing %q", c)
}
}
}
func TestHandlerModifiedTracking(t *testing.T) {
dir := t.TempDir()
handlerDir := filepath.Join(dir, "handler")
os.MkdirAll(handlerDir, 0755)
handlerFile := filepath.Join(handlerDir, "test.go")
// No .micro file → not modified
os.WriteFile(handlerFile, []byte("package handler\n"), 0644)
if handlerModified(dir, handlerFile) {
t.Error("expected not modified when no .micro exists")
}
// Record hash → not modified
recordHandlerHash(dir, handlerFile)
if handlerModified(dir, handlerFile) {
t.Error("expected not modified after recording hash")
}
// Edit the file → modified
os.WriteFile(handlerFile, []byte("package handler\n\nfunc Foo() {}\n"), 0644)
if !handlerModified(dir, handlerFile) {
t.Error("expected modified after editing file")
}
// Re-record → not modified again
recordHandlerHash(dir, handlerFile)
if handlerModified(dir, handlerFile) {
t.Error("expected not modified after re-recording hash")
}
}
func TestMetaReadWrite(t *testing.T) {
dir := t.TempDir()
m := readMeta(dir)
if len(m) != 0 {
t.Error("expected empty meta for new dir")
}
m["handler_hash"] = "abc123"
m["version"] = "1"
writeMeta(dir, m)
m2 := readMeta(dir)
if m2["handler_hash"] != "abc123" || m2["version"] != "1" {
t.Errorf("readMeta() = %v, want handler_hash=abc123, version=1", m2)
}
}
func TestGenerateStructure(t *testing.T) {
dir := t.TempDir()
svcDir := filepath.Join(dir, "test-svc")
svc := ServiceSpec{
Name: "test-svc",
Description: "Test service",
Fields: []FieldSpec{
{Name: "id", Type: "string"},
{Name: "name", Type: "string"},
},
Endpoints: []EndpointSpec{
{Name: "Create"},
{Name: "Read"},
},
}
if err := generateStructure(svcDir, svc); err != nil {
t.Fatal(err)
}
// Check files exist
for _, f := range []string{
"proto/test-svc.proto",
"handler/test-svc.go",
"main.go",
"go.mod",
"Makefile",
".gitignore",
} {
if _, err := os.Stat(filepath.Join(svcDir, f)); err != nil {
t.Errorf("missing %s: %v", f, err)
}
}
// Check .micro was created with handler hash
meta := readMeta(svcDir)
if meta["handler_hash"] == "" {
t.Error("expected handler_hash in .micro after generateStructure")
}
// Run again — should not overwrite main.go
mainBefore, _ := os.ReadFile(filepath.Join(svcDir, "main.go"))
os.WriteFile(filepath.Join(svcDir, "main.go"), []byte("// user edited\n"), 0644)
if err := generateStructure(svcDir, svc); err != nil {
t.Fatal(err)
}
mainAfter, _ := os.ReadFile(filepath.Join(svcDir, "main.go"))
if string(mainAfter) == string(mainBefore) {
t.Error("expected main.go to keep user edit on re-run")
}
// Proto should be protected if user modified it
protoFile := filepath.Join(svcDir, "proto", "test-svc.proto")
protoBefore, _ := os.ReadFile(protoFile)
os.WriteFile(protoFile, []byte("// user-edited proto\n"), 0644)
if err := generateStructure(svcDir, svc); err != nil {
t.Fatal(err)
}
protoAfter, _ := os.ReadFile(protoFile)
if string(protoAfter) != "// user-edited proto\n" {
t.Error("expected proto to be preserved after user edit")
}
// Proto should regenerate if NOT modified
recordFileHash(svcDir, "proto_hash", protoFile)
if err := generateStructure(svcDir, svc); err != nil {
t.Fatal(err)
}
protoAfter2, _ := os.ReadFile(protoFile)
if string(protoAfter2) == string(protoBefore) {
// ok — regenerated from spec
}
}
func TestFileModified(t *testing.T) {
dir := t.TempDir()
f := filepath.Join(dir, "test.txt")
os.WriteFile(f, []byte("original"), 0644)
// No hash → not modified
if fileModified(dir, "test_hash", f) {
t.Error("expected not modified with no saved hash")
}
recordFileHash(dir, "test_hash", f)
// Same content → not modified
if fileModified(dir, "test_hash", f) {
t.Error("expected not modified with matching hash")
}
// Changed content → modified
os.WriteFile(f, []byte("changed"), 0644)
if !fileModified(dir, "test_hash", f) {
t.Error("expected modified after content change")
}
}
func TestDiscoverExisting(t *testing.T) {
dir := t.TempDir()
// Empty directory → empty string
if got := discoverExisting(dir); got != "" {
t.Errorf("expected empty for empty dir, got %q", got)
}
// Non-service directory (no proto) → empty
os.MkdirAll(filepath.Join(dir, "not-a-service"), 0755)
if got := discoverExisting(dir); got != "" {
t.Errorf("expected empty for dir without proto, got %q", got)
}
// Create a real service directory with proto
svcDir := filepath.Join(dir, "order-service")
os.MkdirAll(filepath.Join(svcDir, "proto"), 0755)
os.WriteFile(filepath.Join(svcDir, "proto", "order-service.proto"),
[]byte("syntax = \"proto3\";\nservice OrderService {}"), 0644)
got := discoverExisting(dir)
if !strings.Contains(got, "order-service") {
t.Errorf("expected to find order-service, got %q", got)
}
if !strings.Contains(got, "OrderService") {
t.Errorf("expected to find proto content, got %q", got)
}
// Add a second service
svc2Dir := filepath.Join(dir, "user-service")
os.MkdirAll(filepath.Join(svc2Dir, "proto"), 0755)
os.WriteFile(filepath.Join(svc2Dir, "proto", "user-service.proto"),
[]byte("syntax = \"proto3\";\nservice UserService {}"), 0644)
got = discoverExisting(dir)
if !strings.Contains(got, "order-service") || !strings.Contains(got, "user-service") {
t.Errorf("expected both services, got %q", got)
}
}
func TestIsTruncated(t *testing.T) {
tests := []struct {
name string
code string
want bool
}{
{"complete", "package handler\n\nfunc New() *H { return &H{} }\n", false},
{"empty", "", true},
{"no closing brace", "package handler\n\nfunc Foo() {", true},
{"unbalanced", "package handler\n\nfunc Foo() {\n\tif true {", true},
{"balanced", "package handler\n\nfunc Foo() {\n\tif true {\n\t}\n}", false},
{"trailing whitespace ok", "package handler\n\ntype X struct{}\n\n", false},
{"mid-expression", "package handler\n\nfunc F() {\n\tx := 1 +", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isTruncated(tt.code); got != tt.want {
t.Errorf("isTruncated() = %v, want %v", got, tt.want)
}
})
}
}
-269
View File
@@ -1,269 +0,0 @@
// Package initcmd provides the micro init command for server setup
package initcmd
import (
"fmt"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
"strings"
"github.com/urfave/cli/v2"
"go-micro.dev/v5/cmd"
)
const systemdTemplate = `[Unit]
Description=Micro service: %%i
After=network.target
[Service]
Type=simple
User=%s
Group=%s
WorkingDirectory=%s
ExecStart=%s/bin/%%i
Restart=on-failure
RestartSec=5
EnvironmentFile=-%s/config/%%i.env
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=micro-%%i
# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=%s/data
[Install]
WantedBy=multi-user.target
`
// Init initializes a server to receive micro deployments
func Init(c *cli.Context) error {
if !c.Bool("server") {
return fmt.Errorf("usage: micro init --server\n\nInitialize this machine to receive micro deployments")
}
// Check if we're on Linux
if runtime.GOOS != "linux" {
return fmt.Errorf("micro init --server is only supported on Linux")
}
// Check for remote init
remoteHost := c.String("remote")
if remoteHost != "" {
return initRemote(c, remoteHost)
}
basePath := c.String("path")
userName := c.String("user")
fmt.Println("Initializing micro server...")
fmt.Println()
// Check if running as root (needed for systemd and creating users)
if os.Geteuid() != 0 {
return fmt.Errorf(`micro init --server requires root privileges.
Run with sudo:
sudo micro init --server`)
}
// Create user if needed
if userName == "micro" {
if err := createMicroUser(); err != nil {
return err
}
}
// Create directories
fmt.Println("Creating directories:")
dirs := []string{
filepath.Join(basePath, "bin"),
filepath.Join(basePath, "data"),
filepath.Join(basePath, "config"),
}
for _, dir := range dirs {
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create %s: %w", dir, err)
}
fmt.Printf(" ✓ %s\n", dir)
}
// Set ownership
if userName != "root" {
u, err := user.Lookup(userName)
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 {
return fmt.Errorf("failed to set ownership: %w", err)
}
}
fmt.Println()
// Create systemd template
fmt.Println("Creating systemd template:")
unitContent := fmt.Sprintf(systemdTemplate, userName, userName, basePath, basePath, basePath, basePath)
unitPath := "/etc/systemd/system/micro@.service"
if err := os.WriteFile(unitPath, []byte(unitContent), 0644); err != nil {
return fmt.Errorf("failed to write systemd unit: %w", err)
}
fmt.Printf(" ✓ %s\n", unitPath)
// Reload systemd
reloadCmd := exec.Command("systemctl", "daemon-reload")
if err := reloadCmd.Run(); err != nil {
return fmt.Errorf("failed to reload systemd: %w", err)
}
fmt.Println(" ✓ systemd daemon-reload")
// Write marker file so deploy can detect initialization
markerPath := filepath.Join(basePath, ".micro-initialized")
if err := os.WriteFile(markerPath, []byte("1\n"), 0644); err != nil {
return fmt.Errorf("failed to write marker: %w", err)
}
fmt.Println()
fmt.Println("Server ready!")
fmt.Println()
fmt.Println(" Deploy from your machine:")
fmt.Printf(" micro deploy user@%s\n", getHostname())
fmt.Println()
fmt.Println(" Manage services:")
fmt.Println(" sudo systemctl status micro@myservice")
fmt.Println(" sudo journalctl -u micro@myservice -f")
fmt.Println()
return nil
}
func createMicroUser() error {
// Check if user exists
if _, err := user.Lookup("micro"); err == nil {
return nil // user already exists
}
fmt.Println("Creating micro user:")
createCmd := exec.Command("useradd", "--system", "--no-create-home", "--shell", "/bin/false", "micro")
if err := createCmd.Run(); err != nil {
// Check if it's just because user already exists
if _, lookupErr := user.Lookup("micro"); lookupErr == nil {
return nil
}
return fmt.Errorf("failed to create micro user: %w", err)
}
fmt.Println(" ✓ Created user 'micro'")
return nil
}
func initRemote(c *cli.Context, host string) error {
fmt.Printf("Initializing micro on %s...\n\n", host)
// Check SSH connectivity first
if err := checkSSH(host); err != nil {
return err
}
basePath := c.String("path")
userName := c.String("user")
// 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
if err := sshCmd.Run(); err != nil {
return fmt.Errorf("remote init failed: %w", err)
}
return nil
}
func checkSSH(host string) error {
// Quick SSH test
testCmd := exec.Command("ssh", "-o", "ConnectTimeout=5", "-o", "BatchMode=yes", host, "echo ok")
output, err := testCmd.CombinedOutput()
if err != nil {
return fmt.Errorf(`✗ Cannot connect to %s
SSH connection failed. Check that:
• The server is reachable: ping %s
• SSH is configured: ssh %s
• Your key is added: ssh-add -l
Common fixes:
• Add SSH key: ssh-copy-id %s
• Check hostname in ~/.ssh/config
Error: %s`, host, host, host, host, strings.TrimSpace(string(output)))
}
return nil
}
func getHostname() string {
name, err := os.Hostname()
if err != nil {
return "this-server"
}
return name
}
func init() {
cmd.Register(&cli.Command{
Name: "init",
Usage: "Initialize micro for development or server deployment",
Description: `Initialize micro on a server to receive deployments.
Server setup:
sudo micro init --server
This creates:
• /opt/micro/bin/ - service binaries
• /opt/micro/data/ - persistent data
• /opt/micro/config/ - environment files
• systemd template for managing services
Remote setup:
micro init --server --remote user@host
After init, deploy with:
micro deploy user@host`,
Action: Init,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "server",
Usage: "Initialize as a deployment server",
},
&cli.StringFlag{
Name: "path",
Usage: "Base path for micro (default: /opt/micro)",
Value: "/opt/micro",
},
&cli.StringFlag{
Name: "user",
Usage: "User to run services as (default: micro)",
Value: "micro",
},
&cli.StringFlag{
Name: "remote",
Usage: "Initialize a remote server via SSH",
},
},
})
}
+6 -142
View File
@@ -2,20 +2,14 @@
package new
import (
"bufio"
"context"
"fmt"
"go/build"
"os"
"os/exec"
"os/signal"
"path"
"path/filepath"
"runtime"
"strings"
"syscall"
"go-micro.dev/v5/cmd/micro/cli/generate"
"text/template"
"time"
@@ -90,10 +84,7 @@ func create(c config) error {
return fmt.Errorf("%s already exists", c.Dir)
}
fmt.Println()
fmt.Println(" \033[1mmicro new\033[0m")
fmt.Println()
fmt.Printf(" Creating \033[36m%s\033[0m\n\n", c.Alias)
fmt.Printf("Creating service %s\n\n", c.Alias)
t := treeprint.New()
@@ -144,11 +135,6 @@ func addFileToTree(root treeprint.Tree, file string) {
}
func Run(ctx *cli.Context) error {
// Handle --prompt: design services with AI, then generate each one
if prompt := ctx.String("prompt"); prompt != "" {
return runPrompt(ctx, prompt)
}
dir := ctx.Args().First()
if len(dir) == 0 {
fmt.Println("specify service name")
@@ -188,23 +174,17 @@ func Run(ctx *cli.Context) error {
}
goDir = filepath.Join(goPath, "src", path.Clean(dir))
noMCP := ctx.Bool("no-mcp")
templateName := ctx.String("template")
// Select templates based on --template flag
mainTmpl, handlerTmpl, protoTmpl := selectTemplates(templateName, noMCP)
c := config{
Alias: dir,
Comments: nil,
Comments: nil, // Remove redundant protoComments
Dir: dir,
GoDir: goDir,
GoPath: goPath,
UseGoPath: false,
Files: []file{
{"main.go", mainTmpl},
{"handler/" + dir + ".go", handlerTmpl},
{"proto/" + dir + ".proto", protoTmpl},
{"main.go", tmpl.MainSRV},
{"handler/" + dir + ".go", tmpl.HandlerSRV},
{"proto/" + dir + ".proto", tmpl.ProtoSRV},
{"Makefile", tmpl.Makefile},
{"README.md", tmpl.Readme},
{".gitignore", tmpl.GitIgnore},
@@ -234,53 +214,10 @@ func Run(ctx *cli.Context) error {
fmt.Println("\nProject structure after 'make proto':")
printTree(dir)
fmt.Println()
fmt.Printf(" \033[32m✓\033[0m Service \033[36m%s\033[0m created\n\n", dir)
fmt.Println(" Next steps:")
fmt.Printf(" cd %s\n", dir)
fmt.Println(" go run .")
if !noMCP {
fmt.Println()
fmt.Printf(" MCP tools \033[36mhttp://localhost:3001/mcp/tools\033[0m\n")
fmt.Println(" Claude Code \033[2mmicro mcp serve\033[0m")
}
fmt.Println()
fmt.Println("\nService created successfully! Start coding in your new service directory.")
return nil
}
func selectTemplates(name string, noMCP bool) (mainTmpl, handlerTmpl, protoTmpl string) {
switch name {
case "crud":
if noMCP {
mainTmpl = tmpl.MainSRVNoMCP
} else {
mainTmpl = tmpl.MainSRV
}
return mainTmpl, tmpl.CrudHandlerSRV, tmpl.CrudProtoSRV
case "pubsub":
if noMCP {
mainTmpl = tmpl.PubsubMainSRVNoMCP
} else {
mainTmpl = tmpl.PubsubMainSRV
}
return mainTmpl, tmpl.PubsubHandlerSRV, tmpl.PubsubProtoSRV
case "api":
if noMCP {
mainTmpl = tmpl.MainSRVNoMCP
} else {
mainTmpl = tmpl.MainSRV
}
return mainTmpl, tmpl.ApiHandlerSRV, tmpl.ApiProtoSRV
default:
if noMCP {
mainTmpl = tmpl.MainSRVNoMCP
} else {
mainTmpl = tmpl.MainSRV
}
return mainTmpl, tmpl.HandlerSRV, tmpl.ProtoSRV
}
}
func runInDir(dir, cmd string) error {
parts := strings.Fields(cmd)
c := exec.Command(parts[0], parts[1:]...)
@@ -318,76 +255,3 @@ func printTree(dir string) {
filepath.Walk(dir, walk)
fmt.Println(t.String())
}
func runPrompt(cliCtx *cli.Context, prompt string) error {
provider := cliCtx.String("provider")
apiKey := cliCtx.String("api_key")
if apiKey == "" {
// Try provider-specific env vars
for _, env := range []string{"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY",
"ATLASCLOUD_API_KEY", "GROQ_API_KEY", "MISTRAL_API_KEY", "TOGETHER_API_KEY", "MICRO_AI_API_KEY"} {
if v := os.Getenv(env); v != "" {
apiKey = v
break
}
}
}
if apiKey == "" {
return fmt.Errorf("--api_key or a provider API key env var is required for --prompt")
}
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()
fmt.Println()
fmt.Println(" \033[1mmicro new --prompt\033[0m")
fmt.Println()
fmt.Printf(" \033[2mDesigning services for:\033[0m %s\n\n", prompt)
design, err := generate.Design(ctx, provider, apiKey, "", ".", prompt)
if err != nil {
return fmt.Errorf("design failed: %w", err)
}
fmt.Println(" Services:")
for _, svc := range design.Services {
fmt.Printf(" \033[32m●\033[0m \033[36m%s\033[0m — %s\n", svc.Name, svc.Description)
for _, ep := range svc.Endpoints {
fmt.Printf(" %s: %s\n", ep.Name, ep.Description)
}
}
fmt.Println()
if !confirmGenerate() {
fmt.Println(" Cancelled.")
return nil
}
fmt.Println(" Generating code...")
if err := generate.Generate(ctx, ".", design, provider, apiKey, ""); err != nil {
return fmt.Errorf("generate failed: %w", err)
}
for _, svc := range design.Services {
fmt.Printf(" \033[32m✓\033[0m %s/\n", svc.Name)
}
fmt.Println()
fmt.Println(" \033[32m✓\033[0m All services generated")
fmt.Println()
fmt.Println(" Next steps:")
fmt.Println(" micro run \033[2m# start all services\033[0m")
fmt.Println(" micro chat --provider anthropic \033[2m# talk to them\033[0m")
fmt.Println()
return nil
}
func confirmGenerate() bool {
fmt.Print(" Generate? [Y/n] ")
scanner := bufio.NewScanner(os.Stdin)
if !scanner.Scan() {
return false
}
answer := strings.TrimSpace(strings.ToLower(scanner.Text()))
return answer == "" || answer == "y" || answer == "yes"
}
-122
View File
@@ -1,122 +0,0 @@
package template
var (
ApiProtoSRV = `syntax = "proto3";
package {{dehyphen .Alias}};
option go_package = "./proto;{{dehyphen .Alias}}";
service {{title .Alias}} {
rpc Health(HealthRequest) returns (HealthResponse) {}
rpc Endpoint(EndpointRequest) returns (EndpointResponse) {}
}
message HealthRequest {}
message HealthResponse {
string status = 1;
int64 uptime = 2;
}
message EndpointRequest {
string method = 1;
string path = 2;
string body = 3;
map<string, string> headers = 4;
}
message EndpointResponse {
int32 status_code = 1;
string body = 2;
map<string, string> headers = 3;
}
`
ApiHandlerSRV = `package handler
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
log "go-micro.dev/v5/logger"
pb "{{.Dir}}/proto"
)
type {{title .Alias}} struct {
started time.Time
routes map[string]http.HandlerFunc
}
func New() *{{title .Alias}} {
h := &{{title .Alias}}{
started: time.Now(),
routes: make(map[string]http.HandlerFunc),
}
h.registerRoutes()
return h
}
func (h *{{title .Alias}}) registerRoutes() {
h.routes["GET /hello"] = func(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
name = "World"
}
json.NewEncoder(w).Encode(map[string]string{
"message": fmt.Sprintf("Hello %s", name),
})
}
}
// Health returns the service health status and uptime.
//
// @example {}
func (h *{{title .Alias}}) Health(ctx context.Context, req *pb.HealthRequest, rsp *pb.HealthResponse) error {
rsp.Status = "ok"
rsp.Uptime = int64(time.Since(h.started).Seconds())
return nil
}
// Endpoint handles proxied HTTP requests. The method and path fields
// select the route; body and headers are forwarded.
//
// @example {"method": "GET", "path": "/hello", "body": "", "headers": {}}
func (h *{{title .Alias}}) Endpoint(ctx context.Context, req *pb.EndpointRequest, rsp *pb.EndpointResponse) error {
key := fmt.Sprintf("%s %s", req.Method, req.Path)
handler, ok := h.routes[key]
if !ok {
log.Infof("Route not found: %s", key)
rsp.StatusCode = 404
rsp.Body = ` + "`" + `{"error":"not found"}` + "`" + `
return nil
}
rec := &responseRecorder{headers: make(map[string]string), statusCode: 200}
fakeReq, _ := http.NewRequestWithContext(ctx, req.Method, req.Path, nil)
handler(rec, fakeReq)
rsp.StatusCode = int32(rec.statusCode)
rsp.Body = rec.body
rsp.Headers = rec.headers
return nil
}
type responseRecorder struct {
headers map[string]string
body string
statusCode int
}
func (r *responseRecorder) Header() http.Header { return http.Header{} }
func (r *responseRecorder) WriteHeader(statusCode int) { r.statusCode = statusCode }
func (r *responseRecorder) Write(b []byte) (int, error) {
r.body = string(b)
return len(b), nil
}
`
)
-225
View File
@@ -1,225 +0,0 @@
package template
var (
CrudProtoSRV = `syntax = "proto3";
package {{dehyphen .Alias}};
option go_package = "./proto;{{dehyphen .Alias}}";
service {{title .Alias}} {
rpc Create(CreateRequest) returns (CreateResponse) {}
rpc Read(ReadRequest) returns (ReadResponse) {}
rpc Update(UpdateRequest) returns (UpdateResponse) {}
rpc Delete(DeleteRequest) returns (DeleteResponse) {}
rpc List(ListRequest) returns (ListResponse) {}
}
message {{title .Alias}}Record {
string id = 1;
string name = 2;
string email = 3;
string phone = 4;
string company = 5;
int64 created = 6;
int64 updated = 7;
}
message CreateRequest {
string name = 1;
string email = 2;
string phone = 3;
string company = 4;
}
message CreateResponse {
{{title .Alias}}Record record = 1;
}
message ReadRequest {
string id = 1;
}
message ReadResponse {
{{title .Alias}}Record record = 1;
}
message UpdateRequest {
string id = 1;
string name = 2;
string email = 3;
string phone = 4;
string company = 5;
}
message UpdateResponse {
{{title .Alias}}Record record = 1;
}
message DeleteRequest {
string id = 1;
}
message DeleteResponse {
bool deleted = 1;
}
message ListRequest {
int64 limit = 1;
int64 offset = 2;
}
message ListResponse {
repeated {{title .Alias}}Record records = 1;
int64 total = 2;
}
`
CrudHandlerSRV = `package handler
import (
"context"
"fmt"
"sort"
"sync"
"time"
"github.com/google/uuid"
log "go-micro.dev/v5/logger"
pb "{{.Dir}}/proto"
)
type {{title .Alias}} struct {
mu sync.RWMutex
records map[string]*pb.{{title .Alias}}Record
}
func New() *{{title .Alias}} {
return &{{title .Alias}}{
records: make(map[string]*pb.{{title .Alias}}Record),
}
}
// Create adds a new record and returns it with a generated ID.
//
// @example {"name": "Alice Smith", "email": "alice@example.com", "phone": "+1-555-0100", "company": "Acme Inc"}
func (h *{{title .Alias}}) Create(ctx context.Context, req *pb.CreateRequest, rsp *pb.CreateResponse) error {
log.Infof("Creating record: %s", req.Name)
now := time.Now().Unix()
record := &pb.{{title .Alias}}Record{
Id: uuid.New().String(),
Name: req.Name,
Email: req.Email,
Phone: req.Phone,
Company: req.Company,
Created: now,
Updated: now,
}
h.mu.Lock()
h.records[record.Id] = record
h.mu.Unlock()
rsp.Record = record
return nil
}
// Read retrieves a record by ID.
//
// @example {"id": "some-uuid"}
func (h *{{title .Alias}}) Read(ctx context.Context, req *pb.ReadRequest, rsp *pb.ReadResponse) error {
h.mu.RLock()
record, ok := h.records[req.Id]
h.mu.RUnlock()
if !ok {
return fmt.Errorf("record %s not found", req.Id)
}
rsp.Record = record
return nil
}
// Update modifies an existing record. Only non-empty fields are updated.
//
// @example {"id": "some-uuid", "name": "Alice Johnson", "email": "alice.j@example.com"}
func (h *{{title .Alias}}) Update(ctx context.Context, req *pb.UpdateRequest, rsp *pb.UpdateResponse) error {
h.mu.Lock()
defer h.mu.Unlock()
record, ok := h.records[req.Id]
if !ok {
return fmt.Errorf("record %s not found", req.Id)
}
if req.Name != "" {
record.Name = req.Name
}
if req.Email != "" {
record.Email = req.Email
}
if req.Phone != "" {
record.Phone = req.Phone
}
if req.Company != "" {
record.Company = req.Company
}
record.Updated = time.Now().Unix()
rsp.Record = record
return nil
}
// Delete removes a record by ID.
//
// @example {"id": "some-uuid"}
func (h *{{title .Alias}}) Delete(ctx context.Context, req *pb.DeleteRequest, rsp *pb.DeleteResponse) error {
h.mu.Lock()
_, ok := h.records[req.Id]
if ok {
delete(h.records, req.Id)
}
h.mu.Unlock()
rsp.Deleted = ok
return nil
}
// List returns all records with optional pagination.
//
// @example {"limit": 10, "offset": 0}
func (h *{{title .Alias}}) List(ctx context.Context, req *pb.ListRequest, rsp *pb.ListResponse) error {
h.mu.RLock()
defer h.mu.RUnlock()
all := make([]*pb.{{title .Alias}}Record, 0, len(h.records))
for _, r := range h.records {
all = append(all, r)
}
sort.Slice(all, func(i, j int) bool {
return all[i].Created > all[j].Created
})
rsp.Total = int64(len(all))
offset := int(req.Offset)
if offset > len(all) {
offset = len(all)
}
limit := int(req.Limit)
if limit <= 0 {
limit = 20
}
end := offset + limit
if end > len(all) {
end = len(all)
}
rsp.Records = all[offset:end]
return nil
}
`
)
+3 -8
View File
@@ -13,24 +13,19 @@ import (
type {{title .Alias}} struct{}
// Return a new handler.
// Return a new handler
func New() *{{title .Alias}} {
return &{{title .Alias}}{}
}
// Call greets a person by name and returns a welcome message.
//
// @example {"name": "Alice"}
// Call is a single request handler called via client.Call or the generated client code
func (e *{{title .Alias}}) Call(ctx context.Context, req *pb.Request, rsp *pb.Response) error {
log.Info("Received {{title .Alias}}.Call request")
rsp.Msg = "Hello " + req.Name
return nil
}
// Stream sends a sequence of numbered responses back to the caller.
// Use this for streaming large result sets or real-time updates.
//
// @example {"count": 5}
// Stream is a server side stream handler called via client.Stream or the generated client code
func (e *{{title .Alias}}) Stream(ctx context.Context, req *pb.StreamingRequest, stream pb.{{title .Alias}}_StreamStream) error {
log.Infof("Received {{title .Alias}}.Stream request with count: %d", req.Count)
-1
View File
@@ -3,6 +3,5 @@ package template
var (
GitIgnore = `
{{.Alias}}
.micro
`
)
-27
View File
@@ -3,33 +3,6 @@ package template
var (
MainSRV = `package main
import (
"{{.Dir}}/handler"
pb "{{.Dir}}/proto"
"go-micro.dev/v5"
"go-micro.dev/v5/gateway/mcp"
)
func main() {
// Create service
service := micro.New("{{lower .Alias}}",
mcp.WithMCP(":3001"),
)
// Initialize service
service.Init()
// Register handler
pb.Register{{title .Alias}}Handler(service.Server(), handler.New())
// Run service
service.Run()
}
`
MainSRVNoMCP = `package main
import (
"{{.Dir}}/handler"
pb "{{.Dir}}/proto"
+8 -12
View File
@@ -27,18 +27,6 @@ test-coverage:
go test -v -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
# List MCP tools exposed by this service
mcp-tools:
micro mcp list
# Test an MCP tool interactively
mcp-test:
micro mcp test
# Start MCP server for Claude Code
mcp-serve:
micro mcp serve
# Clean build artifacts
clean:
rm -rf bin/ coverage.out coverage.html
@@ -47,6 +35,14 @@ clean:
docker:
docker build -t {{.Alias}}:latest .
# Run with Docker Compose
docker-up:
docker-compose up -d
# Stop Docker Compose
docker-down:
docker-compose down
# Lint code
lint:
golangci-lint run ./...
+1 -1
View File
@@ -3,7 +3,7 @@ package template
var (
Module = `module {{.Dir}}
go 1.22
go 1.18
require (
go-micro.dev/v5 latest
-4
View File
@@ -17,22 +17,18 @@ message Message {
}
message Request {
// Name of the person to greet
string name = 1;
}
message Response {
// Greeting message
string msg = 1;
}
message StreamingRequest {
// Number of responses to stream back
int64 count = 1;
}
message StreamingResponse {
// Current sequence number in the stream
int64 count = 1;
}
`
-184
View File
@@ -1,184 +0,0 @@
package template
var (
PubsubProtoSRV = `syntax = "proto3";
package {{dehyphen .Alias}};
option go_package = "./proto;{{dehyphen .Alias}}";
service {{title .Alias}} {
rpc Publish(PublishRequest) returns (PublishResponse) {}
rpc Stats(StatsRequest) returns (StatsResponse) {}
}
message Event {
string id = 1;
string type = 2;
string source = 3;
string data = 4;
int64 timestamp = 5;
}
message PublishRequest {
string type = 1;
string data = 2;
}
message PublishResponse {
string id = 1;
}
message StatsRequest {}
message StatsResponse {
int64 published = 1;
int64 received = 2;
}
`
PubsubHandlerSRV = `package handler
import (
"context"
"encoding/json"
"sync/atomic"
"time"
"github.com/google/uuid"
"go-micro.dev/v5/broker"
log "go-micro.dev/v5/logger"
pb "{{.Dir}}/proto"
)
const Topic = "{{lower .Alias}}.events"
type {{title .Alias}} struct {
broker broker.Broker
published atomic.Int64
received atomic.Int64
}
func New(b broker.Broker) *{{title .Alias}} {
return &{{title .Alias}}{broker: b}
}
// Publish sends an event to the message broker.
//
// @example {"type": "user.created", "data": "{\"id\": \"123\", \"name\": \"Alice\"}"}
func (h *{{title .Alias}}) Publish(ctx context.Context, req *pb.PublishRequest, rsp *pb.PublishResponse) error {
event := &pb.Event{
Id: uuid.New().String(),
Type: req.Type,
Source: "{{lower .Alias}}",
Data: req.Data,
Timestamp: time.Now().Unix(),
}
body, err := json.Marshal(event)
if err != nil {
return err
}
if err := h.broker.Publish(Topic, &broker.Message{Body: body}); err != nil {
return err
}
h.published.Add(1)
log.Infof("Published event %s type=%s", event.Id, event.Type)
rsp.Id = event.Id
return nil
}
// Stats returns the number of events published and received.
//
// @example {}
func (h *{{title .Alias}}) Stats(ctx context.Context, req *pb.StatsRequest, rsp *pb.StatsResponse) error {
rsp.Published = h.published.Load()
rsp.Received = h.received.Load()
return nil
}
// Subscribe sets up a subscription to the event topic. Call this
// after the service has started.
func (h *{{title .Alias}}) Subscribe() error {
_, err := h.broker.Subscribe(Topic, func(p broker.Event) error {
h.received.Add(1)
var event pb.Event
if err := json.Unmarshal(p.Message().Body, &event); err != nil {
log.Errorf("Failed to unmarshal event: %v", err)
return nil
}
log.Infof("Received event %s type=%s data=%s", event.Id, event.Type, event.Data)
return nil
})
return err
}
`
PubsubMainSRV = `package main
import (
"{{.Dir}}/handler"
pb "{{.Dir}}/proto"
"go-micro.dev/v5"
"go-micro.dev/v5/gateway/mcp"
log "go-micro.dev/v5/logger"
)
func main() {
service := micro.New("{{lower .Alias}}",
mcp.WithMCP(":3001"),
)
service.Init()
h := handler.New(service.Options().Broker)
pb.Register{{title .Alias}}Handler(service.Server(), h)
// Subscribe to events after service starts
go func() {
if err := h.Subscribe(); err != nil {
log.Fatalf("Failed to subscribe: %v", err)
}
log.Info("Subscribed to ", handler.Topic)
}()
service.Run()
}
`
PubsubMainSRVNoMCP = `package main
import (
"{{.Dir}}/handler"
pb "{{.Dir}}/proto"
"go-micro.dev/v5"
log "go-micro.dev/v5/logger"
)
func main() {
service := micro.New("{{lower .Alias}}")
service.Init()
h := handler.New(service.Options().Broker)
pb.Register{{title .Alias}}Handler(service.Server(), h)
go func() {
if err := h.Subscribe(); err != nil {
log.Fatalf("Failed to subscribe: %v", err)
}
log.Info("Subscribed to ", handler.Topic)
}()
service.Run()
}
`
)
+13 -73
View File
@@ -3,88 +3,28 @@ package template
var (
Readme = `# {{title .Alias}} Service
This is the {{title .Alias}} service
Generated with
` + "```" + `
` + "```" +
`
micro new {{.Alias}}
` + "```" + `
## Getting Started
## Usage
Generate the proto code:
Generate the proto code
` + "```bash" + `
` + "```" +
`
make proto
` + "```" + `
Run the service:
Run the service
` + "```bash" + `
go run .
` + "```" + `
## MCP & AI Agents
This service is MCP-enabled by default. When running, AI agents can discover
and call your service endpoints automatically.
**MCP tools endpoint:** http://localhost:3001/mcp/tools
### Test with curl
` + "```bash" + `
# List available tools
curl http://localhost:3001/mcp/tools | jq
# Call the service via MCP
curl -X POST http://localhost:3001/mcp/call \
-H 'Content-Type: application/json' \
-d '{"tool": "{{lower .Alias}}.{{title .Alias}}.Call", "arguments": {"name": "Alice"}}'
` + "```" + `
### Use with Claude Code
` + "```bash" + `
# Start MCP server for Claude Code
micro mcp serve
` + "```" + `
Or add to your Claude Code config:
` + "```json" + `
{
"mcpServers": {
"{{lower .Alias}}": {
"command": "micro",
"args": ["mcp", "serve"]
}
}
}
` + "```" + `
### Writing Good Tool Descriptions
AI agents work best when your handler methods have clear doc comments:
` + "```go" + `
// CreateUser registers a new user account with the given email and name.
// Returns the created user with their assigned ID.
//
// @example {"email": "alice@example.com", "name": "Alice Smith"}
func (s *Users) CreateUser(ctx context.Context, req *CreateRequest, rsp *CreateResponse) error {
// ...
}
` + "```" + `
See the [tool descriptions guide](https://go-micro.dev/docs/guides/tool-descriptions) for more tips.
## Development
` + "```bash" + `
make proto # Regenerate proto code
make build # Build binary
make test # Run tests
make dev # Run with hot reload (requires air)
` + "```" + `
`
` + "```" +
`
micro run .
` + "```"
)
-367
View File
@@ -1,367 +0,0 @@
// Package remote provides remote server operations for micro
package remote
import (
"bufio"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"github.com/urfave/cli/v2"
"go-micro.dev/v5/cmd"
)
const defaultRemotePath = "/opt/micro"
// Status shows status of services (local or remote)
func Status(c *cli.Context) error {
remoteHost := c.String("remote")
if remoteHost != "" {
return remoteStatus(remoteHost)
}
return localStatus(c)
}
func localStatus(c *cli.Context) error {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home dir: %w", err)
}
runDir := filepath.Join(homeDir, "micro", "run")
files, err := os.ReadDir(runDir)
if err != nil {
fmt.Println("No services running locally.")
fmt.Println("\nStart services with: micro run")
return nil
}
var hasServices bool
fmt.Printf("%-20s %-10s %-8s %s\n", "SERVICE", "STATUS", "PID", "DIRECTORY")
fmt.Println(strings.Repeat("-", 70))
for _, f := range files {
if f.IsDir() || !strings.HasSuffix(f.Name(), ".pid") {
continue
}
hasServices = true
service := f.Name()[:len(f.Name())-4]
pidFilePath := filepath.Join(runDir, f.Name())
pidFile, err := os.Open(pidFilePath)
if err != nil {
continue
}
var pid int
var dir string
scanner := bufio.NewScanner(pidFile)
if scanner.Scan() {
fmt.Sscanf(scanner.Text(), "%d", &pid)
}
if scanner.Scan() {
dir = scanner.Text()
}
pidFile.Close()
status := "\u2717 stopped"
if pid > 0 {
proc, err := os.FindProcess(pid)
if err == nil {
if err := proc.Signal(syscall.Signal(0)); err == nil {
status = "\u25cf running"
}
}
}
fmt.Printf("%-20s %-10s %-8d %s\n", service, status, pid, dir)
}
if !hasServices {
fmt.Println("No services running locally.")
fmt.Println("\nStart services with: micro run")
}
return nil
}
func remoteStatus(host string) error {
// Get list of micro services via systemctl
listCmd := exec.Command("ssh", host, "systemctl list-units 'micro@*' --no-legend --no-pager 2>/dev/null || true")
output, err := listCmd.Output()
if err != nil {
return fmt.Errorf("failed to get status from %s: %w", host, err)
}
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
if len(lines) == 0 || (len(lines) == 1 && lines[0] == "") {
fmt.Printf("%s\n", host)
fmt.Println(strings.Repeat("\u2501", 50))
fmt.Println("\nNo services deployed.")
fmt.Println("\nDeploy with: micro deploy " + host)
return nil
}
fmt.Printf("%s\n", host)
fmt.Println(strings.Repeat("\u2501", 50))
fmt.Println()
for _, line := range lines {
if line == "" {
continue
}
parts := strings.Fields(line)
if len(parts) < 4 {
continue
}
unit := parts[0]
loadState := parts[1]
activeState := parts[2]
subState := parts[3]
// Extract service name from micro@servicename.service
serviceName := strings.TrimPrefix(unit, "micro@")
serviceName = strings.TrimSuffix(serviceName, ".service")
// Get more details
statusIcon := "\u25cf"
statusText := subState
if activeState != "active" || subState != "running" {
statusIcon = "\u2717"
}
_ = loadState // unused but parsed
fmt.Printf(" %-15s %s %s\n", serviceName, statusIcon, statusText)
}
fmt.Println()
return nil
}
// Logs shows logs for services (local or remote)
func Logs(c *cli.Context) error {
remoteHost := c.String("remote")
service := c.Args().First()
follow := c.Bool("follow") || c.Bool("f")
lines := c.Int("lines")
if remoteHost != "" {
return remoteLogs(remoteHost, service, follow, lines)
}
return localLogs(c, service, follow, lines)
}
func localLogs(c *cli.Context, service string, follow bool, lines int) error {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home dir: %w", err)
}
logDir := filepath.Join(homeDir, "micro", "logs")
if service == "" {
// List available logs
files, err := os.ReadDir(logDir)
if err != nil {
fmt.Println("No logs available.")
return nil
}
fmt.Println("Available logs:")
for _, f := range files {
if strings.HasSuffix(f.Name(), ".log") {
name := strings.TrimSuffix(f.Name(), ".log")
fmt.Printf(" %s\n", name)
}
}
fmt.Println("\nView logs: micro logs <service>")
return nil
}
logPath := filepath.Join(logDir, service+".log")
if _, err := os.Stat(logPath); os.IsNotExist(err) {
return fmt.Errorf("no logs for service '%s'", service)
}
if follow {
cmd := exec.Command("tail", "-f", logPath)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
if lines == 0 {
lines = 100
}
cmd := exec.Command("tail", "-n", fmt.Sprintf("%d", lines), logPath)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
func remoteLogs(host, service string, follow bool, lines int) error {
var journalCmd string
if service == "" {
// All micro services
journalCmd = "journalctl -u 'micro@*'"
} else {
journalCmd = fmt.Sprintf("journalctl -u 'micro@%s'", service)
}
if follow {
journalCmd += " -f"
} else {
if lines == 0 {
lines = 100
}
journalCmd += fmt.Sprintf(" -n %d", lines)
}
journalCmd += " --no-pager"
sshCmd := exec.Command("ssh", host, journalCmd)
sshCmd.Stdout = os.Stdout
sshCmd.Stderr = os.Stderr
return sshCmd.Run()
}
// Stop stops a running service
func Stop(c *cli.Context) error {
if c.Args().Len() != 1 {
return fmt.Errorf("Usage: micro stop <service>")
}
service := c.Args().First()
remoteHost := c.String("remote")
if remoteHost != "" {
return remoteStop(remoteHost, service)
}
return localStop(service)
}
func localStop(service string) error {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home dir: %w", err)
}
runDir := filepath.Join(homeDir, "micro", "run")
pidFilePath := filepath.Join(runDir, service+".pid")
pidFile, err := os.Open(pidFilePath)
if err != nil {
return fmt.Errorf("service '%s' is not running", service)
}
var pid int
scanner := bufio.NewScanner(pidFile)
if scanner.Scan() {
fmt.Sscanf(scanner.Text(), "%d", &pid)
}
pidFile.Close()
if pid <= 0 {
_ = os.Remove(pidFilePath)
return fmt.Errorf("service '%s' is not running", service)
}
proc, err := os.FindProcess(pid)
if err != nil {
_ = os.Remove(pidFilePath)
return fmt.Errorf("could not find process for '%s'", service)
}
if err := proc.Signal(syscall.SIGTERM); err != nil {
_ = os.Remove(pidFilePath)
return fmt.Errorf("failed to stop service '%s': %v", service, err)
}
_ = os.Remove(pidFilePath)
fmt.Printf("Stopped %s (pid %d)\n", service, pid)
return nil
}
func remoteStop(host, service string) error {
stopCmd := fmt.Sprintf("sudo systemctl stop micro@%s", service)
sshCmd := exec.Command("ssh", host, stopCmd)
if output, err := sshCmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to stop %s: %s", service, string(output))
}
fmt.Printf("Stopped %s on %s\n", service, host)
return nil
}
func init() {
cmd.Register(&cli.Command{
Name: "status",
Usage: "Check status of running services",
Description: `Show status of running services.
Local status:
micro status
Remote status:
micro status --remote user@host`,
Action: Status,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "remote",
Usage: "Check status on remote server",
},
},
})
cmd.Register(&cli.Command{
Name: "logs",
Usage: "Show logs for a service",
Description: `View service logs.
Local logs:
micro logs # list available logs
micro logs myservice # show logs for myservice
micro logs myservice -f # follow logs
Remote logs:
micro logs --remote user@host
micro logs myservice --remote user@host -f`,
Action: Logs,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "remote",
Usage: "View logs on remote server",
},
&cli.BoolFlag{
Name: "follow",
Aliases: []string{"f"},
Usage: "Follow log output",
},
&cli.IntFlag{
Name: "lines",
Aliases: []string{"n"},
Usage: "Number of lines to show (default: 100)",
Value: 100,
},
},
})
cmd.Register(&cli.Command{
Name: "stop",
Usage: "Stop a running service",
Description: `Stop a running service.
Local:
micro stop myservice
Remote:
micro stop myservice --remote user@host`,
Action: Stop,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "remote",
Usage: "Stop service on remote server",
},
},
})
}
+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)
}
}
})
}
}
-170
View File
@@ -1,170 +0,0 @@
// Package flow implements the 'micro flow' command for event-driven
// LLM orchestration of microservices.
package flow
import (
"context"
"encoding/json"
"fmt"
"os"
"os/signal"
"syscall"
"github.com/urfave/cli/v2"
aiflow "go-micro.dev/v5/ai/flow"
"go-micro.dev/v5/broker"
"go-micro.dev/v5/client"
"go-micro.dev/v5/cmd"
"go-micro.dev/v5/registry"
)
func init() {
cmd.Register(&cli.Command{
Name: "flow",
Usage: "Event-driven LLM orchestration",
Description: `Run flows that subscribe to broker events and use an LLM to
orchestrate service calls in response.
Examples:
# Run a flow that reacts to user creation events
micro flow run --trigger events.user.created \
--prompt "New user: {{.Data}}. Send welcome email." \
--provider anthropic
# Run a one-shot flow with inline data
micro flow exec --prompt "List all users and count them" \
--provider anthropic
# Run a flow with a specific model
micro flow exec --prompt "Create a test user" \
--provider atlascloud --model deepseek-ai/DeepSeek-V3-0324`,
Subcommands: []*cli.Command{
{
Name: "run",
Usage: "Start a flow that listens to broker events",
Flags: flowFlags(),
Action: func(c *cli.Context) error {
return runFlow(c, false)
},
},
{
Name: "exec",
Usage: "Execute a flow once with inline data",
Flags: append(flowFlags(), &cli.StringFlag{
Name: "data",
Usage: "Input data for the flow (default: reads from --prompt only)",
}),
Action: func(c *cli.Context) error {
return runFlow(c, true)
},
},
},
})
}
func flowFlags() []cli.Flag {
return []cli.Flag{
&cli.StringFlag{Name: "trigger", Usage: "Broker topic to subscribe to", EnvVars: []string{"MICRO_FLOW_TRIGGER"}},
&cli.StringFlag{Name: "prompt", Usage: "Prompt template (use {{.Data}} for event data)", EnvVars: []string{"MICRO_FLOW_PROMPT"}},
&cli.StringFlag{Name: "provider", Usage: "AI provider", Value: "openai", EnvVars: []string{"MICRO_AI_PROVIDER"}},
&cli.StringFlag{Name: "api_key", Usage: "API key", EnvVars: []string{"MICRO_AI_API_KEY"}},
&cli.StringFlag{Name: "model", Usage: "Model name", EnvVars: []string{"MICRO_AI_MODEL"}},
&cli.StringFlag{Name: "base_url", Usage: "Provider base URL", EnvVars: []string{"MICRO_AI_BASE_URL"}},
&cli.StringFlag{Name: "name", Usage: "Flow name", Value: "default"},
}
}
func runFlow(c *cli.Context, oneShot bool) error {
prompt := c.String("prompt")
if prompt == "" {
return fmt.Errorf("--prompt is required")
}
provider := c.String("provider")
apiKey := c.String("api_key")
if apiKey == "" {
apiKey = fallbackKey(provider)
}
if apiKey == "" {
return fmt.Errorf("no API key; set --api_key or the provider's env var")
}
opts := []aiflow.Option{
aiflow.Prompt(prompt),
aiflow.Provider(provider),
aiflow.APIKey(apiKey),
}
if v := c.String("trigger"); v != "" {
opts = append(opts, aiflow.Trigger(v))
}
if v := c.String("model"); v != "" {
opts = append(opts, aiflow.Model(v))
}
if v := c.String("base_url"); v != "" {
opts = append(opts, aiflow.BaseURL(v))
}
opts = append(opts, aiflow.OnResult(func(r aiflow.Result) {
out, _ := json.MarshalIndent(r, "", " ")
fmt.Println(string(out))
}))
f := aiflow.New(c.String("name"), opts...)
reg := registry.DefaultRegistry
br := broker.DefaultBroker
cl := client.DefaultClient
if err := br.Connect(); err != nil {
return fmt.Errorf("broker connect: %w", err)
}
if err := f.Register(reg, br, cl); err != nil {
return err
}
if oneShot {
data := c.String("data")
if data == "" {
data = prompt
}
return f.Execute(context.Background(), data)
}
if c.String("trigger") == "" {
return fmt.Errorf("--trigger is required for 'flow run' (use 'flow exec' for one-shot)")
}
fmt.Println()
fmt.Println(" \033[1mmicro flow\033[0m")
fmt.Println()
fmt.Printf(" Flow \033[36m%s\033[0m\n", f.Name())
fmt.Printf(" Topic \033[36m%s\033[0m\n", c.String("trigger"))
fmt.Printf(" Provider \033[36m%s\033[0m\n", provider)
fmt.Println()
fmt.Println(" \033[2mListening for events. Ctrl-C to stop.\033[0m")
fmt.Println()
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig
fmt.Printf("\nStopped. %d executions recorded.\n", len(f.Results()))
return nil
}
func fallbackKey(provider string) string {
envMap := map[string]string{
"anthropic": "ANTHROPIC_API_KEY",
"openai": "OPENAI_API_KEY",
"gemini": "GEMINI_API_KEY",
"groq": "GROQ_API_KEY",
"mistral": "MISTRAL_API_KEY",
"together": "TOGETHER_API_KEY",
"atlascloud": "ATLASCLOUD_API_KEY",
}
if env, ok := envMap[provider]; ok {
return os.Getenv(env)
}
return ""
}
-5
View File
@@ -4,14 +4,9 @@ import (
"embed"
"go-micro.dev/v5/cmd"
_ "go-micro.dev/v5/cmd/micro/api"
_ "go-micro.dev/v5/cmd/micro/chat"
_ "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/flow"
_ "go-micro.dev/v5/cmd/micro/mcp"
_ "go-micro.dev/v5/cmd/micro/resource"
_ "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)
-809
View File
@@ -1,809 +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.Println()
fmt.Println(" \033[1mmicro mcp tools\033[0m")
fmt.Println()
toolCount := 0
for _, svc := range services {
fullSvcs, err := opts.Registry.GetService(svc.Name)
if err != nil || len(fullSvcs) == 0 {
continue
}
fmt.Printf(" \033[1m%s\033[0m\n", svc.Name)
for _, ep := range fullSvcs[0].Endpoints {
toolName := fmt.Sprintf("%s.%s", svc.Name, ep.Name)
fmt.Printf(" \033[32m●\033[0m %s\n", toolName)
toolCount++
}
fmt.Println()
}
fmt.Printf(" \033[2m%d tools\033[0m\n\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")
}
})
}
}
-84
View File
@@ -1,84 +0,0 @@
package resource
import (
"fmt"
"os"
"os/signal"
"syscall"
"github.com/urfave/cli/v2"
"go-micro.dev/v5/broker"
)
// brokerCommand exposes the broker interface: publish, subscribe.
func brokerCommand() *cli.Command {
return &cli.Command{
Name: "broker",
Usage: "Publish and subscribe to broker topics",
Description: `Interact with the message broker.
micro broker publish <topic> <message> Publish a message to a topic
micro broker subscribe <topic> Stream messages from a topic`,
Subcommands: []*cli.Command{
{
Name: "publish",
Usage: "Publish a message to a topic",
ArgsUsage: "<topic> <message>",
Action: brokerPublish,
},
{
Name: "subscribe",
Usage: "Stream messages from a topic",
ArgsUsage: "<topic>",
Action: brokerSubscribe,
},
},
}
}
func brokerPublish(c *cli.Context) error {
topic := c.Args().Get(0)
msg := c.Args().Get(1)
if topic == "" || msg == "" {
return fail("usage: micro broker publish <topic> <message>")
}
b := broker.DefaultBroker
if err := b.Connect(); err != nil {
return fail("broker connect: %v", err)
}
if err := b.Publish(topic, &broker.Message{Body: []byte(msg)}); err != nil {
return fail("publish: %v", err)
}
fmt.Printf("Published to %q\n", topic)
return nil
}
func brokerSubscribe(c *cli.Context) error {
topic := c.Args().First()
if topic == "" {
return fail("usage: micro broker subscribe <topic>")
}
b := broker.DefaultBroker
if err := b.Connect(); err != nil {
return fail("broker connect: %v", err)
}
sub, err := b.Subscribe(topic, func(e broker.Event) error {
fmt.Printf("%s\n", string(e.Message().Body))
return nil
})
if err != nil {
return fail("subscribe: %v", err)
}
defer sub.Unsubscribe()
fmt.Printf("Subscribed to %q (Ctrl-C to stop)...\n", topic)
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig
return nil
}
-79
View File
@@ -1,79 +0,0 @@
package resource
import (
"fmt"
"strings"
"github.com/urfave/cli/v2"
"go-micro.dev/v5/config"
"go-micro.dev/v5/config/source/env"
)
// configCommand exposes the config interface: get, dump.
//
// The CLI loads configuration from environment variables (the source
// that makes sense without a running service). Keys use dot notation,
// e.g. "database.host" reads from DATABASE_HOST.
func configCommand() *cli.Command {
return &cli.Command{
Name: "config",
Usage: "Read dynamic configuration (from environment)",
Description: `Read dynamic configuration loaded from environment variables.
Keys use dot notation: "database.host" maps to DATABASE_HOST.
micro config get <key> Read a config value
micro config dump Print the full config as JSON`,
Subcommands: []*cli.Command{
{
Name: "get",
Usage: "Read a config value",
ArgsUsage: "<key>",
Action: configGet,
},
{
Name: "dump",
Usage: "Print the full config",
Action: configDump,
},
},
}
}
func loadConfig() (config.Config, error) {
conf, err := config.NewConfig()
if err != nil {
return nil, err
}
if err := conf.Load(env.NewSource()); err != nil {
return nil, err
}
return conf, nil
}
func configGet(c *cli.Context) error {
key := c.Args().First()
if key == "" {
return fail("usage: micro config get <key>")
}
conf, err := loadConfig()
if err != nil {
return fail("load config: %v", err)
}
path := strings.Split(key, ".")
val, err := conf.Get(path...)
if err != nil {
return fail("get %q: %v", key, err)
}
fmt.Println(string(val.Bytes()))
return nil
}
func configDump(c *cli.Context) error {
conf, err := loadConfig()
if err != nil {
return fail("load config: %v", err)
}
fmt.Println(string(conf.Bytes()))
return nil
}
-92
View File
@@ -1,92 +0,0 @@
package resource
import (
"fmt"
"github.com/urfave/cli/v2"
"go-micro.dev/v5/registry"
)
// registryCommand exposes the registry interface: list, get, watch.
func registryCommand() *cli.Command {
return &cli.Command{
Name: "registry",
Usage: "Inspect the service registry",
Description: `Interact with the service registry.
micro registry list List all registered services
micro registry get <name> Show nodes and endpoints for a service
micro registry watch Stream registration events`,
Subcommands: []*cli.Command{
{
Name: "list",
Usage: "List all registered services",
Action: registryList,
},
{
Name: "get",
Usage: "Show details for a service",
ArgsUsage: "<name>",
Action: registryGet,
},
{
Name: "watch",
Usage: "Stream registration events",
Action: registryWatch,
},
},
}
}
func registryList(c *cli.Context) error {
services, err := registry.ListServices()
if err != nil {
return fail("list services: %v", err)
}
out := make([]map[string]any, 0, len(services))
for _, s := range services {
out = append(out, map[string]any{
"name": s.Name,
"version": s.Version,
})
}
return printJSON(out)
}
func registryGet(c *cli.Context) error {
name := c.Args().First()
if name == "" {
return fail("usage: micro registry get <name>")
}
services, err := registry.GetService(name)
if err != nil {
return fail("get service %q: %v", name, err)
}
if len(services) == 0 {
return fail("service %q not found", name)
}
return printJSON(services)
}
func registryWatch(c *cli.Context) error {
w, err := registry.Watch()
if err != nil {
return fail("watch registry: %v", err)
}
defer w.Stop()
fmt.Println("Watching registry for changes (Ctrl-C to stop)...")
for {
res, err := w.Next()
if err != nil {
return fail("watch: %v", err)
}
name := ""
version := ""
if res.Service != nil {
name = res.Service.Name
version = res.Service.Version
}
fmt.Printf("%-10s %s %s\n", res.Action, name, version)
}
}
-52
View File
@@ -1,52 +0,0 @@
// Package resource provides CLI commands that map directly onto
// go-micro's core interfaces — registry, broker, store, and config.
//
// Each interface gets its own top-level command with verbs that mirror
// the interface methods, so the framework's building blocks are
// inspectable and manipulable from the terminal:
//
// micro registry list
// micro broker publish <topic> <message>
// micro store read <key>
// micro config get <key>
//
// New resource commands are registered by appending to the commands
// slice in init — see registry.go, broker.go, store.go, config.go for
// the per-interface implementations.
package resource
import (
"encoding/json"
"fmt"
"os"
"github.com/urfave/cli/v2"
"go-micro.dev/v5/cmd"
)
// commandFunc returns a cli.Command for a single core interface. Add a
// new one here to expose another package on the CLI.
var commandFuncs = []func() *cli.Command{
registryCommand,
brokerCommand,
storeCommand,
configCommand,
}
func init() {
for _, fn := range commandFuncs {
cmd.Register(fn())
}
}
// printJSON writes v as indented JSON to stdout.
func printJSON(v any) error {
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
return enc.Encode(v)
}
// fail returns a cli error with a consistent prefix.
func fail(format string, args ...any) error {
return cli.Exit(fmt.Sprintf(format, args...), 1)
}
-29
View File
@@ -1,29 +0,0 @@
package resource
import "testing"
func TestCommandsRegistered(t *testing.T) {
// Each command func must return a command with a name and at least
// one subcommand, so the resource surface stays consistent.
for _, fn := range commandFuncs {
c := fn()
if c.Name == "" {
t.Error("command with empty name")
}
if len(c.Subcommands) == 0 {
t.Errorf("command %q has no subcommands", c.Name)
}
}
}
func TestExpectedCommands(t *testing.T) {
names := map[string]bool{}
for _, fn := range commandFuncs {
names[fn().Name] = true
}
for _, want := range []string{"registry", "broker", "store", "config"} {
if !names[want] {
t.Errorf("missing %q command", want)
}
}
}
-106
View File
@@ -1,106 +0,0 @@
package resource
import (
"fmt"
"github.com/urfave/cli/v2"
"go-micro.dev/v5/store"
)
// storeCommand exposes the store interface: read, write, delete, list.
func storeCommand() *cli.Command {
return &cli.Command{
Name: "store",
Usage: "Read and write records in the store",
Description: `Interact with the data store.
micro store list [prefix] List keys (optionally by prefix)
micro store read <key> Read a record
micro store write <key> <value> Write a record
micro store delete <key> Delete a record`,
Subcommands: []*cli.Command{
{
Name: "list",
Usage: "List keys",
ArgsUsage: "[prefix]",
Action: storeList,
},
{
Name: "read",
Usage: "Read a record",
ArgsUsage: "<key>",
Action: storeRead,
},
{
Name: "write",
Usage: "Write a record",
ArgsUsage: "<key> <value>",
Action: storeWrite,
},
{
Name: "delete",
Usage: "Delete a record",
ArgsUsage: "<key>",
Action: storeDelete,
},
},
}
}
func storeList(c *cli.Context) error {
var opts []store.ListOption
if prefix := c.Args().First(); prefix != "" {
opts = append(opts, store.ListPrefix(prefix))
}
keys, err := store.DefaultStore.List(opts...)
if err != nil {
return fail("list: %v", err)
}
return printJSON(keys)
}
func storeRead(c *cli.Context) error {
key := c.Args().First()
if key == "" {
return fail("usage: micro store read <key>")
}
records, err := store.DefaultStore.Read(key)
if err != nil {
return fail("read %q: %v", key, err)
}
if len(records) == 0 {
return fail("key %q not found", key)
}
// Print the raw value for a single record, JSON for multiple.
if len(records) == 1 {
fmt.Println(string(records[0].Value))
return nil
}
return printJSON(records)
}
func storeWrite(c *cli.Context) error {
key := c.Args().Get(0)
value := c.Args().Get(1)
if key == "" {
return fail("usage: micro store write <key> <value>")
}
rec := &store.Record{Key: key, Value: []byte(value)}
if err := store.DefaultStore.Write(rec); err != nil {
return fail("write %q: %v", key, err)
}
fmt.Printf("Wrote %q\n", key)
return nil
}
func storeDelete(c *cli.Context) error {
key := c.Args().First()
if key == "" {
return fail("usage: micro store delete <key>")
}
if err := store.DefaultStore.Delete(key); err != nil {
return fail("delete %q: %v", key, err)
}
fmt.Printf("Deleted %q\n", key)
return nil
}
+2 -33
View File
@@ -13,16 +13,8 @@ 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"`
}
// DeployTarget represents a deployment target configuration
type DeployTarget struct {
Name string `json:"-"`
SSH string `json:"ssh"`
Path string `json:"path,omitempty"`
}
// Service represents a service configuration
@@ -95,13 +87,11 @@ func ParseMu(path string) (*Config, error) {
cfg := &Config{
Services: make(map[string]*Service),
Envs: make(map[string]map[string]string),
Deploy: make(map[string]*DeployTarget),
}
var currentService *Service
var currentEnv string
var currentEnvMap map[string]string
var currentDeploy *DeployTarget
scanner := bufio.NewScanner(file)
lineNum := 0
@@ -147,21 +137,9 @@ func ParseMu(path string) (*Config, error) {
cfg.Envs[currentEnv] = currentEnvMap
}
currentService = nil
currentDeploy = nil
currentEnv = name
currentEnvMap = make(map[string]string)
case "deploy":
// Save previous env if any
if currentEnv != "" && currentEnvMap != nil {
cfg.Envs[currentEnv] = currentEnvMap
}
currentService = nil
currentEnv = ""
currentEnvMap = nil
currentDeploy = &DeployTarget{Name: name}
cfg.Deploy[name] = currentDeploy
default:
return nil, fmt.Errorf("%s:%d: unknown keyword '%s'", path, lineNum, keyword)
}
@@ -190,20 +168,11 @@ func ParseMu(path string) (*Config, error) {
default:
return nil, fmt.Errorf("%s:%d: unknown service property '%s'", path, lineNum, key)
}
} else if currentDeploy != nil {
switch key {
case "ssh":
currentDeploy.SSH = value
case "path":
currentDeploy.Path = value
default:
return nil, fmt.Errorf("%s:%d: unknown deploy property '%s'", path, lineNum, key)
}
} else if currentEnvMap != nil {
// Environment variable
currentEnvMap[key] = value
} else {
return nil, fmt.Errorf("%s:%d: property outside of service, deploy, or env block", path, lineNum)
return nil, fmt.Errorf("%s:%d: property outside of service or env block", path, lineNum)
}
}
}
+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)
}
+39 -204
View File
@@ -2,7 +2,6 @@ package run
import (
"bufio"
"context"
"crypto/md5"
"fmt"
"io"
@@ -19,10 +18,9 @@ import (
"github.com/urfave/cli/v2"
"go-micro.dev/v5/cmd"
"go-micro.dev/v5/cmd/micro/cli/generate"
"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
@@ -176,11 +174,6 @@ func waitForHealth(port int, timeout time.Duration) bool {
}
func Run(c *cli.Context) error {
// Handle --prompt: generate services first, then run them
if prompt := c.String("prompt"); prompt != "" {
return runWithPrompt(c, prompt)
}
dir := c.Args().Get(0)
if dir == "" {
dir = "."
@@ -327,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)
}
}
@@ -364,7 +357,7 @@ func Run(c *cli.Context) error {
}
// Print startup banner
printBanner(services, gw, !c.Bool("no-watch"), c.String("mcp-address"))
printBanner(services, gw, !c.Bool("no-watch"))
// Setup signal handling
sigCh := make(chan os.Signal, 1)
@@ -393,30 +386,6 @@ func Run(c *cli.Context) error {
}
}
}()
// Scan for new services added by micro chat or micro new
go func() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-sigCh:
return
case <-ticker.C:
newSvcs := discoverNewServices(absDir, servicesByDir, binDir, runDir, logsDir, envVars, len(services))
for _, sp := range newSvcs {
services = append(services, sp)
servicesByDir[sp.dir] = sp
watch.AddDir(sp.dir)
if err := sp.start(logsDir); err != nil {
fmt.Fprintf(os.Stderr, "[%s] %v\n", sp.name, err)
continue
}
fmt.Printf("\n \033[32m●\033[0m %s \033[2m(new)\033[0m\n", sp.name)
}
}
}
}()
}
// Wait for signal
@@ -457,60 +426,21 @@ func processRunning(pidStr string) bool {
return proc.Signal(syscall.Signal(0)) == nil
}
func discoverNewServices(baseDir string, known map[string]*serviceProcess, binDir, runDir, logsDir string, envVars []string, colorOffset int) []*serviceProcess {
var newSvcs []*serviceProcess
entries, err := os.ReadDir(baseDir)
if err != nil {
return nil
}
for _, e := range entries {
if !e.IsDir() {
continue
}
svcDir := filepath.Join(baseDir, e.Name())
absSvcDir, _ := filepath.Abs(svcDir)
if _, exists := known[absSvcDir]; exists {
continue
}
mainFile := filepath.Join(svcDir, "main.go")
if _, err := os.Stat(mainFile); err != nil {
continue
}
name := e.Name()
hash := fmt.Sprintf("%x", md5.Sum([]byte(absSvcDir)))[:8]
sp := &serviceProcess{
name: name,
dir: absSvcDir,
binPath: filepath.Join(binDir, name+"-"+hash),
pidFile: filepath.Join(runDir, name+"-"+hash+".pid"),
logFile: filepath.Join(logsDir, name+"-"+hash+".log"),
color: colorFor(colorOffset + len(newSvcs)),
env: envVars,
}
newSvcs = append(newSvcs, sp)
}
return newSvcs
}
func printBanner(services []*serviceProcess, gw *server.Gateway, watching bool, mcpAddr string) {
fmt.Println()
fmt.Println(" \033[1mMicro\033[0m")
func printBanner(services []*serviceProcess, gw *gateway.Gateway, watching bool) {
fmt.Println()
fmt.Println(" ┌─────────────────────────────────────────────────────────────┐")
fmt.Println(" │ │")
fmt.Println(" │ \033[1mMicro\033[0m │")
fmt.Println(" │ │")
if gw != nil {
fmt.Printf(" Dashboard \033[36mhttp://localhost%s\033[0m\n", gw.Addr())
fmt.Printf(" API \033[36mhttp://localhost%s/api/{service}/{method}\033[0m\n", gw.Addr())
fmt.Printf(" Agent \033[36mhttp://localhost%s/agent\033[0m\n", gw.Addr())
fmt.Printf(" Health \033[36mhttp://localhost%s/health\033[0m\n", gw.Addr())
if mcpAddr != "" {
fmt.Printf(" MCP \033[36mhttp://localhost%s\033[0m\n", mcpAddr)
fmt.Printf(" MCP Tools \033[36mhttp://localhost%s/mcp/tools\033[0m\n", mcpAddr)
fmt.Printf(" WebSocket \033[36mws://localhost%s/mcp/ws\033[0m\n", mcpAddr)
}
fmt.Printf(" │ Web: \033[36mhttp://localhost%s\033[0m\n", gw.Addr())
fmt.Printf(" │ API: \033[36mhttp://localhost%s/api/{service}/{method}\033[0m\n", gw.Addr())
fmt.Printf(" │ Health: \033[36mhttp://localhost%s/health\033[0m\n", gw.Addr())
}
fmt.Println()
fmt.Println(" Services:")
fmt.Println(" │ │")
fmt.Println(" Services:")
for _, svc := range services {
status := "\033[32m●\033[0m" // green dot
@@ -518,37 +448,39 @@ func printBanner(services []*serviceProcess, gw *server.Gateway, watching bool,
status = "\033[31m●\033[0m" // red dot
}
name := svc.name
if len(name) > 40 {
name = name[:37] + "..."
if len(name) > 20 {
name = name[:17] + "..."
}
fmt.Printf(" %s %s\n", status, name)
fmt.Printf(" %s %-20s │\n", status, name)
}
fmt.Println()
fmt.Println(" Auth: \033[32menabled\033[0m (admin / micro)")
fmt.Println(" │ │")
if watching {
fmt.Println(" \033[33mWatching for changes...\033[0m")
fmt.Println(" \033[33mWatching for changes...\033[0m")
fmt.Println(" │ │")
}
fmt.Println()
fmt.Println(" \033[2mmicro chat --provider anthropic # talk to your services\033[0m")
if gw != nil && len(services) > 0 {
svc := services[0]
fmt.Println(" │ Try: │")
fmt.Printf(" │ \033[90mcurl -X POST http://localhost%s/api/%s/...\033[0m │\n", gw.Addr(), svc.name)
fmt.Println(" │ │")
}
fmt.Println(" └─────────────────────────────────────────────────────────────┘")
fmt.Println()
}
func init() {
cmd.Register(&cli.Command{
Name: "run",
Usage: "Development mode: run services with hot reload and API gateway",
Description: `Run discovers and runs services in a directory (development mode).
Usage: "Run services with API gateway and hot reload",
Description: `Run discovers and runs services in a directory.
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 /mcp/tools
- Health checks at /health
With a micro.mu or micro.json config file, services start in dependency order.
@@ -559,9 +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 --prompt "an order system for dropshipping" # Generate and run`,
micro run --env production # Use production environment`,
Action: Run,
Flags: []cli.Flag{
&cli.StringFlag{
@@ -584,101 +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"},
},
&cli.StringFlag{
Name: "prompt",
Usage: "Describe a system to generate and run (AI designs, builds, and starts services)",
EnvVars: []string{"MICRO_RUN_PROMPT"},
},
&cli.StringFlag{
Name: "provider",
Usage: "AI provider for --prompt (anthropic, openai, gemini, atlascloud, groq, mistral, together)",
EnvVars: []string{"MICRO_AI_PROVIDER"},
},
&cli.StringFlag{
Name: "api_key",
Usage: "API key for --prompt (or set ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)",
EnvVars: []string{"MICRO_AI_API_KEY"},
},
},
})
}
func runWithPrompt(c *cli.Context, prompt string) error {
provider := c.String("provider")
apiKey := c.String("api_key")
if apiKey == "" {
for _, env := range []string{"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY",
"ATLASCLOUD_API_KEY", "GROQ_API_KEY", "MISTRAL_API_KEY", "TOGETHER_API_KEY", "MICRO_AI_API_KEY"} {
if v := os.Getenv(env); v != "" {
apiKey = v
break
}
}
}
if apiKey == "" {
return fmt.Errorf("--api_key or a provider API key env var is required for --prompt")
}
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()
fmt.Println()
fmt.Println(" \033[1mmicro run --prompt\033[0m")
fmt.Println()
fmt.Printf(" \033[2mDesigning services for:\033[0m %s\n\n", prompt)
design, err := generate.Design(ctx, provider, apiKey, "", ".", prompt)
if err != nil {
return fmt.Errorf("design failed: %w", err)
}
fmt.Println(" Services:")
for _, svc := range design.Services {
fmt.Printf(" \033[32m●\033[0m \033[36m%s\033[0m — %s\n", svc.Name, svc.Description)
for _, ep := range svc.Endpoints {
fmt.Printf(" %s: %s\n", ep.Name, ep.Description)
}
}
fmt.Println()
if !confirmGenerate() {
fmt.Println(" Cancelled.")
return nil
}
fmt.Println(" Generating code...")
if err := generate.Generate(ctx, ".", design, provider, apiKey, ""); err != nil {
return fmt.Errorf("generate failed: %w", err)
}
for _, svc := range design.Services {
fmt.Printf(" \033[32m✓\033[0m %s/\n", svc.Name)
}
fmt.Println()
// Now run normally — micro run discovers the generated services
fmt.Println(" Starting services...")
fmt.Println()
// Cancel signal context before handing off to Run (which manages its own signals)
cancel()
// Run normally from current directory (services are now generated)
// Set the prompt to empty via a new context so Run doesn't recurse
c.Set("prompt", "")
return Run(c)
}
func confirmGenerate() bool {
fmt.Print(" Generate? [Y/n] ")
scanner := bufio.NewScanner(os.Stdin)
if !scanner.Scan() {
return false
}
answer := strings.TrimSpace(strings.ToLower(scanner.Text()))
return answer == "" || answer == "y" || answer == "yes"
}
-19
View File
@@ -75,25 +75,6 @@ func (w *Watcher) Start() {
go w.watch()
}
// AddDir adds a new directory to watch
func (w *Watcher) AddDir(dir string) {
w.mu.Lock()
defer w.mu.Unlock()
for _, d := range w.dirs {
if d == dir {
return
}
}
w.dirs = append(w.dirs, dir)
}
// Dirs returns the currently watched directories
func (w *Watcher) Dirs() []string {
w.mu.Lock()
defer w.mu.Unlock()
return append([]string{}, w.dirs...)
}
// Stop stops the watcher
func (w *Watcher) Stop() {
close(w.done)
-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()
}

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