Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/setup-codeql-environment/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ inputs:
go-version:
description: 'Go version to install'
required: false
default: '1.21'
default: '1.25'
dotnet-version:
description: '.NET version to install'
required: false
Expand Down
24 changes: 7 additions & 17 deletions client/cmd/integration_tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (

mcpclient "github.com/advanced-security/codeql-development-mcp-server/client/internal/mcp"
itesting "github.com/advanced-security/codeql-development-mcp-server/client/internal/testing"
"github.com/mark3labs/mcp-go/mcp"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -47,38 +46,29 @@ type mcpToolCaller struct {
timeout time.Duration
}

func (c *mcpToolCaller) CallToolRaw(name string, params map[string]any) ([]itesting.ContentBlock, bool, error) {
func (c *mcpToolCaller) CallToolRaw(name string, params map[string]any) ([]mcpclient.ContentBlock, bool, error) {
ctx := context.Background()
if c.timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, c.timeout)
defer cancel()
}
result, err := c.client.CallTool(ctx, name, params)

result, err := mcpclient.CallTool(ctx, c.client, name, params)
if err != nil {
return nil, false, err
}

var blocks []itesting.ContentBlock
for _, item := range result.Content {
if textContent, ok := item.(mcp.TextContent); ok {
blocks = append(blocks, itesting.ContentBlock{
Type: "text",
Text: textContent.Text,
})
}
}

return blocks, result.IsError, nil
return result.Content, result.IsError, nil
}

func (c *mcpToolCaller) ListToolNames() ([]string, error) {
tools, err := c.client.ListTools(context.Background())
infos, err := mcpclient.ListTools(context.Background(), c.client)
if err != nil {
return nil, err
}
names := make([]string, len(tools))
for i, t := range tools {
names := make([]string, len(infos))
for i, t := range infos {
names[i] = t.Name
}
return names, nil
Expand Down
2 changes: 1 addition & 1 deletion client/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func init() {
rootCmd.PersistentFlags().StringVar(&mcpMode, "mode", "stdio", "MCP server transport mode (stdio or http)")
rootCmd.PersistentFlags().StringVar(&mcpHost, "host", "localhost", "MCP server host (http mode)")
rootCmd.PersistentFlags().IntVar(&mcpPort, "port", 3000, "MCP server port (http mode)")
rootCmd.PersistentFlags().StringVar(&outputFmt, "format", "text", "Output format (text or json)")
rootCmd.PersistentFlags().StringVar(&outputFmt, "format", "text", "Output format (text, json, or markdown)")
}

// MCPMode returns the configured MCP transport mode.
Expand Down
58 changes: 58 additions & 0 deletions client/cmd/use.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package cmd

import (
"fmt"

"github.com/spf13/cobra"
)

var useCmd = &cobra.Command{
Use: "use",
Short: "Call an individual MCP server primitive (tool, resource, or prompt)",
Long: `Connect to the MCP server and call a single primitive.

Subcommands:
tool Call a tool by name with key-value arguments
resource Read a resource by URI
prompt Get a prompt by name with key-value arguments`,
}

// parseArgs converts a list of "key=value" strings into a map.
func parseArgs(args []string) (map[string]string, error) {
result := make(map[string]string, len(args))
for _, a := range args {
key, value, found := cutString(a, "=")
if !found || key == "" {
return nil, fmt.Errorf("invalid argument %q: expected key=value format", a)
}
result[key] = value
}
return result, nil
}

// parseArgsAny converts a list of "key=value" strings into a map[string]any.
func parseArgsAny(args []string) (map[string]any, error) {
result := make(map[string]any, len(args))
for _, a := range args {
key, value, found := cutString(a, "=")
if !found || key == "" {
return nil, fmt.Errorf("invalid argument %q: expected key=value format", a)
}
result[key] = value
}
return result, nil
}

// cutString splits s around the first instance of sep.
func cutString(s, sep string) (before, after string, found bool) {
for i := 0; i+len(sep) <= len(s); i++ {
if s[i:i+len(sep)] == sep {
return s[:i], s[i+len(sep):], true
}
}
return s, "", false
}

func init() {
rootCmd.AddCommand(useCmd)
}
68 changes: 68 additions & 0 deletions client/cmd/use_prompt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package cmd

import (
"context"
"fmt"
"os"

mcpclient "github.com/advanced-security/codeql-development-mcp-server/client/internal/mcp"
"github.com/spf13/cobra"
)

var usePromptArgs []string

var usePromptCmd = &cobra.Command{
Use: "prompt <name>",
Short: "Get an MCP server prompt by name",
Long: `Get a specific MCP server prompt with key-value arguments and print the resulting messages.

Example:
gh-ql-mcp-client use prompt explain_codeql_query --arg queryPath=/path/to/query.ql --arg language=javascript
gh-ql-mcp-client use prompt explain_codeql_query --arg queryPath=/path/to/query.ql --format json`,
Args: cobra.ExactArgs(1),
RunE: runUsePrompt,
}

func init() {
useCmd.AddCommand(usePromptCmd)
usePromptCmd.Flags().StringArrayVar(&usePromptArgs, "arg", nil, "Prompt argument in key=value format (repeatable)")
}

func runUsePrompt(_ *cobra.Command, args []string) error {
promptName := args[0]

params, err := parseArgs(usePromptArgs)
if err != nil {
return fmt.Errorf("parse prompt arguments: %w", err)
}

ctx := context.Background()
client, err := connectMCPClient(ctx)
if err != nil {
return err
}
defer client.Close()

result, err := mcpclient.GetPrompt(ctx, client, promptName, params)
if err != nil {
return err
}

return outputPromptMessages(result)
}

func outputPromptMessages(result *mcpclient.PromptMessages) error {
switch OutputFormat() {
case "json":
s, err := mcpclient.FormatJSON(result)
if err != nil {
return err
}
fmt.Fprintln(os.Stdout, s)
case "markdown":
fmt.Fprint(os.Stdout, mcpclient.FormatPromptMessagesMarkdown(result))
default:
fmt.Fprint(os.Stdout, mcpclient.FormatPromptMessagesText(result))
}
return nil
}
60 changes: 60 additions & 0 deletions client/cmd/use_resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package cmd

import (
"context"
"fmt"
"os"

mcpclient "github.com/advanced-security/codeql-development-mcp-server/client/internal/mcp"
"github.com/spf13/cobra"
)

var useResourceCmd = &cobra.Command{
Use: "resource <uri>",
Short: "Read an MCP server resource by URI",
Long: `Read a specific MCP server resource and print its content.

Example:
gh-ql-mcp-client use resource codeql://server/tools
gh-ql-mcp-client use resource codeql://server/overview --format markdown`,
Args: cobra.ExactArgs(1),
RunE: runUseResource,
}

func init() {
useCmd.AddCommand(useResourceCmd)
}

func runUseResource(_ *cobra.Command, args []string) error {
uri := args[0]

ctx := context.Background()
client, err := connectMCPClient(ctx)
if err != nil {
return err
}
defer client.Close()

result, err := mcpclient.ReadResource(ctx, client, uri)
if err != nil {
return err
}

return outputResourceContent(result)
}

func outputResourceContent(result *mcpclient.ResourceContent) error {
switch OutputFormat() {
case "json":
s, err := mcpclient.FormatJSON(result)
if err != nil {
return err
}
fmt.Fprintln(os.Stdout, s)
case "markdown":
fmt.Fprint(os.Stdout, mcpclient.FormatResourceContentMarkdown(result))
default:
fmt.Fprint(os.Stdout, mcpclient.FormatResourceContentText(result))
}
return nil
}
Loading
Loading