fastmcp 2.2.5__py3-none-any.whl → 2.2.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
fastmcp/__init__.py CHANGED
@@ -14,6 +14,7 @@ __all__ = [
14
14
  "FastMCP",
15
15
  "Context",
16
16
  "client",
17
+ "Client",
17
18
  "settings",
18
19
  "Image",
19
20
  ]
fastmcp/client/base.py CHANGED
@@ -1 +0,0 @@
1
-
fastmcp/client/client.py CHANGED
@@ -1,16 +1,13 @@
1
1
  import datetime
2
2
  from contextlib import AbstractAsyncContextManager
3
3
  from pathlib import Path
4
- from typing import Any, Literal, cast, overload
4
+ from typing import Any, cast
5
5
 
6
6
  import mcp.types
7
7
  from mcp import ClientSession
8
- from mcp.client.session import (
9
- LoggingFnT,
10
- MessageHandlerFnT,
11
- )
12
8
  from pydantic import AnyUrl
13
9
 
10
+ from fastmcp.client.logging import LogHandler, MessageHandler
14
11
  from fastmcp.client.roots import (
15
12
  RootsHandler,
16
13
  RootsList,
@@ -22,7 +19,14 @@ from fastmcp.server import FastMCP
22
19
 
23
20
  from .transports import ClientTransport, SessionKwargs, infer_transport
24
21
 
25
- __all__ = ["Client", "RootsHandler", "RootsList"]
22
+ __all__ = [
23
+ "Client",
24
+ "RootsHandler",
25
+ "RootsList",
26
+ "LogHandler",
27
+ "MessageHandler",
28
+ "SamplingHandler",
29
+ ]
26
30
 
27
31
 
28
32
  class Client:
@@ -35,12 +39,12 @@ class Client:
35
39
 
36
40
  def __init__(
37
41
  self,
38
- transport: ClientTransport | FastMCP | AnyUrl | Path | str,
42
+ transport: ClientTransport | FastMCP | AnyUrl | Path | dict[str, Any] | str,
39
43
  # Common args
40
44
  roots: RootsList | RootsHandler | None = None,
41
45
  sampling_handler: SamplingHandler | None = None,
42
- log_handler: LoggingFnT | None = None,
43
- message_handler: MessageHandlerFnT | None = None,
46
+ log_handler: LogHandler | None = None,
47
+ message_handler: MessageHandler | None = None,
44
48
  read_timeout_seconds: datetime.timedelta | None = None,
45
49
  ):
46
50
  self.transport = infer_transport(transport)
@@ -103,6 +107,7 @@ class Client:
103
107
  self._session = None
104
108
 
105
109
  # --- MCP Client Methods ---
110
+
106
111
  async def ping(self) -> None:
107
112
  """Send a ping request."""
108
113
  await self.session.send_ping()
@@ -124,23 +129,100 @@ class Client:
124
129
  """Send a roots/list_changed notification."""
125
130
  await self.session.send_roots_list_changed()
126
131
 
127
- async def list_resources(self) -> list[mcp.types.Resource]:
128
- """Send a resources/list request."""
132
+ # --- Resources ---
133
+
134
+ async def list_resources_mcp(self) -> mcp.types.ListResourcesResult:
135
+ """Send a resources/list request and return the complete MCP protocol result.
136
+
137
+ Returns:
138
+ mcp.types.ListResourcesResult: The complete response object from the protocol,
139
+ containing the list of resources and any additional metadata.
140
+
141
+ Raises:
142
+ RuntimeError: If called while the client is not connected.
143
+ """
129
144
  result = await self.session.list_resources()
145
+ return result
146
+
147
+ async def list_resources(self) -> list[mcp.types.Resource]:
148
+ """Retrieve a list of resources available on the server.
149
+
150
+ Returns:
151
+ list[mcp.types.Resource]: A list of Resource objects.
152
+
153
+ Raises:
154
+ RuntimeError: If called while the client is not connected.
155
+ """
156
+ result = await self.list_resources_mcp()
130
157
  return result.resources
131
158
 
132
- async def list_resource_templates(self) -> list[mcp.types.ResourceTemplate]:
133
- """Send a resources/listResourceTemplates request."""
159
+ async def list_resource_templates_mcp(
160
+ self,
161
+ ) -> mcp.types.ListResourceTemplatesResult:
162
+ """Send a resources/listResourceTemplates request and return the complete MCP protocol result.
163
+
164
+ Returns:
165
+ mcp.types.ListResourceTemplatesResult: The complete response object from the protocol,
166
+ containing the list of resource templates and any additional metadata.
167
+
168
+ Raises:
169
+ RuntimeError: If called while the client is not connected.
170
+ """
134
171
  result = await self.session.list_resource_templates()
172
+ return result
173
+
174
+ async def list_resource_templates(
175
+ self,
176
+ ) -> list[mcp.types.ResourceTemplate]:
177
+ """Retrieve a list of resource templates available on the server.
178
+
179
+ Returns:
180
+ list[mcp.types.ResourceTemplate]: A list of ResourceTemplate objects.
181
+
182
+ Raises:
183
+ RuntimeError: If called while the client is not connected.
184
+ """
185
+ result = await self.list_resource_templates_mcp()
135
186
  return result.resourceTemplates
136
187
 
188
+ async def read_resource_mcp(
189
+ self, uri: AnyUrl | str
190
+ ) -> mcp.types.ReadResourceResult:
191
+ """Send a resources/read request and return the complete MCP protocol result.
192
+
193
+ Args:
194
+ uri (AnyUrl | str): The URI of the resource to read. Can be a string or an AnyUrl object.
195
+
196
+ Returns:
197
+ mcp.types.ReadResourceResult: The complete response object from the protocol,
198
+ containing the resource contents and any additional metadata.
199
+
200
+ Raises:
201
+ RuntimeError: If called while the client is not connected.
202
+ """
203
+ if isinstance(uri, str):
204
+ uri = AnyUrl(uri) # Ensure AnyUrl
205
+ result = await self.session.read_resource(uri)
206
+ return result
207
+
137
208
  async def read_resource(
138
209
  self, uri: AnyUrl | str
139
210
  ) -> list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents]:
140
- """Send a resources/read request."""
211
+ """Read the contents of a resource or resolved template.
212
+
213
+ Args:
214
+ uri (AnyUrl | str): The URI of the resource to read. Can be a string or an AnyUrl object.
215
+
216
+ Returns:
217
+ list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents]: A list of content
218
+ objects, typically containing either text or binary data.
219
+
220
+ Raises:
221
+ RuntimeError: If called while the client is not connected.
222
+ """
141
223
  if isinstance(uri, str):
142
224
  uri = AnyUrl(uri) # Ensure AnyUrl
143
- result = await self.session.read_resource(uri)
225
+ result = await self.read_resource_mcp(uri)
144
226
  return result.contents
145
227
 
146
228
  # async def subscribe_resource(self, uri: AnyUrl | str) -> None:
@@ -155,66 +237,190 @@ class Client:
155
237
  # uri = AnyUrl(uri)
156
238
  # await self.session.unsubscribe_resource(uri)
157
239
 
158
- async def list_prompts(self) -> list[mcp.types.Prompt]:
159
- """Send a prompts/list request."""
240
+ # --- Prompts ---
241
+
242
+ async def list_prompts_mcp(self) -> mcp.types.ListPromptsResult:
243
+ """Send a prompts/list request and return the complete MCP protocol result.
244
+
245
+ Returns:
246
+ mcp.types.ListPromptsResult: The complete response object from the protocol,
247
+ containing the list of prompts and any additional metadata.
248
+
249
+ Raises:
250
+ RuntimeError: If called while the client is not connected.
251
+ """
160
252
  result = await self.session.list_prompts()
253
+ return result
254
+
255
+ async def list_prompts(self) -> list[mcp.types.Prompt]:
256
+ """Retrieve a list of prompts available on the server.
257
+
258
+ Returns:
259
+ list[mcp.types.Prompt]: A list of Prompt objects.
260
+
261
+ Raises:
262
+ RuntimeError: If called while the client is not connected.
263
+ """
264
+ result = await self.list_prompts_mcp()
161
265
  return result.prompts
162
266
 
267
+ # --- Prompt ---
268
+ async def get_prompt_mcp(
269
+ self, name: str, arguments: dict[str, str] | None = None
270
+ ) -> mcp.types.GetPromptResult:
271
+ """Send a prompts/get request and return the complete MCP protocol result.
272
+
273
+ Args:
274
+ name (str): The name of the prompt to retrieve.
275
+ arguments (dict[str, str] | None, optional): Arguments to pass to the prompt. Defaults to None.
276
+
277
+ Returns:
278
+ mcp.types.GetPromptResult: The complete response object from the protocol,
279
+ containing the prompt messages and any additional metadata.
280
+
281
+ Raises:
282
+ RuntimeError: If called while the client is not connected.
283
+ """
284
+ result = await self.session.get_prompt(name=name, arguments=arguments)
285
+ return result
286
+
163
287
  async def get_prompt(
164
288
  self, name: str, arguments: dict[str, str] | None = None
165
289
  ) -> list[mcp.types.PromptMessage]:
166
- """Send a prompts/get request."""
167
- result = await self.session.get_prompt(name, arguments)
290
+ """Retrieve a rendered prompt message list from the server.
291
+
292
+ Args:
293
+ name (str): The name of the prompt to retrieve.
294
+ arguments (dict[str, str] | None, optional): Arguments to pass to the prompt. Defaults to None.
295
+
296
+ Returns:
297
+ list[mcp.types.PromptMessage]: A list of prompt messages.
298
+
299
+ Raises:
300
+ RuntimeError: If called while the client is not connected.
301
+ """
302
+ result = await self.get_prompt_mcp(name=name, arguments=arguments)
168
303
  return result.messages
169
304
 
305
+ # --- Completion ---
306
+
307
+ async def complete_mcp(
308
+ self,
309
+ ref: mcp.types.ResourceReference | mcp.types.PromptReference,
310
+ argument: dict[str, str],
311
+ ) -> mcp.types.CompleteResult:
312
+ """Send a completion request and return the complete MCP protocol result.
313
+
314
+ Args:
315
+ ref (mcp.types.ResourceReference | mcp.types.PromptReference): The reference to complete.
316
+ argument (dict[str, str]): Arguments to pass to the completion request.
317
+
318
+ Returns:
319
+ mcp.types.CompleteResult: The complete response object from the protocol,
320
+ containing the completion and any additional metadata.
321
+
322
+ Raises:
323
+ RuntimeError: If called while the client is not connected.
324
+ """
325
+ result = await self.session.complete(ref=ref, argument=argument)
326
+ return result
327
+
170
328
  async def complete(
171
329
  self,
172
330
  ref: mcp.types.ResourceReference | mcp.types.PromptReference,
173
331
  argument: dict[str, str],
174
332
  ) -> mcp.types.Completion:
175
- """Send a completion request."""
176
- result = await self.session.complete(ref, argument)
333
+ """Send a completion request to the server.
334
+
335
+ Args:
336
+ ref (mcp.types.ResourceReference | mcp.types.PromptReference): The reference to complete.
337
+ argument (dict[str, str]): Arguments to pass to the completion request.
338
+
339
+ Returns:
340
+ mcp.types.Completion: The completion object.
341
+
342
+ Raises:
343
+ RuntimeError: If called while the client is not connected.
344
+ """
345
+ result = await self.complete_mcp(ref=ref, argument=argument)
177
346
  return result.completion
178
347
 
179
- async def list_tools(self) -> list[mcp.types.Tool]:
180
- """Send a tools/list request."""
348
+ # --- Tools ---
349
+
350
+ async def list_tools_mcp(self) -> mcp.types.ListToolsResult:
351
+ """Send a tools/list request and return the complete MCP protocol result.
352
+
353
+ Returns:
354
+ mcp.types.ListToolsResult: The complete response object from the protocol,
355
+ containing the list of tools and any additional metadata.
356
+
357
+ Raises:
358
+ RuntimeError: If called while the client is not connected.
359
+ """
181
360
  result = await self.session.list_tools()
361
+ return result
362
+
363
+ async def list_tools(self) -> list[mcp.types.Tool]:
364
+ """Retrieve a list of tools available on the server.
365
+
366
+ Returns:
367
+ list[mcp.types.Tool]: A list of Tool objects.
368
+
369
+ Raises:
370
+ RuntimeError: If called while the client is not connected.
371
+ """
372
+ result = await self.list_tools_mcp()
182
373
  return result.tools
183
374
 
184
- @overload
375
+ # --- Call Tool ---
376
+
377
+ async def call_tool_mcp(
378
+ self, name: str, arguments: dict[str, Any]
379
+ ) -> mcp.types.CallToolResult:
380
+ """Send a tools/call request and return the complete MCP protocol result.
381
+
382
+ This method returns the raw CallToolResult object, which includes an isError flag
383
+ and other metadata. It does not raise an exception if the tool call results in an error.
384
+
385
+ Args:
386
+ name (str): The name of the tool to call.
387
+ arguments (dict[str, Any]): Arguments to pass to the tool.
388
+
389
+ Returns:
390
+ mcp.types.CallToolResult: The complete response object from the protocol,
391
+ containing the tool result and any additional metadata.
392
+
393
+ Raises:
394
+ RuntimeError: If called while the client is not connected.
395
+ """
396
+ result = await self.session.call_tool(name=name, arguments=arguments)
397
+ return result
398
+
185
399
  async def call_tool(
186
400
  self,
187
401
  name: str,
188
402
  arguments: dict[str, Any] | None = None,
189
- _return_raw_result: Literal[False] = False,
190
403
  ) -> list[
191
404
  mcp.types.TextContent | mcp.types.ImageContent | mcp.types.EmbeddedResource
192
- ]: ...
405
+ ]:
406
+ """Call a tool on the server.
193
407
 
194
- @overload
195
- async def call_tool(
196
- self,
197
- name: str,
198
- arguments: dict[str, Any] | None = None,
199
- _return_raw_result: Literal[True] = True,
200
- ) -> mcp.types.CallToolResult: ...
408
+ Unlike call_tool_mcp, this method raises a ClientError if the tool call results in an error.
201
409
 
202
- async def call_tool(
203
- self,
204
- name: str,
205
- arguments: dict[str, Any] | None = None,
206
- _return_raw_result: bool = False,
207
- ) -> (
208
- list[
209
- mcp.types.TextContent | mcp.types.ImageContent | mcp.types.EmbeddedResource
210
- ]
211
- | mcp.types.CallToolResult
212
- ):
213
- """Send a tools/call request."""
214
- result = await self.session.call_tool(name, arguments)
215
- if _return_raw_result:
216
- return result
217
- elif result.isError:
410
+ Args:
411
+ name (str): The name of the tool to call.
412
+ arguments (dict[str, Any] | None, optional): Arguments to pass to the tool. Defaults to None.
413
+
414
+ Returns:
415
+ list[mcp.types.TextContent | mcp.types.ImageContent | mcp.types.EmbeddedResource]:
416
+ The content returned by the tool.
417
+
418
+ Raises:
419
+ ClientError: If the tool call results in an error.
420
+ RuntimeError: If called while the client is not connected.
421
+ """
422
+ result = await self.call_tool_mcp(name=name, arguments=arguments or {})
423
+ if result.isError:
218
424
  msg = cast(mcp.types.TextContent, result.content[0]).text
219
425
  raise ClientError(msg)
220
426
  return result.content
@@ -0,0 +1,13 @@
1
+ from typing import TypeAlias
2
+
3
+ from mcp.client.session import (
4
+ LoggingFnT,
5
+ MessageHandlerFnT,
6
+ )
7
+ from mcp.types import LoggingMessageNotificationParams
8
+
9
+ LogMessage: TypeAlias = LoggingMessageNotificationParams
10
+ LogHandler: TypeAlias = LoggingFnT
11
+ MessageHandler: TypeAlias = MessageHandlerFnT
12
+
13
+ __all__ = ["LogMessage", "LogHandler", "MessageHandler"]
@@ -9,6 +9,8 @@ from mcp.shared.context import LifespanContextT, RequestContext
9
9
  from mcp.types import CreateMessageRequestParams as SamplingParams
10
10
  from mcp.types import SamplingMessage
11
11
 
12
+ __all__ = ["SamplingMessage", "SamplingParams", "MessageResult", "SamplingHandler"]
13
+
12
14
 
13
15
  class MessageResult(CreateMessageResult):
14
16
  role: mcp.types.Role = "assistant"
@@ -6,9 +6,7 @@ import shutil
6
6
  import sys
7
7
  from collections.abc import AsyncIterator
8
8
  from pathlib import Path
9
- from typing import (
10
- TypedDict,
11
- )
9
+ from typing import Any, TypedDict
12
10
 
13
11
  from exceptiongroup import BaseExceptionGroup, catch
14
12
  from mcp import ClientSession, McpError, StdioServerParameters
@@ -416,7 +414,7 @@ class FastMCPTransport(ClientTransport):
416
414
 
417
415
 
418
416
  def infer_transport(
419
- transport: ClientTransport | FastMCPServer | AnyUrl | Path | str,
417
+ transport: ClientTransport | FastMCPServer | AnyUrl | Path | dict[str, Any] | str,
420
418
  ) -> ClientTransport:
421
419
  """
422
420
  Infer the appropriate transport type from the given transport argument.
@@ -450,6 +448,41 @@ def infer_transport(
450
448
  elif isinstance(transport, AnyUrl | str) and str(transport).startswith("ws"):
451
449
  return WSTransport(url=transport)
452
450
 
451
+ ## if the transport is a config dict
452
+ elif isinstance(transport, dict):
453
+ if "mcpServers" not in transport:
454
+ raise ValueError("Invalid transport dictionary: missing 'mcpServers' key")
455
+ else:
456
+ server = transport["mcpServers"]
457
+ if len(list(server.keys())) > 1:
458
+ raise ValueError(
459
+ "Invalid transport dictionary: multiple servers found - only one expected"
460
+ )
461
+ server_name = list(server.keys())[0]
462
+ # Stdio transport
463
+ if "command" in server[server_name] and "args" in server[server_name]:
464
+ return StdioTransport(
465
+ command=server[server_name]["command"],
466
+ args=server[server_name]["args"],
467
+ env=server[server_name].get("env", None),
468
+ cwd=server[server_name].get("cwd", None),
469
+ )
470
+
471
+ # HTTP transport
472
+ elif "url" in server:
473
+ return SSETransport(
474
+ url=server["url"],
475
+ headers=server.get("headers", None),
476
+ )
477
+
478
+ # WebSocket transport
479
+ elif "ws_url" in server:
480
+ return WSTransport(
481
+ url=server["ws_url"],
482
+ )
483
+
484
+ raise ValueError("Cannot determine transport type from dictionary")
485
+
453
486
  # the transport is an unknown type
454
487
  else:
455
488
  raise ValueError(f"Could not infer a valid transport from: {transport}")
@@ -123,9 +123,7 @@ class BulkToolCaller(MCPMixin):
123
123
  """
124
124
 
125
125
  async with Client(self.connection) as client:
126
- result = await client.call_tool(
127
- name=tool, arguments=arguments, _return_raw_result=True
128
- )
126
+ result = await client.call_tool_mcp(name=tool, arguments=arguments)
129
127
 
130
128
  return CallToolRequestResult(
131
129
  tool=tool,
fastmcp/prompts/prompt.py CHANGED
@@ -3,7 +3,6 @@
3
3
  from __future__ import annotations as _annotations
4
4
 
5
5
  import inspect
6
- import json
7
6
  from collections.abc import Awaitable, Callable, Sequence
8
7
  from typing import TYPE_CHECKING, Annotated, Any, Literal
9
8
 
@@ -13,7 +12,10 @@ from mcp.types import Prompt as MCPPrompt
13
12
  from mcp.types import PromptArgument as MCPPromptArgument
14
13
  from pydantic import BaseModel, BeforeValidator, Field, TypeAdapter, validate_call
15
14
 
16
- from fastmcp.utilities.types import _convert_set_defaults
15
+ from fastmcp.utilities.types import (
16
+ _convert_set_defaults,
17
+ is_class_member_of_type,
18
+ )
17
19
 
18
20
  if TYPE_CHECKING:
19
21
  from mcp.server.session import ServerSessionT
@@ -115,7 +117,7 @@ class Prompt(BaseModel):
115
117
  else:
116
118
  sig = inspect.signature(fn)
117
119
  for param_name, param in sig.parameters.items():
118
- if param.annotation is Context:
120
+ if is_class_member_of_type(param.annotation, Context):
119
121
  context_kwarg = param_name
120
122
  break
121
123
 
@@ -192,7 +194,9 @@ class Prompt(BaseModel):
192
194
  content = TextContent(type="text", text=msg)
193
195
  messages.append(Message(role="user", content=content))
194
196
  else:
195
- content = json.dumps(pydantic_core.to_jsonable_python(msg))
197
+ content = pydantic_core.to_json(
198
+ msg, fallback=str, indent=2
199
+ ).decode()
196
200
  messages.append(Message(role="user", content=content))
197
201
  except Exception:
198
202
  raise ValueError(
@@ -20,7 +20,10 @@ from pydantic import (
20
20
  )
21
21
 
22
22
  from fastmcp.resources.types import FunctionResource, Resource
23
- from fastmcp.utilities.types import _convert_set_defaults
23
+ from fastmcp.utilities.types import (
24
+ _convert_set_defaults,
25
+ is_class_member_of_type,
26
+ )
24
27
 
25
28
  if TYPE_CHECKING:
26
29
  from mcp.server.session import ServerSessionT
@@ -113,7 +116,7 @@ class ResourceTemplate(BaseModel):
113
116
  else:
114
117
  sig = inspect.signature(fn)
115
118
  for param_name, param in sig.parameters.items():
116
- if param.annotation is Context:
119
+ if is_class_member_of_type(param.annotation, Context):
117
120
  context_kwarg = param_name
118
121
  break
119
122
 
@@ -97,15 +97,12 @@ class FunctionResource(Resource):
97
97
 
98
98
  if isinstance(result, Resource):
99
99
  return await result.read(context=context)
100
- if isinstance(result, bytes):
100
+ elif isinstance(result, bytes):
101
101
  return result
102
- if isinstance(result, str):
102
+ elif isinstance(result, str):
103
103
  return result
104
- try:
105
- return json.dumps(pydantic_core.to_jsonable_python(result))
106
- except (TypeError, pydantic_core.PydanticSerializationError):
107
- # If JSON serialization fails, try str()
108
- return str(result)
104
+ else:
105
+ return pydantic_core.to_json(result, fallback=str, indent=2).decode()
109
106
  except Exception as e:
110
107
  raise ValueError(f"Error reading resource {self.uri}: {e}")
111
108
 
fastmcp/server/context.py CHANGED
@@ -1,7 +1,8 @@
1
1
  from __future__ import annotations as _annotations
2
2
 
3
- from typing import Any, Generic, Literal
3
+ from typing import Any, Generic
4
4
 
5
+ from mcp import LoggingLevel
5
6
  from mcp.server.lowlevel.helper_types import ReadResourceContents
6
7
  from mcp.server.session import ServerSessionT
7
8
  from mcp.shared.context import LifespanContextT, RequestContext
@@ -12,10 +13,12 @@ from mcp.types import (
12
13
  SamplingMessage,
13
14
  TextContent,
14
15
  )
15
- from pydantic import BaseModel
16
+ from pydantic import BaseModel, ConfigDict
16
17
  from pydantic.networks import AnyUrl
18
+ from starlette.requests import Request
17
19
 
18
20
  from fastmcp.server.server import FastMCP
21
+ from fastmcp.utilities.http import get_current_starlette_request
19
22
  from fastmcp.utilities.logging import get_logger
20
23
 
21
24
  logger = get_logger(__name__)
@@ -58,6 +61,8 @@ class Context(BaseModel, Generic[ServerSessionT, LifespanContextT]):
58
61
  _request_context: RequestContext[ServerSessionT, LifespanContextT] | None
59
62
  _fastmcp: FastMCP | None
60
63
 
64
+ model_config = ConfigDict(arbitrary_types_allowed=True)
65
+
61
66
  def __init__(
62
67
  self,
63
68
  *,
@@ -122,19 +127,20 @@ class Context(BaseModel, Generic[ServerSessionT, LifespanContextT]):
122
127
 
123
128
  async def log(
124
129
  self,
125
- level: Literal["debug", "info", "warning", "error"],
126
130
  message: str,
127
- *,
131
+ level: LoggingLevel | None = None,
128
132
  logger_name: str | None = None,
129
133
  ) -> None:
130
134
  """Send a log message to the client.
131
135
 
132
136
  Args:
133
- level: Log level (debug, info, warning, error)
134
137
  message: Log message
138
+ level: Optional log level. One of "debug", "info", "notice", "warning", "error", "critical",
139
+ "alert", or "emergency". Default is "info".
135
140
  logger_name: Optional logger name
136
- **extra: Additional structured data to include
137
141
  """
142
+ if level is None:
143
+ level = "info"
138
144
  await self.request_context.session.send_log_message(
139
145
  level=level, data=message, logger=logger_name
140
146
  )
@@ -159,21 +165,21 @@ class Context(BaseModel, Generic[ServerSessionT, LifespanContextT]):
159
165
  return self.request_context.session
160
166
 
161
167
  # Convenience methods for common log levels
162
- async def debug(self, message: str, **extra: Any) -> None:
168
+ async def debug(self, message: str, logger_name: str | None = None) -> None:
163
169
  """Send a debug log message."""
164
- await self.log("debug", message, **extra)
170
+ await self.log(level="debug", message=message, logger_name=logger_name)
165
171
 
166
- async def info(self, message: str, **extra: Any) -> None:
172
+ async def info(self, message: str, logger_name: str | None = None) -> None:
167
173
  """Send an info log message."""
168
- await self.log("info", message, **extra)
174
+ await self.log(level="info", message=message, logger_name=logger_name)
169
175
 
170
- async def warning(self, message: str, **extra: Any) -> None:
176
+ async def warning(self, message: str, logger_name: str | None = None) -> None:
171
177
  """Send a warning log message."""
172
- await self.log("warning", message, **extra)
178
+ await self.log(level="warning", message=message, logger_name=logger_name)
173
179
 
174
- async def error(self, message: str, **extra: Any) -> None:
180
+ async def error(self, message: str, logger_name: str | None = None) -> None:
175
181
  """Send an error log message."""
176
- await self.log("error", message, **extra)
182
+ await self.log(level="error", message=message, logger_name=logger_name)
177
183
 
178
184
  async def list_roots(self) -> list[Root]:
179
185
  """List the roots available to the server, as indicated by the client."""
@@ -220,3 +226,10 @@ class Context(BaseModel, Generic[ServerSessionT, LifespanContextT]):
220
226
  )
221
227
 
222
228
  return result.content
229
+
230
+ def get_http_request(self) -> Request:
231
+ """Get the active starlette request."""
232
+ request = get_current_starlette_request()
233
+ if request is None:
234
+ raise ValueError("Request is not available outside a Starlette request")
235
+ return request