fix(mcp): never marshal nil citations slice as JSON null#74
Open
sourcehawk wants to merge 1 commit into
Open
Conversation
Citation-bearing tool outputs (analyze_change, correlate_with_findings, draft_pr, summarize_thread, analyze_channel) declare a non-nullable `citations` array in their auto-generated MCP output schema. A nil Go slice marshals to JSON `null`, which the go-sdk's own output validation rejects with `validating tool output: ... has type "null", want "array"`, masking the real result with an opaque error. Two nil sources: citations.Run left Citations nil on its soft-fail returns, and the handlers' early-return paths built output structs with a nil slice. The SDK validates the typed Out whenever the handler's Go error return is nil, so every return path was affected — and since these handlers report failures via errorResult (nil Go error), the early returns were validated too. Enforce one invariant: a citation-bearing Out never marshals a nil slice. Fix the shared citations.Run soft-fail paths plus the per-handler early-return literals. Add in-memory wire tests that exercise the real SDK output validation (handler unit tests bypass it); they reproduce the exact error pre-fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Fixes MCP tool failures where citations was being marshaled as JSON null (from a nil Go slice) even though the auto-generated output schema requires a non-nullable array, causing the SDK to replace real tool results with an opaque output-schema validation error. This PR enforces the invariant that citation-bearing outputs always marshal citations as [].
Changes:
- Ensure
pkg/mcp/citations.Runsoft-fail outcomes always returnCitations: []Citation{}(never nil). - Initialize
Citationsto a non-nil empty slice on early-return/error-result paths in git + slack citation-bearing tools. - Add in-memory “wire” tests that exercise the go-sdk’s real output-schema validation, and strengthen runner soft-fail tests to assert
Citationsis non-nil.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/mcp/citations/runner.go | Sets Citations to a non-nil empty slice in soft-fail outcomes to prevent JSON null. |
| pkg/mcp/citations/runner_test.go | Strengthens soft-fail assertions to pin nil-vs-empty behavior (NotNil + Empty). |
| pkg/mcp/git/tools_subagent.go | Initializes Citations on early-return error-result paths for citation-bearing git tools. |
| pkg/mcp/git/tools_draft_pr.go | Initializes draftPR output Citations up front so early returns never marshal null. |
| pkg/mcp/git/tools_subagent_wire_test.go | Adds in-memory transport tests to reproduce/guard the SDK output-schema validation failure. |
| pkg/mcp/slack/tool_summarize.go | Initializes Citations on early-return/error-result paths to avoid schema failures. |
| pkg/mcp/slack/tool_analyze.go | Same as above for analyze_channel. |
| pkg/mcp/slack/tools_subagent_wire_test.go | Adds in-memory transport test covering slack tools’ early-return paths through SDK validation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Citation-bearing MCP tool outputs were failing with an opaque output-schema error instead of returning their result:
Affected tools:
analyze_change,correlate_with_findings(git),draft_pr(git),summarize_thread,analyze_channel(slack).Root cause
The go-sdk auto-generates each tool's output schema from its typed
Outstruct. A field likeCitations []citations.Citation(json"citations", noomitempty) becomes a non-nullable array. A nil Go slice marshals to JSONnull, which the SDK's server-side output validation rejects — replacing the real result (even anerrorResult) with the error above.Two nil sources fed this:
citations.RunleftCitationsnil on its soft-fail returns (runner.go:69,:85). When a sub-agent can't produce a valid citations block after the corrective retry, the graceful soft-fail outcome carried a nil slice. This is the path that surfaces in normal use."ref and question are required", clone failure,"channel_id is required", …) built output structs with a nilCitations. The SDK marshals + validates the typedOutwhenever the handler's Goerrorreturn is nil (mcp/server.go~L330-372), and these handlers report failures viaerrorResultwith a nil Go error — so the early returns were validated too.The existing soft-fail test asserted with
assert.Empty, which passes for bothniland[], so the nil-vs-empty distinction was never pinned. Handler unit tests also call the method directly, bypassing the SDK's validation entirely.Fix
Enforce one invariant: a citation-bearing
Outnever marshals a nil slice.pkg/mcp/citations/runner.go— both soft-fail returns setCitations: []Citation{}. Shared chokepoint: fixes the soft-fail path for all five tools.tools_subagent.go/tools_draft_pr.go, slacktool_analyze.go/tool_summarize.go— early-return literals initializeCitationstoo.Tests
NewInMemoryTransports+CallTool) exercise the real SDK output validation that handler unit tests bypass. They reproduce the exact transcript error pre-fix.citations.Runsoft-fail tests to assertNotNil(not justEmpty)..gochanges: all 5 tests failed with the literal... has type "null", want "array"error, then passed once restored.go test -race ./...clean;golangci-lint0 issues. Frontend untouched.🤖 Generated with Claude Code