Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

375 lines
11 KiB
Go

package implementationhost
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"sort"
"strings"
"sync"
connectorruntime "github.com/tutti-os/tutti/packages/connector/runtime"
connectormcp "github.com/tutti-os/tutti/packages/connector/runtime/mcp"
)
// MCPTool is the connector-scoped projection exposed by the local connector
// MCP server. UpstreamName is deliberately not serialized: callers use the
// namespaced local name and the registry resolves the upstream binding.
type MCPTool struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
InputSchema map[string]any `json:"inputSchema"`
// ConnectorKey and ConnectorVersion are trusted in-process route provenance.
// They are never exposed through the MCP tools/list JSON contract or accepted
// from Agent input.
ConnectorKey string `json:"-"`
ConnectorVersion string `json:"-"`
}
// MCPToolProjection derives the exact transport-validation contract for a
// resolved live Tool. A Session router can use this to inject an
// authority-specific discriminator after it has selected Owner or Caller,
// without asking the transport validator to interpret a presentation oneOf.
type MCPToolProjection func(MCPTool) (MCPTool, error)
type registeredMCPTool struct {
routeID string
localName string
upstreamName string
description string
inputSchema map[string]any
client mcpCaller
}
// MCPRegistry exposes only Connector-owned MCP routes. It intentionally
// shares the implementation host's generation-fenced RouteTable so MCP and
// CLI publication observe the same lifecycle boundary.
type MCPRegistry struct {
mu sync.RWMutex
routes *connectorruntime.RouteTable
subscribers map[uint64]chan struct{}
nextID uint64
authorizationErrorObserver func()
}
func NewMCPRegistry() *MCPRegistry {
return &MCPRegistry{subscribers: make(map[uint64]chan struct{})}
}
func (registry *MCPRegistry) attach(routes *connectorruntime.RouteTable) {
registry.mu.Lock()
registry.routes = routes
registry.mu.Unlock()
}
func (registry *MCPRegistry) activeRoutes() []*connectorRoute {
if registry == nil {
return nil
}
registry.mu.RLock()
table := registry.routes
registry.mu.RUnlock()
if table == nil {
return nil
}
portable := table.PublishedRoutes()
routes := make([]*connectorRoute, 0, len(portable))
for _, candidate := range portable {
if route, ok := candidate.(*connectorRoute); ok && len(route.mcpTools) > 0 {
routes = append(routes, route)
}
}
return routes
}
// Tools reads each active downstream MCP directly. Route activation keeps only
// bootstrap evidence; it is not an Agent-visible tool-list cache.
func (registry *MCPRegistry) Tools(ctx context.Context) ([]MCPTool, error) {
routes := registry.activeRoutes()
type routeToolsResult struct {
route *connectorRoute
tools []mcpTool
err error
}
results := make(chan routeToolsResult, len(routes))
for _, route := range routes {
go func(route *connectorRoute) {
tools, err := registry.listRouteTools(ctx, route)
results <- routeToolsResult{route: route, tools: tools, err: err}
}(route)
}
seen := make(map[string]struct{})
result := make([]MCPTool, 0)
var listErr error
succeeded := 0
for range routes {
listed := <-results
if listed.err != nil {
listErr = errors.Join(listErr, fmt.Errorf("%s: %w", listed.route.connectorKey, listed.err))
slog.Warn("connector MCP tools/list omitted unavailable route", "connectorKey", listed.route.connectorKey, "error", listed.err)
continue
}
succeeded++
for _, tool := range listed.tools {
localName := listed.route.connectorKey + "_" + tool.Name
if _, duplicate := seen[localName]; duplicate {
continue
}
seen[localName] = struct{}{}
result = append(result, MCPTool{Name: localName, Description: tool.Description,
InputSchema: cloneJSONMap(tool.InputSchema), ConnectorKey: listed.route.connectorKey,
ConnectorVersion: listed.route.connectorVersion})
}
}
if len(routes) > 0 && succeeded == 0 {
return nil, listErr
}
sort.Slice(result, func(left, right int) bool { return result[left].Name < result[right].Name })
return result, nil
}
// Call resolves a native namespaced tool against an immutable current route.
// Duplicate bindings fail closed instead of selecting an arbitrary account or
// generation.
func (registry *MCPRegistry) Call(ctx context.Context, name string, arguments map[string]any) (json.RawMessage, error) {
return registry.CallValidated(ctx, name, arguments, nil)
}
// CallValidated resolves the current downstream Tool once, lets the local
// transport validate request-integrity headers against that exact live schema,
// and then calls the same binding.
func (registry *MCPRegistry) CallValidated(ctx context.Context, name string, arguments map[string]any, validate func(MCPTool) error) (json.RawMessage, error) {
return registry.CallProjectedValidated(ctx, name, arguments, nil, validate)
}
// CallProjectedValidated resolves the current downstream Tool once, projects
// an authority-specific validation schema, validates that projected contract,
// and calls the same immutable live binding. Projection cannot rename a Tool,
// change its Connector provenance, or remove its input schema.
func (registry *MCPRegistry) CallProjectedValidated(
ctx context.Context,
name string,
arguments map[string]any,
project MCPToolProjection,
validate func(MCPTool) error,
) (json.RawMessage, error) {
name = strings.TrimSpace(name)
if name == "" {
return nil, errors.New("connector MCP tool was not found")
}
type candidateResult struct {
route *connectorRoute
tools []mcpTool
err error
}
candidates := make([]*connectorRoute, 0)
for _, route := range registry.activeRoutes() {
if strings.HasPrefix(name, route.connectorKey+"_") {
candidates = append(candidates, route)
}
}
results := make(chan candidateResult, len(candidates))
for _, route := range candidates {
go func(route *connectorRoute) {
tools, err := registry.listRouteTools(ctx, route)
results <- candidateResult{route: route, tools: tools, err: err}
}(route)
}
type resolvedBinding struct {
binding registeredMCPTool
owner *connectorRoute
tool MCPTool
}
bindings := make([]resolvedBinding, 0, 1)
var resolveErr error
for range candidates {
listed := <-results
if listed.err != nil {
resolveErr = errors.Join(resolveErr, fmt.Errorf("%s: %w", listed.route.connectorKey, listed.err))
continue
}
for _, tool := range listed.tools {
localName := listed.route.connectorKey + "_" + tool.Name
if localName != name {
continue
}
bindings = append(bindings, resolvedBinding{
binding: registeredMCPTool{localName: localName, upstreamName: tool.Name, client: routeMCPCaller(listed.route)}, owner: listed.route,
tool: MCPTool{Name: localName, Description: tool.Description, InputSchema: cloneJSONMap(tool.InputSchema),
ConnectorKey: listed.route.connectorKey, ConnectorVersion: listed.route.connectorVersion},
})
}
}
if len(bindings) == 0 {
if resolveErr != nil {
return nil, resolveErr
}
return nil, errors.New("connector MCP tool was not found")
}
if len(bindings) != 1 {
return nil, errors.New("connector MCP tool binding is ambiguous")
}
resolved := bindings[0]
validationTool := resolved.tool
if project != nil {
projected, err := project(validationTool)
if err != nil {
return nil, err
}
if strings.TrimSpace(projected.Name) != validationTool.Name || projected.InputSchema == nil ||
strings.TrimSpace(projected.ConnectorKey) != validationTool.ConnectorKey ||
strings.TrimSpace(projected.ConnectorVersion) != validationTool.ConnectorVersion {
return nil, errors.New("connector MCP projected tool contract is invalid")
}
validationTool = projected
}
if validate != nil {
if err := validate(validationTool); err != nil {
return nil, err
}
}
if !registry.routeCurrent(resolved.owner) {
return nil, errors.New("connector MCP route is no longer active")
}
result, err := resolved.binding.client.Call(ctx, "tools/call", map[string]any{
"name": resolved.binding.upstreamName, "arguments": arguments,
})
if err != nil {
registry.observeAuthorizationError(err)
}
return result, err
}
func (registry *MCPRegistry) listRouteTools(ctx context.Context, route *connectorRoute) ([]mcpTool, error) {
client := routeMCPCaller(route)
if client == nil {
return nil, errors.New("connector MCP route is unavailable")
}
var tools []mcpTool
var err error
if route.remoteMCP != nil {
tools, err = listModernMCPTools(ctx, client)
} else {
tools, err = listMCPTools(ctx, client)
}
if err != nil {
registry.observeAuthorizationError(err)
return nil, err
}
if err := validateMCPToolContracts(route.connectorKey, tools); err != nil {
return nil, err
}
if route.remoteMCP != nil {
schemas := make(map[string]map[string]any, len(tools))
for _, tool := range tools {
schemas[tool.Name] = tool.InputSchema
}
if err := route.remoteMCP.ReplaceTools(schemas); err != nil {
return nil, err
}
}
return tools, nil
}
func routeMCPCaller(route *connectorRoute) mcpCaller {
if route == nil {
return nil
}
if route.remoteMCP != nil {
return route.remoteMCP
}
if route.mcpClient != nil {
return route.mcpClient
}
for _, tool := range route.mcpTools {
return tool.client
}
return nil
}
func (registry *MCPRegistry) observeAuthorizationError(err error) {
var rpcErr *connectormcp.RPCError
if !errors.As(err, &rpcErr) ||
(rpcErr.Code != mcpAuthorizationRequiredCode && rpcErr.Code != mcpAuthorizationExpiredCode) {
return
}
registry.mu.RLock()
observer := registry.authorizationErrorObserver
registry.mu.RUnlock()
if observer != nil {
observer()
}
}
func (registry *MCPRegistry) SetAuthorizationErrorObserver(observer func()) {
if registry == nil {
return
}
registry.mu.Lock()
registry.authorizationErrorObserver = observer
registry.mu.Unlock()
}
func (registry *MCPRegistry) routeCurrent(route *connectorRoute) bool {
registry.mu.RLock()
table := registry.routes
registry.mu.RUnlock()
return table != nil && table.IsCurrent(route)
}
// Subscribe is used by the local MCP server to fan out
// notifications/tools/list_changed. Notifications are edge-triggered and
// coalesced for slow consumers.
func (registry *MCPRegistry) Subscribe() (<-chan struct{}, func()) {
if registry == nil {
closed := make(chan struct{})
close(closed)
return closed, func() {}
}
registry.mu.Lock()
registry.nextID++
id := registry.nextID
updates := make(chan struct{}, 1)
registry.subscribers[id] = updates
registry.mu.Unlock()
return updates, func() {
registry.mu.Lock()
if current, ok := registry.subscribers[id]; ok {
delete(registry.subscribers, id)
close(current)
}
registry.mu.Unlock()
}
}
func (registry *MCPRegistry) notifyChanged() {
if registry == nil {
return
}
registry.mu.RLock()
defer registry.mu.RUnlock()
for _, updates := range registry.subscribers {
select {
case updates <- struct{}{}:
default:
}
}
}
func cloneJSONMap(input map[string]any) map[string]any {
if input == nil {
return nil
}
raw, err := json.Marshal(input)
if err != nil {
return nil
}
var result map[string]any
if json.Unmarshal(raw, &result) != nil {
return nil
}
return result
}