e071084ebe
govulncheck / govulncheck (push) Waiting to run
Harness (E2E) / Harnesses (mock LLM) (push) Waiting to run
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Waiting to run
Lint / golangci-lint (push) Waiting to run
Run Tests / Unit Tests (push) Waiting to run
Run Tests / Etcd Integration Tests (push) Waiting to run
132 lines
4.2 KiB
Go
132 lines
4.2 KiB
Go
package mcp
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
|
|
"go-micro.dev/v6/ai"
|
|
"go-micro.dev/v6/client"
|
|
"go-micro.dev/v6/registry"
|
|
)
|
|
|
|
// CallResult is the outcome of a successful tool dispatch. A tool that ran but
|
|
// produced an error sets IsError — per the MCP spec this is returned as a
|
|
// tools/call result with isError:true, not a JSON-RPC protocol error.
|
|
type CallResult struct {
|
|
Text string
|
|
IsError bool
|
|
}
|
|
|
|
// Error lets the package's RPCError (see stdio.go) be returned by a resolver
|
|
// to signal a protocol/pre-check failure with a specific JSON-RPC code; the
|
|
// handler maps it straight to the JSON-RPC error.
|
|
func (e *RPCError) Error() string { return e.Message }
|
|
|
|
// ToolFunc executes a manually-registered tool. Return a *CallResult for tool
|
|
// outcomes (set IsError for tool-level failures); return a non-nil error — an
|
|
// *RPCError for a specific code — for protocol/pre-check failures.
|
|
type ToolFunc func(ctx context.Context, args map[string]any) (*CallResult, error)
|
|
|
|
// Resolver supplies the gateway's tools and executes calls. Swapping the
|
|
// resolver changes where tools come from without touching the MCP protocol or
|
|
// transport:
|
|
//
|
|
// - NewManualResolver: tools you register explicitly (full product control,
|
|
// including tools that are not go-micro services, executed via your own
|
|
// logic — auth, metering, …).
|
|
// - NewRegistryResolver: tools auto-discovered from registered services.
|
|
//
|
|
// The built-in store/broker tools are intentionally NOT exposed by any
|
|
// resolver — they remain a development convenience on the legacy Serve() path.
|
|
type Resolver interface {
|
|
// List returns the current tool catalog.
|
|
List(ctx context.Context) ([]Tool, error)
|
|
// Call executes a tool by name with JSON arguments.
|
|
Call(ctx context.Context, name string, args map[string]any) (*CallResult, error)
|
|
}
|
|
|
|
// ManualResolver exposes an explicitly-registered set of tools.
|
|
type ManualResolver struct {
|
|
mu sync.RWMutex
|
|
order []Tool
|
|
funcs map[string]ToolFunc
|
|
}
|
|
|
|
// NewManualResolver returns an empty manual resolver.
|
|
func NewManualResolver() *ManualResolver {
|
|
return &ManualResolver{funcs: map[string]ToolFunc{}}
|
|
}
|
|
|
|
// Add registers (or replaces) a tool and its handler. Returns the resolver for
|
|
// chaining.
|
|
func (m *ManualResolver) Add(t Tool, fn ToolFunc) *ManualResolver {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if _, ok := m.funcs[t.Name]; ok {
|
|
for i := range m.order {
|
|
if m.order[i].Name == t.Name {
|
|
m.order[i] = t
|
|
}
|
|
}
|
|
} else {
|
|
m.order = append(m.order, t)
|
|
}
|
|
m.funcs[t.Name] = fn
|
|
return m
|
|
}
|
|
|
|
// List returns the registered tools.
|
|
func (m *ManualResolver) List(_ context.Context) ([]Tool, error) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
out := make([]Tool, len(m.order))
|
|
copy(out, m.order)
|
|
return out, nil
|
|
}
|
|
|
|
// Call runs the handler registered for name.
|
|
func (m *ManualResolver) Call(ctx context.Context, name string, args map[string]any) (*CallResult, error) {
|
|
m.mu.RLock()
|
|
fn, ok := m.funcs[name]
|
|
m.mu.RUnlock()
|
|
if !ok {
|
|
return nil, &RPCError{Code: InvalidParams, Message: "Tool not found: " + name, Data: name}
|
|
}
|
|
return fn(ctx, args)
|
|
}
|
|
|
|
// RegistryResolver auto-discovers tools from registered go-micro services and
|
|
// executes them over RPC. It exposes only services — never the internal
|
|
// store/broker tools.
|
|
type RegistryResolver struct {
|
|
tools *ai.Tools
|
|
}
|
|
|
|
// NewRegistryResolver discovers services from reg and calls them with cl.
|
|
func NewRegistryResolver(reg registry.Registry, cl client.Client) *RegistryResolver {
|
|
return &RegistryResolver{tools: ai.NewTools(reg, ai.ToolClient(cl))}
|
|
}
|
|
|
|
// List discovers the current service tools.
|
|
func (r *RegistryResolver) List(_ context.Context) ([]Tool, error) {
|
|
discovered, err := r.tools.Discover()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := make([]Tool, 0, len(discovered))
|
|
for _, t := range discovered {
|
|
out = append(out, Tool{
|
|
Name: t.Name,
|
|
Description: t.Description,
|
|
InputSchema: map[string]interface{}{"type": "object", "properties": t.Properties},
|
|
})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// Call executes a discovered service tool.
|
|
func (r *RegistryResolver) Call(ctx context.Context, name string, args map[string]any) (*CallResult, error) {
|
|
res := r.tools.Handler()(ctx, ai.ToolCall{ID: "1", Name: name, Input: args})
|
|
return &CallResult{Text: res.Content}, nil
|
|
}
|