56
56
57
57
server_id_var : contextvars .ContextVar [str ] = contextvars .ContextVar ("server_id" , default = None )
58
58
59
- ## ------------------------------ Event store ------------------------------
59
+ # ------------------------------ Event store ------------------------------
60
60
61
61
62
62
@dataclass
@@ -92,7 +92,16 @@ def __init__(self, max_events_per_stream: int = 100):
92
92
self .event_index : dict [EventId , EventEntry ] = {}
93
93
94
94
async def store_event (self , stream_id : StreamId , message : JSONRPCMessage ) -> EventId :
95
- """Stores an event with a generated event ID."""
95
+ """
96
+ Stores an event with a generated event ID.
97
+
98
+ Args:
99
+ stream_id (StreamId): The ID of the stream.
100
+ message (JSONRPCMessage): The message to store.
101
+
102
+ Returns:
103
+ EventId: The ID of the stored event.
104
+ """
96
105
event_id = str (uuid4 ())
97
106
event_entry = EventEntry (event_id = event_id , stream_id = stream_id , message = message )
98
107
@@ -117,7 +126,16 @@ async def replay_events_after(
117
126
last_event_id : EventId ,
118
127
send_callback : EventCallback ,
119
128
) -> StreamId | None :
120
- """Replays events that occurred after the specified event ID."""
129
+ """
130
+ Replays events that occurred after the specified event ID.
131
+
132
+ Args:
133
+ last_event_id (EventId): The ID of the last received event. Replay starts after this event.
134
+ send_callback (EventCallback): Async callback to send each replayed event.
135
+
136
+ Returns:
137
+ StreamId | None: The stream ID if the event is found and replayed, otherwise None.
138
+ """
121
139
if last_event_id not in self .event_index :
122
140
logger .warning (f"Event ID { last_event_id } not found in store" )
123
141
return None
@@ -138,7 +156,7 @@ async def replay_events_after(
138
156
return stream_id
139
157
140
158
141
- ## ------------------------------ Streamable HTTP Transport ------------------------------
159
+ # ------------------------------ Streamable HTTP Transport ------------------------------
142
160
143
161
144
162
@asynccontextmanager
@@ -148,7 +166,7 @@ async def get_db():
148
166
149
167
Yields:
150
168
A database session instance from SessionLocal.
151
- Ensures the session is closed after use.
169
+ Ensures the session is closed after use.
152
170
"""
153
171
db = SessionLocal ()
154
172
try :
@@ -168,7 +186,7 @@ async def call_tool(name: str, arguments: dict) -> List[Union[types.TextContent,
168
186
169
187
Returns:
170
188
List of content (TextContent, ImageContent, or EmbeddedResource) from the tool response.
171
- Logs and returns an empty list on failure.
189
+ Logs and returns an empty list on failure.
172
190
"""
173
191
try :
174
192
async with get_db () as db :
@@ -190,7 +208,7 @@ async def list_tools() -> List[types.Tool]:
190
208
191
209
Returns:
192
210
A list of Tool objects containing metadata such as name, description, and input schema.
193
- Logs and returns an empty list on failure.
211
+ Logs and returns an empty list on failure.
194
212
"""
195
213
server_id = server_id_var .get ()
196
214
@@ -260,6 +278,10 @@ async def handle_streamable_http(self, scope: Scope, receive: Receive, send: Sen
260
278
scope (Scope): ASGI scope object containing connection information.
261
279
receive (Receive): ASGI receive callable.
262
280
send (Send): ASGI send callable.
281
+
282
+ Raises:
283
+ Exception: Any exception raised during request handling is logged.
284
+
263
285
Logs any exceptions that occur during request handling.
264
286
"""
265
287
@@ -277,20 +299,30 @@ async def handle_streamable_http(self, scope: Scope, receive: Receive, send: Sen
277
299
raise
278
300
279
301
280
- ## ------------------------- Authentication for /mcp routes ------------------------------
302
+ # ------------------------- Authentication for /mcp routes ------------------------------
281
303
282
304
283
305
async def streamable_http_auth (scope , receive , send ):
284
306
"""
285
307
Perform authentication check in middleware context (ASGI scope).
286
308
287
- If path does not end with "/mcp", just continue (return True).
309
+ This function is intended to be used in middleware wrapping ASGI apps.
310
+ It authenticates only requests targeting paths ending in "/mcp" or "/mcp/".
288
311
289
- Only check Authorization header for Bearer token.
290
- If no Bearer token provided, allow (return True).
312
+ Behavior:
313
+ - If the path does not end with "/mcp", authentication is skipped.
314
+ - If there is no Authorization header, the request is allowed.
315
+ - If a Bearer token is present, it is verified using `verify_credentials`.
316
+ - If verification fails, a 401 Unauthorized JSON response is sent.
291
317
292
- If auth_required is True and Bearer token provided, verify it.
293
- If verification fails, send 401 JSONResponse and return False.
318
+ Args:
319
+ scope: The ASGI scope dictionary, which includes request metadata.
320
+ receive: ASGI receive callable used to receive events.
321
+ send: ASGI send callable used to send events (e.g. a 401 response).
322
+
323
+ Returns:
324
+ bool: True if authentication passes or is skipped.
325
+ False if authentication fails and a 401 response is sent.
294
326
"""
295
327
296
328
path = scope .get ("path" , "" )
0 commit comments