micro--go-micro
ac47a4650a
* Initial plan
* Add MCP per-tool scopes, tracing, rate limiting, and audit logging
- Add Scopes field to Tool struct for per-tool scope requirements
- Add Auth (auth.Auth) integration to Options for token inspection
- Add trace ID generation (UUID) propagated via metadata to downstream RPCs
- Add per-tool rate limiting with configurable requests/sec and burst
- Add AuditFunc callback for immutable tool-call audit records
- Extract tool scopes from registry endpoint metadata ("scopes" key)
- Update both HTTP and stdio transports with auth/trace/rate/audit
- Add comprehensive tests for all new functionality
Co-authored-by: asim <17530+asim@users.noreply.github.com>
* Revert unrelated example go.mod changes
Co-authored-by: asim <17530+asim@users.noreply.github.com>
* Remove auto-generated example go.sum files
Co-authored-by: asim <17530+asim@users.noreply.github.com>
* Add WithEndpointScopes helper, gateway-level ToolScopes, and documentation
- Add server.WithEndpointScopes() for declaring per-endpoint auth scopes at
handler registration time
- Add mcp.Options.ToolScopes for gateway-level scope overrides without
changing individual services
- Update documented example to show WithEndpointScopes usage
- Update examples/mcp/README.md with scopes, tracing, and rate-limiting docs
- Update gateway/mcp/DOCUMENTATION.md with scopes section and FAQ
- Add tests for both new features
Co-authored-by: asim <17530+asim@users.noreply.github.com>
* Fix ToolScopes doc comment: clarify override (not merge) semantics
Co-authored-by: asim <17530+asim@users.noreply.github.com>
* Revert unrelated example go.mod/go.sum changes
Co-authored-by: asim <17530+asim@users.noreply.github.com>
* Rename ToolScopes to Scopes in MCP Options
The field name "Scopes" is more universal and consistent with how
auth scopes are used throughout go-micro. Updated all code references,
tests, and documentation.
Co-authored-by: asim <17530+asim@users.noreply.github.com>
* MCP gateway: add per-tool scopes, tracing, rate limiting, and audit logging
Co-authored-by: asim <17530+asim@users.noreply.github.com>
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: asim <17530+asim@users.noreply.github.com>
631 行
17 KiB
Go
631 行
17 KiB
Go
// Package mcp provides Model Context Protocol (MCP) gateway functionality for go-micro services.
|
|
// It automatically exposes your microservices as AI-accessible tools through MCP.
|
|
//
|
|
// Example usage:
|
|
//
|
|
// service := micro.NewService(micro.Name("myservice"))
|
|
// service.Init()
|
|
//
|
|
// // Add MCP gateway
|
|
// go mcp.Serve(mcp.Options{
|
|
// Registry: service.Options().Registry,
|
|
// Address: ":3000",
|
|
// })
|
|
//
|
|
// service.Run()
|
|
package mcp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"go-micro.dev/v5/auth"
|
|
"go-micro.dev/v5/client"
|
|
"go-micro.dev/v5/codec/bytes"
|
|
"go-micro.dev/v5/metadata"
|
|
"go-micro.dev/v5/registry"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// Metadata keys for MCP tracing and auth propagated via context/metadata.
|
|
const (
|
|
// TraceIDKey is the metadata key for the MCP trace ID.
|
|
TraceIDKey = "Mcp-Trace-Id"
|
|
// ToolNameKey is the metadata key for the tool being invoked.
|
|
ToolNameKey = "Mcp-Tool-Name"
|
|
// AccountIDKey is the metadata key for the authenticated account ID.
|
|
AccountIDKey = "Mcp-Account-Id"
|
|
)
|
|
|
|
// AuditRecord represents an immutable log entry for an MCP tool call.
|
|
type AuditRecord struct {
|
|
// TraceID uniquely identifies this tool call chain.
|
|
TraceID string `json:"trace_id"`
|
|
// Timestamp of the tool call.
|
|
Timestamp time.Time `json:"timestamp"`
|
|
// Tool is the name of the tool that was called.
|
|
Tool string `json:"tool"`
|
|
// AccountID is the ID of the authenticated account (empty if unauthenticated).
|
|
AccountID string `json:"account_id,omitempty"`
|
|
// Scopes that were required for this tool.
|
|
ScopesRequired []string `json:"scopes_required,omitempty"`
|
|
// Allowed indicates whether the call was authorized.
|
|
Allowed bool `json:"allowed"`
|
|
// Denied reason, if the call was not allowed.
|
|
DeniedReason string `json:"denied_reason,omitempty"`
|
|
// Duration of the RPC call (zero if call was denied before execution).
|
|
Duration time.Duration `json:"duration,omitempty"`
|
|
// Error from the RPC call, if any.
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// AuditFunc is called for every tool call with an audit record.
|
|
// Implementations should treat the record as immutable and persist it
|
|
// (e.g. to a log, database, or event stream).
|
|
type AuditFunc func(record AuditRecord)
|
|
|
|
// RateLimitConfig configures rate limiting for the MCP gateway.
|
|
type RateLimitConfig struct {
|
|
// Requests per second allowed per tool (0 = unlimited).
|
|
RequestsPerSecond float64
|
|
// Burst size (maximum number of requests that can be made at once).
|
|
Burst int
|
|
}
|
|
|
|
// Options configures the MCP gateway
|
|
type Options struct {
|
|
// Registry for service discovery (required)
|
|
Registry registry.Registry
|
|
|
|
// Address to listen on for SSE transport (e.g., ":3000")
|
|
// Leave empty for stdio transport
|
|
Address string
|
|
|
|
// Client for making RPC calls (defaults to client.DefaultClient)
|
|
Client client.Client
|
|
|
|
// Context for cancellation (defaults to background context)
|
|
Context context.Context
|
|
|
|
// Logger for debug output (defaults to log.Default())
|
|
Logger *log.Logger
|
|
|
|
// AuthFunc validates requests (optional, legacy)
|
|
// Return error to reject, nil to allow
|
|
AuthFunc func(r *http.Request) error
|
|
|
|
// Auth provider for token inspection (optional).
|
|
// When set, incoming requests must carry a Bearer token which is
|
|
// inspected to obtain an account. The account's scopes are then
|
|
// checked against the tool's required scopes.
|
|
Auth auth.Auth
|
|
|
|
// AuditFunc is called for every tool call with an immutable audit record.
|
|
// Use this to persist tool-call logs for compliance and debugging.
|
|
AuditFunc AuditFunc
|
|
|
|
// RateLimit configures per-tool rate limiting.
|
|
// When set, each tool is limited to the configured requests per second.
|
|
RateLimit *RateLimitConfig
|
|
|
|
// Scopes lets the gateway operator define or override per-tool
|
|
// scope requirements without changing the services themselves.
|
|
// Keys are tool names (e.g. "blog.Blog.Create") and values are the
|
|
// required scopes. When a tool appears in Scopes its scopes
|
|
// replace any scopes declared by the service via endpoint metadata.
|
|
//
|
|
// Example:
|
|
//
|
|
// Scopes: map[string][]string{
|
|
// "blog.Blog.Create": {"blog:write"},
|
|
// "blog.Blog.Delete": {"blog:admin"},
|
|
// }
|
|
Scopes map[string][]string
|
|
}
|
|
|
|
// Server represents a running MCP gateway
|
|
type Server struct {
|
|
opts Options
|
|
tools map[string]*Tool
|
|
toolsMu sync.RWMutex
|
|
server *http.Server
|
|
watching bool
|
|
|
|
// limiters holds per-tool rate limiters (nil if rate limiting is disabled).
|
|
limiters map[string]*rateLimiter
|
|
limitersMu sync.RWMutex
|
|
}
|
|
|
|
// Tool represents an MCP tool (exposed service endpoint)
|
|
type Tool struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
InputSchema map[string]interface{} `json:"inputSchema"`
|
|
// Scopes lists the auth scopes required to call this tool.
|
|
// An empty list means no scope restriction (subject to Auth provider).
|
|
Scopes []string `json:"scopes,omitempty"`
|
|
Service string `json:"-"`
|
|
Endpoint string `json:"-"`
|
|
}
|
|
|
|
// Serve starts an MCP gateway with the given options.
|
|
// For stdio transport, leave Address empty.
|
|
// For SSE transport, set Address (e.g., ":3000").
|
|
func Serve(opts Options) error {
|
|
// Set defaults
|
|
if opts.Client == nil {
|
|
opts.Client = client.DefaultClient
|
|
}
|
|
if opts.Context == nil {
|
|
opts.Context = context.Background()
|
|
}
|
|
if opts.Logger == nil {
|
|
opts.Logger = log.Default()
|
|
}
|
|
if opts.Registry == nil {
|
|
return fmt.Errorf("registry is required")
|
|
}
|
|
|
|
server := &Server{
|
|
opts: opts,
|
|
tools: make(map[string]*Tool),
|
|
limiters: make(map[string]*rateLimiter),
|
|
}
|
|
|
|
// Discover services and build tool list
|
|
if err := server.discoverServices(); err != nil {
|
|
return fmt.Errorf("failed to discover services: %w", err)
|
|
}
|
|
|
|
// Watch for service changes
|
|
go server.watchServices()
|
|
|
|
// Start server based on transport
|
|
if opts.Address != "" {
|
|
return server.serveHTTP()
|
|
}
|
|
return server.serveStdio()
|
|
}
|
|
|
|
// ListenAndServe is a convenience function that starts an MCP gateway on the given address.
|
|
func ListenAndServe(address string, opts Options) error {
|
|
opts.Address = address
|
|
return Serve(opts)
|
|
}
|
|
|
|
// discoverServices queries the registry and builds the tool list
|
|
func (s *Server) discoverServices() error {
|
|
services, err := s.opts.Registry.ListServices()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.toolsMu.Lock()
|
|
defer s.toolsMu.Unlock()
|
|
|
|
for _, svc := range services {
|
|
// Get full service details
|
|
fullSvcs, err := s.opts.Registry.GetService(svc.Name)
|
|
if err != nil || len(fullSvcs) == 0 {
|
|
continue
|
|
}
|
|
|
|
// Convert endpoints to tools
|
|
for _, ep := range fullSvcs[0].Endpoints {
|
|
toolName := fmt.Sprintf("%s.%s", svc.Name, ep.Name)
|
|
|
|
// Build input schema from endpoint request type
|
|
inputSchema := s.buildInputSchema(ep.Request)
|
|
|
|
// Get description from endpoint metadata (set by service during registration)
|
|
description := fmt.Sprintf("Call %s on %s service", ep.Name, svc.Name)
|
|
if ep.Metadata != nil {
|
|
if desc, ok := ep.Metadata["description"]; ok && desc != "" {
|
|
description = desc
|
|
}
|
|
}
|
|
|
|
tool := &Tool{
|
|
Name: toolName,
|
|
Description: description,
|
|
InputSchema: inputSchema,
|
|
Service: svc.Name,
|
|
Endpoint: ep.Name,
|
|
}
|
|
|
|
// Extract scopes from endpoint metadata
|
|
if ep.Metadata != nil {
|
|
if scopes, ok := ep.Metadata["scopes"]; ok && scopes != "" {
|
|
tool.Scopes = strings.Split(scopes, ",")
|
|
}
|
|
}
|
|
|
|
// Gateway-level Scopes override service-level scopes
|
|
if s.opts.Scopes != nil {
|
|
if scopes, ok := s.opts.Scopes[toolName]; ok {
|
|
tool.Scopes = scopes
|
|
}
|
|
}
|
|
|
|
// Add example from metadata if available
|
|
if ep.Metadata != nil {
|
|
if example, ok := ep.Metadata["example"]; ok && example != "" {
|
|
inputSchema["examples"] = []string{example}
|
|
}
|
|
}
|
|
|
|
s.tools[toolName] = tool
|
|
|
|
// Create rate limiter for this tool if rate limiting is configured
|
|
if s.opts.RateLimit != nil && s.opts.RateLimit.RequestsPerSecond > 0 {
|
|
s.limitersMu.Lock()
|
|
if _, exists := s.limiters[toolName]; !exists {
|
|
s.limiters[toolName] = newRateLimiter(
|
|
s.opts.RateLimit.RequestsPerSecond,
|
|
s.opts.RateLimit.Burst,
|
|
)
|
|
}
|
|
s.limitersMu.Unlock()
|
|
}
|
|
}
|
|
}
|
|
|
|
s.opts.Logger.Printf("[mcp] Discovered %d tools from %d services", len(s.tools), len(services))
|
|
return nil
|
|
}
|
|
|
|
// buildInputSchema converts registry value type information to JSON schema
|
|
func (s *Server) buildInputSchema(value *registry.Value) map[string]interface{} {
|
|
schema := map[string]interface{}{
|
|
"type": "object",
|
|
"properties": make(map[string]interface{}),
|
|
}
|
|
|
|
if value == nil || len(value.Values) == 0 {
|
|
return schema
|
|
}
|
|
|
|
properties := schema["properties"].(map[string]interface{})
|
|
for _, field := range value.Values {
|
|
properties[field.Name] = map[string]interface{}{
|
|
"type": s.mapGoTypeToJSON(field.Type),
|
|
"description": fmt.Sprintf("%s field", field.Name),
|
|
}
|
|
}
|
|
|
|
return schema
|
|
}
|
|
|
|
// mapGoTypeToJSON maps Go types to JSON schema types
|
|
func (s *Server) mapGoTypeToJSON(goType string) string {
|
|
switch goType {
|
|
case "string":
|
|
return "string"
|
|
case "int", "int32", "int64", "uint", "uint32", "uint64":
|
|
return "integer"
|
|
case "float32", "float64":
|
|
return "number"
|
|
case "bool":
|
|
return "boolean"
|
|
default:
|
|
return "object"
|
|
}
|
|
}
|
|
|
|
// watchServices watches for service registry changes
|
|
func (s *Server) watchServices() {
|
|
if s.watching {
|
|
return
|
|
}
|
|
s.watching = true
|
|
|
|
watcher, err := s.opts.Registry.Watch()
|
|
if err != nil {
|
|
s.opts.Logger.Printf("[mcp] Failed to watch registry: %v", err)
|
|
return
|
|
}
|
|
defer watcher.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-s.opts.Context.Done():
|
|
return
|
|
default:
|
|
_, err := watcher.Next()
|
|
if err != nil {
|
|
time.Sleep(time.Second)
|
|
continue
|
|
}
|
|
|
|
// Rediscover services on any change
|
|
if err := s.discoverServices(); err != nil {
|
|
s.opts.Logger.Printf("[mcp] Failed to rediscover services: %v", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// serveHTTP starts an HTTP server with SSE transport
|
|
func (s *Server) serveHTTP() error {
|
|
mux := http.NewServeMux()
|
|
|
|
// MCP endpoints
|
|
mux.HandleFunc("/mcp/tools", s.handleListTools)
|
|
mux.HandleFunc("/mcp/call", s.handleCallTool)
|
|
mux.HandleFunc("/health", s.handleHealth)
|
|
|
|
s.server = &http.Server{
|
|
Addr: s.opts.Address,
|
|
Handler: mux,
|
|
}
|
|
|
|
s.opts.Logger.Printf("[mcp] MCP gateway listening on %s", s.opts.Address)
|
|
return s.server.ListenAndServe()
|
|
}
|
|
|
|
// serveStdio starts stdio-based MCP server (for Claude Code, etc.)
|
|
func (s *Server) serveStdio() error {
|
|
transport := NewStdioTransport(s)
|
|
return transport.Serve()
|
|
}
|
|
|
|
// handleListTools returns the list of available tools
|
|
func (s *Server) handleListTools(w http.ResponseWriter, r *http.Request) {
|
|
if s.opts.AuthFunc != nil {
|
|
if err := s.opts.AuthFunc(r); err != nil {
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
}
|
|
|
|
s.toolsMu.RLock()
|
|
tools := make([]*Tool, 0, len(s.tools))
|
|
for _, tool := range s.tools {
|
|
tools = append(tools, tool)
|
|
}
|
|
s.toolsMu.RUnlock()
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"tools": tools,
|
|
})
|
|
}
|
|
|
|
// handleCallTool executes a tool (makes an RPC call)
|
|
func (s *Server) handleCallTool(w http.ResponseWriter, r *http.Request) {
|
|
if s.opts.AuthFunc != nil {
|
|
if err := s.opts.AuthFunc(r); err != nil {
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Parse request
|
|
var req struct {
|
|
Tool string `json:"tool"`
|
|
Input map[string]interface{} `json:"input"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Get tool info
|
|
s.toolsMu.RLock()
|
|
tool, exists := s.tools[req.Tool]
|
|
s.toolsMu.RUnlock()
|
|
|
|
if !exists {
|
|
http.Error(w, "Tool not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
// Generate trace ID for this call
|
|
traceID := uuid.New().String()
|
|
|
|
// Authenticate and authorise
|
|
var account *auth.Account
|
|
if s.opts.Auth != nil {
|
|
token := r.Header.Get("Authorization")
|
|
if strings.HasPrefix(token, "Bearer ") {
|
|
token = strings.TrimPrefix(token, "Bearer ")
|
|
}
|
|
if token == "" {
|
|
s.audit(AuditRecord{TraceID: traceID, Timestamp: time.Now(), Tool: req.Tool, Allowed: false, DeniedReason: "missing token"})
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
acc, err := s.opts.Auth.Inspect(token)
|
|
if err != nil {
|
|
s.audit(AuditRecord{TraceID: traceID, Timestamp: time.Now(), Tool: req.Tool, Allowed: false, DeniedReason: "invalid token"})
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
account = acc
|
|
|
|
// Check per-tool scopes
|
|
if len(tool.Scopes) > 0 {
|
|
if !hasScope(account.Scopes, tool.Scopes) {
|
|
s.audit(AuditRecord{
|
|
TraceID: traceID, Timestamp: time.Now(), Tool: req.Tool,
|
|
AccountID: account.ID, ScopesRequired: tool.Scopes,
|
|
Allowed: false, DeniedReason: "insufficient scopes",
|
|
})
|
|
http.Error(w, "Forbidden: insufficient scopes", http.StatusForbidden)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Rate limit check
|
|
if err := s.allowRate(req.Tool); err != nil {
|
|
accountID := ""
|
|
if account != nil {
|
|
accountID = account.ID
|
|
}
|
|
s.audit(AuditRecord{
|
|
TraceID: traceID, Timestamp: time.Now(), Tool: req.Tool,
|
|
AccountID: accountID, Allowed: false, DeniedReason: "rate limited",
|
|
})
|
|
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
|
|
return
|
|
}
|
|
|
|
// Build context with tracing metadata
|
|
ctx := r.Context()
|
|
md := metadata.Metadata{}
|
|
md.Set(TraceIDKey, traceID)
|
|
md.Set(ToolNameKey, req.Tool)
|
|
if account != nil {
|
|
md.Set(AccountIDKey, account.ID)
|
|
}
|
|
ctx = metadata.MergeContext(ctx, md, true)
|
|
|
|
// Convert input to JSON bytes for RPC call
|
|
inputBytes, err := json.Marshal(req.Input)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Make RPC call
|
|
start := time.Now()
|
|
rpcReq := s.opts.Client.NewRequest(tool.Service, tool.Endpoint, &bytes.Frame{Data: inputBytes})
|
|
var rsp bytes.Frame
|
|
|
|
if err := s.opts.Client.Call(ctx, rpcReq, &rsp); err != nil {
|
|
s.opts.Logger.Printf("[mcp] RPC call failed: %v", err)
|
|
accountID := ""
|
|
if account != nil {
|
|
accountID = account.ID
|
|
}
|
|
s.audit(AuditRecord{
|
|
TraceID: traceID, Timestamp: time.Now(), Tool: req.Tool,
|
|
AccountID: accountID, ScopesRequired: tool.Scopes,
|
|
Allowed: true, Duration: time.Since(start), Error: err.Error(),
|
|
})
|
|
http.Error(w, fmt.Sprintf("RPC call failed: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Audit successful call
|
|
accountID := ""
|
|
if account != nil {
|
|
accountID = account.ID
|
|
}
|
|
s.audit(AuditRecord{
|
|
TraceID: traceID, Timestamp: time.Now(), Tool: req.Tool,
|
|
AccountID: accountID, ScopesRequired: tool.Scopes,
|
|
Allowed: true, Duration: time.Since(start),
|
|
})
|
|
|
|
// Return response with trace ID
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Header().Set(TraceIDKey, traceID)
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"result": json.RawMessage(rsp.Data),
|
|
"trace_id": traceID,
|
|
})
|
|
}
|
|
|
|
// handleHealth returns gateway health status
|
|
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
|
s.toolsMu.RLock()
|
|
toolCount := len(s.tools)
|
|
s.toolsMu.RUnlock()
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"status": "ok",
|
|
"tools": toolCount,
|
|
})
|
|
}
|
|
|
|
// Stop gracefully shuts down the MCP gateway
|
|
func (s *Server) Stop() error {
|
|
if s.server != nil {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
return s.server.Shutdown(ctx)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetTools returns the current list of available tools
|
|
func (s *Server) GetTools() []*Tool {
|
|
s.toolsMu.RLock()
|
|
defer s.toolsMu.RUnlock()
|
|
|
|
tools := make([]*Tool, 0, len(s.tools))
|
|
for _, tool := range s.tools {
|
|
tools = append(tools, tool)
|
|
}
|
|
return tools
|
|
}
|
|
|
|
// audit emits an audit record if an AuditFunc is configured.
|
|
func (s *Server) audit(record AuditRecord) {
|
|
if s.opts.AuditFunc != nil {
|
|
s.opts.AuditFunc(record)
|
|
}
|
|
}
|
|
|
|
// allowRate checks if the tool call is allowed under the configured rate limit.
|
|
// Returns nil if allowed, non-nil error if rate-limited.
|
|
func (s *Server) allowRate(toolName string) error {
|
|
if s.opts.RateLimit == nil {
|
|
return nil
|
|
}
|
|
s.limitersMu.RLock()
|
|
limiter, ok := s.limiters[toolName]
|
|
s.limitersMu.RUnlock()
|
|
if !ok {
|
|
return nil
|
|
}
|
|
if !limiter.Allow() {
|
|
return fmt.Errorf("rate limit exceeded for tool %s", toolName)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// hasScope checks if the account has at least one of the required scopes.
|
|
func hasScope(accountScopes, requiredScopes []string) bool {
|
|
for _, req := range requiredScopes {
|
|
for _, have := range accountScopes {
|
|
if strings.EqualFold(have, req) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Example shows how to use the MCP gateway in your code
|
|
func Example() {
|
|
// This function is never called - it's just documentation
|
|
_ = func() {
|
|
// In your service code:
|
|
// service := micro.NewService(micro.Name("myservice"))
|
|
// service.Init()
|
|
|
|
// Start MCP gateway
|
|
go func() {
|
|
if err := Serve(Options{
|
|
Registry: registry.DefaultRegistry,
|
|
Address: ":3000",
|
|
}); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}()
|
|
|
|
// service.Run()
|
|
}
|
|
}
|