Fix invalid JSON escapes in GitHub remote MCP gateway config generation by Copilot · Pull Request #41864 · github/gh-aw · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 4 additions & 4 deletions .github/workflows/github-remote-mcp-auth-test.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 43 additions & 1 deletion actions/setup/js/start_mcp_gateway.cjs
26 changes: 25 additions & 1 deletion actions/setup/js/start_mcp_gateway.test.cjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { applyOTLPIgnoreIfMissing, detectEngineType, getOTLPIfMissingMode, hasNonEmptyOTLPHeaders, resolveCopilotConfigPaths } from "./start_mcp_gateway.cjs";
import { applyOTLPIgnoreIfMissing, detectEngineType, getJSONParseErrorContext, getOTLPIfMissingMode, hasNonEmptyOTLPHeaders, resolveCopilotConfigPaths } from "./start_mcp_gateway.cjs";

describe("start_mcp_gateway OTLP if-missing helpers", () => {
let originalWarning;
Expand Down Expand Up @@ -197,3 +197,27 @@ describe("start_mcp_gateway detectEngineType", () => {
expect(detectEngineType(configDir, env, existsSync)).toBe("copilot");
});
});

describe("start_mcp_gateway getJSONParseErrorContext", () => {
it("extracts line/column and key for invalid escape values", () => {
const invalidConfig = `{
"mcpServers": {
"github": {
"env": {
"GITHUB_HOST": "\\https://github.com"
}
}
}
}`;
let parseErrorMessage = "";
try {
JSON.parse(invalidConfig);
} catch (err) {
parseErrorMessage = /** @type {Error} */ err.message;
}
const context = getJSONParseErrorContext(invalidConfig, parseErrorMessage);
expect(context).toBeTruthy();
expect(context?.key).toBe("GITHUB_HOST");
expect(context?.lineText).toContain(`"GITHUB_HOST"`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The assertions only verify key and lineTextline and column are part of the returned contract but are unchecked, so regressions in position arithmetic would silently pass.

💡 Suggested additions
expect(context?.line).toBe(5);   // "GITHUB_HOST" is on line 5 of invalidConfig
expect(context?.column).toBeGreaterThan(0);

Pinning at least line turns this into a real specification and catches off-by-one regressions in getJSONParseErrorContext.

@copilot please address this.

});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] No test covers the null return path — when parseErrorMessage contains no "position N" substring the function silently returns null, but that branch is never exercised.

💡 Suggested test
it("returns null when error message has no position info", () => {
  const context = getJSONParseErrorContext('{"x":1}', 'Unexpected token');
  expect(context).toBeNull();
});

This matters because different JS engines (or future Node.js versions) may not include "position N" in their error messages, and the calling code in main() already guards on if (parseContext) — so the null path is a real production path that should be tested.

@copilot please address this.

12 changes: 6 additions & 6 deletions pkg/workflow/github_remote_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ func TestRenderGitHubMCPRemoteConfig(t *testing.T) {
`"list_issues"`,
`"create_issue"`,
`"env": {`,
`"GITHUB_PERSONAL_ACCESS_TOKEN": "\\${GITHUB_MCP_SERVER_TOKEN}"`,
`"GITHUB_HOST": "\\${GITHUB_SERVER_URL}"`,
`"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_MCP_SERVER_TOKEN}"`,
`"GITHUB_HOST": "${GITHUB_SERVER_URL}"`,
},
notExpected: []string{
`"X-MCP-Readonly"`,
Expand All @@ -104,8 +104,8 @@ func TestRenderGitHubMCPRemoteConfig(t *testing.T) {
`"Authorization": "Bearer \\${GITHUB_PERSONAL_ACCESS_TOKEN}"`,
`"X-MCP-Toolsets": "all"`,
`"env": {`,
`"GITHUB_PERSONAL_ACCESS_TOKEN": "\\${GITHUB_MCP_SERVER_TOKEN}"`,
`"GITHUB_HOST": "\\${GITHUB_SERVER_URL}"`,
`"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_MCP_SERVER_TOKEN}"`,
`"GITHUB_HOST": "${GITHUB_SERVER_URL}"`,
},
notExpected: []string{
`"X-MCP-Readonly"`,
Expand All @@ -132,8 +132,8 @@ func TestRenderGitHubMCPRemoteConfig(t *testing.T) {
`"list_repositories"`,
`"get_repository"`,
`"env": {`,
`"GITHUB_PERSONAL_ACCESS_TOKEN": "\\${GITHUB_MCP_SERVER_TOKEN}"`,
`"GITHUB_HOST": "\\${GITHUB_SERVER_URL}"`,
`"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_MCP_SERVER_TOKEN}"`,
`"GITHUB_HOST": "${GITHUB_SERVER_URL}"`,
},
notExpected: []string{},
},
Expand Down
4 changes: 2 additions & 2 deletions pkg/workflow/github_remote_mode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ This is a test workflow for GitHub remote mode configuration.
if !strings.Contains(lockContent, `"Authorization": "Bearer \\${GITHUB_PERSONAL_ACCESS_TOKEN}"`) {
t.Errorf("Expected Authorization header with ${GITHUB_PERSONAL_ACCESS_TOKEN} syntax but didn't find it in:\n%s", lockContent)
}
if !strings.Contains(lockContent, `"GITHUB_PERSONAL_ACCESS_TOKEN": "\\${GITHUB_MCP_SERVER_TOKEN}"`) {
if !strings.Contains(lockContent, `"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_MCP_SERVER_TOKEN}"`) {
t.Errorf("Expected env section with GITHUB_PERSONAL_ACCESS_TOKEN passthrough but didn't find it in:\n%s", lockContent)
}
} else {
Expand Down Expand Up @@ -460,7 +460,7 @@ This tests that GITHUB_PERSONAL_ACCESS_TOKEN is exported and passed to Docker.
}

// Check that the env section still defines the variable
if !strings.Contains(lockContent, `"GITHUB_PERSONAL_ACCESS_TOKEN": "\\${GITHUB_MCP_SERVER_TOKEN}"`) {
if !strings.Contains(lockContent, `"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_MCP_SERVER_TOKEN}"`) {
t.Errorf("Expected env section with GITHUB_PERSONAL_ACCESS_TOKEN but didn't find it in lock file")
}
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/workflow/mcp_renderer_github.go
Loading