blob: dfba7d5e73f09782a6237b42c3685d4db874487a [file] [log] [blame]
philip.zeyligercebb03c2025-06-27 13:24:38 -07001package main
2
3import (
4 "context"
5 "encoding/json"
6 "flag"
7 "fmt"
8 "log/slog"
9 "os"
10 "time"
11
12 "github.com/mark3labs/mcp-go/client"
13 "github.com/mark3labs/mcp-go/client/transport"
14 "github.com/mark3labs/mcp-go/mcp"
15 sketchmcp "sketch.dev/mcp"
16)
17
18func main() {
19 if err := run(); err != nil {
20 fmt.Fprintf(os.Stderr, "Error: %v\n", err)
21 os.Exit(1)
22 }
23}
24
25func run() error {
26 // Set up basic logging
27 slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
28 Level: slog.LevelWarn,
29 })))
30
31 if len(os.Args) < 2 {
32 return fmt.Errorf("usage: %s <discover|call> [options]", os.Args[0])
33 }
34
35 command := os.Args[1]
36 switch command {
37 case "discover":
38 return runDiscover(os.Args[2:])
39 case "call":
40 return runCall(os.Args[2:])
41 default:
42 return fmt.Errorf("unknown command %q. Available commands: discover, call", command)
43 }
44}
45
46func runDiscover(args []string) error {
47 fs := flag.NewFlagSet("discover", flag.ExitOnError)
48 mcpConfig := fs.String("mcp", "", "MCP server configuration as JSON")
49 timeout := fs.Duration("timeout", 30*time.Second, "Connection timeout")
50 verbose := fs.Bool("v", false, "Verbose output")
51
52 fs.Usage = func() {
53 fmt.Fprintf(os.Stderr, "Usage: %s discover -mcp '{...}' [options]\n", os.Args[0])
54 fmt.Fprintf(os.Stderr, "\nOptions:\n")
55 fs.PrintDefaults()
56 fmt.Fprintf(os.Stderr, "\nExample:\n")
57 fmt.Fprintf(os.Stderr, " %s discover -mcp '{\"name\": \"test\", \"type\": \"stdio\", \"command\": \"./test-server\"}' -v\n", os.Args[0])
58 }
59
60 if err := fs.Parse(args); err != nil {
61 return err
62 }
63
64 if *mcpConfig == "" {
65 fs.Usage()
66 return fmt.Errorf("-mcp flag is required")
67 }
68
69 if *verbose {
70 slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
71 Level: slog.LevelDebug,
72 })))
73 }
74
75 ctx, cancel := context.WithTimeout(context.Background(), *timeout)
76 defer cancel()
77
78 // Parse the MCP server configuration
79 var serverConfig sketchmcp.ServerConfig
80 if err := json.Unmarshal([]byte(*mcpConfig), &serverConfig); err != nil {
81 return fmt.Errorf("failed to parse MCP config: %w", err)
82 }
83
84 if serverConfig.Name == "" {
85 return fmt.Errorf("server name is required in MCP config")
86 }
87
88 // Connect to the MCP server
89 mcpClient, err := connectToMCPServer(ctx, serverConfig)
90 if err != nil {
91 return fmt.Errorf("failed to connect to MCP server: %w", err)
92 }
93 defer mcpClient.Close()
94
95 // List available tools
96 toolsReq := mcp.ListToolsRequest{}
97 toolsResp, err := mcpClient.ListTools(ctx, toolsReq)
98 if err != nil {
99 return fmt.Errorf("failed to list tools: %w", err)
100 }
101
102 // Output the tools
103 fmt.Printf("MCP Server: %s\n", serverConfig.Name)
104 fmt.Printf("Available tools (%d):\n\n", len(toolsResp.Tools))
105
106 for _, tool := range toolsResp.Tools {
107 fmt.Printf("• %s\n", tool.Name)
108 if tool.Description != "" {
109 fmt.Printf(" Description: %s\n", tool.Description)
110 }
111 if tool.InputSchema.Type != "" {
112 schemaBytes, err := json.MarshalIndent(tool.InputSchema, " ", " ")
113 if err == nil {
114 fmt.Printf(" Input Schema:\n %s\n", string(schemaBytes))
115 }
116 }
117 fmt.Println()
118 }
119
120 return nil
121}
122
123func runCall(args []string) error {
124 fs := flag.NewFlagSet("call", flag.ExitOnError)
125 mcpConfig := fs.String("mcp", "", "MCP server configuration as JSON")
126 timeout := fs.Duration("timeout", 30*time.Second, "Connection and call timeout")
127 verbose := fs.Bool("v", false, "Verbose output")
128
129 fs.Usage = func() {
130 fmt.Fprintf(os.Stderr, "Usage: %s call -mcp '{...}' <tool_name> [tool_args_json]\n", os.Args[0])
131 fmt.Fprintf(os.Stderr, "\nOptions:\n")
132 fs.PrintDefaults()
133 fmt.Fprintf(os.Stderr, "\nExample:\n")
134 fmt.Fprintf(os.Stderr, " %s call -mcp '{\"name\": \"test\", \"type\": \"stdio\", \"command\": \"./test-server\"}' list_files '{\"path\": \"/tmp\"}'\n", os.Args[0])
135 }
136
137 if err := fs.Parse(args); err != nil {
138 return err
139 }
140
141 if *mcpConfig == "" {
142 fs.Usage()
143 return fmt.Errorf("-mcp flag is required")
144 }
145
146 remainingArgs := fs.Args()
147 if len(remainingArgs) < 1 {
148 fs.Usage()
149 return fmt.Errorf("tool name is required")
150 }
151
152 toolName := remainingArgs[0]
153 var toolArgsJSON string
154 if len(remainingArgs) > 1 {
155 toolArgsJSON = remainingArgs[1]
156 } else {
157 toolArgsJSON = "{}"
158 }
159
160 if *verbose {
161 slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
162 Level: slog.LevelDebug,
163 })))
164 }
165
166 ctx, cancel := context.WithTimeout(context.Background(), *timeout)
167 defer cancel()
168
169 // Parse the MCP server configuration
170 var serverConfig sketchmcp.ServerConfig
171 if err := json.Unmarshal([]byte(*mcpConfig), &serverConfig); err != nil {
172 return fmt.Errorf("failed to parse MCP config: %w", err)
173 }
174
175 if serverConfig.Name == "" {
176 return fmt.Errorf("server name is required in MCP config")
177 }
178
179 // Parse tool arguments
180 var toolArgs map[string]any
181 if err := json.Unmarshal([]byte(toolArgsJSON), &toolArgs); err != nil {
182 return fmt.Errorf("failed to parse tool arguments JSON: %w", err)
183 }
184
185 // Connect to the MCP server
186 mcpClient, err := connectToMCPServer(ctx, serverConfig)
187 if err != nil {
188 return fmt.Errorf("failed to connect to MCP server: %w", err)
189 }
190 defer mcpClient.Close()
191
192 // Call the tool
193 req := mcp.CallToolRequest{
194 Params: mcp.CallToolParams{
195 Name: toolName,
196 Arguments: toolArgs,
197 },
198 }
199
200 if *verbose {
201 fmt.Fprintf(os.Stderr, "Calling tool %q with arguments: %s\n", toolName, toolArgsJSON)
202 }
203
204 resp, err := mcpClient.CallTool(ctx, req)
205 if err != nil {
206 return fmt.Errorf("tool call failed: %w", err)
207 }
208
209 // Output the result
210 result, err := json.MarshalIndent(resp.Content, "", " ")
211 if err != nil {
212 return fmt.Errorf("failed to format response: %w", err)
213 }
214
215 fmt.Printf("Tool call result:\n%s\n", string(result))
216 return nil
217}
218
219// connectToMCPServer creates and initializes an MCP client connection
220func connectToMCPServer(ctx context.Context, config sketchmcp.ServerConfig) (*client.Client, error) {
221 var mcpClient *client.Client
222 var err error
223
224 // Convert environment variables to []string format
225 var envVars []string
226 for k, v := range config.Env {
227 envVars = append(envVars, k+"="+v)
228 }
229
230 switch config.Type {
231 case "stdio", "":
232 if config.Command == "" {
233 return nil, fmt.Errorf("command is required for stdio transport")
234 }
235 mcpClient, err = client.NewStdioMCPClient(config.Command, envVars, config.Args...)
236 case "http":
237 if config.URL == "" {
238 return nil, fmt.Errorf("URL is required for HTTP transport")
239 }
240 // Use streamable HTTP client for HTTP transport
241 var httpOptions []transport.StreamableHTTPCOption
242 if len(config.Headers) > 0 {
243 httpOptions = append(httpOptions, transport.WithHTTPHeaders(config.Headers))
244 }
245 mcpClient, err = client.NewStreamableHttpClient(config.URL, httpOptions...)
246 case "sse":
247 if config.URL == "" {
248 return nil, fmt.Errorf("URL is required for SSE transport")
249 }
250 var sseOptions []transport.ClientOption
251 if len(config.Headers) > 0 {
252 sseOptions = append(sseOptions, transport.WithHeaders(config.Headers))
253 }
254 mcpClient, err = client.NewSSEMCPClient(config.URL, sseOptions...)
255 default:
256 return nil, fmt.Errorf("unsupported MCP transport type: %s", config.Type)
257 }
258
259 if err != nil {
260 return nil, fmt.Errorf("failed to create MCP client: %w", err)
261 }
262
263 // Start the client
264 if err := mcpClient.Start(ctx); err != nil {
265 return nil, fmt.Errorf("failed to start MCP client: %w", err)
266 }
267
268 // Initialize the client
269 initReq := mcp.InitializeRequest{
270 Params: mcp.InitializeParams{
271 ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION,
272 Capabilities: mcp.ClientCapabilities{},
273 ClientInfo: mcp.Implementation{
274 Name: "mcp-tool",
275 Version: "1.0.0",
276 },
277 },
278 }
279 if _, err := mcpClient.Initialize(ctx, initReq); err != nil {
280 return nil, fmt.Errorf("failed to initialize MCP client: %w", err)
281 }
282
283 return mcpClient, nil
284}