package mcpc import ( "context" "encoding/json" "fmt" "strings" "github.com/modelcontextprotocol/go-sdk/mcp" "nub/internal/config" "nub/internal/tool" ) const toolCountWarnThreshold = 15 // bridgeTool bindet ein einzelnes MCP-Tool eines Servers als tool.Tool ein. // ForModel sammelt die Textblöcke, ForUI trägt den strukturierten Content. type bridgeTool struct { server string def *mcp.Tool session *mcp.ClientSession } func (t *bridgeTool) Name() string { return t.server + "__" + t.def.Name } func (t *bridgeTool) Description() string { return t.def.Description } func (t *bridgeTool) Schema() json.RawMessage { b, err := json.Marshal(t.def.InputSchema) if err != nil { return json.RawMessage(`{"type":"object"}`) } return b } func (t *bridgeTool) Run(ctx context.Context, input json.RawMessage, env tool.Env) (tool.Result, error) { var args map[string]any if len(input) > 0 { if err := json.Unmarshal(input, &args); err != nil { return tool.Result{ForModel: "invalid input: " + err.Error(), IsError: true}, nil } } res, err := t.session.CallTool(ctx, &mcp.CallToolParams{Name: t.def.Name, Arguments: args}) if err != nil { // Server-Ausfall zur Laufzeit: IsError statt Panic/Loop-Abbruch (5.6). return tool.Result{ForModel: fmt.Sprintf("mcp %s: %v", t.Name(), err), IsError: true}, nil } var textParts []string for _, c := range res.Content { if tc, ok := c.(*mcp.TextContent); ok { textParts = append(textParts, tc.Text) } } return tool.Result{ ForModel: strings.Join(textParts, "\n"), ForUI: Result{Server: t.server, Content: res.Content}, IsError: res.IsError, }, nil } // Result ist die strukturierte UI-Repräsentation eines MCP-Tool-Aufrufs. type Result struct { Server string Content []mcp.Content } // RegisterTools listet die Tools eines verbundenen Servers, filtert nach der // konfigurierten Allowlist (cfg.Tools) und registriert sie mit // __-Präfix. Ohne Allowlist werden alle Tools geladen, aber ab // mehr als 15 Tools gewarnt (E-07). func RegisterTools(ctx context.Context, reg *tool.Registry, srv *Server, cfg config.MCPServer) ([]string, error) { var all []*mcp.Tool for t, err := range srv.Session.Tools(ctx, nil) { if err != nil { return nil, err } all = append(all, t) } var warnings []string allowed := all if len(cfg.Tools) > 0 { allowSet := make(map[string]bool, len(cfg.Tools)) for _, n := range cfg.Tools { allowSet[n] = true } allowed = make([]*mcp.Tool, 0, len(all)) for _, t := range all { if allowSet[t.Name] { allowed = append(allowed, t) } } } else if len(all) > toolCountWarnThreshold { warnings = append(warnings, fmt.Sprintf( "mcp server %q liefert %d Tools ohne Allowlist — 'tools = [...]' in der Config empfohlen", cfg.Name, len(all))) } for _, t := range allowed { reg.Register(&bridgeTool{server: cfg.Name, def: t, session: srv.Session}) } return warnings, nil }