Skip to content

Releases: KLR-Pattern/fastapi-gql-mcp

v0.12.0

Choose a tag to compare

@github-actions github-actions released this 10 Oct 08:51

Added

  • search_fields(domain, query) (progressive mode): BM25 over field
    names, descriptions and argument names — the shortcut for wide domains
    where reading the whole fragment costs more than searching for the one
    field you need. Field names come from endpoint function names (developer
    vocabulary, keyword-dense), so lexical search lands well without
    embeddings. Indexed lazily per domain, cached (schema is immutable).
    Measured on a 324-route app: a 1,743-token domain fragment collapses to
    a ~200-token search round trip.

v0.11.1

Choose a tag to compare

@github-actions github-actions released this 09 Oct 22:55

Docs

  • Capability boundaries rewritten as the full input/output matrix:
    a structured-inputs table (path convertors, query models, body shapes,
    Depends, scalars, alias wire contract), two new degradation rows (unbound
    TypeVar at a route boundary, unannotated body parameters), mixed-type
    Literal in the skip list, and a "Conventions the bridge follows" table
    collecting the FastAPI-side derivation rules (tags/domains, function
    names, descriptions, requiredness per model family, exclude/deprecated,
    optional headers, multi-verb priority, naming collisions).

Fixed

  • TypeVar-degraded routes now carry the real reason in their SDL note.
    Route-output classification runs ONCE at scan time
    (scanner.OutputPlan) and every reader — startup notice, readiness
    report, SDL field note — consumes the same plan, ending five verified
    disagreements between the three former detectors: the misleading
    "declares no response type" guidance on generic routes, first-match
    audit vs stacked SDL notes, void+filter audited as degraded (a bodyless
    response has nothing to filter), Any+filter showing two different
    stories, and the filter⇒JSON decision smeared across four sites.

Changed

  • Input/output handling systematically reorganized (four refactors,
    zero behavior change beyond the fix above):

    • type_builder's six near-clone field builders (model/TypedDict/
      dataclass × input/output) unified on FieldSpec: three family
      extractors produce normalized field records, one _build_fields
      loop owns sanitize/wire-key/degrade/union/empty handling that was
      copy-pasted six times (1052 → 1041 lines while gaining the
      abstraction)
    • _build_route_info (158 lines) decomposed into a gate sequence
      (_FlatParams, five named gates, _route_params, _trial_route_types)
      with _make_param as the single ParamInfo/gname producer
    • wire-name/default/note helpers single-sourced (request_wire_name,
      output_wire_name, graphql_default, append_note); all three
      cross-module private imports made public or moved home
  • Test suite reorganized into layers. tests/ now mirrors altitude
    instead of git history: unit/ (one module under test, no handler
    execution), integration/ (through RouterGraphQLHandler),
    e2e/ (ASGI / fastmcp Client), guards/ (meta pins), plus a shared
    tests/support/ package (sample models, app factories, MCP/HTTP
    protocol helpers) that replaces ~19 copy-pasted factories and
    per-file model redefinitions. A make_handler conftest fixture owns
    handler lifecycle. Four assertion-level duplicates merged
    (filter_passthrough_headers ×2, introspection, list-type, path-param
    restatement); layered assertions deliberately kept. Ticket-wave labels
    (G5-G11/H1/M1, P0-2, R1/R4, B1, O1/O2, D1) moved from names and
    section markers into docstrings.

Added

  • Seventeen gap tests lift coverage from 95.1% to 96.7%:
    unknown-domain errors for the remaining progressive tools, fragment
    arms of the unroll-limit walk, HEAD/OPTIONS-only routes, empty
    TypedDict/dataclass input raises, output-side AliasChoices fallback,
    input-side hyphenated TypedDict keys, dataclass field degradation on
    both sides, non-JSON 2xx bodies, long error-body truncation, and
    syntax errors on the unguarded parse path.

v0.11.0

Choose a tag to compare

@github-actions github-actions released this 09 Oct 14:11

Added

  • Stdlib @dataclass maps onto real object/input types. A dataclass
    response (or a dataclass field nested inside a model, and vice versa)
    becomes a GraphQLObjectType / GraphQLInputObjectType like a
    BaseModel or TypedDict: annotations from get_type_hints, output
    nullability from the annotation (defaults always materialize), input
    requiredness and literal defaults from the field definitions
    (default_factory fields are optional without a GraphQL default —
    FastAPI materializes them, a 422 surfaces as a field error).
    ClassVar/InitVar never become fields. FastAPI treats dataclasses as
    first-class citizens on both sides, so validation and serialization ride
    the native stack — the runtime path needed no changes. Recursive
    dataclasses get the true-depth contract like any recursive type.

v0.10.0

Choose a tag to compare

@allmonday allmonday released this 09 Oct 11:25

Changed

  • Four deduplications around the recursion machinery. The NonNull/List
    unwrap and the direct-self-reference predicate each have ONE definition
    (recursive_expand.unwrap / is_direct_self_reference) shared by
    type_builder's schema note and recursive_edges, so the two detectors
    cannot drift; UnsupportedFieldTypeError carries a structured
    field_reason attribute that degraded-field reporting reads instead of
    sniffing exception message text; the field-build memo lives once inside
    the register-then-build shells instead of four per-builder cache
    wrappers.

  • Unmappable model fields degrade to raw JSON instead of skipping the
    route
    (issue #3, General Fix Strategy #2). A field whose type has no
    GraphQL mapping (metadata: SomeCustomClass) bridges as the JSON
    scalar — the surrounding model keeps its structured, selectable fields,
    and the degradation lands in readiness().degraded_fields and the
    startup warning, exactly like union fields always have. Input fields
    degrade the same way (FastAPI validates whatever arrives; a 422 surfaces
    as a field error). Unbound-TypeVar generics now degrade their field
    (keeping the parameterize guidance) instead of skipping the route.
    Skips remain only where no fallback can express the shape: top-level
    response/parameter annotations with no mapping at all (-> bytes, a
    bare custom class); a wholly-unusable nested type (an empty model)
    rolls back transactionally and degrades its referencing field.
    TypeBuilder.union_fields is renamed to degraded_fields (unions and
    unmappable types, deduplicated per field+reason).

Added

  • Recursive fields return their true depth. A selection on a recursive
    model field now means "the whole subtree": the agent's stopping selection
    repeats as a template to the data's actual depth, at any position in the
    document (the chain need not start at the root). Previously the finite
    GraphQL document silently truncated recursive data — invisibly, since a
    leaf's children: [] is indistinguishable from a cut-off subtree, and
    trees deeper than max_depth could not be fetched in full at all. The
    route already computed the complete tree (routes are called once; nested
    resolution is pure projection), so unrolling merely removes the document
    limitation. max_depth still guards the document as written; fragments
    in a recursive template and mutual recursion (A.b: B / B.a: A) are
    consciously left unexpanded. The unroll ceiling is a function of the
    process recursion budget (sys.getrecursionlimit() // 16, from a
    measured ~12 frames per served level) — the limit never promises depth
    the executor cannot deliver, and it scales automatically when an
    operator raises the limit to serve deeper trees; data deeper than the
    limit is served as deep as the document goes, with a definite error
    naming the limit (data stays — truncation is never silent). The
    recursive type's schema description carries a one-line contract ("full
    subtree at true depth; your selection repeats per level") once per
    type — local placement at ~15 tokens instead of per-field paragraphs,
    so agents discover it from the SDL.

  • instructions= passthrough on FastAPIMCP — the MCP protocol's
    handshake usage guide, injected into the agent's context once per
    connection; None (default) sends nothing. README documents how to
    write one that saves discovery round-trips.

  • stateless_http=True on mount_to() / run() — one transport per
    request (no session affinity), for multi-worker / multi-pod deployments
    behind non-sticky load balancers where stateful streamable HTTP sessions
    404. Passed through to fastmcp's http_app/run_http_async.

  • MCP tool annotations — discovery tools and graphql_query now carry
    readOnlyHint, graphql_mutation carries destructiveHint, letting
    agents auto-run reads and confirm writes.

  • exclude_deprecated=True filter — drops deprecated=True routes at
    the config level (silent drop, like path/tag filters); by default they
    stay with their GraphQL-native @deprecated mark.

Fixed

  • A subselection-less recursive field is rejected, not crashed on.
    Selecting a back-edge without a subselection (children with no
    { ... }) is invalid GraphQL; it now comes back with the standard
    "must have a selection of subfields" validation error like any other
    field, instead of an uncaught AssertionError surfacing as an opaque
    500.
  • Custom validation rules see the document the agent wrote. Unrolling
    is a system behavior; caller-supplied rules (cost guards, complexity
    caps) now validate the written document, so a 3-field recursive
    selection is no longer rejected for having ~60x the fields the agent
    asked for. The standard rule set still validates the expanded document
    the executor actually runs.
  • Union-field degradation is recorded on every side. A union field
    bridging to raw JSON lands in readiness().degraded_fields and carries
    its schema note whether it sits on a model's output, a model's input, a
    TypedDict's output or a TypedDict's input — the audit no longer depends
    on which side of the API the field happens to be on.
  • The excess walk is gated by the document. Edge-named keys inside a
    JSON passthrough payload are invisible to the document and can no
    longer flag a mixed query (recursive tree + colliding payload keys)
    that truncated nothing.
  • The unroll limit is honest — the crash band is gone. The old floor
    max(100, ...) sat above the measured ~82-level execution ceiling, so
    trees between ~83 and 100 levels returned null with a bare "maximum
    recursion depth exceeded" instead of data. The limit is now
    sys.getrecursionlimit() // 16 (the deepest walk it can produce stays
    at ~3/4 of the measured ceiling) and is pinned per handler instance at
    construction: raising the budget later serves deeper trees on NEW
    handlers, while an existing one reports a definite error on data beyond
    its pinned limit instead of silently truncating against cached,
    shallower documents.
  • Excess detection is a proof, not a guess. The unrolled document's
    innermost repetition is a probe: data occupying it proves deeper data
    exists. Complete data leaves the probe empty and is never flagged (the
    old depth-counting heuristic fired on exactly-complete trees), and
    single-object recursive edges (next: LNode | None) count like list
    edges — they used to truncate with no report at all.
  • Only stamped documents pay the per-response excess walk. Queries
    touching no recursive field never ran it and can no longer pick up
    false flags from JSON passthrough payloads whose keys collide with
    edge names; the walk also reuses edge names computed once per handler
    instead of rebuilding them per response.
  • Nested recursive chains unroll too. Stamping a chain end makes
    graphql-core's visit rebuild every ancestor of the edited node, so the
    id-keyed chain-end table silently missed an outer chain end nested
    inside another chain's template — two recursive types in one document
    truncated invisibly (the very thing unrolling exists to prevent).
    Detection and stamping now share one bottom-up pass: chain-end-ness is
    decided from each node itself (its type context and its own
    selections), so no marking state has to survive the node rebuilds.
  • A template alias reserving the back-edge's response key no longer
    fails validation.
    children: name inside the stopping selection
    collided with the stamped subtree (children { ... }) on the same
    response key, rejecting a legal query with ~100 FieldsConflict
    errors. Such chains now degrade to the written document — the agent's
    own key choice wins — the same honest fallback as fragment-carrying
    templates.
  • A failed model build no longer poisons the shared type cache (issue
    #3, case 1). Object/input registration is now transactional: when a
    field fails to map, the half-built type (and its name) rolls back out of
    the caches. Previously the first route was skipped but the incomplete
    type stayed cached, so a second route reusing the same model passed
    scanning and the GraphQLSchema build then crashed for the WHOLE app
    (TypeError: ... fields cannot be resolved) — one incompatible model
    took every valid route down with it. Nested failures roll back every
    recursion frame, and each reuse of a bad model now gets its own skip
    record.
  • set/frozenset map onto GraphQL lists (issue #3, case 3).
    Pydantic serializes sets to JSON arrays, so set[T] fields, parameters
    and responses bridge as [T!]! exactly like list[T] — previously the
    route was skipped with "Cannot map typing.Set".
  • Enum members inside Literal map to their underlying scalar (issue
    #3, case 2). Literal[Mode.A] now normalizes Mode.A to Mode.A.value
    and rides the existing Literal path (String/Int + the
    "Allowed values" description); mixed members like
    Literal[Mode.A, "other"] work, and route-level Literal responses now
    carry the allowed-values note in their field description, matching model
    fields. Previously such routes were skipped with "Literal of Mode has no
    GraphQL scalar".
  • TypedDict maps onto real object/input types (issue #3, case 6).
    A TypedDict response (or a TypedDict field nested inside a Pydantic
    model) becomes a GraphQLObjectType / GraphQLInputObjectType like a
    BaseModel: field types from get_type_hints, nullability from the
    required/optional key sets (total=False keys bridge as nullable — they
    may be absent from the JSON), recursive TypedDicts resolve through the
    same register-before-fields cycle handling, and failed builds roll back
    transactionally. Detection uses typing_extensions.is_typeddict, which
    recognizes classes declared via BOTH typing.TypedDict and
    typing_extensions.TypedDict (typing.is_typeddict misses the l...
Read more

v0.9.0

Choose a tag to compare

@allmonday allmonday released this 09 Oct 11:25

Changed

  • RouterMCP renamed to FastAPIMCP. The old name said Router while
    the class wraps a whole FastAPI app and IS the MCP server; FastAPIMCP
    names what it is and matches the package. RouterMCP remains as a
    deprecated subclass (DeprecationWarning on construction, removed at
    1.0) — existing imports keep working.

  • (breaking) RouterMCP.mode property renamed to resolved_mode —
    it returns the mode actually in effect and never "auto", unlike the
    mode constructor argument; the old name invited
    assert mcp.mode == "auto", which could never pass.

  • (breaking) ReadinessReport.from_scan removed from the public
    surface (added in 0.8.0): assembling a report required scan's internal
    products (TypeBuilder), which callers cannot obtain — the method was
    unusable externally and produced incomplete reports when forced.
    ReadinessReport is now pure data (three tuples + ready); get one
    from RouterScanner(app, ...).readiness() or handler.readiness().

  • handler.skips docstring now marks it a strict subset of
    readiness().skips and points to readiness() for the full audit.

v0.8.0

Choose a tag to compare

@github-actions github-actions released this 08 Oct 09:38

See CHANGELOG for details.

Full Changelog: v0.7.0...v0.8.0

v0.7.0

Choose a tag to compare

@github-actions github-actions released this 06 Oct 07:08

See CHANGELOG for details.

Full Changelog: v0.6.0...v0.7.0

v0.6.0

Choose a tag to compare

@github-actions github-actions released this 05 Oct 22:38

See CHANGELOG for details.

Full Changelog: v0.5.0...v0.6.0

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 05 Oct 00:55

See CHANGELOG for details.

Full Changelog: v0.4.0...v0.5.0

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 04 Oct 13:34