Skip to content

feat: MCP get_er_diagram tool + strict selection validation (fix silent field swallowing) - #138

Merged
allmonday merged 7 commits into
masterfrom
feat/mcp-er-diagram-and-selection-validation
Aug 19, 2026
Merged

feat: MCP get_er_diagram tool + strict selection validation (fix silent field swallowing)#138
allmonday merged 7 commits into
masterfrom
feat/mcp-er-diagram-and-selection-validation

Conversation

@allmonday

Copy link
Copy Markdown
Collaborator

Summary

Two related changes, surfaced by an MCP "blind test" (an agent reasoning
only from what the MCP server returns):

  1. get_er_diagram on the single-app MCP server — the entity-first
    app had no AI-facing story for its data map. The server now exposes
    the entity-relationship diagram as Mermaid text (generated from the
    same SQLModel metadata that drives the GraphQL schema), with a
    documented discovery order: get_er_diagram (data map) →
    get_schema (operations) → graphql_query (execution). Tool
    docstrings now teach the real entity-rooted query shape. README Quick
    start gains a "For AI agents" section; examples/quickstart_mcp.py
    is the runnable companion (--check exercises all three tools
    through a real MCP client). Additive: default tool count 2 → 3.

  2. Strict selection validation (behavior fix) — a misspelled field
    ({ Team { by_filter { nmae } } }) was silently dropped at
    serialization and returned success: true with empty-object rows:
    zero signal for an AI agent (or any client) to self-correct. Unknown
    group/method names already errored; the entity-field level was never
    checked. The executor now validates selections pre-execution against
    model_fields (FK columns included — queryable though omitted from
    SDL) ∪ relationships (loader registry → get_relation_entity, same
    sources as serialization), erroring with
    Cannot query field 'nmae' on type 'Team'.

    • Paginated relationships judged with the shared
      is_active_paginated_relationship (same source as SDL/introspection);
      items validated against the target, pagination metadata-filtered.
    • __typename exempt; federation by_<key>_in batch roots exempt via
      a marker (mounters' wire selections include DTO-side computed fields
      the member drops by design).

Behavior change

Queries that previously "succeeded" returning empty objects now return
GraphQL-style errors. The federation wire protocol is unaffected
(exempted + covered by existing federation suites).

Test plan

  • New tests/test_selection_validation.py (8 cases: unknown field,
    nested unknown naming the target type, FK queryable, __typename,
    valid queries, sibling methods, items on non-paginated rel,
    batch-root exemption)
  • MCP tool-count/docstring tests updated
    (tests/mcp/test_simple_mcp.py, tests/test_documented_api.py)
  • Full suite: 1602 passed, 0 failed, ruff clean
  • examples/quickstart_mcp.py --check end-to-end; stdio handshake +
    tools/list smoke-tested with raw JSON-RPC
  • MCP blind-test probes: typo → self-correctable error, normal
    queries unaffected

🤖 Generated with Claude Code

allmonday and others added 7 commits August 17, 2026 04:02
The single-app MCP server (create_single_app_mcp_server) exposed only
get_schema + graphql_query, so an AI agent had to read the full SDL to
learn the data map. Add a get_er_diagram tool that returns the
entity-relationship diagram as Mermaid text, built from the same
SQLModel metadata that drives the GraphQL schema.

Discovery order for agents is now: get_er_diagram (data map) ->
get_schema (operations) -> graphql_query (execution). The tool docstring
documents this walkthrough and what happens inside a query; the
graphql_query docstring examples now use the real entity-rooted shape
({ Team { by_filter {...} } }) instead of a misleading flat shape, and
get_schema no longer claims to be the starting point.

Also: examples/quickstart_mcp.py (runnable companion of quickstart.py,
--check mode exercises all three tools through a real MCP client),
README Quick start gains a "For AI agents" section, and
scripts/mcp_agent_view.py dumps the raw payloads an MCP client sees
(explainability regression harness).

Default tool count goes 2 -> 3 (4 with allow_mutation=True) — additive.

Co-Authored-By: Claude <noreply@anthropic.com>
…pping them

A misspelled field in a selection ({ Team { by_filter { nmae } } })
passed through unvalidated and was silently dropped at serialization,
returning success: true with empty-object rows — zero signal for an AI
agent to self-correct (found by an MCP blind test; unknown group/method
names already errored, but the entity-field level was never checked).

Add pre-execution selection validation in QueryExecutor: a field is
valid when it is an entity column (model_fields, FK columns included —
they are queryable even though the SDL omits them) or a relationship,
resolved through the same sources the serializer uses (loader registry
-> get_relation_entity). Unknown fields append a GraphQL-style error
("Cannot query field 'nmae' on type 'Team'") and the method is skipped,
mirroring the unknown-method handling; sibling methods still execute.

Paginated relationships ({Target}Result packages) are detected with the
shared is_active_paginated_relationship predicate (same source as
SDL/introspection): items is validated against the target entity,
pagination is metadata-filtered, and other keys error against
{Target}Result. __typename is exempt.

Federation by_<key>_in batch roots are exempt via a
_nexusx_federation_batch_root marker: mounters' remote loaders select
DTO-side computed fields the member never serves (dropped by design,
recomputed mounter-side), so strict validation there would break the
federation wire protocol.

This is a behavior change: queries that previously "succeeded" with
empty objects now return errors.

Co-Authored-By: Claude <noreply@anthropic.com>
…entry

The mermaid ER diagram was a lossy map: no Result {items, pagination}
wrapping, no field args, link tables collapsed. Under strict selection
validation it actively misled agents into writing queries that fail.
The SDL is the source of truth and agents parse it natively, so
get_schema becomes the single discovery entry point, with its docstring
now documenting both list-relationship shapes (plain [Entity!]! vs
EntityResult! wrapping).

get_er_diagram was added on this branch (a3ddb17) and never released.

Co-Authored-By: Claude <noreply@anthropic.com>
…OR code

A failed method used to leave an empty-object ``Entity: {}`` in data —
a misleading "empty success" for clients that read data and ignore
errors. Method return types are non-null, so the failure now nulls the
whole group (GraphQL null propagation at group granularity; sibling
results are discarded by design). The error entry also carries
extensions.code RESOLVER_ERROR, matching BARE_GROUP_FIELD's style, for
machine-side classification.

Co-Authored-By: Claude <noreply@anthropic.com>
Phase 3 now locks every UseCaseService method's return structure with a
user-confirmed ASCII tree (tree root = the return type annotation)
before dtos.py is written, and the core-principles list gains the
matching rule.

Co-Authored-By: Claude <noreply@anthropic.com>
Research artifact from the ER-diagram exploration: dumps tool payloads
to /tmp for offline analysis. It calls the get_er_diagram tool removed
in 34a5f6c (fails with ToolError) and nothing references it; the
analysis it fed became the SDL-single-entry design instead.

Co-Authored-By: Claude <noreply@anthropic.com>
The {items, pagination} unwrap logic existed twice — once for
pagination roots in _validate_method_selection, once for paginated
relationships in _validate_entity_fields — with divergent behavior:
an unknown key on a pagination-root package fell through to entity-
field validation and errored "on type '<Entity>'", pointing at the
wrong type. Both paths now share _validate_paginated_package; the
root path takes the wrapper name and fk key from the method's
_pagination_root meta, so unknown keys name the actual wrapper type
({Entity}{Field}PagePackage / {Target}Result).

Co-Authored-By: Claude <noreply@anthropic.com>
@allmonday
allmonday merged commit 579dbe1 into master Aug 19, 2026
6 checks passed
allmonday added a commit that referenced this pull request Aug 20, 2026
Version bump for PR #138 (pre-execution selection validation, resolver
error null-propagation) and PR #139 (page_by_*_in order default fallback).
Updates docs/changelog.md, pyproject.toml, uv.lock.

Co-Authored-By: Claude <noreply@anthropic.com>
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.

1 participant