Skip to content

fix(mcp): preserve schema tags on nested fields - #920

Merged
ezynda3 merged 2 commits into
mark3labs:mainfrom
ychampion:fix-legacy-schema-tags
Aug 11, 2026
Merged

fix(mcp): preserve schema tags on nested fields#920
ezynda3 merged 2 commits into
mark3labs:mainfrom
ychampion:fix-legacy-schema-tags

Conversation

@ychampion

@ychampion ychampion commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Description

Builds on #931 by preserving jsonschema_description and jsonschema:"enum=..." tags when a tagged struct is nested inside another tool schema.

Previously, the fallback schema builder skipped those nested fields. The recursive fallback now covers repeated structs, pointers, slices, arrays, and string-keyed maps.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • MCP spec compatibility implementation
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Code refactoring (no functional changes)
  • Performance improvement
  • Tests only (no functional changes)
  • Other (please describe):

Checklist

  • My code follows the code style of this project
  • I have performed a self-review of my own code
  • I have added tests that prove my fix is effective
  • I have updated the documentation accordingly

Validation

  • go test ./... -race -count=1
  • (cd otel && go test ./... -count=1)
  • golangci-lint run in the root and otel modules using v2.8
  • go generate ./...

Summary by CodeRabbit

  • Bug Fixes
    • Improved schema generation when certain jsonschema struct tag options can’t be applied, including better handling of nested structs, pointers, arrays/slices, and string-keyed maps.
    • Ensured jsonschema_description and jsonschema enum annotations are applied recursively through nested and embedded schemas.
    • Recursive tagged structs now fail schema generation predictably with a clear error.
  • Tests
    • Expanded and reorganized test coverage for enum tag spacing, unsupported options, named embedded structs, and recursive tagged structs.

Copilot AI review requested due to automatic review settings July 8, 2026 10:58
@mark-iii-labs-huly

Copy link
Copy Markdown

Connected to Huly®: MCP_G-484

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Changes

Nested schema tag handling

Layer / File(s) Summary
Nested field schema fallback
mcp/struct_schema.go
Recognized jsonschema tag-option errors can use recursive fallback generation for pointers, structs, arrays, slices, and string-keyed maps, while recursive fallback types return errRecursiveSchemaFallback.
Recursive annotation propagation and field naming
mcp/struct_schema.go
Struct-field descriptions and enum annotations are applied through nested schema locations, with anonymous-field traversal gated by explicit JSON names.
Regression coverage
mcp/struct_schema_test.go
Tests cover tag spacing, unsupported options, named anonymous structs, recursive tagged structs, and annotations across nested pointers, arrays, and maps.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • mark3labs/mcp-go#931: Both changes extend struct-tag-driven schema generation in mcp/struct_schema.go, including nested and collection types.

Suggested reviewers: syf2211

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Title check ✅ Passed The title clearly summarizes the main change: preserving schema tags on nested fields.
Description check ✅ Passed The description covers the required sections and is mostly complete, with only optional issue and additional info details omitted.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
mcp/tools.go (2)

862-868: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Log schemaToRaw errors for consistency with schemaFor error handling.

When schemaFor[T]() fails, the error is printed to stderr. However, when schemaToRaw(schema) fails (line 868), the error is silently swallowed at the unchanged lines 869–871. Since schemaToRaw is a new function that may have different failure modes than the previous json.Marshal, adding error logging would improve debugability.

♻️ Suggested fix: log schemaToRaw error before returning
 		mcpSchema, err := schemaToRaw(schema)
 		if err != nil {
+			fmt.Fprintln(os.Stderr, err)
 			return
 		}
🤖 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 `@mcp/tools.go` around lines 862 - 868, The error handling in the schema
conversion path is inconsistent: `schemaFor[T]()` logs failures to stderr, but
`schemaToRaw(schema)` errors are currently dropped. Update the `schemaToRaw`
call site in the same helper to print the returned error to stderr before
returning, matching the existing `schemaFor[T]()` handling and making failures
from `schemaToRaw` visible for debugging.

913-919: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Log schemaToRaw and json.Unmarshal errors for consistency.

Same pattern as WithInputSchema: the schemaFor error is logged to stderr, but both the schemaToRaw error (line 919, handled at unchanged lines 920–922) and the json.Unmarshal error (unchanged lines 924–925) are silently swallowed. Adding error logging would make schema conversion failures debuggable.

♻️ Suggested fix: log errors before returning
 		mcpSchema, err := schemaToRaw(schema)
 		if err != nil {
+			fmt.Fprintln(os.Stderr, err)
 			return
 		}

 		if err := json.Unmarshal(mcpSchema, &t.OutputSchema); err != nil {
+			fmt.Fprintln(os.Stderr, err)
 			return
 		}
🤖 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 `@mcp/tools.go` around lines 913 - 919, The error handling in the schema
conversion flow is inconsistent: `schemaFor` logs to stderr, but `schemaToRaw`
and the later `json.Unmarshal` failure paths in the same helper silently return.
Update the schema-building logic around `schemaFor`, `schemaToRaw`, and
`json.Unmarshal` to emit the error to stderr before returning, matching the
existing `WithInputSchema` pattern so all schema conversion failures are
debuggable.
🤖 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.

Inline comments:
In `@mcp/schema_tags.go`:
- Around line 84-151: The recursion tracking in sanitizedSchemaTypeFor and
sanitizedStructSchemaType is using seen as both an in-progress guard and a
cache, which causes reused struct types to bypass sanitization on later
occurrences. Update the logic so seen only marks structs currently being
traversed, and cache or return the sanitized clone separately; make sure
repeated uses of the same struct still flow through sanitizedStructTag and
sanitizedStructSchemaType rather than returning the original legacy-tagged type.

---

Nitpick comments:
In `@mcp/tools.go`:
- Around line 862-868: The error handling in the schema conversion path is
inconsistent: `schemaFor[T]()` logs failures to stderr, but
`schemaToRaw(schema)` errors are currently dropped. Update the `schemaToRaw`
call site in the same helper to print the returned error to stderr before
returning, matching the existing `schemaFor[T]()` handling and making failures
from `schemaToRaw` visible for debugging.
- Around line 913-919: The error handling in the schema conversion flow is
inconsistent: `schemaFor` logs to stderr, but `schemaToRaw` and the later
`json.Unmarshal` failure paths in the same helper silently return. Update the
schema-building logic around `schemaFor`, `schemaToRaw`, and `json.Unmarshal` to
emit the error to stderr before returning, matching the existing
`WithInputSchema` pattern so all schema conversion failures are debuggable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3467cc41-3c14-4005-8292-9973743546c3

📥 Commits

Reviewing files that changed from the base of the PR and between b6e6224 and d4e70ae.

📒 Files selected for processing (5)
  • mcp/schema_cache.go
  • mcp/schema_cache_test.go
  • mcp/schema_tags.go
  • mcp/tools.go
  • mcp/tools_test.go

Comment thread mcp/schema_tags.go Outdated
@ezynda3

ezynda3 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

@ychampion there is a merge conflict, can you have a quick look please?

Constraint: Build on the struct-tag support merged in mark3labs#931 while retaining useful coverage from this PR
Rejected: Replaying the original schema implementation | duplicates current main and leaves the conflict unresolved
Confidence: high
Scope-risk: narrow
Directive: Keep nested struct tags working through pointers, slices, arrays, and string-keyed maps
Tested: go test ./... -race -count=1; go test ./... -count=1 in otel; golangci-lint v2.8 in root and otel; go generate ./...; git diff --check
@ychampion
ychampion force-pushed the fix-legacy-schema-tags branch from 5aafbd6 to 7afa1c5 Compare July 25, 2026 23:54
@ychampion ychampion changed the title fix(mcp): support documented schema tags fix(mcp): preserve schema tags on nested fields Jul 25, 2026
@ychampion

Copy link
Copy Markdown
Contributor Author

Rebased onto current main. Since #931 landed the base tag support, I narrowed this PR to the remaining nested-struct case: tags are now preserved through repeated structs, pointers, slices, arrays, and maps. The full race suite, root/otel lint, and generation pass at 7afa1c5.

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
mcp/struct_schema.go (2)

21-39: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not swallow unsupported jsonschema options.

isJSONSchemaTagOptionError falls back for every WORD= option. Consequently, jsonschema:"description=foo" no longer returns the library error; parseJSONSchemaTag treats it as plain description text and emits a schema. Only fall back when every option causing the error is supported enum=...; otherwise return the original error. Add a regression test for description=....

Also applies to: 232-251

🤖 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 `@mcp/struct_schema.go` around lines 21 - 39, Restrict the fallback in
isJSONSchemaTagOptionError to errors caused exclusively by supported enum=...
options; unsupported options such as description=foo must preserve and return
the original jsonschema error. Update the schema-generation flow around
jsonschema.For and schemaForStructFields accordingly, and add a regression test
confirming description=... returns an error rather than producing a schema.

60-70: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Respect JSON names on anonymous fields.

Anonymous structs with an explicit JSON field name like Embedded \json:"embedded"`should be treated as a named property, not flattened. Flatten only truly unnamed anonymous structs, and parse thejson` tag before schema/annotation walk paths to avoid writing schema annotations against the wrong property.

🤖 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 `@mcp/struct_schema.go` around lines 60 - 70, Update the anonymous-field
handling in walk to parse the JSON tag before deciding whether to flatten the
field. Only recurse into truly unnamed anonymous structs; when an explicit JSON
name is present, retain the field as a named property and ensure
schema/annotation processing uses that property name.
🧹 Nitpick comments (1)
mcp/struct_schema_test.go (1)

11-49: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Table-drive the enum-whitespace variants.

The two tests differ only by the tag string. Combine them into cases so additional tag-parsing variants do not duplicate tool setup and assertions.

As per coding guidelines, **/*_test.go requires table-driven tests.

🤖 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 `@mcp/struct_schema_test.go` around lines 11 - 49, Combine
TestSchemaFor_JSONSchemaDescriptionTag and
TestSchemaFor_JSONSchemaEnumTagWithoutLeadingSpace into one table-driven test
covering both jsonschema tag variants. Move the differing tag strings into test
cases, then reuse the shared tool creation, schema unmarshalling, and
description/enum assertions for each case.

Source: Coding guidelines

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

Inline comments:
In `@mcp/struct_schema.go`:
- Around line 111-130: Update schemaForFieldType and the schemaForStructFields
traversal to track struct types currently being expanded, including pointer
unwrapping, and detect re-entry for recursive fields such as Node.Next. On
re-entry, emit a schema reference/definition using the existing jsonschema
mechanisms or return a controlled error, instead of calling
schemaForStructFields indefinitely; preserve normal handling for non-recursive
pointers, arrays, slices, maps, and structs.

---

Outside diff comments:
In `@mcp/struct_schema.go`:
- Around line 21-39: Restrict the fallback in isJSONSchemaTagOptionError to
errors caused exclusively by supported enum=... options; unsupported options
such as description=foo must preserve and return the original jsonschema error.
Update the schema-generation flow around jsonschema.For and
schemaForStructFields accordingly, and add a regression test confirming
description=... returns an error rather than producing a schema.
- Around line 60-70: Update the anonymous-field handling in walk to parse the
JSON tag before deciding whether to flatten the field. Only recurse into truly
unnamed anonymous structs; when an explicit JSON name is present, retain the
field as a named property and ensure schema/annotation processing uses that
property name.

---

Nitpick comments:
In `@mcp/struct_schema_test.go`:
- Around line 11-49: Combine TestSchemaFor_JSONSchemaDescriptionTag and
TestSchemaFor_JSONSchemaEnumTagWithoutLeadingSpace into one table-driven test
covering both jsonschema tag variants. Move the differing tag strings into test
cases, then reuse the shared tool creation, schema unmarshalling, and
description/enum assertions for each case.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 51112e3d-50d9-4def-a09d-6e586e515f1e

📥 Commits

Reviewing files that changed from the base of the PR and between 5aafbd6 and 7afa1c5.

📒 Files selected for processing (2)
  • mcp/struct_schema.go
  • mcp/struct_schema_test.go

Comment thread mcp/struct_schema.go
Constraint: Address all review findings without broadening support beyond documented enum tags
Rejected: Silently skipping recursive fallback fields | hides schema loss and risks unbounded expansion
Confidence: high
Scope-risk: moderate
Directive: Preserve library errors for unsupported schema options and keep named anonymous fields nested
Tested: go test ./... -race -count=1; go test ./... -count=1 in otel; golangci-lint v2.8 in root and otel; go generate ./...; git diff --check
@ychampion

Copy link
Copy Markdown
Contributor Author

Addressed the review-body findings in a1e85c8:

  • Unsupported options: fallback now checks the reflected tags and preserves the original jsonschema error for options such as description=...; added a regression.
  • Named anonymous fields: JSON metadata is parsed before flattening, so json:"embedded" remains a nested property with its annotations.
  • Test shape: the leading-space variants now share one table-driven test.

Root/otel lint, generation, and go test ./... -race -count=1 all pass.

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.

3 participants