Skip to content

MCP hardening: nested-schema passthrough + hung-server timeout#124

Merged
gnanam1990 merged 1 commit into
mainfrom
mcp-hardening
Jun 7, 2026
Merged

MCP hardening: nested-schema passthrough + hung-server timeout#124
gnanam1990 merged 1 commit into
mainfrom
mcp-hardening

Conversation

@gnanam1990

@gnanam1990 gnanam1990 commented Jun 7, 2026

Copy link
Copy Markdown
Collaborator

Module — MCP hardening

Independent slice of the runtime-core split (off main). internal/mcp had zero drift on main, so this is a clean extract of the zenline MCP improvements. Deps (config, tools) are on main; uses tools.PropertySchema.Properties/Required from #119. No new deps.

What's in it

  • Nested-schema passthrough (schema.go) — when converting tools.PropertySchema ⇄ MCP JSON schema, serialize nested object Properties/Required (and object-typed Items) instead of flattening them, so tools with nested args are advertised faithfully to/from MCP hosts. (audit H6.)
  • Hung-server timeout (client.go/server.go) — bound the MCP stdio handshake/calls so a hung or non-responsive MCP server can't block the caller forever; surface a timeout instead. (audit H9; hang_test.go covers it.)
  • registry.go recognizes the hardened paths; registry_test.go/schema_test.go added.

Testing

go build ./..., go vet ./..., go test ./..., go test -race ./internal/mcp/, GOOS=windows go build ./... all green.

Companion to #123 (skills & plugins). Together these are the "extensibility" layer. The TUI shell + cli wiring remain blocked on the agent module.

Part of decomposing #101 (draft).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for complex nested parameter schemas, including arrays and objects.
  • Bug Fixes

    • Fixed connection hangs when MCP servers don't respond to initialization requests.
    • Fixed server shutdown hangs when peer connections become unresponsive.
    • Made tool registration atomic—all tools register successfully or none do if any validation fails.

Module of the runtime-core split (off main). internal/mcp had no drift on main, so this is a clean extract of the zenline MCP improvements. Deps (config, tools) are on main; uses tools.PropertySchema.Properties/Required from #119. No new deps.

- schema.go: serialize nested object Properties/Required (and object-typed Items) when converting tools.PropertySchema <-> MCP JSON schema, so tools with nested args are advertised faithfully to/from MCP hosts instead of being flattened (audit H6).

- client.go/server.go: bound MCP stdio handshake/calls so a hung or non-responsive MCP server can't block the caller forever; surface a timeout instead (audit H9). hang_test.go covers it.

- registry.go: recognize the hardened client/schema paths; registry_test/schema_test added.

build/vet/-race/full-suite + GOOS=windows build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Zero automated PR review

Verdict: No blockers found

Blockers

  • None found.

Validation

  • [pass] Diff hygiene: git diff --check
  • [pass] Tests: go test ./...
  • [pass] Build: go run ./cmd/zero-release build
  • [pass] Smoke build: go run ./cmd/zero-release smoke

Scope

Head: d1575b5cceb0
Changed files (7): internal/mcp/client.go, internal/mcp/hang_test.go, internal/mcp/registry.go, internal/mcp/registry_test.go, internal/mcp/schema.go, internal/mcp/schema_test.go, internal/mcp/server.go

This deterministic review checks validation status and basic diff hygiene. A human reviewer still owns product judgment and design quality.

@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR refactors the MCP client and server to handle concurrent requests and graceful cancellation, makes tool registration atomic with rollback semantics, and extends schema conversion to support nested JSON-schema structures.

Changes

MCP Concurrency, Registration Atomicity, and Schema Support

Layer / File(s) Summary
Client concurrent dispatch refactor
internal/mcp/client.go
Client adds a shared reader goroutine that dispatches JSON-RPC responses by id to per-request pending channels. New state: dispatchMu, readerOnce, pending map, readErr, readDone. The request method registers a pending channel and waits for response; Close calls failAll to unblock pending callers. Initialize uses a 30s timeout context for bounded handshake.
Server Serve() cancellation refactor
internal/mcp/server.go
Serve spawns a background goroutine for blocking reads and uses select to handle context cancellation immediately, returning ctx.Err() without hanging on blocked reader.
Cancellation and hang prevention tests
internal/mcp/hang_test.go
Introduces blockingReader helper and non-responsive stdio test process. Tests verify Connect fails fast under timeout, Client.request unblocks immediately on context cancellation (and doesn't hold client.mu), and Serve respects context cancellation even with a blocked reader.
Atomic tool registration with staging
internal/mcp/registry.go, internal/mcp/registry_test.go
RegisterTools stages validated tools and names locally, detecting conflicts against pre-existing and same-call staged tools. Commits to registry only after all servers succeed; failed registration rolls back everything registered in that call. Tests verify rollback and pre-existing tool preservation on failure.
Nested JSON-schema support
internal/mcp/schema.go, internal/mcp/schema_test.go
propertyToMCP emits nested items, properties, and required arrays. propertyFromMCP reconstructs Items and Properties fields from MCP input. Tests validate nested array-of-object conversion, round-trip preservation, and reconstruction accuracy.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Gitlawb/zero#64: The main PR's concurrent dispatch refactor (shared reader goroutine, pending-by-id, rpcMessageID parsing) directly builds on the MCP stdio client implementation introduced in PR #64.
  • Gitlawb/zero#65: Both PRs refactor internal/mcp/server.go's Serve read/cancellation shutdown behavior for the stdio JSON-RPC server.
  • Gitlawb/zero#67: Both PRs touch internal/mcp/client.go's Client.Close behavior around pending/blocked calls.

Suggested reviewers

  • anandh8x
  • Vasanthdev2004
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'MCP hardening: nested-schema passthrough + hung-server timeout' clearly and specifically summarizes the two main changes: schema handling improvements and timeout fixes for unresponsive servers.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mcp-hardening

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
internal/mcp/hang_test.go (2)

86-130: 💤 Low value

Silence errcheck warning on deferred Close.

The linter flags the unchecked reader.Close() return value. In test cleanup this is harmless, but can be silenced:

-	defer reader.Close()
+	defer func() { _ = reader.Close() }()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/mcp/hang_test.go` around lines 86 - 130, Replace the bare deferred
call to reader.Close() in TestClientRequestUnblocksOnContextCancel with a
suppressed-error wrapper so the linter stops complaining; e.g. change defer
reader.Close() to defer func() { _ = reader.Close() }() (locate the call near
newBlockingReader()/reader variable at the top of the test).

134-156: 💤 Low value

Same errcheck suppression.

-	defer reader.Close()
+	defer func() { _ = reader.Close() }()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/mcp/hang_test.go` around lines 134 - 156, The test still has the
same unchecked-error issue in its cleanup path. Update
TestServeReturnsOnContextCancelWithBlockingReader to handle or explicitly
suppress the error returned by reader.Close() in the deferred cleanup, using the
same approach already requested elsewhere, so the blockingReader shutdown is
intentional and errcheck-clean.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/mcp/hang_test.go`:
- Around line 86-130: Replace the bare deferred call to reader.Close() in
TestClientRequestUnblocksOnContextCancel with a suppressed-error wrapper so the
linter stops complaining; e.g. change defer reader.Close() to defer func() { _ =
reader.Close() }() (locate the call near newBlockingReader()/reader variable at
the top of the test).
- Around line 134-156: The test still has the same unchecked-error issue in its
cleanup path. Update TestServeReturnsOnContextCancelWithBlockingReader to handle
or explicitly suppress the error returned by reader.Close() in the deferred
cleanup, using the same approach already requested elsewhere, so the
blockingReader shutdown is intentional and errcheck-clean.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: afb30637-52b6-4c6b-b579-4f3b77d76341

📥 Commits

Reviewing files that changed from the base of the PR and between f3463d6 and d1575b5.

📒 Files selected for processing (7)
  • internal/mcp/client.go
  • internal/mcp/hang_test.go
  • internal/mcp/registry.go
  • internal/mcp/registry_test.go
  • internal/mcp/schema.go
  • internal/mcp/schema_test.go
  • internal/mcp/server.go

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Verdict: approved.

The MCP timeout/dispatch changes look good and the CI smoke suite is green. I left one small hardening note around fractional JSON-RPC ids, but I do not think it should block this PR.

Comment thread internal/mcp/client.go
return typed, true
case int64:
return int(typed), true
case float64:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Small hardening follow-up: this accepts any float64 id by truncating it, so a malformed response id like 1.9 would match pending request 1. JSON-RPC numeric ids should be integral for our dispatcher; consider rejecting non-integral or out-of-range float values here, similar to the integer parsing used elsewhere.

@gnanam1990 gnanam1990 merged commit 0da2ecf into main Jun 7, 2026
6 checks passed
@Vasanthdev2004 Vasanthdev2004 deleted the mcp-hardening branch June 28, 2026 08:27
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.

2 participants