@@ -23,8 +23,62 @@ in the [tests/app/ folder](tests/app).
2323 handling.
2424- ** Type-Safe** : Leverages ` Pydantic ` for robust data validation and
2525 serialization.
26- - ** Stateful Context** : Maintain state across tool calls using a context object.
27- - ** Request Access** : Access the incoming request object from your tools.
26+ - ** Server State Management** : Access shared state through the lifespan context
27+ using the ` get_state_key ` method.
28+ - ** Request Access** : Access the incoming request object from your tools and
29+ prompts.
30+
31+ ## Server Architecture
32+
33+ The library provides a single ` MCPServer ` class that uses lifespan to manage
34+ shared state across the entire application lifecycle.
35+
36+ ### MCPServer
37+
38+ The ` MCPServer ` is designed to work with Starlette's lifespan system for
39+ managing shared server state.
40+
41+ ** Key Characteristics:**
42+
43+ - ** Lifespan Based** : Uses Starlette's lifespan events to initialize and manage
44+ shared server state
45+ - ** Application-Level State** : State persists across the entire application
46+ lifecycle, not per-request
47+ - ** Flexible** : Can be used with any custom context class stored in the lifespan
48+ state
49+
50+ ** Example Usage:**
51+
52+ ``` python
53+ import contextlib
54+ from collections.abc import AsyncIterator
55+ from typing import TypedDict
56+ from dataclasses import dataclass, field
57+ from starlette.applications import Starlette
58+ from http_mcp.server import MCPServer
59+
60+ @dataclass
61+ class Context :
62+ call_count: int = 0
63+ user_preferences: dict = field(default_factory = dict )
64+
65+ class State (TypedDict ):
66+ context: Context
67+
68+ @contextlib.asynccontextmanager
69+ async def lifespan (_app : Starlette) -> AsyncIterator[State]:
70+ yield {" context" : Context()}
71+
72+ mcp_server = MCPServer(
73+ name = " my-server" ,
74+ version = " 1.0.0" ,
75+ tools = my_tools,
76+ prompts = my_prompts
77+ )
78+
79+ app = Starlette(lifespan = lifespan)
80+ app.mount(" /mcp" , mcp_server.app)
81+ ```
2882
2983## Tools
3084
@@ -52,26 +106,26 @@ class GreetOutput(BaseModel):
52106
53107``` python
54108# app/tools/tools.py
55- from http_mcp.tools import ToolArguments
109+ from http_mcp.types import Arguments
56110
57111from app.tools.models import GreetInput, GreetOutput
58112
59- def greet (args : ToolArguments [GreetInput, None ]) -> GreetOutput:
60- return GreetOutput(answer = f " Hello, { args.inputs.question} ! " ) # access inputs via args.inputs
113+ def greet (args : Arguments [GreetInput]) -> GreetOutput:
114+ return GreetOutput(answer = f " Hello, { args.inputs.question} ! " )
61115
62116```
63117
64118``` python
65119# app/tools/__init__.py
66120
67- from http_mcp.tools import Tool
121+ from http_mcp.types import Tool
68122from app.tools.models import GreetInput, GreetOutput
69123from app.tools.tools import greet
70124
71125TOOLS = (
72126 Tool(
73127 func = greet,
74- input = GreetInput,
128+ inputs = GreetInput,
75129 output = GreetOutput,
76130 ),
77131)
@@ -95,16 +149,14 @@ app.mount(
95149 " /mcp" ,
96150 mcp_server.app,
97151)
98-
99152```
100153
101- ## Stateful Context
154+ ## Server State Management
102155
103- This is the server context attribute; it can be seen as a global state for the
104- server.
105-
106- You can use a context object to maintain state across tool calls. The context
107- object is passed to each tool call and can be used to store and retrieve data.
156+ The server uses Starlette's lifespan system to manage shared state across the
157+ entire application lifecycle. State is initialized when the application starts
158+ and persists until it shuts down. Context is accessed through the
159+ ` get_state_key ` method on the ` Arguments ` object.
108160
109161Example:
110162
@@ -126,26 +178,38 @@ class Context:
126178 self .called_tools.append(tool_name)
127179```
128180
129- 1 . ** Instantiate the context and the server :**
181+ 2 . ** Set up the application with lifespan :**
130182
131183``` python
132- from app.tools import TOOLS
184+ import contextlib
185+ from collections.abc import AsyncIterator
186+ from typing import TypedDict
187+ from starlette.applications import Starlette
133188from app.context import Context
134189from http_mcp.server import MCPServer
135190
136- mcp_server: MCPServer[Context] = MCPServer(
191+ class State (TypedDict ):
192+ context: Context
193+
194+ @contextlib.asynccontextmanager
195+ async def lifespan (_app : Starlette) -> AsyncIterator[State]:
196+ yield {" context" : Context(called_tools = [])}
197+
198+ mcp_server = MCPServer(
137199 tools = TOOLS ,
138200 name = " test" ,
139201 version = " 1.0.0" ,
140- context = Context(called_tools = []),
141202)
203+
204+ app = Starlette(lifespan = lifespan)
205+ app.mount(" /mcp" , mcp_server.app)
142206```
143207
144- 1 . ** Access the context in your tools:**
208+ 3 . ** Access the context in your tools:**
145209
146210``` python
147211from pydantic import BaseModel, Field
148- from http_mcp.tools import ToolArguments
212+ from http_mcp.types import Arguments
149213from app.context import Context
150214
151215class MyToolArguments (BaseModel ):
@@ -154,23 +218,24 @@ class MyToolArguments(BaseModel):
154218class MyToolOutput (BaseModel ):
155219 answer: str = Field(description = " The answer to the question" )
156220
157- async def my_tool (args : ToolArguments[MyToolArguments, Context]) -> MyToolOutput:
158- # Access the context
159- args.context.add_called_tool(" my_tool" )
221+ async def my_tool (args : Arguments[MyToolArguments]) -> MyToolOutput:
222+ # Access the context from lifespan state
223+ context = args.get_state_key(" context" , Context)
224+ context.add_called_tool(" my_tool" )
160225 ...
161226
162227 return MyToolOutput(answer = f " Hello, { args.inputs.question} ! " )
163228```
164229
165- ## Stateless Context
230+ ## Request Access
166231
167232You can access the incoming request object from your tools. The request object
168233is passed to each tool call and can be used to access headers, cookies, and
169234other request data (e.g. request.state, request.scope).
170235
171236``` python
172237from pydantic import BaseModel, Field
173- from http_mcp.tools import ToolArguments
238+ from http_mcp.types import Arguments
174239
175240class MyToolArguments (BaseModel ):
176241 question: str = Field(description = " The question to answer" )
@@ -179,26 +244,36 @@ class MyToolOutput(BaseModel):
179244 answer: str = Field(description = " The answer to the question" )
180245
181246
182- async def my_tool (args : ToolArguments [MyToolArguments, None ]) -> MyToolOutput:
247+ async def my_tool (args : Arguments [MyToolArguments]) -> MyToolOutput:
183248 # Access the request
184249 auth_header = args.request.headers.get(" Authorization" )
185250 ...
186251
187252 return MyToolOutput(answer = f " Hello, { args.inputs.question} ! " )
253+
254+ # Use MCPServer:
255+ from http_mcp.server import MCPServer
256+
257+ mcp_server = MCPServer(
258+ name = " my-server" ,
259+ version = " 1.0.0" ,
260+ tools = (my_tool,),
261+ )
188262```
189263
190264## Prompts
191265
192- You can add interactive templates that are invoked by user choice.
266+ You can add interactive templates that are invoked by user choice. Prompts now
267+ support lifespan state access, similar to tools.
193268
194- 1 . ** Define the arguments and output for the prompts:**
269+ 1 . ** Define the arguments for the prompts:**
195270
196271``` python
197272from pydantic import BaseModel, Field
198273
199274from http_mcp.mcp_types.content import TextContent
200275from http_mcp.mcp_types.prompts import PromptMessage
201- from http_mcp.prompts import Prompt
276+ from http_mcp.types import Arguments, Prompt
202277
203278
204279class GetAdvice (BaseModel ):
@@ -208,17 +283,22 @@ class GetAdvice(BaseModel):
208283 )
209284
210285
211- def get_advice (args : GetAdvice) -> tuple[PromptMessage, ... ]:
286+ def get_advice (args : Arguments[ GetAdvice] ) -> tuple[PromptMessage, ... ]:
212287 """ Get advice on a topic."""
213288 template = """
214289 You are a helpful assistant that can give advice on {topic} .
215290 """
216- if args.include_actionable_steps:
291+ if args.inputs. include_actionable_steps:
217292 template += """
218293 The advice should include actionable steps.
219294 """
220295 return (
221- PromptMessage(role = " user" , content = TextContent(text = template.format(topic = args.topic))),
296+ PromptMessage(
297+ role = " user" ,
298+ content = TextContent(
299+ text = template.format(topic = args.inputs.topic)
300+ ),
301+ ),
222302 )
223303
224304
@@ -230,7 +310,49 @@ PROMPTS = (
230310)
231311```
232312
233- 2 . ** Instantiate the server:**
313+ 2 . ** Using prompts with lifespan state:**
314+
315+ ``` python
316+ from pydantic import BaseModel, Field
317+ from http_mcp.mcp_types.content import TextContent
318+ from http_mcp.mcp_types.prompts import PromptMessage
319+ from http_mcp.types import Arguments, Prompt
320+ from app.context import Context
321+
322+ class GetAdvice (BaseModel ):
323+ topic: str = Field(description = " The topic to get advice on" )
324+
325+ def get_advice_with_context (args : Arguments[GetAdvice]) -> tuple[PromptMessage, ... ]:
326+ """ Get advice on a topic with context awareness."""
327+ # Access the context from lifespan state
328+ context = args.get_state_key(" context" , Context)
329+ called_tools = context.get_called_tools()
330+ template = """
331+ You are a helpful assistant that can give advice on {topic} .
332+ Previously called tools: {tools}
333+ """
334+
335+ return (
336+ PromptMessage(
337+ role = " user" ,
338+ content = TextContent(
339+ text = template.format(
340+ topic = args.inputs.topic,
341+ tools = " , " .join(called_tools) if called_tools else " none"
342+ )
343+ )
344+ ),
345+ )
346+
347+ PROMPTS_WITH_CONTEXT = (
348+ Prompt(
349+ func = get_advice_with_context,
350+ arguments_type = GetAdvice,
351+ ),
352+ )
353+ ```
354+
355+ 3 . ** Instantiate the server:**
234356
235357``` python
236358from starlette.applications import Starlette
@@ -245,5 +367,4 @@ app.mount(
245367 " /mcp" ,
246368 mcp_server.app,
247369)
248-
249370```
0 commit comments