Skip to content

Share MCPServerStatsBase across duplicated MCP server health/stats structs - #53693

Merged
pelikhan merged 5 commits into
mainfrom
copilot/deep-report-apply-aggregatedsummarybase-pattern
Aug 18, 2026
Merged

Share MCPServerStatsBase across duplicated MCP server health/stats structs#53693
pelikhan merged 5 commits into
mainfrom
copilot/deep-report-apply-aggregatedsummarybase-pattern

Conversation

Copilot AI commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Three per-server MCP report structs in pkg/cli modeled the same "server name + call count + error count" shape with four different field spellings (TotalCalls / ToolCalls / ToolCallCount, TotalErrors / ErrorCount). This applies the AggregatedSummaryBase composition pattern already used by MissingToolSummary / MCPFailureSummary.

Shared base

  • Added MCPServerStatsBase in pkg/cli/logs_models.go, adjacent to AggregatedSummaryBase.
  • Embedded in MCPServerStats (audit_report.go), MCPServerHealthDetail (audit_expanded.go) and MCPServerCrossRunHealth (audit_cross_run.go); call sites and tests updated to the standardized names.

Wire format preserved

  • MCPServerHealthDetail and MCPServerCrossRunHealth get MarshalJSON overrides emitting their original keys (tool_calls, total_calls, total_errors) — same technique as MCPFailureSummary.
  • The base retains MCPServerStats's error_count,omitempty tags, since it is the only embedder serializing base tags directly.
  • New tests assert the full key set for both overriding types, including the zero-error_count case.
type MCPServerStatsBase struct {
	ServerName    string `json:"server_name" console:"header:Server"`
	ToolCallCount int    `json:"tool_call_count" console:"header:Tool Calls"`
	ErrorCount    int    `json:"error_count,omitempty" console:"header:Errors,omitempty"`
}

type MCPServerCrossRunHealth struct {
	MCPServerStatsBase
	RunsConnected int     `json:"runs_connected"`
	TotalRuns     int     `json:"total_runs"`
	ErrorRate     float64 `json:"error_rate"`
	Unreliable    bool    `json:"unreliable"`
}

Out of scope

MCPServerHealth (also named in the issue) is left unchanged: it has no server name, and its TotalRequests / TotalErrors are rollups over the Servers slice rather than per-server stats, so the base does not apply.

Note: console:"omitempty" is only honored by RenderStruct, not by table rendering, so relocating the Errors column in MCPServerStats does not change table output beyond column order. pkg/cli/README.md's type table was updated to match.

@github-actions

Copy link
Copy Markdown
Contributor

Hey @pelikhan 👋 — thanks for driving this refactoring via the Copilot agent! This draft is well-scoped and follows the established process for core team agentic development.

The PR is currently in draft status with a clear checklist. Here's what's needed to move forward:

  • Implement the changes — Apply AggregatedSummaryBase pattern to MCPServerStats, MCPServerHealthDetail, and MCPServerCrossRunHealth as outlined in the checklist.
  • Add/update tests — Ensure the refactored structs have test coverage (JSON marshaling, field validation).
  • Update documentation — Mark the checklist items as complete once changes are in place.
  • Format and verify — Run make fmt and the targeted test suite.

Once the implementation is complete and tests pass, mark the PR as ready for review.

If you'd like assistance from your coding agent, you can assign this:

Apply the AggregatedSummaryBase pattern to the MCP server health/stats structs as described in github.com/github/gh-aw/issues/53689:
1. Create MCPServerStatsBase in pkg/cli/logs_models.go with ServerName, ToolCallCount, ErrorCount fields
2. Embed it in MCPServerStats, MCPServerHealthDetail, MCPServerCrossRunHealth
3. Use MarshalJSON to preserve existing JSON schema where key names differ
4. Update all call sites and tests
5. Run make fmt and verify tests pass

Generated by ✅ Contribution Check · auto · 50.5 AIC · ⌖ 5.19 AIC · ⊞ 9.2K ·

Copilot AI and others added 2 commits August 18, 2026 13:16
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Apply AggregatedSummaryBase pattern to MCP server health and stats structs Share MCPServerStatsBase across duplicated MCP server health/stats structs Aug 18, 2026
Copilot AI requested a review from pelikhan August 18, 2026 13:19
@pelikhan
pelikhan marked this pull request as ready for review August 18, 2026 13:21
Copilot AI balanced review requested due to automatic review settings August 18, 2026 13:21

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

Shares common MCP server identity and usage fields through MCPServerStatsBase while preserving existing JSON output schemas.

Changes:

  • Embeds the shared base across three MCP reporting types.
  • Updates aggregation, rendering, and tests to standardized field names.
  • Adds custom JSON marshaling and schema-preservation tests.
Show a summary per file
File Description
pkg/cli/README.md Updates generated type documentation.
pkg/cli/logs_usage_activity.go Populates the shared base.
pkg/cli/logs_models.go Defines MCPServerStatsBase.
pkg/cli/logs_mcp_tool_usage_test.go Updates MCP usage fixtures.
pkg/cli/gateway_logs_mcp.go Builds stats using the base.
pkg/cli/audit_report.go Embeds the base in server stats.
pkg/cli/audit_expanded.go Refactors health details and JSON encoding.
pkg/cli/audit_expanded_test.go Tests health-detail JSON output.
pkg/cli/audit_cross_run.go Refactors cross-run health and JSON encoding.
pkg/cli/audit_cross_run_test.go Updates fixtures and JSON tests.
pkg/cli/audit_cross_run_render.go Uses standardized count fields.

Review details

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

  • Files reviewed: 11/11 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment on lines +92 to +110
func (h MCPServerCrossRunHealth) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
ServerName string `json:"server_name"`
RunsConnected int `json:"runs_connected"`
TotalRuns int `json:"total_runs"`
TotalCalls int `json:"total_calls"`
TotalErrors int `json:"total_errors"`
ErrorRate float64 `json:"error_rate"`
Unreliable bool `json:"unreliable"`
}{
ServerName: h.ServerName,
RunsConnected: h.RunsConnected,
TotalRuns: h.TotalRuns,
TotalCalls: h.ToolCallCount,
TotalErrors: h.ErrorCount,
ErrorRate: h.ErrorRate,
Unreliable: h.Unreliable,
})
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a matching UnmarshalJSON on MCPServerCrossRunHealth in 9611a0f, plus a round-trip assertion in TestMCPServerCrossRunHealthJSONSchema.

Comment thread pkg/cli/audit_expanded.go
Comment on lines +101 to +121
func (d MCPServerHealthDetail) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
ServerName string `json:"server_name"`
RequestCount int `json:"request_count"`
ToolCalls int `json:"tool_calls"`
ErrorCount int `json:"error_count"`
ErrorRate float64 `json:"error_rate"`
ErrorRateStr string `json:"error_rate_str"`
AvgLatency string `json:"avg_latency"`
Status string `json:"status"`
}{
ServerName: d.ServerName,
RequestCount: d.RequestCount,
ToolCalls: d.ToolCallCount,
ErrorCount: d.ErrorCount,
ErrorRate: d.ErrorRate,
ErrorRateStr: d.ErrorRateStr,
AvgLatency: d.AvgLatency,
Status: d.Status,
})
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a matching UnmarshalJSON on MCPServerHealthDetail in 9611a0f, plus a round-trip assertion in TestMCPServerHealthDetailJSONSchema.

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

Test Quality Sentinel skipped because pre-fetch PR data was unavailable: unable to fetch test file diff

🧪 Test quality analysis by Test Quality Sentinel

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

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

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Ponytail Reviewer completed successfully!

Generated by Ponytail Reviewer for #53693

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

🔎 Code quality review by PR Code Quality Reviewer

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

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

🏗️ ADR gate enforced by Design Decision Gate 🏗️

@github-actions

Copy link
Copy Markdown
Contributor

Comment Memory

reviewed_at: 2026-08-18T00:00:00Z
review_event: REQUEST_CHANGES
top_themes:
  - one-way JSON compatibility regressions in MCP report structs
  - missing round-trip coverage for custom wire-schema shims
files_reviewed:
  - pkg/cli/audit_cross_run.go
  - pkg/cli/audit_cross_run_render.go
  - pkg/cli/audit_cross_run_test.go
  - pkg/cli/audit_expanded.go
  - pkg/cli/audit_expanded_test.go
  - pkg/cli/audit_report.go
  - pkg/cli/gateway_logs_mcp.go
  - pkg/cli/logs_mcp_tool_usage_test.go
  - pkg/cli/logs_models.go
  - pkg/cli/logs_usage_activity.go
  - pkg/cli/README.md
comment_count: 2

Note

This comment is managed by comment memory.

It stores persistent context for this thread in the code block at the top of this comment.
Edit only the text inside the backtick fences; workflow metadata and the footer are regenerated automatically.

Learn more about comment memory

🔎 Code quality review by PR Code Quality Reviewer · gpt54 · 16.5 AIC · ⌖ 9.09 AIC · ⊞ 7K ·
Comment /review to run again

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

Request changes

This refactor keeps the old JSON keys only on the write path and breaks round-tripping on the read path.

Blocking themes
  • MCPServerCrossRunHealth now marshals total_calls / total_errors, but unmarshaling that same payload back into the struct drops both values.
  • MCPServerHealthDetail has the same regression for tool_calls.

These are wire-compatibility regressions, not just internal renames: persisted reports and downstream readers will silently lose counts after deserialization.

🔎 Code quality review by PR Code Quality Reviewer · gpt54 · 16.5 AIC · ⌖ 9.09 AIC · ⊞ 7K
Comment /review to run again


// MarshalJSON preserves the cross-run MCP health JSON schema while sharing the
// per-server stat fields with the other MCP server report types.
func (h MCPServerCrossRunHealth) MarshalJSON() ([]byte, error) {

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.

This refactor only preserves the legacy wire keys when encoding; any existing JSON consumers that unmarshal total_calls/total_errors back into MCPServerCrossRunHealth will now silently drop those fields, so round-tripping the report loses the call/error counts.

💡 Why this blocks merge

MarshalJSON emits total_calls and total_errors, but the struct now only has embedded fields tagged as tool_call_count / error_count. Without a matching UnmarshalJSON, json.Unmarshal ignores the legacy keys and leaves ToolCallCount/ErrorCount at zero.

That is a real compatibility break for any code that deserializes stored reports, even though the encoder tests pass.

Suggested fix:

func (h *MCPServerCrossRunHealth) UnmarshalJSON(data []byte) error {
    var aux struct {
        ServerName    string  `json:"server_name"`
        RunsConnected int     `json:"runs_connected"`
        TotalRuns     int     `json:"total_runs"`
        TotalCalls    int     `json:"total_calls"`
        TotalErrors   int     `json:"total_errors"`
        ErrorRate     float64 `json:"error_rate"`
        Unreliable    bool    `json:"unreliable"`
    }
    if err := json.Unmarshal(data, &aux); err != nil { return err }
    h.MCPServerStatsBase = MCPServerStatsBase{ServerName: aux.ServerName, ToolCallCount: aux.TotalCalls, ErrorCount: aux.TotalErrors}
    h.RunsConnected, h.TotalRuns, h.ErrorRate, h.Unreliable = aux.RunsConnected, aux.TotalRuns, aux.ErrorRate, aux.Unreliable
    return nil
}

Add a round-trip test (Marshal + Unmarshal) so this regression stays covered.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 9611a0f: added UnmarshalJSON on MCPServerCrossRunHealth mapping total_calls/total_errors back into the embedded base, plus a round-trip test.

Comment thread pkg/cli/audit_expanded.go

// MarshalJSON preserves the MCP server health detail JSON schema while sharing the
// per-server stat fields with the other MCP server report types.
func (d MCPServerHealthDetail) MarshalJSON() ([]byte, error) {

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.

MCPServerHealthDetail has the same one-way compatibility break: the custom marshaler still emits tool_calls, but decoding now relies on the embedded base tag tool_call_count, so any round-trip through JSON zeros the per-server tool count.

💡 Why this blocks merge

This type is part of the audit report payload (servers under mcp_server_health). After this change, json.Marshal still produces the old schema, but json.Unmarshal of that same JSON back into MCPServerHealthDetail will ignore tool_calls and keep ToolCallCount == 0.

That means persisted reports and any downstream consumer that deserializes them lose data without an error, which is worse than an explicit schema change.

Suggested fix: mirror the MarshalJSON shim with an UnmarshalJSON that reads tool_calls into ToolCallCount, then add a round-trip test covering the zero and non-zero cases.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 9611a0f: added UnmarshalJSON on MCPServerHealthDetail mapping tool_calls back into the embedded base, plus a round-trip test.

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

This is a clean refactoring that reduces duplication across MCPServerStats, MCPServerHealthDetail, and MCPServerCrossRunHealth by extracting the shared ServerName/ToolCallCount/ErrorCount fields into MCPServerStatsBase.

The MarshalJSON overrides on MCPServerCrossRunHealth and MCPServerHealthDetail correctly preserve the legacy JSON key names (total_calls, total_errors, tool_calls) for downstream consumers. The schema-preservation tests (TestMCPServerCrossRunHealthJSONSchema, TestMCPServerHealthDetailJSONSchema, TestMCPServerHealthDetailJSONKeepsZeroErrorCount) are a good safeguard.

Existing inline comments already flag the decode asymmetry (encode → legacy keys; decode → new field names). Since these structs are report-output types and are never round-tripped through their own JSON, this is low risk in practice.

No blocking issues found.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 30.6 AIC · ⌖ 8.82 AIC · ⊞ 5.7K

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

Ponytail review: over-engineering only.

Two of the three MCP struct consolidations require a full MarshalJSON override (~20 lines each, plus new dedicated schema tests) to keep the old wire format, because the shared base's JSON tags don't match those types. That's roughly as much new code as the duplication it removes, and the goal — collapsing 4 field spellings into 1 — could largely be achieved by sharing just ServerName (or accepting the JSON key rename) without the override machinery.

net: -40 lines possible.

Generated by ✂️ Ponytail Reviewer for #53693 · auto · 32.3 AIC · ⌖ 3.94 AIC · ⊞ 7.3K
Comment /ponytail to run again


// MarshalJSON preserves the cross-run MCP health JSON schema while sharing the
// per-server stat fields with the other MCP server report types.
func (h MCPServerCrossRunHealth) MarshalJSON() ([]byte, error) {

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.

L92-113: yagni: 20-line MarshalJSON override just to rename ToolCallCount->total_calls and ErrorCount->total_errors for JSON output. Two of three embedders need this bypass, undercutting the shared-base rationale. Simpler: keep TotalCalls/TotalErrors as this type own fields (as before) and only pull ServerName into the base.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Keeping the shared base with a marshal/unmarshal shim rather than reverting to type-local TotalCalls/TotalErrors fields, since the goal is a single shared representation (MCPServerStatsBase) for all three server-stat types, with the shim only needed to preserve each type's existing wire format.

Comment thread pkg/cli/audit_expanded.go

// MarshalJSON preserves the MCP server health detail JSON schema while sharing the
// per-server stat fields with the other MCP server report types.
func (d MCPServerHealthDetail) MarshalJSON() ([]byte, error) {

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.

L101-122: yagni: same pattern - a 22-line MarshalJSON override to rename ToolCallCount->tool_calls, needed only because the shared base JSON tags do not match this type wire format. Two overrides (here and in audit_cross_run.go) plus dedicated schema tests are pure overhead introduced by the base; scoping the base to ServerName only would avoid both.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same rationale as the audit_cross_run.go thread — keeping the shared MCPServerStatsBase and the marshal/unmarshal shim so all three types genuinely share one struct, rather than reverting to per-type duplicated fields.

@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 /codebase-design and /tdd — requesting changes on three targeted issues.

📋 Key Themes & Highlights

Key Themes

  • Encode/decode asymmetry (2 existing Copilot comments + my comment on audit_cross_run.go): MarshalJSON overrides emit legacy keys (total_calls, tool_calls) that the default decoder can't map back into the base fields. The types are effectively write-only; that constraint should be documented or enforced with round-trip tests.
  • Base-field omitempty footgun (logs_models.go): ErrorCount's omitempty tag satisfies MCPServerStats but silently constrains every future embedder. The obligation to override MarshalJSON needs to be visible at the type level.
  • Incomplete schema-override test assertions (audit_cross_run_test.go): Tests check that override keys exist but not that base-field keys don't appear, leaving a class of regressions (e.g. pointer-vs-value receiver flip) uncaught.

Positive Highlights

  • ✅ Clean application of the existing AggregatedSummaryBase composition pattern — consistent with the broader codebase vocabulary.
  • ✅ Wire-format preservation via MarshalJSON anonymous-struct idiom is the right approach (same as MCPFailureSummary).
  • ✅ New tests cover the zero-error_count edge case — good instinct.
  • MCPServerHealth exclusion is well-reasoned in the PR description.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 · 44.1 AIC · ⌖ 10.3 AIC · ⊞ 7.8K
Comment /matt to run again

Comments that could not be inline-anchored

pkg/cli/logs_models.go:582

[/codebase-design] omitempty in the base struct leaks an embedder-specific serialisation concern into the shared type — a footgun for future embedders.

<details>
<summary>💡 Why this matters and how to document it</summary>

A future embedder that wants error_count to always appear in JSON output will silently inherit the omit-when-zero behaviour and will need to remember to override MarshalJSON. The inline comment captures intent but sits inside the struct where it's easy to miss. E…

pkg/cli/audit_cross_run.go:110

[/codebase-design] The MarshalJSON override is encode-only: json.Unmarshal into MCPServerCrossRunHealth will map tool_call_count (not total_calls) — so a round-trip of your own JSON output drops the call count silently.

<details>
<summary>💡 Impact and options</summary>

If MCPServerCrossRunHealth values are ever read back from stored JSON (e.g. audit report files), the total_calls key written by MarshalJSON will be ignored on decode and ToolCallCount will be zero. Same i…

pkg/cli/audit_cross_run_test.go:178

[/tdd] The JSON schema tests verify the marshal output keys but don't assert the full key set — tool_call_count (the base field's own key) could appear alongside tool_calls if the override breaks, and the test wouldn't catch it.

<details>
<summary>💡 Add a negative assertion</summary>

// In TestMCPServerCrossRunHealthJSONSchema, after existing asserts:
assert.NotContains(t, decoded, &quot;tool_call_count&quot;,
    &quot;base field key must not leak into output; override should replace it&quot;)
as</details>

…tern

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (241 new lines in pkg/cli/) but does not have a linked Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/53693-share-mcpserverstatsbase-across-mcp-server-structs.md — review and complete it before merging.

🔒 This PR cannot merge until an ADR is linked in the PR body.

📋 What to do next
  1. Review the draft ADR committed to your branch — it was generated from the PR diff
  2. Complete the missing sections — add context the AI couldn’t infer, refine the decision rationale, and list real alternatives you considered
  3. Commit the finalized ADR to docs/adr/ on your branch
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-53693: Share MCPServerStatsBase Across MCP Server Health/Stats Structs

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

❓ Why ADRs Matter

“AI made me procrastinate on key design decisions. Because refactoring was cheap, I could always say ‘I’ll deal with this later.’ Deferring decisions corroded my ability to think clearly.”

ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you.

📋 Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered complete:

  • Context — What is the problem? What forces are at play?
  • Decision — What did you decide? Why?
  • Alternatives Considered — What else could have been done?
  • Consequences — What are the trade-offs (positive and negative)?

All ADRs are stored in docs/adr/ as Markdown files numbered by PR number (e.g., 0042-use-postgresql.md for PR #42).

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · sonnet46 · 79.8 AIC · ⌖ 22.8 AIC · ⊞ 9.1K ·
Comment /review to run again

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
@pelikhan
pelikhan merged commit 9e8c993 into main Aug 18, 2026
25 of 26 checks passed
@pelikhan
pelikhan deleted the copilot/deep-report-apply-aggregatedsummarybase-pattern branch August 18, 2026 17:32
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.

[deep-report] Apply AggregatedSummaryBase pattern to 4-way duplicated MCP server health/stats structs

3 participants