Skip to content

fix(mcp): return typed structured results from every tool - #750

Merged
tylerkron merged 7 commits into
mainfrom
cursor/mcp-use-structured-content-c623
Sep 28, 2026
Merged

tylerkron merged 7 commits into
mainfrom
cursor/mcp-use-structured-content-c623

Conversation

@tylerkron

@tylerkron tylerkron commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

What was wrong

Every tool returned its result only as a blob of JSON text. MCP clients that consume typed results (scripts, agent frameworks, anything that wants to read sampleRateHz without parsing prose) got no outputSchema in tools/list and no structuredContent in the call result. They had to guess the shape of each response and parse the text themselves.

How it was fixed

Every tool now sets UseStructuredContent = true, next to the safety hints from #744 (descriptions are #761's, unchanged). The server then:

  • advertises an outputSchema for each tool in tools/list. The schema is always an object: the spec requires that, so list and string results are wrapped as {"result": ...}.
  • returns the typed result in structuredContent alongside the same JSON text block.

Null fields are now written out ("latestVersion": null) rather than omitted. The generated schema lists every record property as required, with nullable ones typed ["T","null"], but the SDK's default serializer drops null properties. Without this change, 13 of the 26 tools could return a result with a required key missing. Examples: get_server_info under --no-version-check, configure_analog_channels whenever the rate was not lowered, and list_analog_outputs before any write. A client that validates results against the schema, such as the official TypeScript SDK client, would reject those calls. Tools are now registered through a shared WithDaqifiTools() that passes the SDK's default serializer options with DefaultIgnoreCondition = Never. This one setting covers both structuredContent and the text block.

Tests

  • ToolStructuredContentContractTests builds the tools the way the server does and checks that each one advertises an object output schema. It fails if a tool is added without a row in its table.
  • NullField_IsStillWritten_SoTheResultMatchesItsOutputSchema runs get_server_info (version check off) through the SDK's own invoke path. It asserts that every key the schema requires is in structuredContent and that latestVersion is an explicit null. Setting the ignore condition back to the SDK default makes this test fail.
  • Both contract-test harnesses (this one and fix(mcp): set accurate tool ReadOnly/Destructive/OpenWorld hints #744's ToolAnnotationContractTests) now register through WithDaqifiTools(), the same call Program.cs makes.

Verification

  • Ran the built server over stdio. All 26 tools advertise an object outputSchema. get_server_info returns structuredContent that matches its schema, including latestVersion: null. list_connected_devices returns {"result": []}. disconnect_device returns {"result": "..."} plus the plain-text block. Errors still come back as isError text with no structuredContent.
  • tools/list is byte-identical with and without the serializer change. Input and output schemas are unaffected; only result serialization changes.
  • Full suite green locally (net9 + net10). MCP-only change; no hardware needed.

Notes

  • Visible change for text-only clients: null fields now appear as "field": null in the text block instead of being left out. The tool descriptions already say these fields "come back null", so the text now matches them.
  • tools/list grows by ~10 KB (the schemas). Clients that forward output schemas to the model pay that in context once per session.
  • Once this lands, the output schemas are part of the tool contract: adding, removing or renaming a result field changes what tools/list advertises. (fix(mcp): drop dead digital sampleRateAdjustedFromHz #789 already removed the always-null ConfigureDigitalResult.SampleRateAdjustedFromHz, so that field is never advertised.)
  • A tool added later needs UseStructuredContent = true and a row in both contract-test tables; the completeness tests fail otherwise.

🤖 Generated with Claude Code

@tylerkron
tylerkron marked this pull request as ready for review September 17, 2026 10:14
@tylerkron
tylerkron requested a review from a team as a code owner September 17, 2026 10:14
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Enable structured content for all MCP tools

🐞 Bug fix 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Enable output schemas and typed responses for all 26 MCP tools.
• Prevent MCP SDK 2.2 from degrading tool results into text-only content blocks.
• Add contract coverage enforcing schemas and complete registration for every advertised tool.
Diagram

sequenceDiagram
    actor Client as MCP Client
    participant Server as MCP Server
    participant SDK as MCP SDK 2.2
    participant Tools as DAQiFi Tools
    Client->>Server: tools/list
    Server->>SDK: Discover tools
    SDK->>Tools: Read attributes
    Tools-->>SDK: Structured flag
    SDK-->>Server: Output schemas
    Server-->>Client: Advertised schemas
    Client->>Server: tools/call
    Server->>Tools: Invoke tool
    Tools-->>SDK: Typed result
    SDK-->>Server: structuredContent
    Server-->>Client: Structured response
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Discover contract cases dynamically
  • ➕ Automatically tests every attributed tool without maintaining a separate name table.
  • ➕ Reduces duplication between production attributes and test data.
  • ➖ Would not detect accidental tool removal or renaming.
  • ➖ Provides a less explicit inventory of the expected public tool surface.

Recommendation: Keep the PR's explicit attribute configuration and expected-tool table. The SDK requires the flag at tool registration, while the table verifies both structured schemas and the intended public tool inventory; dynamic discovery alone would miss accidental removals.

Files changed (2) +133 / -26

Bug fix (1) +26 / -26
DaqifiTools.csEnable structured responses across all MCP tools +26/-26

Enable structured responses across all MCP tools

• Sets 'UseStructuredContent = true' on all 26 'McpServerTool' attributes. MCP SDK 2.2 can now expose each return schema through 'tools/list' and serialize typed results into 'structuredContent'.

src/Daqifi.Mcp/Tools/DaqifiTools.cs

Tests (1) +107 / -0
ToolStructuredContentContractTests.csAdd structured-content contracts for every MCP tool +107/-0

Add structured-content contracts for every MCP tool

• Adds protocol-level tests that construct each tool through the SDK and verify it advertises an object output schema. A reflection-based completeness assertion keeps the 26-tool expectation table synchronized with the advertised tool surface.

src/Daqifi.Mcp.Tests/ToolStructuredContentContractTests.cs

@qodo-code-review

qodo-code-review Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. New tool classes escape the contract ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
ToolMethods restricts its reflection scan to DaqifiTools, while the production server discovers
tools across the assembly. Adding another attributed tool class therefore allows an unstructured
production tool to bypass both the expected-name table and output-schema test.
Code

src/Daqifi.Mcp.Tests/ToolStructuredContentContractTests.cs[R99-102]

+    private static IEnumerable<MethodInfo> ToolMethods() =>
+        typeof(DaqifiTools)
+            .GetMethods(BindingFlags.Public | BindingFlags.Static)
+            .Where(m => m.GetCustomAttribute<McpServerToolAttribute>() is not null);
Relevance

●●● Strong

Accepted precedents favor independent reflection discovery to prevent newly added types or entry
points bypassing completeness tests.

PR-#715
PR-#680

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Production calls assembly-wide WithToolsFromAssembly, but the new helper reflects only over
typeof(DaqifiTools), proving that tools declared in another class are outside the contract.

src/Daqifi.Mcp.Tests/ToolStructuredContentContractTests.cs[99-102]
src/Daqifi.Mcp/Program.cs[37-40]

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 structured-content contract scans only `DaqifiTools`, but production uses assembly-wide MCP tool discovery. Tools added in another attributed class would be registered without entering this contract.

## Fix Focus Areas
- src/Daqifi.Mcp.Tests/ToolStructuredContentContractTests.cs[99-102]
- src/Daqifi.Mcp/Program.cs[37-40]

## Recommended Fix
Enumerate all types in the production MCP assembly and collect every public method carrying `McpServerToolAttribute`, matching the assembly-wide discovery used by the server. Use that complete set for both the expected-name comparison and output-schema checks.

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


Grey Divider

Context sources
✅ Compliance rules (platform): 13 rules
✅ Cross-repo context — repo relationships
Review mode: 🧠 Deep: This push contains substantial, independent behavioral changes across concurrency, caching, CI/release publishing, and MCP tool contracts, creating a dense set of easy-to-miss defects.

Grey Divider

Tip of the day
💡 Did you know, you can turn on the rule miner and Qodo learns your standards from review history

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

@tylerkron tylerkron changed the title fix(mcp): enable UseStructuredContent on all tools fix(mcp): return typed structured results from every tool Sep 18, 2026
@tylerkron

Copy link
Copy Markdown
Contributor Author

/agentic_review

Comment thread src/Daqifi.Mcp.Tests/ToolStructuredContentContractTests.cs Outdated
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 532d08b

@tylerkron

Copy link
Copy Markdown
Contributor Author

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit c1ca79d

@tylerkron

Copy link
Copy Markdown
Contributor Author

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 4ca5fde

@tylerkron

Copy link
Copy Markdown
Contributor Author

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 8aed4b1

@tylerkron

Copy link
Copy Markdown
Contributor Author

Reviewed (Claude): verified end-to-end over stdio that all 26 tools advertise an object outputSchema and return structuredContent alongside the unchanged text; contract test now reads the real WithToolsFromAssembly registration. Qodo-clean on 8aed4b1, CI green — ready for review. Note: conflicts textually with #744 (every attribute line) and #738; trivial rebase for whichever merges later.

cursoragent and others added 4 commits September 18, 2026 09:07
SDK 2.2 serializes POCO/list returns as a single text ContentBlock unless
the flag is set. Clients then get outputSchema / structuredContent instead.

Co-authored-by: Tyler Kron <tylerkron@gmail.com>
Mirror WithToolsFromAssembly so a tool added in a new class can't skip it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…gistration

Replaces the hand-rolled reflection scan, whose flags could only ever
approximate the SDK's discovery rules, with the SDK registration itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tylerkron

Copy link
Copy Markdown
Contributor Author

/agentic_review

@tylerkron
tylerkron force-pushed the cursor/mcp-use-structured-content-c623 branch from 8aed4b1 to 8a36cc6 Compare September 18, 2026 19:08
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 8a36cc6

@tylerkron

Copy link
Copy Markdown
Contributor Author

Rebased onto main after #738 merged; Qodo-clean on 8a36cc6, CI green.

tylerkron and others added 2 commits September 27, 2026 10:14
Resolve DaqifiTools.cs against #744: keep main's ReadOnly/Destructive/OpenWorld hints on every tool and add UseStructuredContent = true alongside them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hema

The SDK's default serializer drops null properties, but the outputSchema it generates lists every record property as required (a nullable one as ["T","null"]). With UseStructuredContent on, 14 of the 26 tools could return a result with a required key missing - get_server_info under --no-version-check, configure_*_channels whenever the rate was not lowered, list_analog_outputs before a write, and so on - and a client that validates results against the schema rejects the call.

Register tools through a shared WithDaqifiTools() that passes the SDK defaults with DefaultIgnoreCondition = Never, so null fields are written in both structuredContent and the text block. tools/list is byte-identical. Both contract-test harnesses now register through the same call Program.cs makes, and a new test runs get_server_info through the SDK's invoke path and checks every required key is present.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tylerkron

Copy link
Copy Markdown
Contributor Author

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit ef38167

@tylerkron

Copy link
Copy Markdown
Contributor Author

Qodo-clean, CI green — ready for review

@tylerkron
tylerkron added this pull request to the merge queue Sep 27, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 27, 2026
Resolve DaqifiTools.cs against #761: keep main's [McpServerTool] lines and trimmed [Description] text on every tool, and add UseStructuredContent = true. Program.cs auto-merged: main's ServerInstructions plus WithDaqifiTools().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tylerkron

Copy link
Copy Markdown
Contributor Author

/agentic_review

@qodo-code-review

qodo-code-review Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

No code changes since the last review — review skipped

Qodo Logo

@tylerkron

Copy link
Copy Markdown
Contributor Author

Qodo-clean, CI green — ready for review

@tylerkron
tylerkron added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit 83649ce Sep 28, 2026
4 checks passed
@tylerkron
tylerkron deleted the cursor/mcp-use-structured-content-c623 branch September 28, 2026 03:50
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