package context import ( "fmt" jsoniter "github.com/json-iterator/go" "github.com/yaoapp/gou/mcp" "github.com/yaoapp/gou/mcp/types" "github.com/yaoapp/yao/agent/i18n" traceTypes "github.com/yaoapp/yao/trace/types" ) // MCP Client Operations with automatic trace logging and resource management // Resource Operations // ================== // ListResources lists all available resources from an MCP client // Automatically creates trace node and handles client lifecycle func (ctx *Context) ListResources(mcpID string, cursor string) (*types.ListResourcesResponse, error) { // Get MCP client client, err := mcp.Select(mcpID) if err != nil { return nil, fmt.Errorf("failed to select MCP client '%s': %w", mcpID, err) } // Get client label for display clientLabel := client.GetMetaInfo().Label if clientLabel == "" { clientLabel = mcpID } // Get trace manager trace, _ := ctx.Trace() // Create trace node var node traceTypes.Node if trace != nil { node, _ = trace.Add( map[string]any{ "mcp": mcpID, "cursor": cursor, }, traceTypes.TraceNodeOption{ Label: i18n.T(ctx.Locale, "mcp.list_resources.label"), // "MCP: List Resources" Type: "mcp", Icon: "list", Description: fmt.Sprintf(i18n.T(ctx.Locale, "mcp.list_resources.description"), clientLabel), // "List resources from MCP client '%s'" }, ) } // Call ListResources result, err := client.ListResources(ctx.Context, cursor) if err != nil { if node != nil { node.Fail(err) } return nil, err } // Complete trace node with result if node != nil { node.Complete(map[string]any{ "resources": len(result.Resources), "nextCursor": result.NextCursor, }) } return result, nil } // ReadResource reads a specific resource from an MCP client // Automatically creates trace node and handles client lifecycle func (ctx *Context) ReadResource(mcpID string, uri string) (*types.ReadResourceResponse, error) { // Get MCP client client, err := mcp.Select(mcpID) if err != nil { return nil, fmt.Errorf("failed to select MCP client '%s': %w", mcpID, err) } // Get client label for display clientLabel := client.GetMetaInfo().Label if clientLabel == "" { clientLabel = mcpID } // Get trace manager trace, _ := ctx.Trace() // Create trace node var node traceTypes.Node if trace != nil { node, _ = trace.Add( map[string]any{ "mcp": mcpID, "uri": uri, }, traceTypes.TraceNodeOption{ Label: i18n.T(ctx.Locale, "mcp.read_resource.label"), // "MCP: Read Resource" Type: "mcp", Icon: "description", Description: fmt.Sprintf(i18n.T(ctx.Locale, "mcp.read_resource.description"), uri, clientLabel), // "Read resource '%s' from MCP client '%s'" }, ) } // Call ReadResource result, err := client.ReadResource(ctx.Context, uri) if err != nil { if node != nil { node.Fail(err) } return nil, err } // Complete trace node with result if node != nil { node.Complete(map[string]any{ "contents": len(result.Contents), }) } return result, nil } // Tool Operations // =============== // ListTools lists all available tools from an MCP client // Automatically creates trace node and handles client lifecycle func (ctx *Context) ListTools(mcpID string, cursor string) (*types.ListToolsResponse, error) { // Get MCP client client, err := mcp.Select(mcpID) if err != nil { return nil, fmt.Errorf("failed to select MCP client '%s': %w", mcpID, err) } // Get client label for display clientLabel := client.GetMetaInfo().Label if clientLabel == "" { clientLabel = mcpID } // Get trace manager trace, _ := ctx.Trace() // Create trace node var node traceTypes.Node if trace != nil { node, _ = trace.Add( map[string]any{ "mcp": mcpID, "cursor": cursor, }, traceTypes.TraceNodeOption{ Label: i18n.T(ctx.Locale, "mcp.list_tools.label"), // "MCP: List Tools" Type: "mcp", Icon: "build", Description: fmt.Sprintf(i18n.T(ctx.Locale, "mcp.list_tools.description"), clientLabel), // "List tools from MCP client '%s'" }, ) } // Call ListTools result, err := client.ListTools(ctx.Context, cursor) if err != nil { if node != nil { node.Fail(err) } return nil, err } // Complete trace node with result if node != nil { node.Complete(map[string]any{ "tools": len(result.Tools), "nextCursor": result.NextCursor, }) } return result, nil } // CallTool calls a single tool from an MCP client // Automatically creates trace node and handles client lifecycle func (ctx *Context) CallTool(mcpID string, name string, arguments interface{}) (*types.CallToolResponse, error) { // Get MCP client client, err := mcp.Select(mcpID) if err != nil { return nil, fmt.Errorf("failed to select MCP client '%s': %w", mcpID, err) } // Get client label for display clientLabel := client.GetMetaInfo().Label if clientLabel == "" { clientLabel = mcpID } // Get trace manager trace, _ := ctx.Trace() // Create trace node var node traceTypes.Node if trace != nil { node, _ = trace.Add( map[string]any{ "mcp": mcpID, "tool": name, "arguments": arguments, }, traceTypes.TraceNodeOption{ Label: i18n.T(ctx.Locale, "mcp.call_tool.label"), // "MCP: Call Tool" Type: "mcp", Icon: "settings", Description: fmt.Sprintf(i18n.T(ctx.Locale, "mcp.call_tool.description"), name, clientLabel), // "Call tool '%s' from MCP client '%s'" }, ) } // Call tool (pass ctx as extraArgs for Process transport to propagate Authorized()) result, err := client.CallTool(ctx.Context, name, arguments, ctx) if err != nil { if node != nil { node.Fail(err) } return nil, err } // Complete trace node with result if node != nil { node.Complete(map[string]any{ "contents": len(result.Content), }) } return result, nil } // CallTools calls multiple tools sequentially from an MCP client // Automatically creates trace node and handles client lifecycle func (ctx *Context) CallTools(mcpID string, tools []types.ToolCall) (*types.CallToolsResponse, error) { // Get MCP client client, err := mcp.Select(mcpID) if err != nil { return nil, fmt.Errorf("failed to select MCP client '%s': %w", mcpID, err) } // Get client label for display clientLabel := client.GetMetaInfo().Label if clientLabel == "" { clientLabel = mcpID } // Get trace manager trace, _ := ctx.Trace() // Create trace node var node traceTypes.Node if trace != nil { node, _ = trace.Add( map[string]any{ "mcp": mcpID, "tools": tools, "count": len(tools), }, traceTypes.TraceNodeOption{ Label: i18n.T(ctx.Locale, "mcp.call_tools.label"), // "MCP: Call Tools" Type: "mcp", Icon: "settings", Description: fmt.Sprintf(i18n.T(ctx.Locale, "mcp.call_tools.description"), len(tools), clientLabel), // "Call %d tools sequentially from MCP client '%s'" }, ) } // Call tools sequentially (pass ctx as extraArgs for Process transport to propagate Authorized()) result, err := client.CallTools(ctx.Context, tools, ctx) if err != nil { if node != nil { node.Fail(err) } return nil, err } // Complete trace node with result if node != nil { node.Complete(map[string]any{ "results": len(result.Results), }) } return result, nil } // CallToolsParallel calls multiple tools in parallel from an MCP client // Automatically creates trace node and handles client lifecycle func (ctx *Context) CallToolsParallel(mcpID string, tools []types.ToolCall) (*types.CallToolsResponse, error) { // Get MCP client client, err := mcp.Select(mcpID) if err != nil { return nil, fmt.Errorf("failed to select MCP client '%s': %w", mcpID, err) } // Get client label for display clientLabel := client.GetMetaInfo().Label if clientLabel == "" { clientLabel = mcpID } // Get trace manager trace, _ := ctx.Trace() // Create trace node var node traceTypes.Node if trace != nil { node, _ = trace.Add( map[string]any{ "mcp": mcpID, "tools": tools, "count": len(tools), }, traceTypes.TraceNodeOption{ Label: i18n.T(ctx.Locale, "mcp.call_tools_parallel.label"), // "MCP: Call Tools (Parallel)" Type: "mcp", Icon: "settings", Description: fmt.Sprintf(i18n.T(ctx.Locale, "mcp.call_tools_parallel.description"), len(tools), clientLabel), // "Call %d tools in parallel from MCP client '%s'" }, ) } // Call tools in parallel (pass ctx as extraArgs for Process transport to propagate Authorized()) result, err := client.CallToolsParallel(ctx.Context, tools, ctx) if err != nil { if node != nil { node.Fail(err) } return nil, err } // Complete trace node with result if node != nil { node.Complete(map[string]any{ "results": len(result.Results), }) } return result, nil } // Prompt Operations // ================= // ListPrompts lists all available prompts from an MCP client // Automatically creates trace node and handles client lifecycle func (ctx *Context) ListPrompts(mcpID string, cursor string) (*types.ListPromptsResponse, error) { // Get MCP client client, err := mcp.Select(mcpID) if err != nil { return nil, fmt.Errorf("failed to select MCP client '%s': %w", mcpID, err) } // Get client label for display clientLabel := client.GetMetaInfo().Label if clientLabel == "" { clientLabel = mcpID } // Get trace manager trace, _ := ctx.Trace() // Create trace node var node traceTypes.Node if trace != nil { node, _ = trace.Add( map[string]any{ "mcp": mcpID, "cursor": cursor, }, traceTypes.TraceNodeOption{ Label: i18n.T(ctx.Locale, "mcp.list_prompts.label"), // "MCP: List Prompts" Type: "mcp", Icon: "chat", Description: fmt.Sprintf(i18n.T(ctx.Locale, "mcp.list_prompts.description"), clientLabel), // "List prompts from MCP client '%s'" }, ) } // Call ListPrompts result, err := client.ListPrompts(ctx.Context, cursor) if err != nil { if node != nil { node.Fail(err) } return nil, err } // Complete trace node with result if node != nil { node.Complete(map[string]any{ "prompts": len(result.Prompts), "nextCursor": result.NextCursor, }) } return result, nil } // GetPrompt gets a prompt with arguments from an MCP client // Automatically creates trace node and handles client lifecycle func (ctx *Context) GetPrompt(mcpID string, name string, arguments map[string]interface{}) (*types.GetPromptResponse, error) { // Get MCP client client, err := mcp.Select(mcpID) if err != nil { return nil, fmt.Errorf("failed to select MCP client '%s': %w", mcpID, err) } // Get client label for display clientLabel := client.GetMetaInfo().Label if clientLabel == "" { clientLabel = mcpID } // Get trace manager trace, _ := ctx.Trace() // Create trace node var node traceTypes.Node if trace != nil { node, _ = trace.Add( map[string]any{ "mcp": mcpID, "prompt": name, "arguments": arguments, }, traceTypes.TraceNodeOption{ Label: i18n.T(ctx.Locale, "mcp.get_prompt.label"), // "MCP: Get Prompt" Type: "mcp", Icon: "chat", Description: fmt.Sprintf(i18n.T(ctx.Locale, "mcp.get_prompt.description"), name, clientLabel), // "Get prompt '%s' from MCP client '%s'" }, ) } // Get prompt result, err := client.GetPrompt(ctx.Context, name, arguments) if err != nil { if node != nil { node.Fail(err) } return nil, err } // Complete trace node with result if node != nil { node.Complete(map[string]any{ "messages": len(result.Messages), }) } return result, nil } // Sample Operations // ================= // ListSamples lists samples for a tool or resource from an MCP client // Automatically creates trace node and handles client lifecycle func (ctx *Context) ListSamples(mcpID string, itemType types.SampleItemType, itemName string) (*types.ListSamplesResponse, error) { // Get MCP client client, err := mcp.Select(mcpID) if err != nil { return nil, fmt.Errorf("failed to select MCP client '%s': %w", mcpID, err) } // Get client label for display clientLabel := client.GetMetaInfo().Label if clientLabel == "" { clientLabel = mcpID } // Get trace manager trace, _ := ctx.Trace() // Create trace node var node traceTypes.Node if trace != nil { node, _ = trace.Add( map[string]any{ "mcp": mcpID, "itemType": itemType, "itemName": itemName, }, traceTypes.TraceNodeOption{ Label: i18n.T(ctx.Locale, "mcp.list_samples.label"), // "MCP: List Samples" Type: "mcp", Icon: "library_books", Description: fmt.Sprintf(i18n.T(ctx.Locale, "mcp.list_samples.description"), itemName, clientLabel), // "List samples for '%s' from MCP client '%s'" }, ) } // Call ListSamples result, err := client.ListSamples(ctx.Context, itemType, itemName) if err != nil { if node != nil { node.Fail(err) } return nil, err } // Complete trace node with result if node != nil { node.Complete(map[string]any{ "samples": len(result.Samples), }) } return result, nil } // GetSample gets a specific sample by index from an MCP client // Automatically creates trace node and handles client lifecycle func (ctx *Context) GetSample(mcpID string, itemType types.SampleItemType, itemName string, index int) (*types.SampleData, error) { // Get MCP client client, err := mcp.Select(mcpID) if err != nil { return nil, fmt.Errorf("failed to select MCP client '%s': %w", mcpID, err) } // Get client label for display clientLabel := client.GetMetaInfo().Label if clientLabel == "" { clientLabel = mcpID } // Get trace manager trace, _ := ctx.Trace() // Create trace node var node traceTypes.Node if trace != nil { node, _ = trace.Add( map[string]any{ "mcp": mcpID, "itemType": itemType, "itemName": itemName, "index": index, }, traceTypes.TraceNodeOption{ Label: i18n.T(ctx.Locale, "mcp.get_sample.label"), // "MCP: Get Sample" Type: "mcp", Icon: "library_books", Description: fmt.Sprintf(i18n.T(ctx.Locale, "mcp.get_sample.description"), index, itemName, clientLabel), // "Get sample #%d for '%s' from MCP client '%s'" }, ) } // Get sample result, err := client.GetSample(ctx.Context, itemType, itemName, index) if err != nil { if node != nil { node.Fail(err) } return nil, err } // Complete trace node with result if node != nil { node.Complete(result) } return result, nil } // Single-Server Tool Response Helpers // ==================================== // parseCallToolResponse parses a CallToolResponse and returns the parsed content directly func parseCallToolResponse(response *types.CallToolResponse) interface{} { if response == nil { return nil } return parseToolResponseContent(response) } // parseCallToolsResponse parses a CallToolsResponse and returns an array of parsed results func parseCallToolsResponse(response *types.CallToolsResponse) []interface{} { if response == nil { return nil } results := make([]interface{}, len(response.Results)) for i, r := range response.Results { results[i] = parseToolResponseContent(&r) } return results } // Cross-Server Tool Operations // ============================ // MCPToolRequest represents a request to call a tool on a specific MCP server type MCPToolRequest struct { MCP string `json:"mcp"` // MCP server ID Tool string `json:"tool"` // Tool name Arguments interface{} `json:"arguments"` // Tool arguments } // MCPToolResult represents the result of a cross-server tool call // Returns parsed result directly, with error field for failures type MCPToolResult struct { MCP string `json:"mcp"` // MCP server ID Tool string `json:"tool"` // Tool name Result interface{} `json:"result,omitempty"` // Parsed result content (directly usable) Error string `json:"error,omitempty"` // Error message (on failure) } // callToolResult is used internally to pass results through channels type callToolResult struct { idx int result *MCPToolResult } // CallToolAll calls tools on multiple MCP servers concurrently and waits for all to complete // Returns results in the same order as requests, regardless of completion order (like Promise.all) func (ctx *Context) CallToolAll(requests []*MCPToolRequest) []*MCPToolResult { if len(requests) == 0 { return []*MCPToolResult{} } results := make([]*MCPToolResult, len(requests)) done := make(chan struct{}) remaining := len(requests) for i, req := range requests { go func(idx int, r *MCPToolRequest) { defer func() { if err := recover(); err != nil { results[idx] = &MCPToolResult{ MCP: r.MCP, Tool: r.Tool, Error: fmt.Sprintf("panic: %v", err), } } done <- struct{}{} }() results[idx] = ctx.callToolSingle(r) }(i, req) } // Wait for all to complete for remaining > 0 { <-done remaining-- } return results } // CallToolAny calls tools on multiple MCP servers concurrently and returns when any succeeds // Returns all results received so far when first success is found (like Promise.any) func (ctx *Context) CallToolAny(requests []*MCPToolRequest) []*MCPToolResult { if len(requests) == 0 { return []*MCPToolResult{} } resultChan := make(chan callToolResult, len(requests)) remaining := len(requests) for i, req := range requests { go func(idx int, r *MCPToolRequest) { defer func() { if err := recover(); err != nil { resultChan <- callToolResult{ idx: idx, result: &MCPToolResult{ MCP: r.MCP, Tool: r.Tool, Error: fmt.Sprintf("panic: %v", err), }, } } }() resultChan <- callToolResult{idx: idx, result: ctx.callToolSingle(r)} }(i, req) } // Collect results until we find a success or all fail results := make([]*MCPToolResult, len(requests)) for remaining > 0 { cr := <-resultChan remaining-- results[cr.idx] = cr.result // Check if this is a success (no error) if cr.result.Error == "" { break // Stop waiting, we have a success } } // Drain remaining results in background (don't block) if remaining > 0 { go func(count int) { for i := 0; i < count; i++ { <-resultChan } }(remaining) } return results } // CallToolRace calls tools on multiple MCP servers concurrently and returns when any completes // Returns all results received so far when first completion (like Promise.race) func (ctx *Context) CallToolRace(requests []*MCPToolRequest) []*MCPToolResult { if len(requests) == 0 { return []*MCPToolResult{} } resultChan := make(chan callToolResult, len(requests)) remaining := len(requests) for i, req := range requests { go func(idx int, r *MCPToolRequest) { defer func() { if err := recover(); err != nil { resultChan <- callToolResult{ idx: idx, result: &MCPToolResult{ MCP: r.MCP, Tool: r.Tool, Error: fmt.Sprintf("panic: %v", err), }, } } }() resultChan <- callToolResult{idx: idx, result: ctx.callToolSingle(r)} }(i, req) } // Get first result (success or failure) results := make([]*MCPToolResult, len(requests)) cr := <-resultChan remaining-- results[cr.idx] = cr.result // Drain remaining results in background (don't block) if remaining > 0 { go func(count int) { for i := 0; i < count; i++ { <-resultChan } }(remaining) } return results } // callToolSingle executes a single tool call on an MCP server // This is a helper method for the parallel call methods func (ctx *Context) callToolSingle(req *MCPToolRequest) *MCPToolResult { result := &MCPToolResult{ MCP: req.MCP, Tool: req.Tool, } // Call the tool using existing CallTool method response, err := ctx.CallTool(req.MCP, req.Tool, req.Arguments) if err != nil { result.Error = err.Error() return result } // Parse and return result directly result.Result = parseToolResponseContent(response) return result } // parseToolResponseContent extracts and parses the actual content from a CallToolResponse // Similar to ToolCallResult.ParsedContent() in assistant/types.go // - For "text" type, parses the Text field as JSON (or returns as string if not JSON) // - For "image" type, returns the Data and MimeType // - For "resource" type, returns the Resource object // - If only one content item, returns it directly (not as array) func parseToolResponseContent(response *types.CallToolResponse) interface{} { if response == nil || len(response.Content) == 0 { return nil } var results []interface{} for _, tc := range response.Content { switch tc.Type { case types.ToolContentTypeText: // For text type, try to parse as JSON if tc.Text != "" { var parsed interface{} if err := jsoniter.UnmarshalFromString(tc.Text, &parsed); err == nil { results = append(results, parsed) } else { // If not JSON, return as plain string results = append(results, tc.Text) } } case types.ToolContentTypeImage: // For image type, return data and mimeType results = append(results, map[string]interface{}{ "type": "image", "data": tc.Data, "mimeType": tc.MimeType, }) case types.ToolContentTypeResource: // For resource type, return the resource object if tc.Resource != nil { results = append(results, tc.Resource) } default: // Unknown type, include as-is with type info results = append(results, map[string]interface{}{ "type": tc.Type, "text": tc.Text, }) } } // If only one result, return it directly (not as array) if len(results) == 1 { return results[0] } return results }