UseCase MCP Service
This page solves one problem: you have business service methods powering FastAPI routes, and you want to expose the same logic to AI agents — without duplicating code.
Goal
You have this:
class TaskService(UseCaseService):
@query
async def list_tasks(cls) -> list[TaskSummary]:
...
@query
async def get_task(cls, task_id: int) -> TaskSummary | None:
...
You want AI agents to discover and call these methods through a standard MCP protocol:
Agent → "What services are available?"
→ list_apps() → ["project"]
Agent → "What can TaskService do?"
→ describe_service("project", "TaskService")
→ [list_tasks(), get_task(task_id)]
Agent → "Show me task 1"
→ call_use_case("project", "TaskService", "get_task", {"task_id": 1})
→ {id: 1, title: "Design docs", owner_name: "Ada"}
The same TaskService classmethods power both FastAPI routes and MCP tool calls. Business logic lives in one place.
Install
Step 1: Define Services
Create service classes with @query and @mutation decorators. Docstrings become descriptions visible to AI agents:
from pydantic import BaseModel
from pydantic_resolve import query
from pydantic_resolve.use_case import UseCaseService
class UserSummary(BaseModel):
id: int
name: str
class TaskSummary(BaseModel):
id: int
title: str
owner_name: str
class UserService(UseCaseService): # (1)
"""User management service."""
@query # (2)
async def list_users(cls) -> list[UserSummary]:
"""Get all users."""
...
class TaskService(UseCaseService):
"""Task management service."""
@query
async def list_tasks(cls) -> list[TaskSummary]:
"""Get all tasks."""
...
@query
async def get_task(cls, task_id: int) -> TaskSummary | None:
"""Get a task by ID."""
...
UseCaseServiceuses a metaclass to discover methods decorated with@queryor@mutation.- The docstring becomes the tool description that AI agents see.
Step 2: Create MCP Server
from pydantic_resolve.use_case import UseCaseAppConfig, create_use_case_mcp_server
mcp = create_use_case_mcp_server( # (1)
apps=[
UseCaseAppConfig(
name="project", # (2)
services=[UserService, TaskService], # (3)
description="Project management with users and tasks",
),
],
name="Project UseCase API",
)
mcp.run(transport="streamable-http", port=8080) # (4)
create_use_case_mcp_serverscans services and generates MCP tools.nameis the app identifier agents use to target a specific group of services.- Pass service classes directly — no need to instantiate them.
- Starts an HTTP server that MCP clients connect to.
How the Discovery Works
The MCP server exposes four tools that guide AI agents step by step:
sequenceDiagram
participant A as AI Agent
participant M as MCP Server
participant S as TaskService
A->>M: list_apps()
M-->>A: ["project"]
A->>M: list_services("project")
M-->>A: ["UserService", "TaskService"]
A->>M: describe_service("project", "TaskService")
M-->>A: methods, schemas, selection hints
A->>M: call_use_case("project", "TaskService", "get_task", {"task_id": 1})
M->>S: TaskService.get_task(task_id=1)
S-->>M: TaskSummary(...)
M-->>A: {id: 1, title: "Design docs", ...}
list_apps— discover available applications.list_services— list services within an app.describe_service— show method signatures, parameter schemas, and DTO type definitions.call_use_case— execute a method and return the result.
Selection support
For methods returning Pydantic DTOs, call_use_case accepts an optional selection parameter — a rootless GraphQL-like projection string. It filters the response fields without changing method parameters, data loading, or business execution.
describe_service includes selection_supported and selection_example on each method so agents know when and how to use it.
FromContext: Inject Request Context
When a method needs user identity or other request-scoped data, mark parameters with FromContext:
from typing import Annotated
from pydantic_resolve.use_case import FromContext
class TaskService(UseCaseService):
@query
async def get_my_tasks(
cls,
user_id: Annotated[int, FromContext()], # (1)
) -> list[TaskSummary]:
"""Get tasks owned by the authenticated user."""
...
FromContext()tells the MCP server to inject this parameter from the request context, not from the agent'sparamsJSON.
Then configure a context_extractor on the app:
from fastmcp.server.context import Context
from fastmcp.server.dependencies import get_http_headers
def extract_user_context(ctx: Context) -> dict:
headers = get_http_headers(include={"authorization"}) # (1)
auth = headers.get("authorization", "")
if auth.startswith("Bearer "):
token = auth[7:]
return {"user_id": int(token)}
return {}
mcp = create_use_case_mcp_server(
apps=[
UseCaseAppConfig(
name="project",
services=[TaskService],
context_extractor=extract_user_context, # (2)
),
],
)
get_http_headers()excludesauthorizationby default. Passinclude={"authorization"}to receive it.- The extractor runs on every request. Its return dict is merged into method kwargs.
Data flow:
flowchart LR
A["HTTP Request<br/>Bearer token"] --> B["FastMCP Context"]
B --> C["context_extractor"]
C --> D["call_use_case"]
D --> E["TaskService.get_my_tasks<br/>user_id=1"]
The method signature stays identical for FastAPI usage — just pass user_id directly:
from fastapi import Depends
@app.get("/my-tasks")
async def my_tasks(user_id: int = Depends(get_current_user_id)):
return await TaskService.get_my_tasks(user_id=user_id)
Share Services with FastAPI
The same classmethods power both HTTP API and MCP:
from pydantic_resolve.utils.types import get_return_annotation
@app.get("/api/tasks", tags=[TaskService.get_tag_name()])
async def get_tasks():
return await TaskService.list_tasks()
@app.get(
"/api/tasks/{task_id}",
response_model=get_return_annotation(TaskService.get_task), # (1)
tags=[TaskService.get_tag_name()],
)
async def get_task(task_id: int):
result = await TaskService.get_task(task_id=task_id)
if result is None:
raise HTTPException(status_code=404)
return result
get_return_annotationextracts the return type from the classmethod, so you can use it as FastAPI'sresponse_modelwithout repeating the type.
Control Mutation Visibility
To hide mutation methods from AI agents, set enable_mutation=False:
from pydantic_resolve import query, mutation
class TaskService(UseCaseService):
@query
async def list_tasks(cls) -> list[TaskSummary]:
...
@mutation
async def create_task(cls, title: str) -> TaskSummary:
...
mcp = create_use_case_mcp_server(
apps=[
UseCaseAppConfig(
name="readonly-project",
services=[TaskService],
enable_mutation=False, # (1)
),
],
)
- When
enable_mutation=False:list_servicesexcludes mutation methods from the count,describe_serviceomits them, andcall_use_casereturns an error if one is called.
Multi-App Support
Serve multiple independent application groups from one MCP server:
mcp = create_use_case_mcp_server(
apps=[
UseCaseAppConfig(name="project", services=[SprintService, TaskService]),
UseCaseAppConfig(name="admin", services=[UserService, RoleService]),
],
name="My Platform",
)
Each app has its own services and optional context_extractor.
GraphQL MCP vs UseCase MCP
| GraphQL MCP | UseCase MCP | |
|---|---|---|
| Input | ER Diagram | UseCaseService classes |
| Query | GraphQL syntax | Method signatures |
| Best for | Flexible ad-hoc queries | Fixed business operations |
| Setup | create_mcp_server + ERD |
create_use_case_mcp_server + services |
If you already have UseCaseService classes powering your FastAPI endpoints, UseCase MCP is the natural choice — zero duplication.
Next
- UseCase MCP API — detailed API signatures.
- MCP Service — the GraphQL-based MCP approach.