Skip to content

Commit 0092f97

Browse files
Copilotdata-douser
andauthored
Address PR review feedback: fix Close() kill, remove enableAnnotationTools setting, record skipped results, fix schema default, clarify changelog
Agent-Logs-Url: https://github.com/advanced-security/codeql-development-mcp-server/sessions/a90a53a5-b2ad-4775-8b61-f11d16b33749 Co-authored-by: data-douser <70299490+data-douser@users.noreply.github.com>
1 parent 06f3a6e commit 0092f97

File tree

12 files changed

+167
-38
lines changed

12 files changed

+167
-38
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ _Changes on `main` since the latest tagged release that have not yet been includ
1818
1919
### Highlights
2020

21-
- **Annotation, audit, cache, and SARIF tools are now always enabled** — Removed the `ENABLE_ANNOTATION_TOOLS` opt-in gate; all annotation, audit, query result cache, and SARIF analysis tools are registered by default. The `ENABLE_ANNOTATION_TOOLS` environment variable is still respected (set to `false` to disable) but defaults to `true`. ([#223](https://github.com/advanced-security/codeql-development-mcp-server/pull/223))
21+
- **Annotation, audit, cache, and SARIF tools are now always enabled** — Removed the `ENABLE_ANNOTATION_TOOLS` opt-in gate; all annotation, audit, query result cache, and SARIF analysis tools are registered by default. The `ENABLE_ANNOTATION_TOOLS` environment variable no longer controls tool availability; when set to `false`, it only disables the related auto-caching behaviour in result processing. ([#223](https://github.com/advanced-security/codeql-development-mcp-server/pull/223))
2222
- **Go-based `ql-mcp-client` rewrite** — Replaced the Node.js `ql-mcp-client.js` integration test runner with a Go CLI (`gh-ql-mcp-client`) built with Cobra and `mcp-go`. Adds `list tools/prompts/resources` commands and assertion-based integration test validation. ([#223](https://github.com/advanced-security/codeql-development-mcp-server/pull/223))
2323
- **Persistent MRVA workflow state and caching** — Introduced a new `SqliteStore` backend plus annotation, audit, and query result cache tools to support the next phase of MCP-assisted CodeQL development and `seclab-taskflow-agent` integration. ([#169](https://github.com/advanced-security/codeql-development-mcp-server/pull/169))
2424
- **Rust language support** — Added first-class Rust support with `PrintAST`, `PrintCFG`, `CallGraphFrom`, `CallGraphTo`, and `CallGraphFromTo` queries, bringing the total supported languages to 10. ([#195](https://github.com/advanced-security/codeql-development-mcp-server/pull/195))

client/Makefile

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export CGO_ENABLED = 0
1212
# Build flags
1313
LDFLAGS := -s -w
1414

15-
.PHONY: all build test test-unit test-integration lint clean install build-all
15+
.PHONY: all build test test-unit test-integration test-integration-fast lint clean install build-all
1616

1717
all: lint test build
1818

@@ -27,8 +27,12 @@ test: test-unit test-integration
2727
test-unit:
2828
go test ./...
2929

30-
## test-integration: Build binary and run integration tests via run-integration-tests.sh
30+
## test-integration: Build binary and run integration tests (including codeql_pack_install)
3131
test-integration: build
32+
scripts/run-integration-tests.sh
33+
34+
## test-integration-fast: Build binary and run integration tests, skipping pack installation
35+
test-integration-fast: build
3236
scripts/run-integration-tests.sh --no-install-packs
3337

3438
## test-verbose: Run all unit tests with verbose output

client/internal/mcp/client.go

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,21 @@ import (
1010
"time"
1111

1212
mcpclient "github.com/mark3labs/mcp-go/client"
13+
"github.com/mark3labs/mcp-go/client/transport"
1314
"github.com/mark3labs/mcp-go/mcp"
1415
)
1516

17+
// innerClient is satisfied by *mcpclient.Client and allows the Close() timeout
18+
// path to be tested without a live MCP server.
19+
type innerClient interface {
20+
Initialize(context.Context, mcp.InitializeRequest) (*mcp.InitializeResult, error)
21+
Close() error
22+
CallTool(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error)
23+
ListTools(context.Context, mcp.ListToolsRequest) (*mcp.ListToolsResult, error)
24+
ListPrompts(context.Context, mcp.ListPromptsRequest) (*mcp.ListPromptsResult, error)
25+
ListResources(context.Context, mcp.ListResourcesRequest) (*mcp.ListResourcesResult, error)
26+
}
27+
1628
const (
1729
// ModeStdio spawns the MCP server as a child process.
1830
ModeStdio = "stdio"
@@ -40,8 +52,9 @@ type Config struct {
4052

4153
// Client wraps an MCP client with convenience methods for tool calls.
4254
type Client struct {
43-
inner *mcpclient.Client
44-
config Config
55+
inner innerClient
56+
config Config
57+
serverCmd *exec.Cmd // stdio mode only; retained so Close() can force-kill on timeout
4558
}
4659

4760
// NewClient creates a new MCP client with the given config but does not connect.
@@ -67,14 +80,27 @@ func (c *Client) connectStdio(ctx context.Context) error {
6780
return fmt.Errorf("cannot locate MCP server: %w", err)
6881
}
6982

70-
client, err := mcpclient.NewStdioMCPClient(
83+
// Capture the spawned exec.Cmd so Close() can force-kill the process on timeout.
84+
var spawnedCmd *exec.Cmd
85+
cmdFunc := func(spawnCtx context.Context, command string, env []string, args []string) (*exec.Cmd, error) {
86+
cmd := exec.CommandContext(spawnCtx, command, args...)
87+
if len(env) > 0 {
88+
cmd.Env = append(os.Environ(), env...)
89+
}
90+
spawnedCmd = cmd
91+
return cmd, nil
92+
}
93+
94+
client, err := mcpclient.NewStdioMCPClientWithOptions(
7195
"node", nil,
72-
serverPath,
96+
[]string{serverPath},
97+
transport.WithCommandFunc(cmdFunc),
7398
)
7499
if err != nil {
75100
return fmt.Errorf("failed to create stdio MCP client: %w", err)
76101
}
77102
c.inner = client
103+
c.serverCmd = spawnedCmd
78104

79105
initCtx, cancel := context.WithTimeout(ctx, ConnectTimeout)
80106
defer cancel()
@@ -135,8 +161,8 @@ func (c *Client) Close() error {
135161

136162
// For stdio transport, closing stdin signals the server to exit.
137163
// However, the Node.js server may not exit immediately, so we
138-
// run Close in a goroutine with a timeout and kill the process
139-
// if it doesn't exit within 3 seconds.
164+
// run Close in a goroutine with a timeout and force-kill the
165+
// subprocess if it doesn't exit within 3 seconds.
140166
done := make(chan error, 1)
141167
go func() {
142168
done <- c.inner.Close()
@@ -146,8 +172,10 @@ func (c *Client) Close() error {
146172
case err := <-done:
147173
return err
148174
case <-time.After(3 * time.Second):
149-
// Close didn't complete in time; the process is likely stuck.
150-
// This is expected with Node.js stdio servers.
175+
// Close didn't complete in time; force-kill the subprocess.
176+
if c.serverCmd != nil && c.serverCmd.Process != nil {
177+
_ = c.serverCmd.Process.Kill()
178+
}
151179
return fmt.Errorf("%s", CloseTimeoutErr)
152180
}
153181
}

client/internal/mcp/client_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,46 @@
11
package mcp
22

33
import (
4+
"context"
5+
"os/exec"
6+
"strings"
47
"testing"
58
"time"
9+
10+
"github.com/mark3labs/mcp-go/mcp"
611
)
712

13+
// hangCloser is a stub innerClient whose Close() blocks until released.
14+
// It satisfies the innerClient interface to allow timeout-path testing.
15+
type hangCloser struct {
16+
released chan struct{}
17+
}
18+
19+
func (h *hangCloser) Close() error {
20+
<-h.released
21+
return nil
22+
}
23+
24+
func (h *hangCloser) Initialize(_ context.Context, _ mcp.InitializeRequest) (*mcp.InitializeResult, error) {
25+
return nil, nil
26+
}
27+
28+
func (h *hangCloser) CallTool(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
29+
return nil, nil
30+
}
31+
32+
func (h *hangCloser) ListTools(_ context.Context, _ mcp.ListToolsRequest) (*mcp.ListToolsResult, error) {
33+
return nil, nil
34+
}
35+
36+
func (h *hangCloser) ListPrompts(_ context.Context, _ mcp.ListPromptsRequest) (*mcp.ListPromptsResult, error) {
37+
return nil, nil
38+
}
39+
40+
func (h *hangCloser) ListResources(_ context.Context, _ mcp.ListResourcesRequest) (*mcp.ListResourcesResult, error) {
41+
return nil, nil
42+
}
43+
844
func TestTimeoutForTool_CodeQLTools(t *testing.T) {
945
codeqlTools := []string{
1046
"codeql_query_run",
@@ -82,6 +118,61 @@ func TestClose_TimeoutReturnsError(t *testing.T) {
82118
}
83119
}
84120

121+
func TestClose_KillsSubprocessOnTimeout(t *testing.T) {
122+
t.Parallel()
123+
124+
// Start a long-running subprocess to simulate a stuck server.
125+
proc := exec.Command("sleep", "60")
126+
if err := proc.Start(); err != nil {
127+
t.Skipf("cannot start subprocess for test: %v", err)
128+
}
129+
130+
hang := &hangCloser{released: make(chan struct{})}
131+
132+
c := &Client{
133+
inner: hang,
134+
serverCmd: proc,
135+
}
136+
137+
start := time.Now()
138+
err := c.Close()
139+
elapsed := time.Since(start)
140+
141+
// Release the hanging closer goroutine (cleanup).
142+
close(hang.released)
143+
144+
if err == nil {
145+
t.Fatal("Close() should return a timeout error, got nil")
146+
}
147+
if !strings.Contains(err.Error(), "timed out") {
148+
t.Errorf("Close() error = %q; want a message containing 'timed out'", err.Error())
149+
}
150+
151+
// Elapsed time should be roughly the 3-second timeout window.
152+
if elapsed > 5*time.Second {
153+
t.Errorf("Close() took %v, expected ~3s", elapsed)
154+
}
155+
156+
// proc.Wait() should return quickly if the process was killed.
157+
// Use a channel with timeout to avoid hanging the test if Kill() failed.
158+
waitCh := make(chan error, 1)
159+
go func() {
160+
waitCh <- proc.Wait()
161+
}()
162+
163+
select {
164+
case waitErr := <-waitCh:
165+
// Process exited (possibly as a zombie until Wait() is called).
166+
// A killed process returns an error from Wait().
167+
if waitErr == nil {
168+
t.Error("subprocess exited with success; expected it to be killed (non-zero exit)")
169+
}
170+
case <-time.After(2 * time.Second):
171+
_ = proc.Process.Kill()
172+
t.Error("subprocess did not exit after Close() timeout; expected force-kill")
173+
}
174+
}
175+
85176
func TestConstants(t *testing.T) {
86177
if DefaultTimeout != 60*time.Second {
87178
t.Errorf("DefaultTimeout = %v, want 60s", DefaultTimeout)

client/internal/testing/runner.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,12 +144,14 @@ func (r *Runner) runToolTests(toolName, testsDir string) {
144144
// Skip codeql_pack_install when --no-install-packs is set
145145
if r.options.NoInstallPacks && toolName == "codeql_pack_install" {
146146
fmt.Printf("\n %s (skipped: --no-install-packs)\n", toolName)
147+
r.recordResult(toolName, "", false, "skipped: --no-install-packs", 0)
147148
return
148149
}
149150

150151
// Deprecated monitoring/session tools — skip entirely
151152
if isDeprecatedTool(toolName) {
152153
fmt.Printf("\n %s (skipped: deprecated)\n", toolName)
154+
r.recordResult(toolName, "", false, "skipped: deprecated", 0)
153155
return
154156
}
155157

client/internal/testing/runner_test.go

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -129,29 +129,38 @@ func TestRunnerWithMockCaller(t *testing.T) {
129129
func TestRunnerNoInstallPacks(t *testing.T) {
130130
caller := newMockCaller()
131131

132-
// Create a valid repo root with an empty fixtures directory
132+
// Create a repo root with a codeql_pack_install fixture directory
133133
dir := t.TempDir()
134134
testsDir := filepath.Join(dir, "client", "integration-tests", "primitives", "tools")
135-
os.MkdirAll(testsDir, 0o755)
135+
// Create the fixture dir so the runner encounters it and records a skip
136+
os.MkdirAll(filepath.Join(testsDir, "codeql_pack_install"), 0o755)
136137

137138
opts := RunnerOptions{
138139
RepoRoot: dir,
139140
NoInstallPacks: true,
140-
FilterTools: []string{"codeql_pack_install"},
141141
}
142142

143143
runner := NewRunner(caller, opts)
144144
if runner == nil {
145145
t.Fatal("NewRunner returned nil")
146146
}
147147

148-
// codeql_pack_install should be skipped — no results
148+
// codeql_pack_install should be skipped — one skipped result recorded
149149
allPassed, results := runner.Run()
150150
if !allPassed {
151-
t.Error("expected allPassed=true when pack install is skipped")
151+
t.Error("expected allPassed=true when only skipped results")
152152
}
153-
if len(results) != 0 {
154-
t.Errorf("expected 0 results (skipped), got %d", len(results))
153+
if len(results) != 1 {
154+
t.Fatalf("expected 1 skipped result, got %d", len(results))
155+
}
156+
if results[0].ToolName != "codeql_pack_install" {
157+
t.Errorf("expected skipped result for codeql_pack_install, got %q", results[0].ToolName)
158+
}
159+
if results[0].Error != "skipped: --no-install-packs" {
160+
t.Errorf("unexpected skip reason: %q", results[0].Error)
161+
}
162+
if results[0].Passed {
163+
t.Error("skipped result should have Passed=false")
155164
}
156165
}
157166

extensions/vscode/package.json

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,6 @@
9595
"default": true,
9696
"markdownDescription": "Copy CodeQL databases from the `GitHub.vscode-codeql` extension storage into a managed directory, removing query-server lock files so the MCP server CLI can operate without contention. Disable to use databases in-place (may fail when the CodeQL query server is running)."
9797
},
98-
"codeql-mcp.enableAnnotationTools": {
99-
"type": "boolean",
100-
"default": true,
101-
"markdownDescription": "Enable annotation, audit, and query results caching tools. When enabled, the MCP server registers `annotation_*`, `audit_*`, and `query_results_cache_*` tools. Disable to reduce the tool surface if these capabilities are not needed."
102-
},
10398
"codeql-mcp.serverArgs": {
10499
"type": "array",
105100
"items": {

extensions/vscode/test/suite/mcp-tool-e2e.integration.test.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -271,9 +271,10 @@ suite('MCP Server Tool Integration Tests', () => {
271271
});
272272

273273
/**
274-
* Integration tests for annotation and audit tools.
275-
* These tests spawn a separate server instance with ENABLE_ANNOTATION_TOOLS=true
276-
* to validate opt-in annotation and MRVA audit tool functionality.
274+
* Integration tests for annotation, audit, cache, and SARIF tools.
275+
* These tests spawn a separate server instance to validate annotation
276+
* and MRVA audit tool functionality. All these tools are always registered
277+
* by default (no ENABLE_ANNOTATION_TOOLS opt-in required).
277278
*/
278279
suite('MCP Annotation & Audit Tool Integration Tests', () => {
279280
let client: Client;
@@ -295,7 +296,6 @@ suite('MCP Annotation & Audit Tool Integration Tests', () => {
295296
const env: Record<string, string> = {
296297
...process.env as Record<string, string>,
297298
TRANSPORT_MODE: 'stdio',
298-
ENABLE_ANNOTATION_TOOLS: 'true',
299299
CODEQL_DATABASES_BASE_DIRS: path.join(fixtureStorage, 'databases'),
300300
CODEQL_QUERY_RUN_RESULTS_DIRS: path.join(fixtureStorage, 'queries'),
301301
CODEQL_MRVA_RUN_RESULTS_DIRS: path.join(fixtureStorage, 'variant-analyses'),
@@ -310,7 +310,7 @@ suite('MCP Annotation & Audit Tool Integration Tests', () => {
310310

311311
client = new Client({ name: 'annotation-test', version: '1.0.0' });
312312
await client.connect(transport);
313-
console.log('[mcp-annotation-e2e] Connected to MCP server with annotation tools enabled');
313+
console.log('[mcp-annotation-e2e] Connected to MCP server');
314314
});
315315

316316
suiteTeardown(async function () {
@@ -319,7 +319,7 @@ suite('MCP Annotation & Audit Tool Integration Tests', () => {
319319
try { if (transport) await transport.close(); } catch { /* best-effort */ }
320320
});
321321

322-
test('Annotation tools should be available when ENABLE_ANNOTATION_TOOLS=true', async function () {
322+
test('Annotation, audit, cache, and SARIF tools should always be available', async function () {
323323
this.timeout(15_000);
324324

325325
const response = await client.listTools();
@@ -333,19 +333,19 @@ suite('MCP Annotation & Audit Tool Integration Tests', () => {
333333
assert.ok(toolNames.includes('annotation_delete'), 'Should include annotation_delete');
334334
assert.ok(toolNames.includes('annotation_search'), 'Should include annotation_search');
335335

336-
// Layer 2: audit tools (gated by same flag)
336+
// Layer 2: audit tools
337337
assert.ok(toolNames.includes('audit_store_findings'), 'Should include audit_store_findings');
338338
assert.ok(toolNames.includes('audit_list_findings'), 'Should include audit_list_findings');
339339
assert.ok(toolNames.includes('audit_add_notes'), 'Should include audit_add_notes');
340340
assert.ok(toolNames.includes('audit_clear_repo'), 'Should include audit_clear_repo');
341341

342-
// Layer 3: query results cache tools (gated by same flag)
342+
// Layer 3: query results cache tools
343343
assert.ok(toolNames.includes('query_results_cache_lookup'), 'Should include query_results_cache_lookup');
344344
assert.ok(toolNames.includes('query_results_cache_retrieve'), 'Should include query_results_cache_retrieve');
345345
assert.ok(toolNames.includes('query_results_cache_clear'), 'Should include query_results_cache_clear');
346346
assert.ok(toolNames.includes('query_results_cache_compare'), 'Should include query_results_cache_compare');
347347

348-
// Layer 4: SARIF analysis tools (gated by same flag)
348+
// Layer 4: SARIF analysis tools
349349
assert.ok(toolNames.includes('sarif_extract_rule'), 'Should include sarif_extract_rule');
350350
assert.ok(toolNames.includes('sarif_list_rules'), 'Should include sarif_list_rules');
351351
assert.ok(toolNames.includes('sarif_rule_to_markdown'), 'Should include sarif_rule_to_markdown');

server/dist/codeql-development-mcp-server.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190283,8 +190283,8 @@ var MonitoringConfigSchema = external_exports.object({
190283190283
enableRecommendations: external_exports.boolean().default(true),
190284190284
enableMonitoringTools: external_exports.boolean().default(false),
190285190285
// Opt-in: session_* tools disabled by default for end-users
190286-
enableAnnotationTools: external_exports.boolean().default(false)
190287-
// Controls auto-caching behavior in result-processor; tools are always registered
190286+
enableAnnotationTools: external_exports.boolean().default(true)
190287+
// Controls auto-caching behaviour in result-processor; tools are always registered
190288190288
});
190289190289

190290190290
// src/lib/session-data-manager.ts

server/dist/codeql-development-mcp-server.js.map

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)