Skip to content

Commit 8a53b1c

Browse files
authored
feat: add mcp command for JSON-RPC tools-list and tools-call (#3)
Wires up the existing MCP JSON-RPC client as CLI commands: pipeboard mcp tools-list --server meta-ads-mcp pipeboard mcp tools-call --server meta-ads-mcp --tool <name> --args '{...}' Supports --filter and --verbose flags for tools-list.
1 parent cffab04 commit 8a53b1c

3 files changed

Lines changed: 190 additions & 0 deletions

File tree

‎cmd/mcp.go‎

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package cmd
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
"strings"
8+
9+
"github.com/pipeboard-co/pipeboard-cli/internal/client"
10+
"github.com/spf13/cobra"
11+
)
12+
13+
var mcpCmd = &cobra.Command{
14+
Use: "mcp",
15+
Short: "Interact with Pipeboard MCP servers via JSON-RPC",
16+
}
17+
18+
var (
19+
mcpServer string
20+
)
21+
22+
var mcpToolsListCmd = &cobra.Command{
23+
Use: "tools-list",
24+
Short: "List available tools from an MCP server",
25+
RunE: runMCPToolsList,
26+
}
27+
28+
var (
29+
mcpToolsListFilter string
30+
mcpToolsListVerbose bool
31+
)
32+
33+
var mcpToolsCallCmd = &cobra.Command{
34+
Use: "tools-call",
35+
Short: "Call a tool on an MCP server",
36+
RunE: runMCPToolsCall,
37+
}
38+
39+
var (
40+
mcpToolName string
41+
mcpToolArgs string
42+
)
43+
44+
func init() {
45+
mcpCmd.PersistentFlags().StringVar(&mcpServer, "server", "", "MCP server path, e.g. meta-ads-mcp (required)")
46+
mcpCmd.MarkPersistentFlagRequired("server")
47+
48+
mcpToolsCallCmd.Flags().StringVar(&mcpToolName, "tool", "", "Tool name to call (required)")
49+
mcpToolsCallCmd.Flags().StringVar(&mcpToolArgs, "args", "{}", "JSON object of tool arguments")
50+
mcpToolsCallCmd.MarkFlagRequired("tool")
51+
52+
mcpToolsListCmd.Flags().StringVar(&mcpToolsListFilter, "filter", "", "Filter tools by name substring")
53+
mcpToolsListCmd.Flags().BoolVar(&mcpToolsListVerbose, "verbose", false, "Show full tool descriptions and schemas")
54+
55+
mcpCmd.AddCommand(mcpToolsListCmd)
56+
mcpCmd.AddCommand(mcpToolsCallCmd)
57+
}
58+
59+
func newMCPClient() (*client.Client, error) {
60+
token, err := getToken()
61+
if err != nil {
62+
return nil, err
63+
}
64+
return client.New(apiURL, token), nil
65+
}
66+
67+
func runMCPToolsList(cmd *cobra.Command, args []string) error {
68+
c, err := newMCPClient()
69+
if err != nil {
70+
return err
71+
}
72+
73+
fmt.Fprintf(os.Stderr, "Connecting to %s/%s...\n", apiURL, mcpServer)
74+
75+
if err := c.Initialize(mcpServer); err != nil {
76+
return fmt.Errorf("handshake failed: %w", err)
77+
}
78+
79+
result, err := c.ListTools(mcpServer)
80+
if err != nil {
81+
return fmt.Errorf("listing tools: %w", err)
82+
}
83+
84+
// Filter if requested
85+
tools := result.Tools
86+
if mcpToolsListFilter != "" {
87+
var filtered []client.ToolDefinition
88+
for _, t := range tools {
89+
if strings.Contains(strings.ToLower(t.Name), strings.ToLower(mcpToolsListFilter)) {
90+
filtered = append(filtered, t)
91+
}
92+
}
93+
tools = filtered
94+
}
95+
96+
fmt.Fprintf(os.Stderr, "Found %d tools (showing %d)\n\n", len(result.Tools), len(tools))
97+
98+
if mcpToolsListVerbose {
99+
for _, tool := range tools {
100+
fmt.Printf("=== %s ===\n", tool.Name)
101+
fmt.Printf("Description:\n%s\n", tool.Description)
102+
if len(tool.InputSchema) > 0 {
103+
var schema interface{}
104+
if err := json.Unmarshal(tool.InputSchema, &schema); err == nil {
105+
schemaJSON, _ := json.MarshalIndent(schema, "", " ")
106+
fmt.Printf("Schema:\n%s\n", string(schemaJSON))
107+
}
108+
}
109+
fmt.Println()
110+
}
111+
} else {
112+
for _, tool := range tools {
113+
fmt.Printf("%-40s %s\n", tool.Name, truncate(tool.Description, 80))
114+
}
115+
}
116+
117+
return nil
118+
}
119+
120+
func runMCPToolsCall(cmd *cobra.Command, args []string) error {
121+
c, err := newMCPClient()
122+
if err != nil {
123+
return err
124+
}
125+
126+
fmt.Fprintf(os.Stderr, "Connecting to %s/%s...\n", apiURL, mcpServer)
127+
128+
if err := c.Initialize(mcpServer); err != nil {
129+
return fmt.Errorf("handshake failed: %w", err)
130+
}
131+
132+
var toolArgs map[string]interface{}
133+
if err := json.Unmarshal([]byte(mcpToolArgs), &toolArgs); err != nil {
134+
return fmt.Errorf("invalid JSON in --args: %w", err)
135+
}
136+
137+
fmt.Fprintf(os.Stderr, "Calling %s...\n", mcpToolName)
138+
139+
result, err := c.CallTool(mcpServer, mcpToolName, toolArgs)
140+
if err != nil {
141+
return fmt.Errorf("tool call failed: %w", err)
142+
}
143+
144+
// Pretty-print the result
145+
var parsed interface{}
146+
if err := json.Unmarshal(result, &parsed); err != nil {
147+
// Not JSON, print raw
148+
fmt.Println(string(result))
149+
return nil
150+
}
151+
152+
enc := json.NewEncoder(os.Stdout)
153+
enc.SetIndent("", " ")
154+
return enc.Encode(parsed)
155+
}
156+
157+
func truncate(s string, max int) string {
158+
s = strings.ReplaceAll(s, "\n", " ")
159+
if len(s) <= max {
160+
return s
161+
}
162+
return s[:max-3] + "..."
163+
}

‎cmd/mcp_test.go‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package cmd
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestTruncate(t *testing.T) {
8+
tests := []struct {
9+
input string
10+
max int
11+
expected string
12+
}{
13+
{"short", 10, "short"},
14+
{"exactly10!", 10, "exactly10!"},
15+
{"this is a longer string", 10, "this is..."},
16+
{"has\nnewlines\nin it", 80, "has newlines in it"},
17+
{"", 5, ""},
18+
}
19+
20+
for _, tt := range tests {
21+
got := truncate(tt.input, tt.max)
22+
if got != tt.expected {
23+
t.Errorf("truncate(%q, %d) = %q, want %q", tt.input, tt.max, got, tt.expected)
24+
}
25+
}
26+
}

‎cmd/root.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ func init() {
3939
rootCmd.AddCommand(loginCmd)
4040
rootCmd.AddCommand(configCmd)
4141
rootCmd.AddCommand(debugCmd)
42+
rootCmd.AddCommand(mcpCmd)
4243
}
4344

4445
func getDefaultAPIURL() string {

0 commit comments

Comments
 (0)