Skip to content

MCP GitHub tool surface over local-git backend + branch diff tools - #11

Merged
Atul-Chahar merged 3 commits into
mainfrom
feat/mcp-layer
Aug 26, 2026
Merged

MCP GitHub tool surface over local-git backend + branch diff tools#11
Atul-Chahar merged 3 commits into
mainfrom
feat/mcp-layer

Conversation

@Atul-Chahar

Copy link
Copy Markdown
Owner

Closes #2

What

  • GitHub tool surface over MCP (harness/mcp/): list issues, read repo, branch, commit, open PR — spoken over the Model Context Protocol so any MCP-aware agent can drive Foreman's repo operations.
  • Local-git backend: a complete offline adapter implementing the same tool contract against plain git. Development and tests run with zero network, zero tokens.
  • Retry with backoff on transport failures.
  • Branch diff + read-file-at-ref tools: the read-only primitives the pre-merge reviewer stage needs to see what a swarm PR actually changes.

Why

Tool calls are only trustworthy if their surface is small and typed. Routing every GitHub interaction through one MCP server means the policy engine classifies actions in exactly one place, and the local-git adapter keeps the whole harness testable without touching github.com.

Test plan

7 new tests (47 total green): tool contract parity between backends, diff/read-at-ref correctness, retry/backoff behavior.

DarkBird10020 and others added 3 commits August 26, 2026 05:16
…ckoff

- real JSON-RPC 2.0 stdio MCP client (initialize handshake, tools/call,
  timeouts) — drives any MCP server, e.g. the GitHub MCP server
- LocalGitMCP: same tool surface backed by a real local git repo, so the
  full swarm path runs offline with zero credentials; branches, commits,
  diffs and merges are real git operations; conflicts throw MERGE_CONFLICT
  and leave the repo clean — nothing ever force-resolves
- one PR per branch (idempotent create)
- rate limiter: token bucket + in-flight cap + Retry-After-aware backoff
- get_branch_diff: git diff base...branch without needing a PR number —
  the reviewer runs before any PR exists
- read_file_at_ref: file contents at a branch ref, null when absent
- remote backend maps to compare_commits / get_file_contents with arg
  translation; branch diff normalized to unified-diff text
…mic commits, path safety

- facade: merge/close are structurally impossible without a satisfied T2
  approval ({approvalId, decidedBy}); the decision is audited at execution
- remote schema: full arg translation (owner/repo injection, head/base,
  pullNumber/issue_number) matching the official GitHub MCP schemas
- remote commits: per-file loop replaced with one atomic push_files batch
  (raw content) — no partial branches, no base64/sha bookkeeping errors
- remote diffs: patchless/binary files emit explicit metadata records so an
  empty diff can only mean no changes
- local-git: agent paths validated against traversal, absolute escapes, and
  symlinks (incl. dangling ones via lstat walk); review diffs fail closed
  instead of surfacing git errors as 'no changes'
- client: typed MCPTransportError; spawn 'error' handler registered before
  handshake; failed init reaps the child; initialize has its own timeout
- ratelimit: transient transport failures retry with backoff; tool errors do
  not; backoff sleeps outside the concurrency slot
- tests: +11 (client fixture server, escape attempts, fail-closed diffs,
  gated merge, translation, slot-free backoff) — 58 total
@coderabbitai

coderabbitai Bot commented Aug 25, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 670503df-5711-4331-80cb-392f12cadc75


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add MCP GitHub facade with offline local-git backend

✨ Enhancement 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Adds a typed MCP facade for remote GitHub and offline repository operations.
• Adds branch diff and ref-based file reads for pre-PR review.
• Limits concurrency and retries rate-limited requests with backoff.
Diagram

graph TD
  A["Foreman Harness"] --> B["GitHub Facade"] --> C["Rate Limiter"] --> D["Backend Router"]
  D --> E["MCP Client"] --> F["GitHub MCP"]
  D --> G["Local Git"] --> H[("Git State")]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use GitHub SDK directly
  • ➕ Provides strongly supported API bindings
  • ➕ Avoids MCP process and JSON-RPC transport management
  • ➖ Couples the harness directly to GitHub
  • ➖ Bypasses the single MCP policy surface
  • ➖ Requires separate offline abstractions
2. Mock GitHub operations in tests
  • ➕ Simpler and faster unit tests
  • ➕ Avoids manipulating temporary repositories
  • ➖ Cannot validate real branch, diff, commit, merge, or conflict behavior
  • ➖ Provides weaker confidence in backend contract parity

Recommendation: Keep the shared facade with remote MCP and real local-git implementations. It centralizes the policy-facing tool surface while giving offline tests realistic git semantics; direct SDK integration or mocks would sacrifice portability or behavioral confidence.

Files changed (5) +711 / -0

Enhancement (4) +558 / -0
client.mjsImplement stdio MCP JSON-RPC client +141/-0

Implement stdio MCP JSON-RPC client

• Adds MCP initialization, tool discovery and invocation, request correlation, per-call timeouts, process lifecycle handling, and protocol error propagation over newline-delimited stdio JSON-RPC.

harness/mcp_clients/client.mjs

github.mjsAdd unified GitHub MCP facade +153/-0

Add unified GitHub MCP facade

• Introduces typed repository helpers that route through remote MCP or local-git backends under a shared rate limiter. It translates remote arguments and composes branch diff and multi-file commit operations where direct tools are unavailable.

harness/mcp_clients/github.mjs

local_git.mjsImplement offline local-git MCP backend +192/-0

Implement offline local-git MCP backend

• Implements issue and PR bookkeeping alongside real branch, commit, diff, ref-read, and merge operations. Pull-request creation is idempotent, and merge conflicts abort cleanly with a MERGE_CONFLICT error.

harness/mcp_clients/local_git.mjs

ratelimit.mjsAdd concurrency limiting and retry backoff +72/-0

Add concurrency limiting and retry backoff

• Adds token-bucket admission, an in-flight cap, rate-limit detection, Retry-After handling, and bounded exponential retries for backend calls.

harness/mcp_clients/ratelimit.mjs

Tests (1) +153 / -0
mcp.test.mjsCover MCP facade, local git, and retries +153/-0

Cover MCP facade, local git, and retries

• Adds integration-style tests for issue lifecycle, real branch and PR workflows, merge-conflict cleanup, pre-PR diff and ref reads, limiter concurrency and backoff, and default backend selection.

harness/test/mcp.test.mjs

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (9) 📘 Rule violations (0) 📜 Skill insights (2)

Grey Divider


Action required

1. mergePR bypasses approval gate 📜 Skill insight ⛨ Security
Description
The new public mergePR() helper dispatches a protected merge directly, despite documentation
requiring merge-queue invocation only after approval. The tier: 'T2' value is not enforced because
call() only rate-limits and dispatches.
Code

harness/mcp_clients/github.mjs[142]

+  mergePR(number) { return this.call('merge_pull_request', { number }, { tier: 'T2' }); }
Evidence
Rule 2905083 requires access control where documentation promises protection. The facade states that
merges happen only after the approval gate, but its public mergePR() method directly reaches
backend dispatch; the policy engine and approval gate are not invoked, and the local merge
implementation checks only PR state before merging.

harness/mcp_clients/github.mjs[9-11]
harness/mcp_clients/github.mjs[74-92]
harness/mcp_clients/github.mjs[142-142]
harness/mcp_clients/local_git.mjs[150-175]
harness/policy/engine.mjs[83-105]
harness/gate/approval_gate.mjs[53-99]
Skill: security-review

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`GitHubMCP.mergePR()` can execute a merge without validating the required T2 policy decision and approval gate.

## Issue Context
The new facade documents that merges occur only through the merge queue after approval, but `call()` merely dispatches to the selected backend. Ensure merge execution fails closed unless a verified approval authorizes it, and align the merge action name with the policy tier registry.

## Fix Focus Areas
- harness/mcp_clients/github.mjs[74-92]
- harness/mcp_clients/github.mjs[142-142]
- harness/policy/tiers.mjs[47-55]
- harness/gate/approval_gate.mjs[53-99]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Remote PR schema is wrong 🐞 Bug ≡ Correctness
Description
createPR() sends {branch,title,body} unchanged, omitting owner, repo, and required base
while using branch instead of required head. The official GitHub MCP server therefore rejects
every remote pull-request creation call.
Code

harness/mcp_clients/github.mjs[R90-100]

+      const res = await this.remote.callTool(remoteName, this._translateArgs(tool, args));
+      return JSON.parse(findText(res) ?? 'null');
+    });
+  }
+
+  /** The GitHub MCP server names some args differently than our tool surface. */
+  _translateArgs(tool, args) {
+    if (tool === 'read_file_at_ref') {
+      return { owner: this.owner, repo: this.repo, path: args.path, ref: args.branch };
+    }
+    return args;
Evidence
The facade stores the configured owner/repository but only injects them for file reads; its PR
helper supplies branch and no base. The official server documents owner, repo, head, and
base as required create-pull-request inputs.

harness/mcp_clients/github.mjs[44-53]
harness/mcp_clients/github.mjs[95-101]
harness/mcp_clients/github.mjs[137-137]
🌐 The official create_pull_request schema requires owner, repo, head, base, and title.

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Remote facade arguments do not match the official GitHub MCP schemas, causing remote operations such as PR creation to fail validation.

## Issue Context
Inject repository identity and explicitly translate each internal tool's field names and required defaults instead of passing most argument objects through unchanged.

## Fix Focus Areas
- harness/mcp_clients/github.mjs[84-101]
- harness/mcp_clients/github.mjs[133-143]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Remote commits corrupt content 🐞 Bug ≡ Correctness
Description
_remoteCommitFiles() base64-encodes file contents even though the official MCP tool requires raw
content and performs encoding itself, so newly created files contain base64 text rather than the
requested source. It also omits the blob sha required to update an existing file, making ordinary
edits fail.
Code

harness/mcp_clients/github.mjs[R123-126]

+      await this.remote.callTool('create_or_update_file', {
+        owner: this.owner, repo: this.repo, path: f.path, branch, message,
+        content: encodeBase64(f.content),
+      });
Evidence
The implementation explicitly base64-encodes every file and never supplies a SHA. The official
server source states that content must not be base64-encoded and that SHA is required when replacing
an existing file.

harness/mcp_clients/github.mjs[118-129]
🌐 The create_or_update_file schema says content must be supplied exactly as written and not base64-encoded; its description requires SHA for existing-file updates.

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Remote file creation sends encoded text and remote updates omit the required existing blob SHA.

## Issue Context
Use the server's batch file tool where possible; otherwise send raw content and resolve/pass each existing file's SHA before updating it.

## Fix Focus Areas
- harness/mcp_clients/github.mjs[118-129]
- harness/mcp_clients/github.mjs[151-153]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View high (6)
4. Multi-file commits are partial 🐞 Bug ☼ Reliability
Description
The remote commit_files implementation performs one persistent MCP mutation per file, so a failure
on a later file leaves earlier files committed even though the overall call rejects. Retrying the
outer operation then repeats already-applied writes and cannot preserve the local backend's
single-commit behavior.
Code

harness/mcp_clients/github.mjs[R122-126]

+    for (const f of files) {
+      await this.remote.callTool('create_or_update_file', {
+        owner: this.owner, repo: this.repo, path: f.path, branch, message,
+        content: encodeBase64(f.content),
+      });
Evidence
Each loop iteration invokes a remote mutation before the next begins, with no transaction or
rollback. By contrast, the local backend stages all files and creates one commit after the loop.

harness/mcp_clients/github.mjs[118-129]
harness/mcp_clients/local_git.mjs[92-103]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A failed remote multi-file commit leaves a partially modified branch and retries previously successful mutations.

## Issue Context
Replace the per-file loop with the GitHub MCP batch/push-files operation, or implement rollback/idempotency so the facade only reports failure without partial persistence.

## Fix Focus Areas
- harness/mcp_clients/github.mjs[118-129]
- harness/mcp_clients/github.mjs[79-92]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Binary changes disappear from review 🐞 Bug ≡ Correctness
Description
_remoteBranchDiff() drops every changed file whose comparison entry lacks patch, so binary
changes are completely absent from the text given to the pre-merge reviewer. A branch containing
only binary changes is incorrectly represented as an empty diff.
Code

harness/mcp_clients/github.mjs[R112-115]

+    return (parsed.files ?? [])
+      .map((f) => f.patch ? `diff --git a/${f.filename} b/${f.filename}\n${f.patch}` : '')
+      .filter(Boolean)
+      .join('\n');
Evidence
The map returns an empty string whenever f.patch is absent and the subsequent filter removes that
file. GitHub's comparison API explicitly documents that binary files have no patch property.

harness/mcp_clients/github.mjs[107-115]
🌐 GitHub documents that diffs containing binary data have no patch property.

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Remote branch diffs silently omit patchless changed files, including binary files.

## Issue Context
Emit a diff metadata record for every comparison entry and explicitly mark binary or unavailable patches so an empty result can only mean no changes.

## Fix Focus Areas
- harness/mcp_clients/github.mjs[103-116]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. File paths escape repository 🐞 Bug ⛨ Security
Description
commit_files() joins untrusted file paths to repoDir without checking containment, allowing
../ paths or in-tree symlinks to overwrite files outside the target repository. The later `git
add` failure does not undo the external write.
Code

harness/mcp_clients/local_git.mjs[R94-98]

+    for (const f of files) {
+      const abs = path.join(this.repoDir, f.path);
+      fs.mkdirSync(path.dirname(abs), { recursive: true });
+      fs.writeFileSync(abs, f.content);
+      this._git(['add', f.path]);
Evidence
The public facade forwards file objects unchanged, and the local backend writes `path.join(repoDir,
f.path)` before Git validates whether the path belongs to the work tree.

harness/mcp_clients/github.mjs[136-136]
harness/mcp_clients/local_git.mjs[92-100]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Agent-supplied paths can escape the local repository and overwrite arbitrary filesystem locations.

## Issue Context
Resolve and validate paths against the canonical repository root, reject absolute/traversal paths, and prevent writes through symlinks that leave the tree before creating or writing files.

## Fix Focus Areas
- harness/mcp_clients/local_git.mjs[92-100]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Diff failures look clean 🐞 Bug ≡ Correctness
Description
Both local diff methods suppress every Git failure and return stdout without checking status, so a
missing branch, malformed ref, or repository error is reported as an empty diff. The review stage
can consequently treat an unreadable branch as having no changes.
Code

harness/mcp_clients/local_git.mjs[R139-140]

+    const r = this._git(['diff', `${base}...${branch}`], { allowFail: true });
+    return r.stdout;
Evidence
PR and branch diff methods both opt out of _git error throwing and unconditionally return stdout;
_git otherwise has the error information and throws on nonzero status.

harness/mcp_clients/local_git.mjs[130-140]
harness/mcp_clients/local_git.mjs[182-190]
harness/mcp_clients/github.mjs[139-140]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Failed Git diff commands are indistinguishable from valid empty diffs.

## Issue Context
Do not use `allowFail` for review diffs; validate refs and propagate stderr/status so reviewers fail closed when changes cannot be read.

## Fix Focus Areas
- harness/mcp_clients/local_git.mjs[130-140]
- harness/mcp_clients/local_git.mjs[182-190]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Transport failures never retry 🐞 Bug ☼ Reliability
Description
Despite the advertised retry-on-transport-failure behavior, RateLimiter.run() retries only errors
whose message looks like an HTTP rate limit. MCP timeouts, server exits, and other transport
failures from MCPClient are immediately rethrown on the first attempt.
Code

harness/mcp_clients/ratelimit.mjs[R48-52]

+      } catch (err) {
+        lastErr = err;
+        if (!this._isRateLimit(err)) throw err;
+        const waitMs = this._retryAfterMs(err) ?? 2 ** attempt * 1000;
+        await sleep(waitMs);
Evidence
The MCP client produces timeout and process-exit errors, while the limiter's sole retry predicate
recognizes only rate-limit text. Any other caught error is rethrown before backoff.

harness/mcp_clients/client.mjs[40-40]
harness/mcp_clients/client.mjs[118-128]
harness/mcp_clients/ratelimit.mjs[41-67]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
MCP transport failures bypass the newly added retry/backoff mechanism.

## Issue Context
Introduce typed transport errors or error codes in MCPClient and retry only safe, transient transport failures with bounded backoff; preserve non-retryable tool errors.

## Fix Focus Areas
- harness/mcp_clients/client.mjs[40-40]
- harness/mcp_clients/client.mjs[118-131]
- harness/mcp_clients/ratelimit.mjs[41-67]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Spawn errors crash client 🐞 Bug ☼ Reliability
Description
connect() never registers a ChildProcess error listener, so an invalid or unavailable MCP
command emits an unhandled error event instead of rejecting connect() cleanly. The
spawned-process lifecycle also receives no cleanup when initialization rejects or times out.
Code

harness/mcp_clients/client.mjs[R38-40]

+    this._proc.stdout.on('data', (d) => this._onStdout(d));
+    this._proc.stderr.on('data', (d) => process.stderr.write(`[mcp] ${d}`));
+    this._proc.on('exit', (code) => this._rejectAll(new Error(`MCP server exited (${code})`)));
Evidence
Only stdout, stderr, and exit handlers are attached after spawn; there is no error handler or
catch/finally around initialization. Explicit cleanup exists in close() but is never invoked by a
failing connect().

harness/mcp_clients/client.mjs[33-48]
harness/mcp_clients/client.mjs[69-81]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
MCP server spawn failures can terminate the Node process, and failed initialization can leak the child.

## Issue Context
Attach an error handler before starting initialization, reject pending RPCs with that error, and close/kill/reset the process in a catch path around initialization.

## Fix Focus Areas
- harness/mcp_clients/client.mjs[33-48]
- harness/mcp_clients/client.mjs[69-81]
- harness/mcp_clients/client.mjs[110-116]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

10. MCPClient lacks tests 📜 Skill insight ▣ Testability
Description
The new MCPClient source file has no accompanying or existing test coverage; repository search
finds it only in its definition and production import. Its JSON-RPC handshake, timeout, parsing,
error, and shutdown behavior are therefore unverified.
Code

harness/mcp_clients/client.mjs[10]

+export class MCPClient {
Evidence
Rule 2905097 requires tests for every new source file. client.mjs is entirely new, while the added
MCP test imports and exercises LocalGitMCP, RateLimiter, and GitHubMCP but never imports or
directly exercises MCPClient; repository-wide search found no other test reference.

harness/mcp_clients/client.mjs[10-141]
harness/test/mcp.test.mjs[1-10]
Skill: security-review

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The newly added `MCPClient` implementation is not covered by tests.

## Issue Context
Rule 2905097 requires new source files to have accompanying test coverage. Add tests using a controlled stdio child process to verify initialization, tool calls, protocol errors, malformed output, timeouts, process exits, and close behavior.

## Fix Focus Areas
- harness/mcp_clients/client.mjs[10-141]
- harness/test/mcp.test.mjs[1-153]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


11. Backoff blocks unrelated calls 🐞 Bug ➹ Performance
Description
A rate-limited attempt sleeps before its finally releases the in-flight slot, so enough long
Retry-After responses occupy every slot and prevent unrelated operations from starting. With the
default capacity, 30 sleeping retries stall the entire facade for the full server-provided delay.
Code

harness/mcp_clients/ratelimit.mjs[R50-55]

+        if (!this._isRateLimit(err)) throw err;
+        const waitMs = this._retryAfterMs(err) ?? 2 ** attempt * 1000;
+        await sleep(waitMs);
+      } finally {
+        this._release();
+      }
Evidence
acquire() refuses work at the in-flight capacity, while the catch block awaits the complete
backoff before control reaches the finally that decrements _inFlight.

harness/mcp_clients/ratelimit.mjs[19-33]
harness/mcp_clients/ratelimit.mjs[41-57]
harness/mcp_clients/github.mjs[79-92]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Retry backoff incorrectly counts sleeping operations as active in-flight requests.

## Issue Context
Release the concurrency slot immediately after each attempt and perform backoff outside that slot before reacquiring for the next attempt.

## Fix Focus Areas
- harness/mcp_clients/ratelimit.mjs[19-33]
- harness/mcp_clients/ratelimit.mjs[41-57]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 8 rules
✅ Skills: security-review
✅ Web pages:
  +19 more
Review mode: ⚖️ Balanced: Downgraded extended -> standard: change is below the extended eligibility bar (hunks 5/18, lines 711/200; both must reach the floor). Router rationale: This introduces substantial, security-sensitive MCP/GitHub transport and mutation logic plus a separate git backend, retries, and diff semantics across several independent paths, making multiple subtle defects plausible.

Grey Divider

Tip of the day
💡 Did you know, you can hide the parts of a finding you never read, like the evidence or the agent prompt

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread harness/mcp_clients/github.mjs Outdated
getPRDiff(number) { return this.call('get_pull_request_diff', { number }, { tier: 'T0' }); }
getBranchDiff(branch, base = 'main') { return this.call('get_branch_diff', { branch, base }, { tier: 'T0' }); }
readFileAtRef(branch, filePath) { return this.call('read_file_at_ref', { branch, path: filePath }, { tier: 'T0' }); }
mergePR(number) { return this.call('merge_pull_request', { number }, { tier: 'T2' }); }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. mergepr bypasses approval gate 📜 Skill insight ⛨ Security

The new public mergePR() helper dispatches a protected merge directly, despite documentation
requiring merge-queue invocation only after approval. The tier: 'T2' value is not enforced because
call() only rate-limits and dispatches.
Agent Prompt
## Issue description
`GitHubMCP.mergePR()` can execute a merge without validating the required T2 policy decision and approval gate.

## Issue Context
The new facade documents that merges occur only through the merge queue after approval, but `call()` merely dispatches to the selected backend. Ensure merge execution fails closed unless a verified approval authorizes it, and align the merge action name with the policy tier registry.

## Fix Focus Areas
- harness/mcp_clients/github.mjs[74-92]
- harness/mcp_clients/github.mjs[142-142]
- harness/policy/tiers.mjs[47-55]
- harness/gate/approval_gate.mjs[53-99]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


import { spawn } from 'node:child_process';

export class MCPClient {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. mcpclient lacks tests 📜 Skill insight ▣ Testability

The new MCPClient source file has no accompanying or existing test coverage; repository search
finds it only in its definition and production import. Its JSON-RPC handshake, timeout, parsing,
error, and shutdown behavior are therefore unverified.
Agent Prompt
## Issue description
The newly added `MCPClient` implementation is not covered by tests.

## Issue Context
Rule 2905097 requires new source files to have accompanying test coverage. Add tests using a controlled stdio child process to verify initialization, tool calls, protocol errors, malformed output, timeouts, process exits, and close behavior.

## Fix Focus Areas
- harness/mcp_clients/client.mjs[10-141]
- harness/test/mcp.test.mjs[1-153]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread harness/mcp_clients/github.mjs Outdated
Comment on lines +90 to +100
const res = await this.remote.callTool(remoteName, this._translateArgs(tool, args));
return JSON.parse(findText(res) ?? 'null');
});
}

/** The GitHub MCP server names some args differently than our tool surface. */
_translateArgs(tool, args) {
if (tool === 'read_file_at_ref') {
return { owner: this.owner, repo: this.repo, path: args.path, ref: args.branch };
}
return args;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. Remote pr schema is wrong 🐞 Bug ≡ Correctness

createPR() sends {branch,title,body} unchanged, omitting owner, repo, and required base
while using branch instead of required head. The official GitHub MCP server therefore rejects
every remote pull-request creation call.
Agent Prompt
## Issue description
Remote facade arguments do not match the official GitHub MCP schemas, causing remote operations such as PR creation to fail validation.

## Issue Context
Inject repository identity and explicitly translate each internal tool's field names and required defaults instead of passing most argument objects through unchanged.

## Fix Focus Areas
- harness/mcp_clients/github.mjs[84-101]
- harness/mcp_clients/github.mjs[133-143]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread harness/mcp_clients/github.mjs Outdated
Comment on lines +123 to +126
await this.remote.callTool('create_or_update_file', {
owner: this.owner, repo: this.repo, path: f.path, branch, message,
content: encodeBase64(f.content),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

4. Remote commits corrupt content 🐞 Bug ≡ Correctness

_remoteCommitFiles() base64-encodes file contents even though the official MCP tool requires raw
content and performs encoding itself, so newly created files contain base64 text rather than the
requested source. It also omits the blob sha required to update an existing file, making ordinary
edits fail.
Agent Prompt
## Issue description
Remote file creation sends encoded text and remote updates omit the required existing blob SHA.

## Issue Context
Use the server's batch file tool where possible; otherwise send raw content and resolve/pass each existing file's SHA before updating it.

## Fix Focus Areas
- harness/mcp_clients/github.mjs[118-129]
- harness/mcp_clients/github.mjs[151-153]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread harness/mcp_clients/github.mjs Outdated
Comment on lines +122 to +126
for (const f of files) {
await this.remote.callTool('create_or_update_file', {
owner: this.owner, repo: this.repo, path: f.path, branch, message,
content: encodeBase64(f.content),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

5. Multi-file commits are partial 🐞 Bug ☼ Reliability

The remote commit_files implementation performs one persistent MCP mutation per file, so a failure
on a later file leaves earlier files committed even though the overall call rejects. Retrying the
outer operation then repeats already-applied writes and cannot preserve the local backend's
single-commit behavior.
Agent Prompt
## Issue description
A failed remote multi-file commit leaves a partially modified branch and retries previously successful mutations.

## Issue Context
Replace the per-file loop with the GitHub MCP batch/push-files operation, or implement rollback/idempotency so the facade only reports failure without partial persistence.

## Fix Focus Areas
- harness/mcp_clients/github.mjs[118-129]
- harness/mcp_clients/github.mjs[79-92]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +94 to +98
for (const f of files) {
const abs = path.join(this.repoDir, f.path);
fs.mkdirSync(path.dirname(abs), { recursive: true });
fs.writeFileSync(abs, f.content);
this._git(['add', f.path]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

7. File paths escape repository 🐞 Bug ⛨ Security

commit_files() joins untrusted file paths to repoDir without checking containment, allowing
../ paths or in-tree symlinks to overwrite files outside the target repository. The later `git
add` failure does not undo the external write.
Agent Prompt
## Issue description
Agent-supplied paths can escape the local repository and overwrite arbitrary filesystem locations.

## Issue Context
Resolve and validate paths against the canonical repository root, reject absolute/traversal paths, and prevent writes through symlinks that leave the tree before creating or writing files.

## Fix Focus Areas
- harness/mcp_clients/local_git.mjs[92-100]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread harness/mcp_clients/local_git.mjs Outdated
Comment on lines +139 to +140
const r = this._git(['diff', `${base}...${branch}`], { allowFail: true });
return r.stdout;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

8. Diff failures look clean 🐞 Bug ≡ Correctness

Both local diff methods suppress every Git failure and return stdout without checking status, so a
missing branch, malformed ref, or repository error is reported as an empty diff. The review stage
can consequently treat an unreadable branch as having no changes.
Agent Prompt
## Issue description
Failed Git diff commands are indistinguishable from valid empty diffs.

## Issue Context
Do not use `allowFail` for review diffs; validate refs and propagate stderr/status so reviewers fail closed when changes cannot be read.

## Fix Focus Areas
- harness/mcp_clients/local_git.mjs[130-140]
- harness/mcp_clients/local_git.mjs[182-190]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread harness/mcp_clients/ratelimit.mjs Outdated
Comment on lines +48 to +52
} catch (err) {
lastErr = err;
if (!this._isRateLimit(err)) throw err;
const waitMs = this._retryAfterMs(err) ?? 2 ** attempt * 1000;
await sleep(waitMs);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

9. Transport failures never retry 🐞 Bug ☼ Reliability

Despite the advertised retry-on-transport-failure behavior, RateLimiter.run() retries only errors
whose message looks like an HTTP rate limit. MCP timeouts, server exits, and other transport
failures from MCPClient are immediately rethrown on the first attempt.
Agent Prompt
## Issue description
MCP transport failures bypass the newly added retry/backoff mechanism.

## Issue Context
Introduce typed transport errors or error codes in MCPClient and retry only safe, transient transport failures with bounded backoff; preserve non-retryable tool errors.

## Fix Focus Areas
- harness/mcp_clients/client.mjs[40-40]
- harness/mcp_clients/client.mjs[118-131]
- harness/mcp_clients/ratelimit.mjs[41-67]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread harness/mcp_clients/client.mjs Outdated
Comment on lines +38 to +40
this._proc.stdout.on('data', (d) => this._onStdout(d));
this._proc.stderr.on('data', (d) => process.stderr.write(`[mcp] ${d}`));
this._proc.on('exit', (code) => this._rejectAll(new Error(`MCP server exited (${code})`)));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

10. Spawn errors crash client 🐞 Bug ☼ Reliability

connect() never registers a ChildProcess error listener, so an invalid or unavailable MCP
command emits an unhandled error event instead of rejecting connect() cleanly. The
spawned-process lifecycle also receives no cleanup when initialization rejects or times out.
Agent Prompt
## Issue description
MCP server spawn failures can terminate the Node process, and failed initialization can leak the child.

## Issue Context
Attach an error handler before starting initialization, reject pending RPCs with that error, and close/kill/reset the process in a catch path around initialization.

## Fix Focus Areas
- harness/mcp_clients/client.mjs[33-48]
- harness/mcp_clients/client.mjs[69-81]
- harness/mcp_clients/client.mjs[110-116]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread harness/mcp_clients/ratelimit.mjs Outdated
Comment on lines +50 to +55
if (!this._isRateLimit(err)) throw err;
const waitMs = this._retryAfterMs(err) ?? 2 ** attempt * 1000;
await sleep(waitMs);
} finally {
this._release();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

11. Backoff blocks unrelated calls 🐞 Bug ➹ Performance

A rate-limited attempt sleeps before its finally releases the in-flight slot, so enough long
Retry-After responses occupy every slot and prevent unrelated operations from starting. With the
default capacity, 30 sleeping retries stall the entire facade for the full server-provided delay.
Agent Prompt
## Issue description
Retry backoff incorrectly counts sleeping operations as active in-flight requests.

## Issue Context
Release the concurrency slot immediately after each attempt and perform backoff outside that slot before reacquiring for the next attempt.

## Fix Focus Areas
- harness/mcp_clients/ratelimit.mjs[19-33]
- harness/mcp_clients/ratelimit.mjs[41-57]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@Atul-Chahar

Copy link
Copy Markdown
Owner Author

All findings addressed (commit b2bc26e):

# Finding Resolution
1 mergePR bypasses approval gate mergePR/closeIssue now require {approvalId, decidedBy} from the gate — structurally impossible without it; execution audited. Covered by new facade test
2 MCPClient lacks tests New client.test.mjs with a controlled stdio fixture server: handshake, tool calls, isError, spawn failure, exit mid-call, timeout
3 Remote PR schema wrong Full _translateArgs: owner/repo injected, branch→head, number→pullNumber/issue_number, fromRef→from_branch, defaults explicit
4 Remote commits corrupt content Per-file create_or_update_file loop removed entirely — replaced by atomic push_files batch with raw content
5 Multi-file commits are partial Same fix: one batch commit; failure leaves the branch untouched, retries never re-apply partial mutations
6 Binary changes vanish from diffs Patchless entries emit explicit metadata records (status + additions/deletions); empty diff now means no changes
7 Paths escape repository _safePath: absolute/traversal rejection + per-component lstat walk catches real AND dangling symlinks (found via test: existsSync misses dangling links)
8 Diff failures look clean Review diffs fail closed — git errors propagate as exceptions, never as empty output
9 Transport failures never retry Typed MCPTransportError; rate limiter retries transport failures + rate limits, never tool errors
10 Spawn errors crash client error handler registered before handshake; failed init reaps the child and rejects pending RPCs cleanly
11 Backoff holds concurrency slot Slot released after every attempt; backoff sleeps outside the slot

58/58 tests green.

@Atul-Chahar
Atul-Chahar merged commit ca4a6e4 into main Aug 26, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP GitHub tool surface over local-git backend

2 participants