on main help sign in
fix(mcp): tool/prompt list update

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

commite519d6fe2a48210ab563dff92f2ca8da8c796d94 authorCarlos Alexandro Becker <caarlos0@users.noreply.github.com> dateMon 3 Nov 2025, 13:33 (1 year ago) parent54076c0
7 files changed+94−31
internal/agent/tools/mcp-tools.go+1−1 internal/agent/tools/mcp/init.go+3 internal/agent/tools/mcp/prompts.go+32−17 internal/agent/tools/mcp/tools.go+33−8 internal/app/app.go+1−4 internal/tui/components/dialogs/commands/loader.go+1−1 internal/tui/tui.go+23
internal/agent/tools/mcp-tools.go+1−1
@@ -12,7 +12,7 @@ import (
1212// GetMCPTools gets all the currently available MCP tools.
1313func GetMCPTools(permissions permission.Service, wd string) []*Tool {
1414 var result []*Tool
15− for name, tool := range mcp.GetMCPTools() {
15+ for name, tool := range mcp.Tools() {
1616 result = append(result, &Tool{
1717 mcpName: name,
1818 tool: tool,
internal/agent/tools/mcp/init.go+3
@@ -281,6 +281,9 @@ func createSession(ctx context.Context, name string, m config.MCPConfig, resolve
281281 Name: name,
282282 })
283283 },
284+ LoggingMessageHandler: func(_ context.Context, req *mcp.LoggingMessageRequest) {
285+ slog.Info("mcp log", "name", name, "data", req.Params.Data)
286+ },
284287 KeepAlive: time.Minute * 10,
285288 },
286289 )
internal/agent/tools/mcp/prompts.go+32−17
@@ -3,6 +3,7 @@ package mcp
33import (
44 "context"
55 "iter"
6+ "log/slog"
67
78 "github.com/charmbracelet/crush/internal/csync"
89 "github.com/modelcontextprotocol/go-sdk/mcp"
@@ -11,25 +12,15 @@ import (
1112type Prompt = mcp.Prompt
1213
1314var (
14− allPrompts = csync.NewMap[string, *Prompt]()
15− client2Prompts = csync.NewMap[string, []*Prompt]()
15+ allPrompts = csync.NewMap[string, *Prompt]()
16+ clientPrompts = csync.NewMap[string, []*Prompt]()
1617)
1718
18−// GetPrompts returns all available MCP prompts.
19−func GetPrompts() iter.Seq2[string, *Prompt] {
19+// Prompts returns all available MCP prompts.
20+func Prompts() iter.Seq2[string, *Prompt] {
2021 return allPrompts.Seq2()
2122}
2223
23−// GetPrompt returns a specific MCP prompt by name.
24−func GetPrompt(name string) (*Prompt, bool) {
25− return allPrompts.Get(name)
26−}
27−
28−// GetPromptsByClient returns all prompts for a specific MCP client.
29−func GetPromptsByClient(clientName string) ([]*Prompt, bool) {
30− return client2Prompts.Get(clientName)
31−}
32−
3324// GetPromptMessages retrieves the content of an MCP prompt with the given arguments.
3425func GetPromptMessages(ctx context.Context, clientName, promptName string, args map[string]string) ([]string, error) {
3526 c, err := getOrRenewClient(ctx, clientName)
@@ -56,6 +47,30 @@ func GetPromptMessages(ctx context.Context, clientName, promptName string, args
5647 return messages, nil
5748}
5849
50+// RefreshPrompts gets the updated list of prompts from the MCP and updates the
51+// global state.
52+func RefreshPrompts(ctx context.Context, name string) {
53+ session, ok := sessions.Get(name)
54+ if !ok {
55+ slog.Warn("refresh prompts: no session", "name", name)
56+ return
57+ }
58+
59+ prompts, err := getPrompts(ctx, session)
60+ if err != nil {
61+ updateState(name, StateError, err, nil, Counts{})
62+ return
63+ }
64+
65+ updatePrompts(name, prompts)
66+
67+ prev, _ := states.Get(name)
68+ updateState(name, StateConnected, nil, session, Counts{
69+ Prompts: len(prompts),
70+ Tools: prev.Counts.Tools,
71+ })
72+}
73+
5974func getPrompts(ctx context.Context, c *mcp.ClientSession) ([]*Prompt, error) {
6075 if c.InitializeResult().Capabilities.Prompts == nil {
6176 return nil, nil
@@ -70,11 +85,11 @@ func getPrompts(ctx context.Context, c *mcp.ClientSession) ([]*Prompt, error) {
7085// updatePrompts updates the global mcpPrompts and mcpClient2Prompts maps
7186func updatePrompts(mcpName string, prompts []*Prompt) {
7287 if len(prompts) == 0 {
73− client2Prompts.Del(mcpName)
88+ clientPrompts.Del(mcpName)
7489 } else {
75− client2Prompts.Set(mcpName, prompts)
90+ clientPrompts.Set(mcpName, prompts)
7691 }
77− for mcpName, prompts := range client2Prompts.Seq2() {
92+ for mcpName, prompts := range clientPrompts.Seq2() {
7893 for _, p := range prompts {
7994 key := mcpName + ":" + p.Name
8095 allPrompts.Set(key, p)
internal/agent/tools/mcp/tools.go+33−8
@@ -5,6 +5,7 @@ import (
55 "encoding/json"
66 "fmt"
77 "iter"
8+ "log/slog"
89 "strings"
910
1011 "github.com/charmbracelet/crush/internal/csync"
@@ -14,12 +15,12 @@ import (
1415type Tool = mcp.Tool
1516
1617var (
17− allTools = csync.NewMap[string, *Tool]()
18− client2Tools = csync.NewMap[string, []*Tool]()
18+ allTools = csync.NewMap[string, *Tool]()
19+ clientTools = csync.NewMap[string, []*Tool]()
1920)
2021
21−// GetTools returns all available MCP tools.
22−func GetTools() iter.Seq2[string, *Tool] {
22+// Tools returns all available MCP tools.
23+func Tools() iter.Seq2[string, *Tool] {
2324 return allTools.Seq2()
2425}
2526
@@ -53,6 +54,30 @@ func RunTool(ctx context.Context, name, toolName string, input string) (string,
5354 return strings.Join(output, "\n"), nil
5455}
5556
57+// RefreshTools gets the updated list of tools from the MCP and updates the
58+// global state.
59+func RefreshTools(ctx context.Context, name string) {
60+ session, ok := sessions.Get(name)
61+ if !ok {
62+ slog.Warn("refresh tools: no session", "name", name)
63+ return
64+ }
65+
66+ tools, err := getTools(ctx, session)
67+ if err != nil {
68+ updateState(name, StateError, err, nil, Counts{})
69+ return
70+ }
71+
72+ updateTools(name, tools)
73+
74+ prev, _ := states.Get(name)
75+ updateState(name, StateConnected, nil, session, Counts{
76+ Tools: len(tools),
77+ Prompts: prev.Counts.Prompts,
78+ })
79+}
80+
5681func getTools(ctx context.Context, session *mcp.ClientSession) ([]*Tool, error) {
5782 if session.InitializeResult().Capabilities.Tools == nil {
5883 return nil, nil
@@ -65,13 +90,13 @@ func getTools(ctx context.Context, session *mcp.ClientSession) ([]*Tool, error)
6590}
6691
6792// updateTools updates the global mcpTools and mcpClient2Tools maps
68−func updateTools(mcpName string, tools []*Tool) {
93+func updateTools(name string, tools []*Tool) {
6994 if len(tools) == 0 {
70− client2Tools.Del(mcpName)
95+ clientTools.Del(name)
7196 } else {
72− client2Tools.Set(mcpName, tools)
97+ clientTools.Set(name, tools)
7398 }
74− for name, tools := range client2Tools.Seq2() {
99+ for name, tools := range clientTools.Seq2() {
75100 for _, t := range tools {
76101 allTools.Set(name, t)
77102 }
internal/app/app.go+1−4
@@ -97,7 +97,7 @@ func New(ctx context.Context, conn *sql.DB, cfg *config.Config) (*App, error) {
9797 }()
9898
9999 // cleanup database upon app shutdown
100− app.cleanupFuncs = append(app.cleanupFuncs, conn.Close)
100+ app.cleanupFuncs = append(app.cleanupFuncs, conn.Close, mcp.Close)
101101
102102 // TODO: remove the concept of agent config, most likely.
103103 if !cfg.IsConfigured() {
@@ -327,9 +327,6 @@ func (app *App) InitCoderAgent(ctx context.Context) error {
327327 slog.Error("Failed to create coder agent", "err", err)
328328 return err
329329 }
330−
331− // Add MCP client cleanup to shutdown process
332− app.cleanupFuncs = append(app.cleanupFuncs, mcp.Close)
333330 return nil
334331}
335332
internal/tui/components/dialogs/commands/loader.go+1−1
@@ -223,7 +223,7 @@ type CommandRunCustomMsg struct {
223223
224224func loadMCPPrompts() []Command {
225225 var commands []Command
226− for key, prompt := range mcp.GetPrompts() {
226+ for key, prompt := range mcp.Prompts() {
227227 clientName, promptName, ok := strings.Cut(key, ":")
228228 if !ok {
229229 slog.Warn("prompt not found", "key", key)
internal/tui/tui.go+23
@@ -10,6 +10,7 @@ import (
1010
1111 "github.com/charmbracelet/bubbles/v2/key"
1212 tea "github.com/charmbracelet/bubbletea/v2"
13+ "github.com/charmbracelet/crush/internal/agent/tools/mcp"
1314 "github.com/charmbracelet/crush/internal/app"
1415 "github.com/charmbracelet/crush/internal/config"
1516 "github.com/charmbracelet/crush/internal/event"
@@ -140,6 +141,14 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
140141 a.completions.Update(msg)
141142 return a, a.handleWindowResize(msg.Width, msg.Height)
142143
144+ case pubsub.Event[mcp.Event]:
145+ switch msg.Payload.Type {
146+ case mcp.EventPromptsListChanged:
147+
148+ case mcp.EventToolsListChanged:
149+ return a, a.handleMCPToolsEvent(context.Background(), msg.Payload.Name)
150+ }
151+
143152 // Completions messages
144153 case completions.OpenCompletionsMsg, completions.FilterCompletionsMsg,
145154 completions.CloseCompletionsMsg, completions.RepositionCompletionsMsg:
@@ -618,6 +627,20 @@ func (a *appModel) View() tea.View {
618627 return view
619628}
620629
630+func (a *appModel) handleMCPPromptsEvent(ctx context.Context, name string) tea.Cmd {
631+ return func() tea.Msg {
632+ mcp.RefreshPrompts(ctx, name)
633+ return nil
634+ }
635+}
636+
637+func (a *appModel) handleMCPToolsEvent(ctx context.Context, name string) tea.Cmd {
638+ return func() tea.Msg {
639+ mcp.RefreshTools(ctx, name)
640+ return nil
641+ }
642+}
643+
621644// New creates and initializes a new TUI application model.
622645func New(app *app.App) *appModel {
623646 chatPage := chat.New(app)