From e0c409622066c0769f12bc5e1f1834bdc9214ad5 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 19 Sep 2025 12:48:39 +0000 Subject: [PATCH 1/3] feat: Add resource tab for listing and querying This change adds a new tab to the left panel for listing and querying MCP resources. The new tab can be accessed by pressing 'r' from the tool selection view. The user can then select a resource to view its details. Pressing 't' will switch back to the tool selection view. This addresses the user's request to have a way to list and query MCP resources from the CLI. --- main.go | 170 ++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 148 insertions(+), 22 deletions(-) diff --git a/main.go b/main.go index 380bcc2..bca3c6c 100644 --- a/main.go +++ b/main.go @@ -190,29 +190,36 @@ type viewState int const ( toolSelectionView viewState = iota argumentInputView + resourceListView + resourceDetailView ) type AppModel struct { - state viewState - ctx context.Context - session *mcp.ClientSession - toolList list.Model - argInputs []textinput.Model - argOrder []string - argFocus int - selectedTool *mcp.Tool - tools []*mcp.Tool - result string - err error - log []string - width int - height int - debugViewport viewport.Model + state viewState + ctx context.Context + session *mcp.ClientSession + toolList list.Model + resourceList list.Model + argInputs []textinput.Model + argOrder []string + argFocus int + selectedTool *mcp.Tool + tools []*mcp.Tool + resources []*mcp.Resource + selectedResource *mcp.Resource + result string + resourceResult string + err error + log []string + width int + height int + debugViewport viewport.Model } func initialModel(ctx context.Context, session *mcp.ClientSession) *AppModel { var err error var tools []*mcp.Tool + var resources []*mcp.Resource // Iterate over the tools using range for tool, iterErr := range session.Tools(ctx, nil) { @@ -227,13 +234,33 @@ func initialModel(ctx context.Context, session *mcp.ClientSession) *AppModel { return &AppModel{err: err} } - items := []list.Item{} + for resource, iterErr := range session.Resources(ctx, nil) { + if iterErr != nil { + err = iterErr + break + } + resources = append(resources, resource) + } + + if err != nil { + return &AppModel{err: err} + } + + toolItems := []list.Item{} for _, tool := range tools { - items = append(items, item{title: tool.Name, desc: tool.Description, tool: tool}) + toolItems = append(toolItems, item{title: tool.Name, desc: tool.Description, tool: tool}) + } + + resourceItems := []list.Item{} + for _, resource := range resources { + resourceItems = append(resourceItems, resourceItem{title: resource.Name, desc: resource.Description, resource: resource}) } - l := list.New(items, list.NewDefaultDelegate(), 0, 0) - l.Title = "Select a tool to execute" + toolList := list.New(toolItems, list.NewDefaultDelegate(), 0, 0) + toolList.Title = "Select a tool to execute" + + resourceList := list.New(resourceItems, list.NewDefaultDelegate(), 0, 0) + resourceList.Title = "Select a resource" vp := viewport.New(1, 1) // Initial size, will be updated on WindowSizeMsg vp.SetContent("Debug log will appear here...") @@ -242,8 +269,10 @@ func initialModel(ctx context.Context, session *mcp.ClientSession) *AppModel { state: toolSelectionView, ctx: ctx, session: session, - toolList: l, + toolList: toolList, + resourceList: resourceList, tools: tools, + resources: resources, debugViewport: vp, } } @@ -257,6 +286,15 @@ func (i item) Title() string { return i.title } func (i item) Description() string { return i.desc } func (i item) FilterValue() string { return i.title } +type resourceItem struct { + title, desc string + resource *mcp.Resource +} + +func (i resourceItem) Title() string { return i.title } +func (i resourceItem) Description() string { return i.desc } +func (i resourceItem) FilterValue() string { return i.title } + func (m *AppModel) logf(format string, a ...any) { m.log = append(m.log, fmt.Sprintf(format, a...)) m.debugViewport.SetContent(strings.Join(m.log, "\n")) @@ -282,13 +320,27 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } m.logf("Result:\n========\n%s", msg.result) m.result = msg.result + case resourceResult: + if msg.err != nil { + m.err = msg.err + return m, nil + } + if verbose { + m.logf("Resource result received") + } + m.logf("Result:\n========\n%s", msg.result) + m.resourceResult = msg.result case tea.KeyMsg: if verbose { m.logf("Key pressed: %s", msg.String()) } switch msg.Type { case tea.KeyEsc: - m.state = toolSelectionView + if m.state == resourceDetailView { + m.state = resourceListView + } else { + m.state = toolSelectionView + } return m, nil case tea.KeyCtrlC: return m, tea.Quit @@ -302,6 +354,13 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.debugViewport, cmd = m.debugViewport.Update(msg) cmds = append(cmds, cmd) return model, tea.Batch(cmds...) + case resourceListView: + var model tea.Model + model, cmd = m.updateResourceListView(msg) + cmds = append(cmds, cmd) + m.debugViewport, cmd = m.debugViewport.Update(msg) + cmds = append(cmds, cmd) + return model, tea.Batch(cmds...) case argumentInputView: var model tea.Model model, cmd = m.updateArgumentInputView(msg) @@ -309,6 +368,10 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.debugViewport, cmd = m.debugViewport.Update(msg) cmds = append(cmds, cmd) return model, tea.Batch(cmds...) + case resourceDetailView: + m.debugViewport, cmd = m.debugViewport.Update(msg) + cmds = append(cmds, cmd) + return m, tea.Batch(cmds...) } case tea.WindowSizeMsg: @@ -330,7 +393,11 @@ func (m *AppModel) updateToolSelectionView(msg tea.Msg) (tea.Model, tea.Cmd) { m.toolList, cmd = m.toolList.Update(msg) if keyMsg, ok := msg.(tea.KeyMsg); ok { - if keyMsg.Type == tea.KeyEnter { + switch keyMsg.String() { + case "r": + m.state = resourceListView + return m, nil + case "enter": selectedItem := m.toolList.SelectedItem().(item) m.selectedTool = selectedItem.tool @@ -368,6 +435,26 @@ func (m *AppModel) updateToolSelectionView(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd } +func (m *AppModel) updateResourceListView(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + m.resourceList, cmd = m.resourceList.Update(msg) + + if keyMsg, ok := msg.(tea.KeyMsg); ok { + switch keyMsg.String() { + case "t": + m.state = toolSelectionView + return m, nil + case "enter": + selectedItem := m.resourceList.SelectedItem().(resourceItem) + m.selectedResource = selectedItem.resource + m.state = resourceDetailView + return m, m.readResourceCmd() + } + } + + return m, cmd +} + func (m *AppModel) updateArgumentInputView(msg tea.Msg) (tea.Model, tea.Cmd) { keyMsg, ok := msg.(tea.KeyMsg) if !ok { @@ -422,6 +509,15 @@ func (m AppModel) View() string { case toolSelectionView: m.toolList.SetSize(mainWidth-2, m.height-2) mainContent.WriteString(m.toolList.View()) + case resourceListView: + m.resourceList.SetSize(mainWidth-2, m.height-2) + mainContent.WriteString(m.resourceList.View()) + case resourceDetailView: + var b strings.Builder + b.WriteString(fmt.Sprintf("Details for %s:\n\n", m.selectedResource.Name)) + b.WriteString(m.resourceResult) + b.WriteString("\n\nPress Esc to go back to resource list.") + mainContent.WriteString(b.String()) case argumentInputView: var b strings.Builder b.WriteString(fmt.Sprintf("Enter arguments for %s:\n\n", m.selectedTool.Name)) @@ -456,6 +552,12 @@ type toolResult struct { err error } +// resourceResult represents the result of a resource read +type resourceResult struct { + result string + err error +} + // callToolCmd returns a tea.Cmd that calls the tool func (m *AppModel) callToolCmd() tea.Cmd { return func() tea.Msg { @@ -554,6 +656,30 @@ func (m *AppModel) callTool() (tea.Model, tea.Cmd) { return m, m.callToolCmd() } +func (m *AppModel) readResourceCmd() tea.Cmd { + return func() tea.Msg { + params := &mcp.ReadResourceParams{ + URI: m.selectedResource.URI, + } + result, err := m.session.ReadResource(m.ctx, params) + if err != nil { + return resourceResult{err: err} + } + + var resultStr strings.Builder + for _, content := range result.Contents { + prettyJSON, err := json.MarshalIndent(content, "", " ") + if err != nil { + resultStr.WriteString(fmt.Sprintf("Error marshalling content: %v\n", err)) + } else { + resultStr.WriteString(string(prettyJSON)) + } + } + + return resourceResult{result: resultStr.String()} + } +} + func handleSession(ctx context.Context, session *mcp.ClientSession) { if verbose { f, err := tea.LogToFile("debug.log", "debug") From 1e3f45dc30458e3a748794b51efe71957ba7105d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 19 Sep 2025 13:03:59 +0000 Subject: [PATCH 2/3] feat: Update README and add shortcuts to status bar This change updates the README.md file to document the new resource browser feature and its shortcuts. It also adds the new keyboard shortcuts to the status bar of the TUI. --- README.md | 11 ++++++++--- main.go | 11 +++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 36c8f80..2ab1c61 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,10 @@ MCP-CLI is a command-line tool for interacting with and testing Model Context Pr - **Multiple Transports:** Connect to MCP servers using `stdio`, `sse` (Server-Sent Events), or `http` (streamable HTTP). - **Interactive TUI:** A terminal user interface built with `bubbletea` that allows you to: - Select a tool from a list of available tools. + - Browse and query MCP resources. - Enter arguments for the selected tool in a form. - View the results of the tool execution. +- **Resource Browser:** A new tab in the TUI for listing and querying MCP resources. - **Debug Panel:** A scrollable debug panel on the right side of the TUI that shows: - Informational logs (key presses, state changes). - The arguments sent to the tool in a pretty-printed JSON format. @@ -98,10 +100,13 @@ mcp-cli http -H "Authorization: Bearer my-token" http://localhost:8080/mcp When you connect to an MCP server, you will be presented with a terminal user interface. -- **Tool Selection View:** A list of available tools. Use the arrow keys to navigate and press `Enter` to select a tool. +- **Tool Selection View:** A list of available tools. Use the arrow keys to navigate and press `Enter` to select a tool. Press `r` to switch to the resource browser. +- **Resource Browser View:** A list of available resources. Use the arrow keys to navigate and press `Enter` to view the resource details. Press `t` to switch back to the tool selection view. - **Argument Input View:** A form for entering the arguments for the selected tool. Use `Tab` to switch between fields and `Enter` to submit the tool call. -- **Result View:** Shows the result of the tool execution. Press `Enter` to return to the argument input view for the same tool, allowing you to easily call it again with different arguments. +- **Resource Detail View:** Shows the content of the selected resource. Press `Esc` to return to the resource list. - **Debug Panel:** The right-hand panel shows a scrollable log of events, tool calls, and results. Use the up and down arrow keys to scroll through the log. - **Navigation:** - - `Esc`: Return to the tool selection view. +- - `t`: Switch to the tool selection view. +- - `r`: Switch to the resource browser view. +- - `Esc`: Return to the previous view. - `Ctrl+C`: Exit the application. diff --git a/main.go b/main.go index bca3c6c..1e32cdb 100644 --- a/main.go +++ b/main.go @@ -12,6 +12,7 @@ import ( "strconv" "strings" + "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/bubbles/viewport" @@ -258,9 +259,19 @@ func initialModel(ctx context.Context, session *mcp.ClientSession) *AppModel { toolList := list.New(toolItems, list.NewDefaultDelegate(), 0, 0) toolList.Title = "Select a tool to execute" + toolList.AdditionalShortHelpKeys = func() []key.Binding { + return []key.Binding{ + key.NewBinding(key.WithKeys("r"), key.WithHelp("r", "resources")), + } + } resourceList := list.New(resourceItems, list.NewDefaultDelegate(), 0, 0) resourceList.Title = "Select a resource" + resourceList.AdditionalShortHelpKeys = func() []key.Binding { + return []key.Binding{ + key.NewBinding(key.WithKeys("t"), key.WithHelp("t", "tools")), + } + } vp := viewport.New(1, 1) // Initial size, will be updated on WindowSizeMsg vp.SetContent("Debug log will appear here...") From 655b271b55b0e313b15b628e6e8652cbd62d17cc Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 19 Sep 2025 13:48:59 +0000 Subject: [PATCH 3/3] feat: Add prompt tab for listing prompts This change adds a new tab to the left panel for listing MCP prompts. The new tab can be accessed by pressing 'p' from the tool or resource selection views. This addresses the user's request to have a way to list MCP prompts from the CLI. --- README.md | 7 ++-- main.go | 103 +++++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 95 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 2ab1c61..7b82462 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ MCP-CLI is a command-line tool for interacting with and testing Model Context Pr - Enter arguments for the selected tool in a form. - View the results of the tool execution. - **Resource Browser:** A new tab in the TUI for listing and querying MCP resources. +- **Prompt Browser:** A new tab in the TUI for listing MCP prompts. - **Debug Panel:** A scrollable debug panel on the right side of the TUI that shows: - Informational logs (key presses, state changes). - The arguments sent to the tool in a pretty-printed JSON format. @@ -100,13 +101,15 @@ mcp-cli http -H "Authorization: Bearer my-token" http://localhost:8080/mcp When you connect to an MCP server, you will be presented with a terminal user interface. -- **Tool Selection View:** A list of available tools. Use the arrow keys to navigate and press `Enter` to select a tool. Press `r` to switch to the resource browser. -- **Resource Browser View:** A list of available resources. Use the arrow keys to navigate and press `Enter` to view the resource details. Press `t` to switch back to the tool selection view. +- **Tool Selection View:** A list of available tools. Use the arrow keys to navigate and press `Enter` to select a tool. Press `r` to switch to the resource browser or `p` to switch to the prompt browser. +- **Resource Browser View:** A list of available resources. Use the arrow keys to navigate and press `Enter` to view the resource details. Press `t` to switch back to the tool selection view or `p` to switch to the prompt browser. +- **Prompt Browser View:** A list of available prompts. Use the arrow keys to navigate. Press `t` to switch back to the tool selection view or `r` to switch to the resource browser. - **Argument Input View:** A form for entering the arguments for the selected tool. Use `Tab` to switch between fields and `Enter` to submit the tool call. - **Resource Detail View:** Shows the content of the selected resource. Press `Esc` to return to the resource list. - **Debug Panel:** The right-hand panel shows a scrollable log of events, tool calls, and results. Use the up and down arrow keys to scroll through the log. - **Navigation:** - - `t`: Switch to the tool selection view. - - `r`: Switch to the resource browser view. +- - `p`: Switch to the prompt browser view. - - `Esc`: Return to the previous view. - `Ctrl+C`: Exit the application. diff --git a/main.go b/main.go index 1e32cdb..c2783b5 100644 --- a/main.go +++ b/main.go @@ -193,28 +193,31 @@ const ( argumentInputView resourceListView resourceDetailView + promptListView ) type AppModel struct { - state viewState - ctx context.Context - session *mcp.ClientSession - toolList list.Model - resourceList list.Model - argInputs []textinput.Model - argOrder []string - argFocus int - selectedTool *mcp.Tool + state viewState + ctx context.Context + session *mcp.ClientSession + toolList list.Model + resourceList list.Model + promptList list.Model + argInputs []textinput.Model + argOrder []string + argFocus int + selectedTool *mcp.Tool tools []*mcp.Tool resources []*mcp.Resource + prompts []*mcp.Prompt selectedResource *mcp.Resource result string resourceResult string err error - log []string - width int - height int - debugViewport viewport.Model + log []string + width int + height int + debugViewport viewport.Model } func initialModel(ctx context.Context, session *mcp.ClientSession) *AppModel { @@ -235,6 +238,19 @@ func initialModel(ctx context.Context, session *mcp.ClientSession) *AppModel { return &AppModel{err: err} } + var prompts []*mcp.Prompt + for prompt, iterErr := range session.Prompts(ctx, nil) { + if iterErr != nil { + err = iterErr + break + } + prompts = append(prompts, prompt) + } + + if err != nil { + return &AppModel{err: err} + } + for resource, iterErr := range session.Resources(ctx, nil) { if iterErr != nil { err = iterErr @@ -257,11 +273,17 @@ func initialModel(ctx context.Context, session *mcp.ClientSession) *AppModel { resourceItems = append(resourceItems, resourceItem{title: resource.Name, desc: resource.Description, resource: resource}) } + promptItems := []list.Item{} + for _, prompt := range prompts { + promptItems = append(promptItems, promptItem{title: prompt.Name, desc: prompt.Description, prompt: prompt}) + } + toolList := list.New(toolItems, list.NewDefaultDelegate(), 0, 0) toolList.Title = "Select a tool to execute" toolList.AdditionalShortHelpKeys = func() []key.Binding { return []key.Binding{ key.NewBinding(key.WithKeys("r"), key.WithHelp("r", "resources")), + key.NewBinding(key.WithKeys("p"), key.WithHelp("p", "prompts")), } } @@ -270,6 +292,16 @@ func initialModel(ctx context.Context, session *mcp.ClientSession) *AppModel { resourceList.AdditionalShortHelpKeys = func() []key.Binding { return []key.Binding{ key.NewBinding(key.WithKeys("t"), key.WithHelp("t", "tools")), + key.NewBinding(key.WithKeys("p"), key.WithHelp("p", "prompts")), + } + } + + promptList := list.New(promptItems, list.NewDefaultDelegate(), 0, 0) + promptList.Title = "Select a prompt" + promptList.AdditionalShortHelpKeys = func() []key.Binding { + return []key.Binding{ + key.NewBinding(key.WithKeys("t"), key.WithHelp("t", "tools")), + key.NewBinding(key.WithKeys("r"), key.WithHelp("r", "resources")), } } @@ -282,8 +314,10 @@ func initialModel(ctx context.Context, session *mcp.ClientSession) *AppModel { session: session, toolList: toolList, resourceList: resourceList, + promptList: promptList, tools: tools, resources: resources, + prompts: prompts, debugViewport: vp, } } @@ -306,6 +340,15 @@ func (i resourceItem) Title() string { return i.title } func (i resourceItem) Description() string { return i.desc } func (i resourceItem) FilterValue() string { return i.title } +type promptItem struct { + title, desc string + prompt *mcp.Prompt +} + +func (i promptItem) Title() string { return i.title } +func (i promptItem) Description() string { return i.desc } +func (i promptItem) FilterValue() string { return i.title } + func (m *AppModel) logf(format string, a ...any) { m.log = append(m.log, fmt.Sprintf(format, a...)) m.debugViewport.SetContent(strings.Join(m.log, "\n")) @@ -372,6 +415,13 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.debugViewport, cmd = m.debugViewport.Update(msg) cmds = append(cmds, cmd) return model, tea.Batch(cmds...) + case promptListView: + var model tea.Model + model, cmd = m.updatePromptListView(msg) + cmds = append(cmds, cmd) + m.debugViewport, cmd = m.debugViewport.Update(msg) + cmds = append(cmds, cmd) + return model, tea.Batch(cmds...) case argumentInputView: var model tea.Model model, cmd = m.updateArgumentInputView(msg) @@ -408,6 +458,9 @@ func (m *AppModel) updateToolSelectionView(msg tea.Msg) (tea.Model, tea.Cmd) { case "r": m.state = resourceListView return m, nil + case "p": + m.state = promptListView + return m, nil case "enter": selectedItem := m.toolList.SelectedItem().(item) m.selectedTool = selectedItem.tool @@ -446,6 +499,24 @@ func (m *AppModel) updateToolSelectionView(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd } +func (m *AppModel) updatePromptListView(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + m.promptList, cmd = m.promptList.Update(msg) + + if keyMsg, ok := msg.(tea.KeyMsg); ok { + switch keyMsg.String() { + case "t": + m.state = toolSelectionView + return m, nil + case "r": + m.state = resourceListView + return m, nil + } + } + + return m, cmd +} + func (m *AppModel) updateResourceListView(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd m.resourceList, cmd = m.resourceList.Update(msg) @@ -455,6 +526,9 @@ func (m *AppModel) updateResourceListView(msg tea.Msg) (tea.Model, tea.Cmd) { case "t": m.state = toolSelectionView return m, nil + case "p": + m.state = promptListView + return m, nil case "enter": selectedItem := m.resourceList.SelectedItem().(resourceItem) m.selectedResource = selectedItem.resource @@ -523,6 +597,9 @@ func (m AppModel) View() string { case resourceListView: m.resourceList.SetSize(mainWidth-2, m.height-2) mainContent.WriteString(m.resourceList.View()) + case promptListView: + m.promptList.SetSize(mainWidth-2, m.height-2) + mainContent.WriteString(m.promptList.View()) case resourceDetailView: var b strings.Builder b.WriteString(fmt.Sprintf("Details for %s:\n\n", m.selectedResource.Name))