Skip to content

Fix invalid JSON escapes in GitHub remote MCP gateway config generation#41864

Merged
pelikhan merged 2 commits into
mainfrom
copilot/fix-gateway-env-value-escaping
Jun 27, 2026
Merged

Fix invalid JSON escapes in GitHub remote MCP gateway config generation#41864
pelikhan merged 2 commits into
mainfrom
copilot/fix-gateway-env-value-escaping

Conversation

Copilot AI commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Start MCP Gateway could fail before launch because GitHub remote MCP env placeholders were emitted with a leading backslash, producing invalid JSON escapes (for example \https://...). This change removes the bad escaping path and improves parse-failure diagnostics so malformed config points to the likely key/location.

  • Gateway config rendering (root cause)

    • Updated GitHub remote MCP env generation to emit plain ${...} placeholders instead of \${...} in JSON values.
    • This preserves runtime substitution while keeping the JSON document valid.
  • Failure diagnostics in gateway startup

    • Added targeted parse-context extraction in start_mcp_gateway.cjs to report likely line/column and key when JSON parsing fails.
    • Error output now surfaces actionable context instead of only a byte offset.
  • Generated workflow + expectations

    • Updated affected lock output and assertions to match the corrected env format for GitHub remote MCP.
"env": {
  "GITHUB_HOST": "${GITHUB_SERVER_URL}",
  "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_MCP_SERVER_TOKEN}"
}

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix gateway env-value escaping in start_mcp_gateway config Fix invalid JSON escapes in GitHub remote MCP gateway config generation Jun 27, 2026
Copilot AI requested a review from pelikhan June 27, 2026 08:21
@github-actions

Copy link
Copy Markdown
Contributor

PR Triage — §28289524040

Field Value
Category bug
Risk medium
Score 58 / 100 — impact 30, urgency 18, quality 10
Action defer — still draft; MCP gateway JSON-escape fix with targeted tests; promote to ready once CI confirms
Age ~4h

Fix targets start_mcp_gateway.cjs and mcp_renderer_github.go. High-value bug fix — escalate to fast_track once draft is lifted and CI passes.

Generated by 🔧 PR Triage Agent · 90.3 AIC · ⌖ 11.7 AIC · ⊞ 5.4K ·

@pelikhan pelikhan marked this pull request as ready for review June 27, 2026 14:33
Copilot AI review requested due to automatic review settings June 27, 2026 14:33
@github-actions

github-actions Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

No ADR enforcement needed: PR does not have the 'implementation' label and has ≤100 new lines of code in business logic directories (9 lines, threshold is 100).

@github-actions

github-actions Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

Copilot AI left a comment

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.

Pull request overview

Fixes malformed JSON emitted in MCP gateway config generation for GitHub remote mode by removing the problematic escaping on GitHub remote MCP env placeholders, and improves start_mcp_gateway.cjs JSON parse failure diagnostics by surfacing likely line/column/key context.

Changes:

  • Emit ${...} placeholders (instead of backslash-prefixed forms) for GitHub remote MCP env values so the generated JSON remains parseable.
  • Add getJSONParseErrorContext to start_mcp_gateway.cjs and a Vitest covering invalid-escape scenarios.
  • Update lock output and Go tests to match the corrected env placeholder format.
Show a summary per file
File Description
pkg/workflow/mcp_renderer_github.go Updates GitHub remote MCP env placeholder rendering to avoid invalid JSON escapes.
pkg/workflow/github_remote_mode_test.go Updates assertions to match new env placeholder output in generated lock content.
pkg/workflow/github_remote_config_test.go Updates expected remote config strings for env placeholders.
actions/setup/js/start_mcp_gateway.cjs Adds targeted JSON parse error context extraction and logs it on parse failures.
actions/setup/js/start_mcp_gateway.test.cjs Adds unit test coverage for the new parse-context helper.
.github/workflows/github-remote-mcp-auth-test.lock.yml Regenerates lock output for corrected env placeholder rendering.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 6/6 changed files
  • Comments generated: 2
  • Review effort level: Low

Comment on lines 721 to +723
"env": {
"GITHUB_HOST": "\\${GITHUB_SERVER_URL}",
"GITHUB_PERSONAL_ACCESS_TOKEN": "\\${GITHUB_MCP_SERVER_TOKEN}"
"GITHUB_HOST": "${GITHUB_SERVER_URL}",
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_MCP_SERVER_TOKEN}"
return null;
}

const safePos = Math.min(pos, Math.max(0, jsonText.length - 1));

@github-actions github-actions Bot left a comment

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.

Skills-Based Review 🧠

Applied /diagnose and /tdd — approving with improvement suggestions on the new diagnostic helper.

📋 Key Themes & Highlights

Key Themes

  • Test spec completeness: getJSONParseErrorContext is exported and documented but the single test only asserts key and lineText, leaving line and column unverified; the null return path is also untested.
  • Diagnostic precision: keyMatch is position-agnostic (works for prettified JSON but could misattribute keys in minified config); safePos clipping at EOF is correct but unexplained.

Positive Highlights

  • ✅ Surgical root-cause fix: a single \ removal in mcp_renderer_github.go correctly unblocks gateway startup without side effects.
  • ✅ Valuable diagnostic improvement: surfacing line/column/key context on JSON.parse failures is a meaningful observability win.
  • ✅ Good JSDoc coverage on the new helper, and the fix is validated across all affected test files.
  • ✅ Lock-file hash rotation is expected and correct given the content change.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 55.7 AIC · ⌖ 7.59 AIC · ⊞ 6.6K

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.

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] 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.

const resolvedLineEnd = lineEnd === -1 ? jsonText.length : lineEnd;
const lineText = jsonText.slice(lineStart, resolvedLineEnd);
const column = safePos - lineStart + 1;
const keyMatch = lineText.match(/"([^"]+)"\s*:/);

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.

[/diagnose] keyMatch scans all of lineText for the first "key": pattern, regardless of where in the line the error pos falls. For prettified JSON (one key per line) this works fine, but for minified or compacted JSON it could report a key that precedes the actual error position.

💡 Improvement suggestion

Consider searching only within the substring up to column so the match is anchored to the error site:

const keyMatch = lineText.slice(0, column).match(/"([^"]+)"\s*:\s*$/)
             || lineText.match(/"([^"]+)"\s*:/);

The fallback preserves the existing behaviour when no key appears before the error column (e.g., the error is inside a key name itself).

@copilot please address this.

return null;
}

const safePos = Math.min(pos, Math.max(0, jsonText.length - 1));

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.

[/diagnose] safePos clips pos to jsonText.length - 1 when the parse error position equals the string length (EOF). This shifts the reported column one character back, which could mislead debugging for truncated or unterminated-string errors. A brief comment explaining the intent would help future readers.

💡 Suggested clarification
// Clip to last valid index; pos === length (EOF) is safe to round down by one.
const safePos = Math.min(pos, Math.max(0, jsonText.length - 1));

Alternatively, special-case pos >= length to report line: <last line>, column: <EOL+1> for a more accurate EOF diagnosis.

@copilot please address this.

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 100/100 — Excellent

Analyzed 1 new test (JavaScript): 1 design, 0 implementation, 0 guideline violations. Go file changes are correctness fixes to existing assertions only — no new Go test functions added.

📊 Metrics & Test Classification (1 test analyzed)
Metric Value
New/modified tests analyzed 1
✅ Design tests (behavioral contracts) 1 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 1 (100%)
Duplicate test clusters 0
Test inflation detected No (test:prod ratio ≈ 0.58 — below 2:1 threshold)
🚨 Coding-guideline violations 0
Test File Classification Issues Detected
it("extracts line/column and key for invalid escape values") actions/setup/js/start_mcp_gateway.test.cjs:202 ✅ Design

Go: 2 *_test.go files modified (existing assertion strings updated, no new test functions); JavaScript: 1 *.test.cjs (1 new test added).

Go test changes: github_remote_config_test.go and github_remote_mode_test.go updated 5 assertion strings from "\\${...}" to "${...}" — correctness fixes after the JSON escaping bug fix, not new test coverage. Both files have required //go:build !integration build tags ✓

Verdict

Check passed. 0% implementation tests (threshold: 30%). The new test directly invokes getJSONParseErrorContext with a real invalid JSON string, catches the actual parse error, and asserts on three observable return-value properties (context non-null, key === "GITHUB_HOST", lineText containing the key). No mocks used. Solid behavioral contract covering the primary error-path use case.

🧪 Test quality analysis by Test Quality Sentinel · 66.7 AIC · ⌖ 18.8 AIC · ⊞ 8.4K ·

@github-actions github-actions Bot left a comment

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.

✅ Test Quality Sentinel: 100/100. Test quality is acceptable — 0% of new tests are implementation tests (threshold: 30%).

@github-actions github-actions Bot left a comment

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.

Non-blocking observations on the diagnostic helper

The root fix (removing the \\${ invalid-JSON-escape from env values) is correct and the lock file + Go test updates are consistent. The notes below are all on the newly introduced getJSONParseErrorContext utility.

Findings (all non-blocking)

safePos off-by-oneMath.max(0, jsonText.length - 1) as the upper bound silently miscounts when V8 reports pos === jsonText.length (the typical "Unexpected end of JSON input" path). Fix: Math.min(pos, jsonText.length). See inline comment.

Misplaced JSDoc cast/** @type {Error} */ err.message casts the string property, not the unknown err binding. TypeScript infers parseMessage : Error instead of string. The pre-existing pattern is copied into both new files. Fix: /** @type {Error} */ (err).message or an instanceof guard.

Thin test coverage — one test covers the happy path; the null-return path and the end-of-input (pos === length) edge case are unexercised.

🔎 Code quality review by PR Code Quality Reviewer · 131.3 AIC · ⌖ 9.26 AIC · ⊞ 5.2K

Comments that could not be inline-anchored

actions/setup/js/start_mcp_gateway.cjs:86

safePos is off-by-one for "Unexpected end of JSON input" errors — the most common parse failure when a heredoc is truncated or unterminated.

<details>
<summary>💡 Details and suggested fix</summary>

Math.max(0, jsonText.length - 1) caps safePos one short of the valid upper bound. JSON.parse can legally return pos === jsonText.length (e.g., V8: &quot;Unexpected end of JSON input at position N&quot;). When that happens the before slice misses the last character, so the reported column

actions/setup/js/start_mcp_gateway.cjs:402

JSDoc cast is applied to .message (a string) rather than to err (the unknown catch binding), so TypeScript infers parseMessage as Error instead of string.

<details>
<summary>💡 Details and suggested fix</summary>

The intent of the JSDoc comment is to assert that the caught value is an Error instance so .message can be accessed without a TS error. The annotation /** @type {Error} */ err.message applies the cast to the result of .message, making parseMessage type…

actions/setup/js/start_mcp_gateway.test.cjs:223

Test coverage covers only one happy path, leaving the null-return and end-of-input paths untested.

<details>
<summary>💡 Suggested additional cases</summary>

Three cases worth adding:

  1. Error message without "position N"getJSONParseErrorContext(&quot;{}&quot;, &quot;Unexpected token&quot;) should return null (the guard exists but is untested).

  2. pos === jsonText.length — the "Unexpected end of JSON input" case. Currently triggers the safePos off-by-one noted above. A test makes any f…

actions/setup/js/start_mcp_gateway.cjs:415

The full config — now containing the live GITHUB_MCP_SERVER_TOKEN value — is dumped to the Actions log on any JSON parse failure.

<details>
<summary>💡 Why this PR introduces the risk and how to fix it</summary>

Before this PR, the heredoc contained &quot;\\\&quot;. The leading backslash prevented shell expansion, so the literal placeholder string flowed through to mcpConfig and no real secret ever reached this variable.

After this PR, the heredoc delimiter is unquoted, so `${GITHUB_MCP_SERV…

@pelikhan pelikhan merged commit 15d7e27 into main Jun 27, 2026
85 of 99 checks passed
@pelikhan pelikhan deleted the copilot/fix-gateway-env-value-escaping branch June 27, 2026 14:55
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.82.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants