Skip to content

Commit 3b13236

Browse files
Copilotdata-douser
andauthored
feat: add use subcommands and shared primitives library for MCP client
- Add client/internal/mcp/primitives.go with shared CallTool, ReadResource, GetPrompt, ListTools, ListResources, ListPrompts functions and formatters - Add client/internal/mcp/primitives_test.go with comprehensive unit tests - Add GetPrompt and ReadResource to client/internal/mcp/client.go - Add client/cmd/use.go parent subcommand - Add client/cmd/use_tool.go, use_resource.go, use_prompt.go subcommands - Add client/cmd/use_test.go with CLI tests - Refactor client/cmd/integration_tests.go to use shared primitives - Refactor client/internal/testing/runner.go to use shared ContentBlock type - Support --format json|text|markdown across all subcommands Agent-Logs-Url: https://github.com/advanced-security/codeql-development-mcp-server/sessions/78e52dbe-a4f9-4a7a-b042-c14ad89421b1 Co-authored-by: data-douser <70299490+data-douser@users.noreply.github.com>
1 parent 3f521f5 commit 3b13236

13 files changed

Lines changed: 1160 additions & 40 deletions

File tree

client/cmd/integration_tests.go

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010

1111
mcpclient "github.com/advanced-security/codeql-development-mcp-server/client/internal/mcp"
1212
itesting "github.com/advanced-security/codeql-development-mcp-server/client/internal/testing"
13-
"github.com/mark3labs/mcp-go/mcp"
1413
"github.com/spf13/cobra"
1514
)
1615

@@ -47,38 +46,29 @@ type mcpToolCaller struct {
4746
timeout time.Duration
4847
}
4948

50-
func (c *mcpToolCaller) CallToolRaw(name string, params map[string]any) ([]itesting.ContentBlock, bool, error) {
49+
func (c *mcpToolCaller) CallToolRaw(name string, params map[string]any) ([]mcpclient.ContentBlock, bool, error) {
5150
ctx := context.Background()
5251
if c.timeout > 0 {
5352
var cancel context.CancelFunc
5453
ctx, cancel = context.WithTimeout(ctx, c.timeout)
5554
defer cancel()
5655
}
57-
result, err := c.client.CallTool(ctx, name, params)
56+
57+
result, err := mcpclient.CallTool(ctx, c.client, name, params)
5858
if err != nil {
5959
return nil, false, err
6060
}
6161

62-
var blocks []itesting.ContentBlock
63-
for _, item := range result.Content {
64-
if textContent, ok := item.(mcp.TextContent); ok {
65-
blocks = append(blocks, itesting.ContentBlock{
66-
Type: "text",
67-
Text: textContent.Text,
68-
})
69-
}
70-
}
71-
72-
return blocks, result.IsError, nil
62+
return result.Content, result.IsError, nil
7363
}
7464

7565
func (c *mcpToolCaller) ListToolNames() ([]string, error) {
76-
tools, err := c.client.ListTools(context.Background())
66+
infos, err := mcpclient.ListTools(context.Background(), c.client)
7767
if err != nil {
7868
return nil, err
7969
}
80-
names := make([]string, len(tools))
81-
for i, t := range tools {
70+
names := make([]string, len(infos))
71+
for i, t := range infos {
8272
names[i] = t.Name
8373
}
8474
return names, nil

client/cmd/root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func init() {
5151
rootCmd.PersistentFlags().StringVar(&mcpMode, "mode", "stdio", "MCP server transport mode (stdio or http)")
5252
rootCmd.PersistentFlags().StringVar(&mcpHost, "host", "localhost", "MCP server host (http mode)")
5353
rootCmd.PersistentFlags().IntVar(&mcpPort, "port", 3000, "MCP server port (http mode)")
54-
rootCmd.PersistentFlags().StringVar(&outputFmt, "format", "text", "Output format (text or json)")
54+
rootCmd.PersistentFlags().StringVar(&outputFmt, "format", "text", "Output format (text, json, or markdown)")
5555
}
5656

5757
// MCPMode returns the configured MCP transport mode.

client/cmd/use.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/spf13/cobra"
7+
)
8+
9+
var useCmd = &cobra.Command{
10+
Use: "use",
11+
Short: "Call an individual MCP server primitive (tool, resource, or prompt)",
12+
Long: `Connect to the MCP server and call a single primitive.
13+
14+
Subcommands:
15+
tool Call a tool by name with key-value arguments
16+
resource Read a resource by URI
17+
prompt Get a prompt by name with key-value arguments`,
18+
}
19+
20+
// parseArgs converts a list of "key=value" strings into a map.
21+
func parseArgs(args []string) (map[string]string, error) {
22+
result := make(map[string]string, len(args))
23+
for _, a := range args {
24+
key, value, found := cutString(a, "=")
25+
if !found || key == "" {
26+
return nil, fmt.Errorf("invalid argument %q: expected key=value format", a)
27+
}
28+
result[key] = value
29+
}
30+
return result, nil
31+
}
32+
33+
// parseArgsAny converts a list of "key=value" strings into a map[string]any.
34+
func parseArgsAny(args []string) (map[string]any, error) {
35+
result := make(map[string]any, len(args))
36+
for _, a := range args {
37+
key, value, found := cutString(a, "=")
38+
if !found || key == "" {
39+
return nil, fmt.Errorf("invalid argument %q: expected key=value format", a)
40+
}
41+
result[key] = value
42+
}
43+
return result, nil
44+
}
45+
46+
// cutString splits s around the first instance of sep.
47+
func cutString(s, sep string) (before, after string, found bool) {
48+
for i := 0; i+len(sep) <= len(s); i++ {
49+
if s[i:i+len(sep)] == sep {
50+
return s[:i], s[i+len(sep):], true
51+
}
52+
}
53+
return s, "", false
54+
}
55+
56+
func init() {
57+
rootCmd.AddCommand(useCmd)
58+
}

client/cmd/use_prompt.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
8+
mcpclient "github.com/advanced-security/codeql-development-mcp-server/client/internal/mcp"
9+
"github.com/spf13/cobra"
10+
)
11+
12+
var usePromptArgs []string
13+
14+
var usePromptCmd = &cobra.Command{
15+
Use: "prompt <name>",
16+
Short: "Get an MCP server prompt by name",
17+
Long: `Get a specific MCP server prompt with key-value arguments and print the resulting messages.
18+
19+
Example:
20+
gh-ql-mcp-client use prompt explain_codeql_query --arg queryPath=/path/to/query.ql --arg language=javascript
21+
gh-ql-mcp-client use prompt explain_codeql_query --arg queryPath=/path/to/query.ql --format json`,
22+
Args: cobra.ExactArgs(1),
23+
RunE: runUsePrompt,
24+
}
25+
26+
func init() {
27+
useCmd.AddCommand(usePromptCmd)
28+
usePromptCmd.Flags().StringArrayVar(&usePromptArgs, "arg", nil, "Prompt argument in key=value format (repeatable)")
29+
}
30+
31+
func runUsePrompt(_ *cobra.Command, args []string) error {
32+
promptName := args[0]
33+
34+
params, err := parseArgs(usePromptArgs)
35+
if err != nil {
36+
return fmt.Errorf("parse prompt arguments: %w", err)
37+
}
38+
39+
ctx := context.Background()
40+
client, err := connectMCPClient(ctx)
41+
if err != nil {
42+
return err
43+
}
44+
defer client.Close()
45+
46+
result, err := mcpclient.GetPrompt(ctx, client, promptName, params)
47+
if err != nil {
48+
return err
49+
}
50+
51+
return outputPromptMessages(result)
52+
}
53+
54+
func outputPromptMessages(result *mcpclient.PromptMessages) error {
55+
switch OutputFormat() {
56+
case "json":
57+
s, err := mcpclient.FormatJSON(result)
58+
if err != nil {
59+
return err
60+
}
61+
fmt.Fprintln(os.Stdout, s)
62+
case "markdown":
63+
fmt.Fprint(os.Stdout, mcpclient.FormatPromptMessagesMarkdown(result))
64+
default:
65+
fmt.Fprint(os.Stdout, mcpclient.FormatPromptMessagesText(result))
66+
}
67+
return nil
68+
}

client/cmd/use_resource.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
8+
mcpclient "github.com/advanced-security/codeql-development-mcp-server/client/internal/mcp"
9+
"github.com/spf13/cobra"
10+
)
11+
12+
var useResourceCmd = &cobra.Command{
13+
Use: "resource <uri>",
14+
Short: "Read an MCP server resource by URI",
15+
Long: `Read a specific MCP server resource and print its content.
16+
17+
Example:
18+
gh-ql-mcp-client use resource codeql://server/tools
19+
gh-ql-mcp-client use resource codeql://server/overview --format markdown`,
20+
Args: cobra.ExactArgs(1),
21+
RunE: runUseResource,
22+
}
23+
24+
func init() {
25+
useCmd.AddCommand(useResourceCmd)
26+
}
27+
28+
func runUseResource(_ *cobra.Command, args []string) error {
29+
uri := args[0]
30+
31+
ctx := context.Background()
32+
client, err := connectMCPClient(ctx)
33+
if err != nil {
34+
return err
35+
}
36+
defer client.Close()
37+
38+
result, err := mcpclient.ReadResource(ctx, client, uri)
39+
if err != nil {
40+
return err
41+
}
42+
43+
return outputResourceContent(result)
44+
}
45+
46+
func outputResourceContent(result *mcpclient.ResourceContent) error {
47+
switch OutputFormat() {
48+
case "json":
49+
s, err := mcpclient.FormatJSON(result)
50+
if err != nil {
51+
return err
52+
}
53+
fmt.Fprintln(os.Stdout, s)
54+
case "markdown":
55+
fmt.Fprint(os.Stdout, mcpclient.FormatResourceContentMarkdown(result))
56+
default:
57+
fmt.Fprint(os.Stdout, mcpclient.FormatResourceContentText(result))
58+
}
59+
return nil
60+
}

0 commit comments

Comments
 (0)