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

157 lines
5.3 KiB
Go

package implementationhost
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
market "github.com/tutti-os/tutti/packages/connector/daemon/core"
)
func (host *Host) buildRemoteRoute(ctx context.Context, request market.RuntimeReconcileRequest) (*connectorRoute, error) {
remote := request.Connector.Release.Manifest.Implementation.RemoteStreamableHTTP
if remote == nil {
return nil, errors.New("remote_streamable_http connector config is unavailable")
}
accountID := strings.TrimSpace(request.Scope.AccountID)
if accountID == "" || host.remoteMCPClientFactory == nil {
return nil, errors.New("remote MCP product client factory is unavailable")
}
client, err := host.remoteMCPClientFactory.NewRemoteMCPClient(ctx, RemoteMCPClientRequest{
OperationID: request.OperationID, ConnectionID: request.ConnectionID,
ConnectorKey: request.Connector.Key, AccountID: accountID,
ReleaseDigest: request.Connector.Release.ReleaseDigest,
Version: request.Connector.Release.Version,
Generation: request.Generation,
Implementation: *remote,
})
if err != nil {
return nil, fmt.Errorf("create remote connector MCP client: %w", err)
}
closeClient := func() { _ = client.Close(context.Background()) }
if _, err := client.Call(ctx, "server/discover", map[string]any{}); err != nil {
closeClient()
return nil, wrapRemoteMCPAuthorizationError(fmt.Errorf("discover remote connector MCP: %w", err))
}
cacheKey := remoteMCPToolCacheKeyFrom(request)
tools, cached := host.remoteMCPTools.lookup(cacheKey)
if !cached {
var err error
tools, err = listModernMCPTools(ctx, client)
if err != nil {
closeClient()
return nil, wrapRemoteMCPAuthorizationError(err)
}
}
for _, tool := range tools {
if err := client.RegisterTool(tool.Name, tool.InputSchema); err != nil {
closeClient()
return nil, fmt.Errorf("register remote connector MCP Tool %q: %w", tool.Name, err)
}
}
route := newConnectorRoute(request)
if err := host.registerMCPTools(route, client, tools); err != nil {
closeClient()
return nil, err
}
if !cached {
host.remoteMCPTools.store(cacheKey, tools)
}
route.remoteMCP = client
return route, nil
}
type mcpCaller interface {
Call(context.Context, string, any) (json.RawMessage, error)
}
func (*Host) registerMCPTools(route *connectorRoute, client mcpCaller, tools []mcpTool) error {
if len(tools) == 0 {
return errors.New("connector MCP tools/list response is invalid")
}
if err := validateMCPToolContracts(route.connectorKey, tools); err != nil {
return err
}
for _, tool := range tools {
localName := route.connectorKey + "_" + tool.Name
// Keep the upstream JSON Schema intact. MCP schemas are not constrained
// by Tutti's legacy command-input schema subset.
routeID := "connector." + route.connectorKey + ".mcp." + tool.Name
route.mcpTools[localName] = registeredMCPTool{routeID: routeID, localName: localName,
upstreamName: tool.Name, description: tool.Description, inputSchema: cloneJSONMap(tool.InputSchema), client: client}
}
return nil
}
func validateMCPToolContracts(connectorKey string, tools []mcpTool) error {
seen := make(map[string]struct{}, len(tools))
for _, tool := range tools {
localName := connectorKey + "_" + tool.Name
if strings.TrimSpace(tool.Name) == "" || tool.InputSchema == nil || len(localName) > 255 || !mcpLocalToolNamePattern.MatchString(localName) {
return errors.New("connector MCP tool contract is invalid")
}
if _, duplicate := seen[localName]; duplicate {
return errors.New("connector MCP tool capability id is duplicated")
}
seen[localName] = struct{}{}
}
return nil
}
type mcpTool struct {
Name string `json:"name"`
Description string `json:"description"`
InputSchema map[string]any `json:"inputSchema"`
}
func listMCPTools(ctx context.Context, client mcpCaller) ([]mcpTool, error) {
return listMCPToolsWithProtocol(ctx, client, false)
}
func listModernMCPTools(ctx context.Context, client mcpCaller) ([]mcpTool, error) {
return listMCPToolsWithProtocol(ctx, client, true)
}
func listMCPToolsWithProtocol(ctx context.Context, client mcpCaller, requireComplete bool) ([]mcpTool, error) {
const maxPages = 64
const maxTools = 512
result := make([]mcpTool, 0)
var cursor *string
seen := map[string]struct{}{}
for page := 0; page < maxPages; page++ {
params := map[string]any{}
if cursor != nil {
params["cursor"] = *cursor
}
raw, err := client.Call(ctx, "tools/list", params)
if err != nil {
return nil, fmt.Errorf("list connector MCP tools: %w", err)
}
var listing struct {
ResultType string `json:"resultType"`
Tools []mcpTool `json:"tools"`
NextCursor *string `json:"nextCursor"`
}
if err := json.Unmarshal(raw, &listing); err != nil ||
(requireComplete && listing.ResultType != "" && listing.ResultType != "complete") {
return nil, errors.New("connector MCP tools/list response is invalid")
}
result = append(result, listing.Tools...)
if len(result) > maxTools {
return nil, errors.New("connector MCP tools/list exceeds tool limit")
}
if listing.NextCursor == nil {
return result, nil
}
next := *listing.NextCursor
if _, duplicate := seen[next]; duplicate {
return nil, errors.New("connector MCP tools/list cursor repeated")
}
seen[next] = struct{}{}
cursor = &next
}
return nil, errors.New("connector MCP tools/list exceeds page limit")
}