Skip to content

Migration Guide: 3.0 — UseCase GraphQL MCP

This guide walks through migrating from the removed direct-call UseCase MCP entries (create_use_case_mcp_server, create_use_case_flat_server) to the new GraphQL-based MCP entry (create_use_case_graphql_mcp_server).

What changed

Removed (2.x) Replacement (3.0)
create_use_case_mcp_server create_use_case_graphql_mcp_server
create_use_case_flat_server create_use_case_graphql_mcp_server

The old MCP exposed service methods via direct Python calls with JSON-encoded parameters. The new MCP exposes them through a real GraphQL schema generated from UseCaseService signatures, with Layer 3 accepting standard GraphQL query strings.

Unchanged (no migration needed): - UseCaseService, BusinessMeta, @query, @mutation, FromContext, UseCaseAppConfig — service declaration is identical. - create_use_case_router (FastAPI REST) — orthogonal surface, untouched. - create_jsonrpc_router (JSON-RPC) — orthogonal surface, untouched. - create_use_case_voyager (visualization) — orthogonal surface, untouched.

Migrating from create_use_case_mcp_server

Before (2.x)

from nexusx import UseCaseAppConfig, create_use_case_mcp_server

mcp = create_use_case_mcp_server(
    apps=[
        UseCaseAppConfig(
            name="project",
            services=[UserService, TaskService, SprintService],
            description="Project management",
        ),
    ],
    name="Project MCP",
)
mcp.run()

LLM agents consumed this via 4 progressive-disclosure tools: - list_apps() — discover apps - list_services(app_name) — list services & method names - describe_service(app_name, service_name) — method signatures - call_use_case(app_name, service_name, method_name, params='{"arg": "val"}') — execute

After (3.0)

from nexusx import UseCaseAppConfig, create_use_case_graphql_mcp_server

mcp = create_use_case_graphql_mcp_server(
    apps=[
        UseCaseAppConfig(
            name="project",
            services=[UserService, TaskService, SprintService],
            description="Project management",
        ),
    ],
    name="Project MCP",
)
mcp.run()

LLM agents now consume this via 4 progressive-disclosure tools: - list_apps() — discover apps (same) - describe_compose_schema(app_name) — list services & method names (renamed) - describe_compose_method(app_name, service_name, method_name) — method signature + SDL fragment (renamed) - compose_query(app_name, query)execute via GraphQL string (replaces call_use_case)

The same UserService / TaskService / SprintService classes work unchanged.

Calling a method

Before — call_use_case with JSON params:

result = await call_use_case(
    app_name="project",
    service_name="TaskService",
    method_name="get_tasks_by_sprint",
    params='{"sprint_id": 42}',
    selection="{ id title owner { name } }",  # optional rootless selection
)

After — compose_query with GraphQL:

result = await compose_query(
    app_name="project",
    query="""
        query {
          TaskService {
            get_tasks_by_sprint(sprint_id: 42) {
              id
              title
              owner { name }
            }
          }
        }
    """,
)
# result = {"data": {"TaskService": {"get_tasks_by_sprint": [...]}}}

Response envelope

Before — all 4 tools returned {success, data, hint} or {success, error, error_type}.

After — split: - Layers 0–2 (list_apps / describe_compose_schema / describe_compose_method) still return {success, data} / {success, error, error_type}. - Layer 3 (compose_query) returns GraphQL standard {data, errors} to match what GraphQL clients and AI agents expect from a GraphQL execution surface.

Introspection is rejected in compose_query

The old MCP didn't have this concept. The new compose_query rejects queries that touch __schema, __type, or __typename, returning:

{
  "data": null,
  "errors": [{
    "message": "GraphQL introspection is not available via compose_query. Use describe_compose_schema(app_name=...) and describe_compose_method(...) to discover the schema."
  }]
}

Use describe_compose_schema (Layer 1) for service/method overview and describe_compose_method (Layer 2) for full SDL fragments.

Migrating from create_use_case_flat_server

The flat MCP exposed one tool per method ({ServiceName}_{method_name}), each taking JSON params. The 3.0 release does not provide a direct flat replacement; the recommended migration path is to use the GraphQL MCP.

Before (2.x)

from nexusx import create_flat_mcp_server  # renamed in 2.9.0 to create_use_case_flat_server

mcp = create_use_case_flat_server(
    apps=[UseCaseAppConfig(name="order", services=[OrderService])],
)
mcp.run()
# Tools: OrderService_create_order, OrderService_get_order, ...

After (3.0)

from nexusx import create_use_case_graphql_mcp_server

mcp = create_use_case_graphql_mcp_server(
    apps=[UseCaseAppConfig(name="order", services=[OrderService])],
)
mcp.run()
# Single compose_query tool; agents call methods via GraphQL:
#   query { OrderService { create_order(...) { id } } }

If you specifically need a flat tool-per-method surface (e.g., for clients that can't compose GraphQL strings), build it yourself on top of build_compose_schema:

from nexusx import build_compose_schema, UseCaseAppConfig
from fastmcp import FastMCP

app_config = UseCaseAppConfig(name="order", services=[OrderService])
schema = build_compose_schema(app_config)
mcp = FastMCP("Custom Flat MCP")

# Iterate schema's services/methods and register one tool per method.
# (Implementation left to the user — see ComposeSchema.render_method_sdl for SDL.)

FAQ

Do I need to change my UseCaseService classes?

No. The same @query / @mutation decorated async classmethods work unchanged. FromContext parameters continue to be filtered out of the public schema and injected via context_extractor at execution time.

Do I need to change my DTOs?

No — Pydantic models (including DefineSubset-generated DTOs) work as-is. They get converted to GraphQL OBJECT types automatically.

Does Resolver().resolve(dtos) still work inside service methods?

Yes, and you must call it yourself if you want Resolver behavior (e.g., auto-load relationship fields on returned DTOs). The new GraphQL execution layer does not wrap results in Resolver — service methods own that responsibility. This matches the 2.x behavior.

What if my DTO has resolve_* methods but I don't call Resolver().resolve()?

The resolve_* methods will not fire — returned DTOs will have None / default values for those fields. This is intentional and matches the 2.x pattern where the service method was responsible for invoking Resolver.

Where did ServiceIntrospector go?

It was an internal class that generated SDL-style strings for the old MCP's describe_service tool. It's replaced by ComposeSchema, which generates a real GraphQL schema (introspection JSON + SDL). You shouldn't have been importing ServiceIntrospector directly; if you were, use build_compose_schema(app) and then schema.render_sdl() or schema.render_method_sdl(service, method).