tools.go

 1package mcp
 2
 3import (
 4	"context"
 5	"encoding/json"
 6	"fmt"
 7	"iter"
 8	"log/slog"
 9	"strings"
10
11	"git.secluded.site/crush/internal/csync"
12	"github.com/modelcontextprotocol/go-sdk/mcp"
13)
14
15type Tool = mcp.Tool
16
17var allTools = csync.NewMap[string, []*Tool]()
18
19// Tools returns all available MCP tools.
20func Tools() iter.Seq2[string, []*Tool] {
21	return allTools.Seq2()
22}
23
24// RunTool runs an MCP tool with the given input parameters.
25func RunTool(ctx context.Context, name, toolName string, input string) (string, error) {
26	var args map[string]any
27	if err := json.Unmarshal([]byte(input), &args); err != nil {
28		return "", fmt.Errorf("error parsing parameters: %s", err)
29	}
30
31	c, err := getOrRenewClient(ctx, name)
32	if err != nil {
33		return "", err
34	}
35	result, err := c.CallTool(ctx, &mcp.CallToolParams{
36		Name:      toolName,
37		Arguments: args,
38	})
39	if err != nil {
40		return "", err
41	}
42
43	output := make([]string, 0, len(result.Content))
44	for _, v := range result.Content {
45		if vv, ok := v.(*mcp.TextContent); ok {
46			output = append(output, vv.Text)
47		} else {
48			output = append(output, fmt.Sprintf("%v", v))
49		}
50	}
51	return strings.Join(output, "\n"), nil
52}
53
54// RefreshTools gets the updated list of tools from the MCP and updates the
55// global state.
56func RefreshTools(ctx context.Context, name string) {
57	session, ok := sessions.Get(name)
58	if !ok {
59		slog.Warn("refresh tools: no session", "name", name)
60		return
61	}
62
63	tools, err := getTools(ctx, session)
64	if err != nil {
65		updateState(name, StateError, err, nil, Counts{})
66		return
67	}
68
69	updateTools(name, tools)
70
71	prev, _ := states.Get(name)
72	prev.Counts.Tools = len(tools)
73	updateState(name, StateConnected, nil, session, prev.Counts)
74}
75
76func getTools(ctx context.Context, session *mcp.ClientSession) ([]*Tool, error) {
77	// Always call ListTools to get the actual available tools.
78	// The InitializeResult Capabilities.Tools field may be an empty object {},
79	// which is valid per MCP spec, but we still need to call ListTools to discover tools.
80	result, err := session.ListTools(ctx, &mcp.ListToolsParams{})
81	if err != nil {
82		return nil, err
83	}
84	return result.Tools, nil
85}
86
87func updateTools(name string, tools []*Tool) {
88	if len(tools) == 0 {
89		allTools.Del(name)
90		return
91	}
92	allTools.Set(name, tools)
93}