Skip to content

fastmcp(feat): Support FastMCP 4 and collect tools from a live server - #78

Open
tony wants to merge 28 commits into
mainfrom
fastmcp-4.x
Open

fastmcp(feat): Support FastMCP 4 and collect tools from a live server#78
tony wants to merge 28 commits into
mainfrom
fastmcp-4.x

Conversation

@tony

@tony tony commented Sep 5, 2026

Copy link
Copy Markdown
Member

Summary

  • Fix the collector's reads against MCP SDK v2, which FastMCP 4 builds on: the annotation model fields are snake_case now and the camelCase spellings survive only as serialization aliases, so tool hints resolved through a deprecated shim and a resource's lastModified resolved to nothing at all.
  • Add live-server tool collection. When fastmcp_server_module is set, tools are read off the running FastMCP instance instead of being replayed through a hand-written collector whose tool() accepted four keyword arguments where FastMCP's accepts many more — one unrecognised keyword aborted the rest of its module, and the build still succeeded.
  • Add rendering for a resource's audience, priority and lastModified. They were collected onto ResourceInfo and never emitted, so no reader could see who a resource is for or when it last changed.
  • Fix silent loss of same-named tools and prompts. FastMCP permits two registrations to share a name, so both are genuinely served; the name-keyed docs index kept whichever came last and said nothing.
  • Fix incremental builds, which died with PicklingError once the server's own callable reached ToolInfo — a tool registered inside a register(mcp) factory is a closure, and Sphinx pickles its environment between builds.
  • Add FastMCP as a dev-only dependency and test the collector against a real server. The extension declared no FastMCP dependency of any kind, so its tests built components from hand-written shims, and a shim keeps answering the spelling it was written with.

Changes by area

Collector

  • _collector.py: _HINTS and _RESOURCE_ANNOTATION_FIELDS become documented-name/field-name pairs, so reads use the SDK v2 field while the rendered vocabulary keeps the camelCase names the MCP schema publishes.
  • _collector.py: _tools_from_server() and _tool_from_component() build ToolInfo from registered Tool components; collect_tools() prefers that path when a server is configured. Module attribution comes from Tool.fn.__module__ rather than a tool's position in fastmcp_tool_modules.
  • _collector.py: _index_by_unique_name() gives the name-keyed tool and prompt indexes first-wins semantics with a warning, matching what the URI-keyed resource index already did.
  • _collector.py: _strip_schema_note() matches FastMCP's generated schema hint by shape rather than by its exact sentence, and only in a description's final paragraph.

Rendering

  • _directives.py: _annotation_fact_rows() emits set annotations as resource-card facts beside the MIME type, for both resources and resource templates.
  • _models.py: ToolInfo.func becomes optional, with a __getstate__ that drops it so the environment pickles. Everything rendered is captured at collection time.

Dependencies and tests

  • pyproject.toml: adds fastmcp>=4.0.2 to the dev group, and exempts fastmcp and fastmcp-slim from the release cooldown in [tool.uv.exclude-newer-package] — the metapackage pins its companion to an exact version, so exempting one without the other leaves the pair unresolvable.
  • tests/ext/fastmcp/test_real_server.py: a real FastMCP server fixture covering tool annotations, a resource with last_modified, and a generated prompt-argument schema.
  • tests/ext/fastmcp/test_directives_integration.py: asserts the annotation facts reach the rendered page.

Design decisions

Read the component registry, not list_tools(). list_tools() routes through middleware, so a server that hides tools at runtime would have them erased from its own documentation. Reading the provider registry directly is what prompts and resources already do.

Keep the documented names camelCase. Only the attribute reads move to snake_case. Pages, term_from_annotations and resolve_axes still speak readOnlyHint and lastModified, which is what the MCP schema publishes — renaming them would be a breaking change to consumers for no gain.

Drop func on pickle rather than making it lazy. Nothing reads the field after collection, so a class-level __getstate__ enforces that structurally instead of by convention.

Fail on the compatibility shim, not just on the value. FastMCP answers the SDK v1 spellings through a warn-once shim it plans to remove, so a value-only assertion would stay green right through the removal. The hint test raises on the warning instead.

Leave the module-scanning modes alone. Projects without fastmcp_server_module keep the previous behaviour, including the catch-all around register(). Narrowing that is a separate change with its own compatibility question.

Test plan

  • uv run ruff check . — clean
  • uv run ruff format . --check — clean
  • uv run mypy . — clean
  • uv run pytest — the FastMCP extension suite passes, including the new real-server tests; doctests in _strip_schema_note cover the reworded and multi-paragraph cases
  • test_tool_hints_survive_the_object_model — hints resolve without tripping FastMCP's deprecation warning
  • test_a_configured_server_yields_tools_the_mock_would_drop — a module the mock collector would have truncated is collected whole
  • test_duplicate_tool_names_warn_instead_of_vanishing — the first registration is kept and the collision is named
  • test_collected_tools_survive_environment_pickling — a closure-registered tool pickles
  • test_resource_annotations_render — the annotation facts reach the built HTML
  • uv run sphinx-build -W -b dirhtml docs docs/_build/html — builds clean with warnings as errors
  • uv lock --check — the lock matches the manifest

@codecov-commenter

codecov-commenter commented Sep 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.94350% with 89 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.28%. Comparing base (3b0bdc1) to head (99666c7).

Files with missing lines Patch % Lines
tests/ext/fastmcp/test_real_server.py 90.06% 60 Missing ⚠️
...c-fastmcp/src/sphinx_autodoc_fastmcp/_collector.py 88.94% 24 Missing ⚠️
docs/_ext/fastmcp_demo_server.py 73.33% 4 Missing ⚠️
...doc-fastmcp/src/sphinx_autodoc_fastmcp/_parsing.py 95.65% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #78      +/-   ##
==========================================
+ Coverage   93.05%   93.28%   +0.22%     
==========================================
  Files         276      278       +2     
  Lines       23095    23942     +847     
==========================================
+ Hits        21491    22334     +843     
- Misses       1604     1608       +4     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

tony added 12 commits September 5, 2026 11:53
The global `exclude-newer = "3 days"` cooldown holds every release back,
including FastMCP's, so the collector's real-object tests would run against a
FastMCP a patch behind the one being targeted. `false` exempts a package from
any `exclude-newer` constraint without committing a date that would age into
the lockfile.

Both halves need it. `fastmcp` is a code-free metapackage whose only job is
depending on `fastmcp-slim[client,server]` at its exact version, so exempting
one without the other leaves the pair unresolvable.

Filed ahead of the gp-libs entry, which keeps its own note.

https://gofastmcp.com/getting-started/installation
https://docs.astral.sh/uv/reference/settings/#exclude-newer-package
SDK v2 renamed the annotation model fields to snake_case, keeping camelCase
as serialization aliases. Tool hints survived only through a FastMCP shim
that warns and is going away; a resource lastModified resolved to nothing,
so it vanished from pages silently.

Match FastMCP prompt schema note by shape rather than by its exact
sentence, which v4 reworded, and only in a final paragraph so a
multi-paragraph description survives.

Documented names are unchanged; only the reads move.
sphinx-autodoc-fastmcp introspects a FastMCP server but declared no FastMCP
dependency, so its tests could only use hand-written stubs -- which answer
whichever spelling they were written with, and so cannot catch an SDK rename.
Dev-only; the extension still works without FastMCP installed.
Builds a real server with real ToolAnnotations, a last_modified resource and a
generated prompt-argument schema, so a rename in FastMCP object model
(https://gofastmcp.com/servers/tools) fails here rather than silently emptying
a documentation page. Skips when FastMCP is absent.
The mock collector took 4 kwargs where FastMCP takes many more, and the module
loop swallowed the resulting TypeError with a warning -- so an unrecognised
keyword dropped the rest of its module and the build still passed. Measured: a
three-tool module documented one.

Read Tool components off the server instead, taking module attribution from
Tool.fn.__module__ rather than the config list position. Reads the provider
registry directly like prompts and resources do, not list_tools(), so a runtime
toolset gate cannot erase tools from the docs.

Ledger: .git/spike/fastmcp-v4-tool-collection.md
FastMCP permits two tools or prompts to register under one name when they
differ another way, so both are served. The name-keyed docs index silently
kept the last. Keep the first and warn, as the URI-keyed resource index
already does.

Ledger: .git/spike/collector-collision-pass2.md
audience, priority and lastModified were collected onto ResourceInfo and then
dropped: the card builder never took them, so they reached no page. Emit them
as card facts beside the MIME type, only when set.

Ledger: .git/spike/fastmcp-v4-metadata.md
Reading tools off a live server put the server callable on ToolInfo, and a
tool registered inside a register(mcp) factory is a closure -- unpicklable, so
Sphinx died on the environment cache and no incremental build completed.

Drop the field when pickling. Nothing reads it; params, return_annotation and
docstring are all captured at collection time.

Found by an end-to-end docs build, which the collector-level check could not
have caught.
The lockfile manifest still carried the resolved `exclude-newer` timestamp for
`fastmcp` and `fastmcp-slim`, so it disagreed with the `false` now in
pyproject.toml and `uv lock --check` failed.

No version moves: fastmcp stays at 4.0.2. `uv lock` rewrites only the two
manifest entries, because it holds existing pins unless a constraint forces
them to change.
Records the branch under the unreleased entry: the SDK v2 field-name reads,
collecting tools off the live server, the pickling and duplicate-name fixes,
rendered resource annotations, and the cooldown exemption.

Restores the maintainers placeholder the earlier draft overwrote. It marks
where forthcoming notes go and has sat under the unreleased header since the
project was scaffolded; nothing but a release moves it.

Applies the fixed subheading order from WRITING.md. Rendering a resource's
annotations is a feature, not a fix -- it moves to `### What's new`, which
belongs above `### Fixes`.

Drops the FastMCP patch-cadence narrative from the dependency entry. A version
range that was true this week is the fragile-metric anti-pattern; the
capability is that a refreshed lock no longer lags what the tests target.
why: docs.yml deploys only on push to main, so the FastMCP 4 collector
work cannot be seen rendered before it merges. Reviewing the resource
annotation cards and the live-server tool sections needs a real build.

what: add fastmcp-4.x to the workflow's branch list. The paths-filter
already lists .github/workflows/docs.yml as publishable, so this commit
triggers its own deploy.

Revert before merge. The deploy is not branch-aware: it syncs to the
production bucket with --delete and purges the CDN, so while this is
live gp-sphinx.git-pull.com serves unreleased docs.
why: fastmcp_server_module was documented as collecting prompts and
resources only. This branch makes it collect tools too, and take
precedence over fastmcp_tool_modules, so three passages described
behaviour the code no longer has.

what: correct the confval description, the conf.py snippet and the
"Live server collection" section; state the precedence rule and the
duplicate-name warning next to the on_duplicate sentence that would
otherwise mislead. Both resource directives now say which facts their
card renders, which is what autodirectives publishes.

Add docs/_ext/fastmcp_demo_server.py so the site demonstrates what it
documents: a resource carrying every annotation, one carrying none, a
template and a prompt. It registers no tools, so collect_tools still
falls through to fastmcp_tool_modules and the demo tool cards are
unaffected.

Also tighten _SCHEMA_NOTE_RE. Matching by shape alone ate a written
final paragraph asking the reader for a JSON schema; requiring the
schema object after the colon separates the generated note from prose.
Found while checking a downstream report of curly quotes inside the
note.
why: under PEP 563 an annotation reaches the collector as the author's
source text, which is the best display available -- until it carries
Annotated metadata, because that metadata is code. A parameter declared
with a description built at import time rendered the call that produced
it rather than its value.

what: _strip_annotated reduces Annotated[X, ...] to X, handling both the
source-string and resolved-object forms. Every site that enriches a type
from a signature routes through it: extract_params for the module modes,
and the prompt argument enrichment.

Text-level stripping rather than typing.get_type_hints, which fails on
TYPE_CHECKING-only names -- sphinx.util.typing falls back to raw
__annotations__ on three exception types for exactly that reason, and
sphinx-autodoc-typehints goes further and still falls back.
why: parameter tables were built from the Python signature, which
disagrees with what the server serves. An injected Context was documented
though FastMCP excludes it from the schema and no caller can pass it, and
its default rendered as the repr of a sentinel -- an object address, so
two builds of one source emitted different HTML.

what: _params_from_schema takes the parameter set, required flags,
defaults and descriptions from the tool's published schema, and keeps the
signature only for the type display the schema cannot express in Python
terms. Defaults render from JSON values, so they are stable.

The schema already carries both NumPy docstring text and evaluated Field
descriptions, so reading it costs no authoring convenience.

Module-scanning modes are unchanged; they have no schema to read.
why: reading the server's registry replaced the module modes rather than
adding to them, and read less than the registry holds. Four consequences,
each measured:

- A mounted child server's tools are absent from the parent's registry,
  and the early return discarded the module entries that were the only
  record of them. A parent with one mounted child documented one tool
  where the server serves two.
- ProxyTool and FastMCPProviderTool carry no `fn`, so filtering on one
  dropped every proxied or provider-backed tool without a warning.
- MCP SDK v1 publishes the documented camelCase names as attributes.
  Reading only the v2 fields emptied hints and lastModified for FastMCP 3
  consumers, and the extension declares no version floor.
- collect_tools and collect_prompts_and_resources each resolved
  fastmcp_server_module, so a factory ran twice and the two collectors
  read different servers.
- A parameter declared with Field(alias=...) publishes the alias, which
  names no signature parameter, so its type rendered as an em dash.

what: merge server-collected tools over module-collected ones instead of
replacing them; document components with no callable from the schema
alone; fall back to the v1 attribute only when the v2 field is absent, so
a v2 model never touches its deprecated alias; resolve the server once per
build; and describe an aliased parameter from its schema type.
…unts

why: components were read from the server's own registry, which a mounted
child server does not appear in. A parent with one mounted child served two
tools and documented one, and the module-scanning fallback was the only
thing keeping such a tool on the page at all.

what: walk `server.providers` recursively, following a provider's wrapped
server, with a depth cap and a cycle guard. A namespaced mount renames what
it carries, so reproduce the prefix from the transform rather than reading
past it -- a name the server does not serve is worse than a missing page,
and a transform whose renaming cannot be reproduced is refused with a
warning instead.

`list_tools()` is not the primitive here even with middleware disabled: it
still filters by enabled state and auth, so a tool switched off at runtime
would vanish from its own documentation.

The test asserts the collected names equal `list_tools(run_middleware=False)`
for a plain and a namespaced mount, so the two cannot drift apart silently.
… does

why: the walk prefixed every component's name. A namespace moves into a
resource's URI, not its name, so a resource mounted under one was
documented as one_thing at data://thing where the server serves thing at
data://one/thing -- both halves wrong, and a directive using the served
URI could not resolve. Checking tools alone had proved nothing about
resources.

The cycle guard also suppressed any server already visited, so one child
mounted under two namespaces documented one of its two served names. A
repeated mount is not a cycle.

what: apply each transform through its own `_transform_uri` and
`_transform_name`, choosing by whether the component is URI-keyed, so the
rule stays FastMCP's rather than a copy of it. Scope the guard to the
active traversal path.

The test now asserts collected identity equals what the server lists for
tools, resources and prompts, and for a child mounted twice.
…sforms

why: four ways the collected identity still diverged from the served one.
Nested mounts applied the outer namespace first, documenting
inner_outer_hello where the server serves outer_inner_hello. A transform
added to the server itself was ignored, so a server serving public_hi
documented hi. A component with no callable lost its description, leaving
newly documented proxy tools with an empty card. And a union parameter
reaching the schema fallback kept only its first alternative.

what: build the transform chain inner-to-outer; collect a server's own
transforms alongside its providers'; fall back to the component's
description when there is no callable docstring; and give
_schema_type_text the union handling _template_params_from_schema already
had, so both callers share one renderer rather than two.
why: seven more ways the collected identity diverged from the served one,
each measured against list_tools(run_middleware=False):

- mount(..., tool_names=...) wraps the provider in a ToolTransform, which
  has no namespace methods, so the whole mount was refused with a warning
  that named nothing; with a namespace around it the provider was wrapped
  twice and peeling one layer found nothing and said nothing.
- A transform added to a provider directly, or to a child server, was
  either ignored or applied after the namespace it sits inside, so
  api_hello documented as hello and outer_inner_hello as inner_outer_hello.
- Annotated metadata nested inside a container survived stripping, so
  dict[str, Annotated[int, Field(description=f'...')]] still leaked code.
- A schema property with no default key documented None, which a
  default_factory parameter does not accept.
- Server-collected tools were merged over module ones with a dict update,
  discarding a same-named module entry without the collision report every
  other duplicate gets.
- A mount tree past the depth cap was cut off silently.
- An empty annotation list rendered a blank fact row as if set.

what: treat a ToolTransform's rename map as reproducible alongside a
Namespace; peel every wrapper layer and lead the chain with the innermost
provider's own transforms; strip Annotated anywhere in the expression
tree; render a default only when the schema publishes one; merge served
tools first through _index_by_unique_name; warn at the depth cap; skip an
empty list annotation.

Every case is asserted equal to what the server lists, for tools,
resources and prompts.
…nnot be read

why: four more divergences from what the server serves, each measured.

- A ToolTransformConfig renames arguments and changes title, description
  and tags; copying only its name documented lookup(raw_query) with the
  original title where the server publishes lookup(query) titled Look Up.
- An OpenAPI or other dynamic provider lists its tools only
  asynchronously and holds no registry, so the walk skipped it without a
  word and documented an empty index for a server that serves tools.
- A boolean property subschema -- {"properties": {"payload": true}} is
  valid JSON Schema -- raised AttributeError and aborted the build.
- @tool(description=...) on a function with no docstring documented
  nothing, because the fallback only covered a missing callable.

what: apply a matching ToolTransformConfig to the component through its
own apply(), which is synchronous and yields the same TransformedTool the
server lists; warn naming any provider with no registry to read; treat a
boolean subschema as one with no fields; fall back to the component's
description whenever the docstring is empty.
…roduce

why: four more divergences from what the server serves, each measured.

- A ToolTransform was applied to any component carrying `parameters`,
  which a resource template also does; a template sharing a targeted
  tool's name raised TypeError and aborted the build.
- A transformed tool's callable is FastMCP's forwarding function, so
  module attribution landed on FastMCP's own module and every summary
  link for a renamed tool pointed at tool_transform/.
- A custom Transform on a server or provider was filtered out and the
  pre-transform names published, where the mount path had refused and
  warned. The Transform contract is async-only, so a rule that is not
  data cannot be reproduced; publishing past it is the wrong name.
- A registry-less provider inside a namespace fell through without the
  diagnostic its un-namespaced form already got.

what: apply a ToolTransform only to a Tool; follow parent_tool to the
original callable for attribution; fail closed with a warning at the
server and provider levels, matching the mount path; warn on the
namespaced fallthrough. Remove the local-registry fallback, which
re-published exactly the identities a refusal had declined.
why: reproducing each server-side rename as data could never be general.
The Transform contract is entirely async, so a custom transform, a dynamic
provider, and -- measured -- a tool disabled with server.disable() all
forced the walk to refuse, and the last emptied the whole server: a
silently empty index, the exact failure the refusal existed to prevent.

what: list through FastMCP's own Provider.list_tools/resources/templates/
prompts on an event loop owned by a background thread, the shape
sphinx_vite_builder._internal.bus already uses; asyncio.run raises inside
sphinx-autobuild's loop, a thread-owned one does not. Those listings apply
every mount, namespace, rename and custom transform exactly as the server
does, and keep disabled components -- filtering by enabled state and auth
happens one level up in FastMCP.list_*, which is deliberately not used.

Through that listing a mounted tool is a proxy with no callable, so
_origin_tool follows parent_tool and the proxy's server back to the tool
that has one, matching a namespaced name by Namespace's documented
namespace_name shape. Attribution, docstrings and Python type display are
unchanged from the registry walk on every shape it handled.

Measured equal to server.list_*(run_middleware=False) on ten shapes, plus
a disabled tool kept where that listing drops it. The refuse-and-warn
tests for custom transforms and dynamic providers become served-identity
assertions, since those are now documented rather than refused.
why: the listing was only unfiltered at the root. FastMCPProvider lists a
mounted child by calling the child's own list_tools(), which drops disabled
components and runs the child's middleware -- so a tool switched off or
gated inside a mounted server disappeared from its documentation, the
failure listing through the provider level exists to prevent. Measured: a
child's disabled tool survives direct collection and vanishes under a
mount.

Two more, both from the proxy that mounting inserts:

- Recovering the wrapped tool by stripping the namespace off the served
  name picks a sibling literally called namespace_name. A child defining
  hello and ns_hello, mounted under ns, documented the decoy's description
  and types for both.
- Replacing the callable with the origin's made a transformed tool
  contradict its own schema: Tool.from_tool retyping an argument to str
  still documented int.

what: descend into a mounted child and compose each provider's and
server's transforms the way Provider.list_* does, instead of asking the
child for its own listing. Match a proxy on the _original_name it
recorded. Use the origin callable only where it is the truth -- module
attribution, and a proxy with no transform between; a transformed tool's
types come from the schema it publishes.
…egate

why: descending past a provider to read it unfiltered also stepped past
FastMCP's own handling of two things it does around that call.

- An aggregate defaults to provider_error_strategy="warn": an unreachable
  remote is logged and its siblings still serve. Listing providers directly
  inherited none of that, so one failing provider raised out of
  builder-inited and the build produced no documentation at all, where the
  server itself still answered with its healthy tools.
- An AggregateProvider holds providers rather than a server or an inner
  provider, so it fell through to its own listing, which re-enters each
  child's filtered one. A mount's disabled tool survived directly and
  vanished once wrapped in an aggregate -- the boundary fix from 8512ace,
  one wrapper out.

Separately, the docstring outranked the published description, so an
explicit description= or one a transform rewrote lost to the function's own
docstring. What the server publishes is what a caller reads.

what: gather providers through the holder's configured failure strategy,
recurse into an aggregate's providers, and prefer the component's
description with the docstring as fallback.
…rote

why: three ways collection still lost to something it should have survived.

- The listing was bounded as a whole, on the future the Sphinx thread waits
  on, so the timeout fired outside the per-provider failure policy. A
  provider that hangs aborted the build with TimeoutError even under
  provider_error_strategy="warn", where a provider that raises was already
  logged and skipped.
- The schema-note pattern matched any final paragraph asking for a JSON
  schema, so an authored description -- 'Provide a JSON schema: {"type":
  "object"}.' -- was erased entirely. Both generated spellings say
  "matching the following"; an author does not.
- A rename-only transform yields an unannotated forwarding callable, so the
  Returns fact disappeared from a tool that returns exactly what it did
  before. Its published output schema still describes the result.

what: bound each provider with asyncio.wait_for inside the failure policy,
so a slow remote is logged and skipped like a failing one; require
"matching the following" in the note pattern; and fall back to the output
schema for the return display, unwrapping the single `result` property
FastMCP uses for a non-object return.
…t matrix

why: each round of review has found a defect on a shape the previous tests
did not vary, and a passing suite kept reading as coverage it was not. The
64-cell matrix that returned no mismatches was built entirely with mount(),
so it missed both an aggregate provider re-entering a filtered listing and a
failing provider aborting the build.

what: parametrize over how a child is attached -- mount, add_provider, an
aggregate wrapper, each with and without a namespace, plus a child carrying
its own transform -- at one and two levels, asserting collected identity
equals what the server lists for tools, resources, templates and prompts.
A second case asserts a disabled tool survives every attachment, since each
reaches its child by a different path.

Removing either boundary fix fails the matrix on exactly the shapes it
governs, which the point tests could not do.
…ring it

why: three places inferred a fact the server states outright, and two of
them were regressions from the previous round's own fixes.

- The provider timeout wrapped a whole subtree, so a slow descendant
  discarded the healthy components already gathered beneath it. Measured: a
  child listing ['local'] directly listed [] through a mount.
- A Field(alias=...) publishes under a name that may be another parameter's
  own. Looking the published name up in the signature borrowed that other
  parameter's annotation, so the table said str for a property the server
  accepts as integer. Alias ownership is in the declaration.
- A TypedDict whose only field is result publishes the same shape as a
  generated wrapper, so a renamed tool returning an object documented the
  inner type. FastMCP marks the wrappers it makes with
  x-fastmcp-wrap-result.

what: bound the leaf listing rather than a subtree; map published alias to
declaring parameter before consulting the signature, resolving deferred
annotations first since PEP 563 hides the metadata that carries the alias;
and unwrap only a schema carrying the marker.
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