-
Notifications
You must be signed in to change notification settings - Fork 2
Replace JavaScript client with Go binary and integration test runner #221
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
data-douser
merged 5 commits into
dd/ql-mcp-client/2
from
copilot/remove-javascript-client
Apr 6, 2026
Merged
Changes from 4 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
dbc2bdb
Initial plan
Copilot 53498e7
Replace JavaScript client with Go binary and integration test runner
Copilot 0fa538f
Final validation complete
Copilot 72bae5b
Revert unrelated go.mod changes in server/ql/go/tools/test/
Copilot 213a788
Replace actual repo reference with placeholder in helpers_test.go
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package cmd | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "strings" | ||
| ) | ||
|
|
||
| // parseRepo splits an "owner/repo" string into owner and repo components. | ||
| func parseRepo(nwo string) (string, string, error) { | ||
| parts := strings.SplitN(nwo, "/", 2) | ||
| if len(parts) != 2 || parts[0] == "" || parts[1] == "" { | ||
| return "", "", fmt.Errorf("invalid repo format %q: expected owner/repo", nwo) | ||
| } | ||
| return parts[0], parts[1], nil | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| package cmd | ||
|
|
||
| import "testing" | ||
|
|
||
| func TestParseRepo_Valid(t *testing.T) { | ||
| owner, repo, err := parseRepo("has-ghas/dubbo") | ||
| if err != nil { | ||
| t.Fatalf("unexpected error: %v", err) | ||
| } | ||
| if owner != "has-ghas" { | ||
| t.Errorf("owner = %q, want %q", owner, "has-ghas") | ||
| } | ||
| if repo != "dubbo" { | ||
| t.Errorf("repo = %q, want %q", repo, "dubbo") | ||
| } | ||
| } | ||
|
|
||
| func TestParseRepo_Invalid(t *testing.T) { | ||
| tests := []string{ | ||
| "", | ||
| "noslash", | ||
| "/norepo", | ||
| "noowner/", | ||
| } | ||
| for _, input := range tests { | ||
| _, _, err := parseRepo(input) | ||
| if err == nil { | ||
| t.Errorf("parseRepo(%q) should return error", input) | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,168 @@ | ||
| package cmd | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "os" | ||
| "path/filepath" | ||
| "strings" | ||
|
|
||
| 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" | ||
| ) | ||
|
|
||
| var integrationTestsCmd = &cobra.Command{ | ||
| Use: "integration-tests", | ||
| Short: "Run MCP server integration tests from client/integration-tests/", | ||
| Long: `Discovers and runs integration test fixtures against a connected MCP server. | ||
|
|
||
| Test fixtures live in client/integration-tests/primitives/tools/<tool>/<test>/ | ||
| and use test-config.json or monitoring-state.json to define tool parameters.`, | ||
| RunE: runIntegrationTests, | ||
| } | ||
|
|
||
| var integrationTestsFlags struct { | ||
| tools string | ||
| tests string | ||
| noInstall bool | ||
| timeout int | ||
| } | ||
|
|
||
| func init() { | ||
| rootCmd.AddCommand(integrationTestsCmd) | ||
|
|
||
| f := integrationTestsCmd.Flags() | ||
| f.StringVar(&integrationTestsFlags.tools, "tools", "", "Comma-separated list of tool names to test") | ||
| f.StringVar(&integrationTestsFlags.tests, "tests", "", "Comma-separated list of test case names to run") | ||
| f.BoolVar(&integrationTestsFlags.noInstall, "no-install-packs", false, "Skip CodeQL pack installation") | ||
| f.IntVar(&integrationTestsFlags.timeout, "timeout", 30, "Per-tool-call timeout in seconds") | ||
| } | ||
|
|
||
| // mcpToolCaller adapts the MCP client to the ToolCaller interface. | ||
| type mcpToolCaller struct { | ||
| client *mcpclient.Client | ||
| } | ||
|
|
||
| func (c *mcpToolCaller) CallToolRaw(name string, params map[string]any) ([]itesting.ContentBlock, bool, error) { | ||
| result, err := c.client.CallTool(context.Background(), 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 | ||
| } | ||
|
|
||
| func (c *mcpToolCaller) ListToolNames() ([]string, error) { | ||
| tools, err := c.client.ListTools(context.Background()) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| names := make([]string, len(tools)) | ||
| for i, t := range tools { | ||
| names[i] = t.Name | ||
| } | ||
| return names, nil | ||
| } | ||
|
|
||
| func runIntegrationTests(cmd *cobra.Command, _ []string) error { | ||
| // Determine repo root | ||
| repoRoot, err := findRepoRoot() | ||
| if err != nil { | ||
| return fmt.Errorf("cannot determine repo root: %w", err) | ||
| } | ||
|
|
||
| // Change CWD to repo root so the MCP server subprocess resolves | ||
| // relative paths (from test-config.json, monitoring-state.json) | ||
| // correctly. The codeql CLI also resolves paths from CWD. | ||
| if err := os.Chdir(repoRoot); err != nil { | ||
| return fmt.Errorf("chdir to repo root: %w", err) | ||
| } | ||
| fmt.Printf("Working directory: %s\n", repoRoot) | ||
|
|
||
| // Connect to MCP server | ||
| client := mcpclient.NewClient(mcpclient.Config{ | ||
| Mode: MCPMode(), | ||
| Host: MCPHost(), | ||
| Port: MCPPort(), | ||
| }) | ||
|
|
||
| fmt.Println("🔌 Connecting to MCP server...") | ||
| ctx := context.Background() | ||
| if err := client.Connect(ctx); err != nil { | ||
| return fmt.Errorf("connect to MCP server: %w", err) | ||
| } | ||
| fmt.Println("✅ Connected to MCP server") | ||
|
|
||
| // Parse filters | ||
| var filterTools, filterTests []string | ||
| if integrationTestsFlags.tools != "" { | ||
| filterTools = strings.Split(integrationTestsFlags.tools, ",") | ||
| } | ||
| if integrationTestsFlags.tests != "" { | ||
| filterTests = strings.Split(integrationTestsFlags.tests, ",") | ||
| } | ||
|
|
||
| // Create and run the test runner | ||
| runner := itesting.NewRunner(&mcpToolCaller{client: client}, itesting.RunnerOptions{ | ||
| RepoRoot: repoRoot, | ||
| FilterTools: filterTools, | ||
| FilterTests: filterTests, | ||
| }) | ||
|
|
||
| allPassed, _ := runner.Run() | ||
|
|
||
| // Close the MCP client (and its stdio subprocess) before returning. | ||
| client.Close() | ||
|
|
||
| if !allPassed { | ||
| return fmt.Errorf("some integration tests failed") | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // findRepoRoot walks up from the current directory to find the repo root | ||
| // (identified by the presence of codeql-workspace.yml). | ||
| func findRepoRoot() (string, error) { | ||
| // Try from current working directory | ||
| dir, err := os.Getwd() | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| for { | ||
| if _, err := os.Stat(filepath.Join(dir, "codeql-workspace.yml")); err == nil { | ||
| return dir, nil | ||
| } | ||
| parent := filepath.Dir(dir) | ||
| if parent == dir { | ||
| break | ||
| } | ||
| dir = parent | ||
| } | ||
|
|
||
| // Fallback: try relative to the binary | ||
| exe, err := os.Executable() | ||
| if err == nil { | ||
| dir = filepath.Dir(exe) | ||
| for i := 0; i < 5; i++ { | ||
| if _, err := os.Stat(filepath.Join(dir, "codeql-workspace.yml")); err == nil { | ||
| return dir, nil | ||
| } | ||
| dir = filepath.Dir(dir) | ||
| } | ||
| } | ||
|
|
||
| return "", fmt.Errorf("could not find repo root (codeql-workspace.yml)") | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.