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