-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathquickstart_mcp.py
More file actions
148 lines (115 loc) · 5.82 KB
/
Copy pathquickstart_mcp.py
File metadata and controls
148 lines (115 loc) · 5.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
"""Quick start (MCP) — serve the same entity app to AI agents.
The companion of ``examples/quickstart.py``: same Team/Hero entities, same
in-memory database, but delivered over MCP instead of a GraphQL HTTP endpoint.
Run as a real MCP server (stdio transport, ready for any MCP client):
uv run python examples/quickstart_mcp.py
Register it with an MCP client, e.g. Claude Code (one line):
claude mcp add quickstart --
uv --directory /path/to/nexusx run python examples/quickstart_mcp.py
Run the built-in self-check (seeds the database, calls every tool in-process
through a real MCP client, and prints the results):
uv run python examples/quickstart_mcp.py --check
Requires the optional MCP integration:
pip install "nexusx[fastmcp]"
"""
import asyncio
import sys
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from sqlalchemy.pool import StaticPool
from sqlmodel import Field, Relationship, SQLModel
from sqlmodel.ext.asyncio.session import AsyncSession
from nexusx import AutoQueryConfig
from nexusx.mcp import create_single_app_mcp_server
class BaseEntity(SQLModel):
pass
class Team(BaseEntity, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
heroes: list["Hero"] = Relationship(back_populates="team")
class Hero(BaseEntity, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
team_id: int | None = Field(default=None, foreign_key="team.id")
team: Team | None = Relationship(back_populates="heroes")
engine = create_async_engine(
"sqlite+aiosqlite:///:memory:",
poolclass=StaticPool,
)
session_factory = async_sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False,
)
# One call replaces the Quick start's FastAPI wiring. The server exposes
# two tools (see the walkthrough below) instead of a /graphql endpoint.
mcp = create_single_app_mcp_server(
base=BaseEntity,
session_factory=session_factory,
auto_query_config=AutoQueryConfig(),
name="Quickstart API",
desc="Teams and heroes",
)
# ─────────────────────────────────────────────────────────────────────────────
# How an AI agent uses this server
# ─────────────────────────────────────────────────────────────────────────────
# The agent sees two tools and walks them in order:
#
# 1. get_schema() → the map and the operations in one read:
# GraphQL SDL listing every entity type with
# its fields and relationships in their exact
# query shape (a plain `[Hero!]!` list vs a
# wrapped `Result { items, pagination }`),
# plus what can be queried. AutoQueryConfig()
# gave every entity `by_id` and `by_filter`
# roots.
#
# 2. graphql_query(query) → execution: send a GraphQL query, get JSON.
#
# The SDL is the single source of truth for writing queries — the agent
# pulls it once and then only pays tokens for data.
#
# What happens inside graphql_query("{ Team { by_filter { name heroes { name } } } }"):
#
# query string
# → parsed and validated against the schema generated from the entities
# → the root field `Team.by_filter` resolves to one SQL query on `team`
# → the nested `heroes` selection does NOT run per row: a DataLoader
# batches the team IDs and loads ALL heroes in one extra SQL query
# (relationship query count grows with nesting depth, not row count)
# → only the selected fields are serialized; unselected columns are
# never loaded from SQL in the first place
#
# ─────────────────────────────────────────────────────────────────────────────
async def seed() -> None:
"""Create tables and one row of sample data (same as the Quick start)."""
async with engine.begin() as connection:
await connection.run_sync(SQLModel.metadata.create_all)
async with session_factory() as session:
team = Team(name="Avengers")
session.add(team)
await session.flush()
session.add(Hero(name="Spider-Man", team_id=team.id))
await session.commit()
async def check() -> None:
"""Call every tool in-process through a real MCP client and print results."""
from fastmcp import Client
async with Client(mcp) as client:
print("── tool 1: get_schema (SDL excerpt) ────────────────────")
schema = await client.call_tool("get_schema", {})
sdl: str = schema.data["data"]["sdl"]
print("\n".join(sdl.splitlines()[:8]) + "\n...")
print("── tool 2: graphql_query ───────────────────────────────")
query = "{ Team { by_filter { name heroes { name } } } }"
result = await client.call_tool("graphql_query", {"query": query})
print(f"query: {query}")
print(f"result: {result.data}")
if __name__ == "__main__":
if "--check" in sys.argv:
asyncio.run(seed()) # one loop for DB setup...
asyncio.run(check()) # ...one for the MCP client session
else:
asyncio.run(seed())
# stdio transport: stdout carries the MCP protocol, so keep it clean —
# any banner text goes to stderr.
print("Quickstart MCP server running on stdio", file=sys.stderr)
mcp.run()