Jeremiah Lowin commited on
Commit
3d30e82
·
1 Parent(s): 9a66e03

Add context support to all objects

Browse files
src/fastmcp/prompts/prompt.py CHANGED
@@ -1,9 +1,11 @@
1
  """Base classes for FastMCP prompts."""
2
 
 
 
3
  import inspect
4
  import json
5
  from collections.abc import Awaitable, Callable, Sequence
6
- from typing import Annotated, Any, Literal
7
 
8
  import pydantic_core
9
  from mcp.types import EmbeddedResource, ImageContent, TextContent
@@ -13,6 +15,12 @@ from pydantic import BaseModel, BeforeValidator, Field, TypeAdapter, validate_ca
13
 
14
  from fastmcp.utilities.types import _convert_set_defaults
15
 
 
 
 
 
 
 
16
  CONTENT_TYPES = TextContent | ImageContent | EmbeddedResource
17
 
18
 
@@ -72,6 +80,9 @@ class Prompt(BaseModel):
72
  None, description="Arguments that can be passed to the prompt"
73
  )
74
  fn: Callable[..., PromptResult | Awaitable[PromptResult]]
 
 
 
75
 
76
  @classmethod
77
  def from_function(
@@ -80,7 +91,8 @@ class Prompt(BaseModel):
80
  name: str | None = None,
81
  description: str | None = None,
82
  tags: set[str] | None = None,
83
- ) -> "Prompt":
 
84
  """Create a Prompt from a function.
85
 
86
  The function can return:
@@ -89,11 +101,24 @@ class Prompt(BaseModel):
89
  - A dict (converted to a message)
90
  - A sequence of any of the above
91
  """
 
 
92
  func_name = name or fn.__name__
93
 
94
  if func_name == "<lambda>":
95
  raise ValueError("You must provide a name for lambda functions")
96
 
 
 
 
 
 
 
 
 
 
 
 
97
  # Get schema from TypeAdapter - will fail if function isn't properly typed
98
  parameters = TypeAdapter(fn).json_schema()
99
 
@@ -101,6 +126,10 @@ class Prompt(BaseModel):
101
  arguments: list[PromptArgument] = []
102
  if "properties" in parameters:
103
  for param_name, param in parameters["properties"].items():
 
 
 
 
104
  required = param_name in parameters.get("required", [])
105
  arguments.append(
106
  PromptArgument(
@@ -119,9 +148,14 @@ class Prompt(BaseModel):
119
  arguments=arguments,
120
  fn=fn,
121
  tags=tags or set(),
 
122
  )
123
 
124
- async def render(self, arguments: dict[str, Any] | None = None) -> list[Message]:
 
 
 
 
125
  """Render the prompt with arguments."""
126
  # Validate required arguments
127
  if self.arguments:
@@ -132,8 +166,13 @@ class Prompt(BaseModel):
132
  raise ValueError(f"Missing required arguments: {missing}")
133
 
134
  try:
 
 
 
 
 
135
  # Call function and check if result is a coroutine
136
- result = self.fn(**(arguments or {}))
137
  if inspect.iscoroutine(result):
138
  result = await result
139
 
 
1
  """Base classes for FastMCP prompts."""
2
 
3
+ from __future__ import annotations as _annotations
4
+
5
  import inspect
6
  import json
7
  from collections.abc import Awaitable, Callable, Sequence
8
+ from typing import TYPE_CHECKING, Annotated, Any, Literal
9
 
10
  import pydantic_core
11
  from mcp.types import EmbeddedResource, ImageContent, TextContent
 
15
 
16
  from fastmcp.utilities.types import _convert_set_defaults
17
 
18
+ if TYPE_CHECKING:
19
+ from mcp.server.session import ServerSessionT
20
+ from mcp.shared.context import LifespanContextT
21
+
22
+ from fastmcp.server import Context
23
+
24
  CONTENT_TYPES = TextContent | ImageContent | EmbeddedResource
25
 
26
 
 
80
  None, description="Arguments that can be passed to the prompt"
81
  )
82
  fn: Callable[..., PromptResult | Awaitable[PromptResult]]
83
+ context_kwarg: str | None = Field(
84
+ None, description="Name of the kwarg that should receive context"
85
+ )
86
 
87
  @classmethod
88
  def from_function(
 
91
  name: str | None = None,
92
  description: str | None = None,
93
  tags: set[str] | None = None,
94
+ context_kwarg: str | None = None,
95
+ ) -> Prompt:
96
  """Create a Prompt from a function.
97
 
98
  The function can return:
 
101
  - A dict (converted to a message)
102
  - A sequence of any of the above
103
  """
104
+ from fastmcp import Context
105
+
106
  func_name = name or fn.__name__
107
 
108
  if func_name == "<lambda>":
109
  raise ValueError("You must provide a name for lambda functions")
110
 
111
+ # Auto-detect context parameter if not provided
112
+ if context_kwarg is None:
113
+ if inspect.ismethod(fn) and hasattr(fn, "__func__"):
114
+ sig = inspect.signature(fn.__func__)
115
+ else:
116
+ sig = inspect.signature(fn)
117
+ for param_name, param in sig.parameters.items():
118
+ if param.annotation is Context:
119
+ context_kwarg = param_name
120
+ break
121
+
122
  # Get schema from TypeAdapter - will fail if function isn't properly typed
123
  parameters = TypeAdapter(fn).json_schema()
124
 
 
126
  arguments: list[PromptArgument] = []
127
  if "properties" in parameters:
128
  for param_name, param in parameters["properties"].items():
129
+ # Skip context parameter
130
+ if param_name == context_kwarg:
131
+ continue
132
+
133
  required = param_name in parameters.get("required", [])
134
  arguments.append(
135
  PromptArgument(
 
148
  arguments=arguments,
149
  fn=fn,
150
  tags=tags or set(),
151
+ context_kwarg=context_kwarg,
152
  )
153
 
154
+ async def render(
155
+ self,
156
+ arguments: dict[str, Any] | None = None,
157
+ context: Context[ServerSessionT, LifespanContextT] | None = None,
158
+ ) -> list[Message]:
159
  """Render the prompt with arguments."""
160
  # Validate required arguments
161
  if self.arguments:
 
166
  raise ValueError(f"Missing required arguments: {missing}")
167
 
168
  try:
169
+ # Prepare arguments with context
170
+ kwargs = arguments.copy() if arguments else {}
171
+ if self.context_kwarg is not None and context is not None:
172
+ kwargs[self.context_kwarg] = context
173
+
174
  # Call function and check if result is a coroutine
175
+ result = self.fn(**kwargs)
176
  if inspect.iscoroutine(result):
177
  result = await result
178
 
src/fastmcp/prompts/prompt_manager.py CHANGED
@@ -1,13 +1,21 @@
1
  """Prompt management functionality."""
2
 
 
 
3
  from collections.abc import Awaitable, Callable
4
- from typing import Any
5
 
6
  from fastmcp.exceptions import NotFoundError
7
  from fastmcp.prompts.prompt import Message, Prompt, PromptResult
8
  from fastmcp.settings import DuplicateBehavior
9
  from fastmcp.utilities.logging import get_logger
10
 
 
 
 
 
 
 
11
  logger = get_logger(__name__)
12
 
13
 
@@ -69,14 +77,17 @@ class PromptManager:
69
  return prompt
70
 
71
  async def render_prompt(
72
- self, name: str, arguments: dict[str, Any] | None = None
 
 
 
73
  ) -> list[Message]:
74
  """Render a prompt by name with arguments."""
75
  prompt = self.get_prompt(name)
76
  if not prompt:
77
  raise NotFoundError(f"Unknown prompt: {name}")
78
 
79
- return await prompt.render(arguments)
80
 
81
  def has_prompt(self, key: str) -> bool:
82
  """Check if a prompt exists."""
 
1
  """Prompt management functionality."""
2
 
3
+ from __future__ import annotations as _annotations
4
+
5
  from collections.abc import Awaitable, Callable
6
+ from typing import TYPE_CHECKING, Any
7
 
8
  from fastmcp.exceptions import NotFoundError
9
  from fastmcp.prompts.prompt import Message, Prompt, PromptResult
10
  from fastmcp.settings import DuplicateBehavior
11
  from fastmcp.utilities.logging import get_logger
12
 
13
+ if TYPE_CHECKING:
14
+ from mcp.server.session import ServerSessionT
15
+ from mcp.shared.context import LifespanContextT
16
+
17
+ from fastmcp.server import Context
18
+
19
  logger = get_logger(__name__)
20
 
21
 
 
77
  return prompt
78
 
79
  async def render_prompt(
80
+ self,
81
+ name: str,
82
+ arguments: dict[str, Any] | None = None,
83
+ context: Context[ServerSessionT, LifespanContextT] | None = None,
84
  ) -> list[Message]:
85
  """Render a prompt by name with arguments."""
86
  prompt = self.get_prompt(name)
87
  if not prompt:
88
  raise NotFoundError(f"Unknown prompt: {name}")
89
 
90
+ return await prompt.render(arguments, context=context)
91
 
92
  def has_prompt(self, key: str) -> bool:
93
  """Check if a prompt exists."""
src/fastmcp/resources/resource.py CHANGED
@@ -1,7 +1,9 @@
1
  """Base classes and interfaces for FastMCP resources."""
2
 
 
 
3
  import abc
4
- from typing import Annotated, Any
5
 
6
  from mcp.types import Resource as MCPResource
7
  from pydantic import (
@@ -17,6 +19,12 @@ from pydantic import (
17
 
18
  from fastmcp.utilities.types import _convert_set_defaults
19
 
 
 
 
 
 
 
20
 
21
  class Resource(BaseModel, abc.ABC):
22
  """Base class for all resources."""
@@ -58,7 +66,9 @@ class Resource(BaseModel, abc.ABC):
58
  raise ValueError("Either name or uri must be provided")
59
 
60
  @abc.abstractmethod
61
- async def read(self) -> str | bytes:
 
 
62
  """Read the resource content."""
63
  pass
64
 
 
1
  """Base classes and interfaces for FastMCP resources."""
2
 
3
+ from __future__ import annotations
4
+
5
  import abc
6
+ from typing import TYPE_CHECKING, Annotated, Any
7
 
8
  from mcp.types import Resource as MCPResource
9
  from pydantic import (
 
19
 
20
  from fastmcp.utilities.types import _convert_set_defaults
21
 
22
+ if TYPE_CHECKING:
23
+ from mcp.server.session import ServerSessionT
24
+ from mcp.shared.context import LifespanContextT
25
+
26
+ from fastmcp.server import Context
27
+
28
 
29
  class Resource(BaseModel, abc.ABC):
30
  """Base class for all resources."""
 
66
  raise ValueError("Either name or uri must be provided")
67
 
68
  @abc.abstractmethod
69
+ async def read(
70
+ self, context: Context[ServerSessionT, LifespanContextT] | None = None
71
+ ) -> str | bytes:
72
  """Read the resource content."""
73
  pass
74
 
src/fastmcp/resources/resource_manager.py CHANGED
@@ -212,9 +212,13 @@ class ResourceManager:
212
  return True
213
  return False
214
 
215
- async def get_resource(self, uri: AnyUrl | str) -> Resource:
216
  """Get resource by URI, checking concrete resources first, then templates.
217
 
 
 
 
 
218
  Raises:
219
  NotFoundError: If no resource or template matching the URI is found.
220
  """
@@ -230,7 +234,9 @@ class ResourceManager:
230
  # Try to match against the storage key (which might be a custom key)
231
  if params := match_uri_template(uri_str, storage_key):
232
  try:
233
- return await template.create_resource(uri_str, params)
 
 
234
  except Exception as e:
235
  raise ValueError(f"Error creating resource from template: {e}")
236
 
 
212
  return True
213
  return False
214
 
215
+ async def get_resource(self, uri: AnyUrl | str, context=None) -> Resource:
216
  """Get resource by URI, checking concrete resources first, then templates.
217
 
218
+ Args:
219
+ uri: The URI of the resource to get
220
+ context: Optional context object to pass to template resources
221
+
222
  Raises:
223
  NotFoundError: If no resource or template matching the URI is found.
224
  """
 
234
  # Try to match against the storage key (which might be a custom key)
235
  if params := match_uri_template(uri_str, storage_key):
236
  try:
237
+ return await template.create_resource(
238
+ uri_str, params, context=context
239
+ )
240
  except Exception as e:
241
  raise ValueError(f"Error creating resource from template: {e}")
242
 
src/fastmcp/resources/template.py CHANGED
@@ -5,7 +5,7 @@ from __future__ import annotations
5
  import inspect
6
  import re
7
  from collections.abc import Callable
8
- from typing import Annotated, Any
9
  from urllib.parse import unquote
10
 
11
  from mcp.types import ResourceTemplate as MCPResourceTemplate
@@ -22,6 +22,12 @@ from pydantic import (
22
  from fastmcp.resources.types import FunctionResource, Resource
23
  from fastmcp.utilities.types import _convert_set_defaults
24
 
 
 
 
 
 
 
25
 
26
  def build_regex(template: str) -> re.Pattern:
27
  parts = re.split(r"(\{[^}]+\})", template)
@@ -70,6 +76,9 @@ class ResourceTemplate(BaseModel):
70
  parameters: dict[str, Any] = Field(
71
  description="JSON schema for function parameters"
72
  )
 
 
 
73
 
74
  @field_validator("mime_type", mode="before")
75
  @classmethod
@@ -88,18 +97,34 @@ class ResourceTemplate(BaseModel):
88
  description: str | None = None,
89
  mime_type: str | None = None,
90
  tags: set[str] | None = None,
 
91
  ) -> ResourceTemplate:
92
  """Create a template from a function."""
 
 
93
  func_name = name or fn.__name__
94
  if func_name == "<lambda>":
95
  raise ValueError("You must provide a name for lambda functions")
96
 
 
 
 
 
 
 
 
 
 
 
 
97
  # Validate that URI params match function params
98
  uri_params = set(re.findall(r"{(\w+)(?:\*)?}", uri_template))
99
  if not uri_params:
100
  raise ValueError("URI template must contain at least one parameter")
101
 
102
  func_params = set(inspect.signature(fn).parameters.keys())
 
 
103
 
104
  # get the parameters that are required
105
  required_params = {
@@ -107,6 +132,8 @@ class ResourceTemplate(BaseModel):
107
  for p in func_params
108
  if inspect.signature(fn).parameters[p].default is inspect.Parameter.empty
109
  }
 
 
110
 
111
  if not required_params.issubset(uri_params):
112
  raise ValueError(
@@ -132,17 +159,28 @@ class ResourceTemplate(BaseModel):
132
  fn=fn,
133
  parameters=parameters,
134
  tags=tags or set(),
 
135
  )
136
 
137
  def matches(self, uri: str) -> dict[str, Any] | None:
138
  """Check if URI matches template and extract parameters."""
139
  return match_uri_template(uri, self.uri_template)
140
 
141
- async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
 
 
 
 
 
142
  """Create a resource from the template with the given parameters."""
143
  try:
 
 
 
 
 
144
  # Call function and check if result is a coroutine
145
- result = self.fn(**params)
146
  if inspect.iscoroutine(result):
147
  result = await result
148
 
@@ -153,6 +191,7 @@ class ResourceTemplate(BaseModel):
153
  mime_type=self.mime_type,
154
  fn=lambda: result, # Capture result in closure
155
  tags=self.tags,
 
156
  )
157
  except Exception as e:
158
  raise ValueError(f"Error creating resource from template: {e}")
 
5
  import inspect
6
  import re
7
  from collections.abc import Callable
8
+ from typing import TYPE_CHECKING, Annotated, Any
9
  from urllib.parse import unquote
10
 
11
  from mcp.types import ResourceTemplate as MCPResourceTemplate
 
22
  from fastmcp.resources.types import FunctionResource, Resource
23
  from fastmcp.utilities.types import _convert_set_defaults
24
 
25
+ if TYPE_CHECKING:
26
+ from mcp.server.session import ServerSessionT
27
+ from mcp.shared.context import LifespanContextT
28
+
29
+ from fastmcp.server import Context
30
+
31
 
32
  def build_regex(template: str) -> re.Pattern:
33
  parts = re.split(r"(\{[^}]+\})", template)
 
76
  parameters: dict[str, Any] = Field(
77
  description="JSON schema for function parameters"
78
  )
79
+ context_kwarg: str | None = Field(
80
+ None, description="Name of the kwarg that should receive context"
81
+ )
82
 
83
  @field_validator("mime_type", mode="before")
84
  @classmethod
 
97
  description: str | None = None,
98
  mime_type: str | None = None,
99
  tags: set[str] | None = None,
100
+ context_kwarg: str | None = None,
101
  ) -> ResourceTemplate:
102
  """Create a template from a function."""
103
+ from fastmcp import Context
104
+
105
  func_name = name or fn.__name__
106
  if func_name == "<lambda>":
107
  raise ValueError("You must provide a name for lambda functions")
108
 
109
+ # Auto-detect context parameter if not provided
110
+ if context_kwarg is None:
111
+ if inspect.ismethod(fn) and hasattr(fn, "__func__"):
112
+ sig = inspect.signature(fn.__func__)
113
+ else:
114
+ sig = inspect.signature(fn)
115
+ for param_name, param in sig.parameters.items():
116
+ if param.annotation is Context:
117
+ context_kwarg = param_name
118
+ break
119
+
120
  # Validate that URI params match function params
121
  uri_params = set(re.findall(r"{(\w+)(?:\*)?}", uri_template))
122
  if not uri_params:
123
  raise ValueError("URI template must contain at least one parameter")
124
 
125
  func_params = set(inspect.signature(fn).parameters.keys())
126
+ if context_kwarg:
127
+ func_params.discard(context_kwarg)
128
 
129
  # get the parameters that are required
130
  required_params = {
 
132
  for p in func_params
133
  if inspect.signature(fn).parameters[p].default is inspect.Parameter.empty
134
  }
135
+ if context_kwarg and context_kwarg in required_params:
136
+ required_params.discard(context_kwarg)
137
 
138
  if not required_params.issubset(uri_params):
139
  raise ValueError(
 
159
  fn=fn,
160
  parameters=parameters,
161
  tags=tags or set(),
162
+ context_kwarg=context_kwarg,
163
  )
164
 
165
  def matches(self, uri: str) -> dict[str, Any] | None:
166
  """Check if URI matches template and extract parameters."""
167
  return match_uri_template(uri, self.uri_template)
168
 
169
+ async def create_resource(
170
+ self,
171
+ uri: str,
172
+ params: dict[str, Any],
173
+ context: Context[ServerSessionT, LifespanContextT] | None = None,
174
+ ) -> Resource:
175
  """Create a resource from the template with the given parameters."""
176
  try:
177
+ # Add context to parameters if needed
178
+ kwargs = params.copy()
179
+ if self.context_kwarg is not None and context is not None:
180
+ kwargs[self.context_kwarg] = context
181
+
182
  # Call function and check if result is a coroutine
183
+ result = self.fn(**kwargs)
184
  if inspect.iscoroutine(result):
185
  result = await result
186
 
 
191
  mime_type=self.mime_type,
192
  fn=lambda: result, # Capture result in closure
193
  tags=self.tags,
194
+ context_kwarg=self.context_kwarg,
195
  )
196
  except Exception as e:
197
  raise ValueError(f"Error creating resource from template: {e}")
src/fastmcp/resources/types.py CHANGED
@@ -1,10 +1,12 @@
1
  """Concrete resource implementations."""
2
 
 
 
3
  import inspect
4
  import json
5
  from collections.abc import Callable
6
  from pathlib import Path
7
- from typing import Any
8
 
9
  import anyio
10
  import anyio.to_thread
@@ -15,13 +17,21 @@ from pydantic import Field, ValidationInfo
15
 
16
  from fastmcp.resources.resource import Resource
17
 
 
 
 
 
 
 
18
 
19
  class TextResource(Resource):
20
  """A resource that reads from a string."""
21
 
22
  text: str = Field(description="Text content of the resource")
23
 
24
- async def read(self) -> str:
 
 
25
  """Read the text content."""
26
  return self.text
27
 
@@ -31,7 +41,9 @@ class BinaryResource(Resource):
31
 
32
  data: bytes = Field(description="Binary content of the resource")
33
 
34
- async def read(self) -> bytes:
 
 
35
  """Read the binary content."""
36
  return self.data
37
 
@@ -50,13 +62,23 @@ class FunctionResource(Resource):
50
  """
51
 
52
  fn: Callable[[], Any]
 
 
 
53
 
54
- async def read(self) -> str | bytes:
 
 
55
  """Read the resource by calling the wrapped function."""
56
  try:
57
- result = (
58
- await self.fn() if inspect.iscoroutinefunction(self.fn) else self.fn()
59
- )
 
 
 
 
 
60
  if isinstance(result, Resource):
61
  return await result.read()
62
  if isinstance(result, bytes):
 
1
  """Concrete resource implementations."""
2
 
3
+ from __future__ import annotations
4
+
5
  import inspect
6
  import json
7
  from collections.abc import Callable
8
  from pathlib import Path
9
+ from typing import TYPE_CHECKING, Any
10
 
11
  import anyio
12
  import anyio.to_thread
 
17
 
18
  from fastmcp.resources.resource import Resource
19
 
20
+ if TYPE_CHECKING:
21
+ from mcp.server.session import ServerSessionT
22
+ from mcp.shared.context import LifespanContextT
23
+
24
+ from fastmcp.server import Context
25
+
26
 
27
  class TextResource(Resource):
28
  """A resource that reads from a string."""
29
 
30
  text: str = Field(description="Text content of the resource")
31
 
32
+ async def read(
33
+ self, context: Context[ServerSessionT, LifespanContextT] | None = None
34
+ ) -> str:
35
  """Read the text content."""
36
  return self.text
37
 
 
41
 
42
  data: bytes = Field(description="Binary content of the resource")
43
 
44
+ async def read(
45
+ self, context: Context[ServerSessionT, LifespanContextT] | None = None
46
+ ) -> bytes:
47
  """Read the binary content."""
48
  return self.data
49
 
 
62
  """
63
 
64
  fn: Callable[[], Any]
65
+ context_kwarg: str | None = Field(
66
+ default=None, description="Name of the kwarg that should receive context"
67
+ )
68
 
69
+ async def read(
70
+ self, context: Context[ServerSessionT, LifespanContextT] | None = None
71
+ ) -> str | bytes:
72
  """Read the resource by calling the wrapped function."""
73
  try:
74
+ kwargs = {}
75
+ if self.context_kwarg is not None:
76
+ kwargs[self.context_kwarg] = context
77
+
78
+ result = self.fn(**kwargs)
79
+ if inspect.iscoroutinefunction(self.fn):
80
+ result = await result
81
+
82
  if isinstance(result, Resource):
83
  return await result.read()
84
  if isinstance(result, bytes):
src/fastmcp/server/openapi.py CHANGED
@@ -1,11 +1,13 @@
1
  """FastMCP server implementation for OpenAPI integration."""
2
 
 
 
3
  import enum
4
  import json
5
  import re
6
  from dataclasses import dataclass
7
  from re import Pattern
8
- from typing import Any, Literal
9
 
10
  import httpx
11
  from mcp.types import TextContent
@@ -22,6 +24,12 @@ from fastmcp.utilities.openapi import (
22
  format_description_with_responses,
23
  )
24
 
 
 
 
 
 
 
25
  logger = get_logger(__name__)
26
 
27
  HttpMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]
@@ -347,11 +355,17 @@ class OpenAPIResourceTemplate(ResourceTemplate):
347
  fn=lambda **kwargs: None,
348
  parameters=parameters,
349
  tags=tags,
 
350
  )
351
  self._client = client
352
  self._route = route
353
 
354
- async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
 
 
 
 
 
355
  """Create a resource with the given parameters."""
356
  # Generate a URI for this resource instance
357
  uri_parts = []
 
1
  """FastMCP server implementation for OpenAPI integration."""
2
 
3
+ from __future__ import annotations
4
+
5
  import enum
6
  import json
7
  import re
8
  from dataclasses import dataclass
9
  from re import Pattern
10
+ from typing import TYPE_CHECKING, Any, Literal
11
 
12
  import httpx
13
  from mcp.types import TextContent
 
24
  format_description_with_responses,
25
  )
26
 
27
+ if TYPE_CHECKING:
28
+ from mcp.server.session import ServerSessionT
29
+ from mcp.shared.context import LifespanContextT
30
+
31
+ from fastmcp.server import Context
32
+
33
  logger = get_logger(__name__)
34
 
35
  HttpMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]
 
355
  fn=lambda **kwargs: None,
356
  parameters=parameters,
357
  tags=tags,
358
+ context_kwarg=None,
359
  )
360
  self._client = client
361
  self._route = route
362
 
363
+ async def create_resource(
364
+ self,
365
+ uri: str,
366
+ params: dict[str, Any],
367
+ context: Context[ServerSessionT, LifespanContextT] | None = None,
368
+ ) -> Resource:
369
  """Create a resource with the given parameters."""
370
  # Generate a URI for this resource instance
371
  uri_parts = []
src/fastmcp/server/proxy.py CHANGED
@@ -1,4 +1,6 @@
1
- from typing import Any, cast
 
 
2
  from urllib.parse import quote
3
 
4
  import mcp.types
@@ -25,6 +27,12 @@ from fastmcp.tools.tool import Tool
25
  from fastmcp.utilities.func_metadata import func_metadata
26
  from fastmcp.utilities.logging import get_logger
27
 
 
 
 
 
 
 
28
  logger = get_logger(__name__)
29
 
30
 
@@ -33,12 +41,12 @@ def _proxy_passthrough():
33
 
34
 
35
  class ProxyTool(Tool):
36
- def __init__(self, client: "Client", **kwargs):
37
  super().__init__(**kwargs)
38
  self._client = client
39
 
40
  @classmethod
41
- async def from_client(cls, client: "Client", tool: mcp.types.Tool) -> "ProxyTool":
42
  return cls(
43
  client=client,
44
  name=tool.name,
@@ -50,7 +58,9 @@ class ProxyTool(Tool):
50
  )
51
 
52
  async def run(
53
- self, arguments: dict[str, Any], context: Context | None = None
 
 
54
  ) -> Any:
55
  # the client context manager will swallow any exceptions inside a TaskGroup
56
  # so we return the raw result and raise an exception ourselves
@@ -64,17 +74,15 @@ class ProxyTool(Tool):
64
 
65
 
66
  class ProxyResource(Resource):
67
- def __init__(
68
- self, client: "Client", *, _value: str | bytes | None = None, **kwargs
69
- ):
70
  super().__init__(**kwargs)
71
  self._client = client
72
  self._value = _value
73
 
74
  @classmethod
75
  async def from_client(
76
- cls, client: "Client", resource: mcp.types.Resource
77
- ) -> "ProxyResource":
78
  return cls(
79
  client=client,
80
  uri=resource.uri,
@@ -83,7 +91,9 @@ class ProxyResource(Resource):
83
  mime_type=resource.mimeType,
84
  )
85
 
86
- async def read(self) -> str | bytes:
 
 
87
  if self._value is not None:
88
  return self._value
89
 
@@ -98,14 +108,14 @@ class ProxyResource(Resource):
98
 
99
 
100
  class ProxyTemplate(ResourceTemplate):
101
- def __init__(self, client: "Client", **kwargs):
102
  super().__init__(**kwargs)
103
  self._client = client
104
 
105
  @classmethod
106
  async def from_client(
107
- cls, client: "Client", template: mcp.types.ResourceTemplate
108
- ) -> "ProxyTemplate":
109
  return cls(
110
  client=client,
111
  uri_template=template.uriTemplate,
@@ -115,7 +125,12 @@ class ProxyTemplate(ResourceTemplate):
115
  parameters={},
116
  )
117
 
118
- async def create_resource(self, uri: str, params: dict[str, Any]) -> ProxyResource:
 
 
 
 
 
119
  # dont use the provided uri, because it may not be the same as the
120
  # uri_template on the remote server.
121
  # quote params to ensure they are valid for the uri_template
@@ -144,14 +159,12 @@ class ProxyTemplate(ResourceTemplate):
144
 
145
 
146
  class ProxyPrompt(Prompt):
147
- def __init__(self, client: "Client", **kwargs):
148
  super().__init__(**kwargs)
149
  self._client = client
150
 
151
  @classmethod
152
- async def from_client(
153
- cls, client: "Client", prompt: mcp.types.Prompt
154
- ) -> "ProxyPrompt":
155
  return cls(
156
  client=client,
157
  name=prompt.name,
@@ -160,14 +173,18 @@ class ProxyPrompt(Prompt):
160
  fn=_proxy_passthrough,
161
  )
162
 
163
- async def render(self, arguments: dict[str, Any]) -> list[Message]:
 
 
 
 
164
  async with self._client:
165
  result = await self._client.get_prompt(self.name, arguments)
166
  return [Message(role=m.role, content=m.content) for m in result]
167
 
168
 
169
  class FastMCPProxy(FastMCP):
170
- def __init__(self, client: "Client", **kwargs):
171
  super().__init__(**kwargs)
172
  self.client = client
173
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, cast
4
  from urllib.parse import quote
5
 
6
  import mcp.types
 
27
  from fastmcp.utilities.func_metadata import func_metadata
28
  from fastmcp.utilities.logging import get_logger
29
 
30
+ if TYPE_CHECKING:
31
+ from mcp.server.session import ServerSessionT
32
+ from mcp.shared.context import LifespanContextT
33
+
34
+ from fastmcp.server import Context
35
+
36
  logger = get_logger(__name__)
37
 
38
 
 
41
 
42
 
43
  class ProxyTool(Tool):
44
+ def __init__(self, client: Client, **kwargs):
45
  super().__init__(**kwargs)
46
  self._client = client
47
 
48
  @classmethod
49
+ async def from_client(cls, client: Client, tool: mcp.types.Tool) -> ProxyTool:
50
  return cls(
51
  client=client,
52
  name=tool.name,
 
58
  )
59
 
60
  async def run(
61
+ self,
62
+ arguments: dict[str, Any],
63
+ context: Context[ServerSessionT, LifespanContextT] | None = None,
64
  ) -> Any:
65
  # the client context manager will swallow any exceptions inside a TaskGroup
66
  # so we return the raw result and raise an exception ourselves
 
74
 
75
 
76
  class ProxyResource(Resource):
77
+ def __init__(self, client: Client, *, _value: str | bytes | None = None, **kwargs):
 
 
78
  super().__init__(**kwargs)
79
  self._client = client
80
  self._value = _value
81
 
82
  @classmethod
83
  async def from_client(
84
+ cls, client: Client, resource: mcp.types.Resource
85
+ ) -> ProxyResource:
86
  return cls(
87
  client=client,
88
  uri=resource.uri,
 
91
  mime_type=resource.mimeType,
92
  )
93
 
94
+ async def read(
95
+ self, context: Context[ServerSessionT, LifespanContextT] | None = None
96
+ ) -> str | bytes:
97
  if self._value is not None:
98
  return self._value
99
 
 
108
 
109
 
110
  class ProxyTemplate(ResourceTemplate):
111
+ def __init__(self, client: Client, **kwargs):
112
  super().__init__(**kwargs)
113
  self._client = client
114
 
115
  @classmethod
116
  async def from_client(
117
+ cls, client: Client, template: mcp.types.ResourceTemplate
118
+ ) -> ProxyTemplate:
119
  return cls(
120
  client=client,
121
  uri_template=template.uriTemplate,
 
125
  parameters={},
126
  )
127
 
128
+ async def create_resource(
129
+ self,
130
+ uri: str,
131
+ params: dict[str, Any],
132
+ context: Context[ServerSessionT, LifespanContextT] | None = None,
133
+ ) -> ProxyResource:
134
  # dont use the provided uri, because it may not be the same as the
135
  # uri_template on the remote server.
136
  # quote params to ensure they are valid for the uri_template
 
159
 
160
 
161
  class ProxyPrompt(Prompt):
162
+ def __init__(self, client: Client, **kwargs):
163
  super().__init__(**kwargs)
164
  self._client = client
165
 
166
  @classmethod
167
+ async def from_client(cls, client: Client, prompt: mcp.types.Prompt) -> ProxyPrompt:
 
 
168
  return cls(
169
  client=client,
170
  name=prompt.name,
 
173
  fn=_proxy_passthrough,
174
  )
175
 
176
+ async def render(
177
+ self,
178
+ arguments: dict[str, Any],
179
+ context: Context[ServerSessionT, LifespanContextT] | None = None,
180
+ ) -> list[Message]:
181
  async with self._client:
182
  result = await self._client.get_prompt(self.name, arguments)
183
  return [Message(role=m.role, content=m.content) for m in result]
184
 
185
 
186
  class FastMCPProxy(FastMCP):
187
+ def __init__(self, client: Client, **kwargs):
188
  super().__init__(**kwargs)
189
  self.client = client
190
 
src/fastmcp/server/server.py CHANGED
@@ -398,7 +398,8 @@ class FastMCP(Generic[LifespanResultT]):
398
  server.
399
  """
400
  if self._resource_manager.has_resource(uri):
401
- resource = await self._resource_manager.get_resource(uri)
 
402
  try:
403
  content = await resource.read()
404
  return [
@@ -424,7 +425,10 @@ class FastMCP(Generic[LifespanResultT]):
424
 
425
  """
426
  if self._prompt_manager.has_prompt(name):
427
- messages = await self._prompt_manager.render_prompt(name, arguments)
 
 
 
428
  return GetPromptResult(messages=pydantic_core.to_jsonable_python(messages))
429
  else:
430
  for server in self._mounted_servers.values():
@@ -562,6 +566,10 @@ class FastMCP(Generic[LifespanResultT]):
562
  - bytes for binary content
563
  - other types will be converted to JSON
564
 
 
 
 
 
565
  If the URI contains parameters (e.g. "resource://{param}") or the function
566
  has parameters, it will be registered as a template resource.
567
 
@@ -586,6 +594,11 @@ class FastMCP(Generic[LifespanResultT]):
586
  def get_weather(city: str) -> str:
587
  return f"Weather for {city}"
588
 
 
 
 
 
 
589
  @server.resource("resource://{city}/weather")
590
  async def get_weather(city: str) -> str:
591
  data = await fetch_weather(city)
@@ -639,6 +652,10 @@ class FastMCP(Generic[LifespanResultT]):
639
  ) -> Callable[[AnyFunction], AnyFunction]:
640
  """Decorator to register a prompt.
641
 
 
 
 
 
642
  Args:
643
  name: Optional name for the prompt (defaults to function name)
644
  description: Optional description of what the prompt does
@@ -655,6 +672,17 @@ class FastMCP(Generic[LifespanResultT]):
655
  }
656
  ]
657
 
 
 
 
 
 
 
 
 
 
 
 
658
  @server.prompt()
659
  async def analyze_file(path: str) -> list[Message]:
660
  content = await read_file(path)
 
398
  server.
399
  """
400
  if self._resource_manager.has_resource(uri):
401
+ context = self.get_context()
402
+ resource = await self._resource_manager.get_resource(uri, context=context)
403
  try:
404
  content = await resource.read()
405
  return [
 
425
 
426
  """
427
  if self._prompt_manager.has_prompt(name):
428
+ context = self.get_context()
429
+ messages = await self._prompt_manager.render_prompt(
430
+ name, arguments, context=context
431
+ )
432
  return GetPromptResult(messages=pydantic_core.to_jsonable_python(messages))
433
  else:
434
  for server in self._mounted_servers.values():
 
566
  - bytes for binary content
567
  - other types will be converted to JSON
568
 
569
+ Resources can optionally request a Context object by adding a parameter with the
570
+ Context type annotation. The context provides access to MCP capabilities like
571
+ logging, progress reporting, and session information.
572
+
573
  If the URI contains parameters (e.g. "resource://{param}") or the function
574
  has parameters, it will be registered as a template resource.
575
 
 
594
  def get_weather(city: str) -> str:
595
  return f"Weather for {city}"
596
 
597
+ @server.resource("resource://{city}/weather")
598
+ def get_weather_with_context(city: str, ctx: Context) -> str:
599
+ ctx.info(f"Fetching weather for {city}")
600
+ return f"Weather for {city}"
601
+
602
  @server.resource("resource://{city}/weather")
603
  async def get_weather(city: str) -> str:
604
  data = await fetch_weather(city)
 
652
  ) -> Callable[[AnyFunction], AnyFunction]:
653
  """Decorator to register a prompt.
654
 
655
+ Prompts can optionally request a Context object by adding a parameter with the
656
+ Context type annotation. The context provides access to MCP capabilities like
657
+ logging, progress reporting, and session information.
658
+
659
  Args:
660
  name: Optional name for the prompt (defaults to function name)
661
  description: Optional description of what the prompt does
 
672
  }
673
  ]
674
 
675
+ @server.prompt()
676
+ def analyze_with_context(table_name: str, ctx: Context) -> list[Message]:
677
+ ctx.info(f"Analyzing table {table_name}")
678
+ schema = read_table_schema(table_name)
679
+ return [
680
+ {
681
+ "role": "user",
682
+ "content": f"Analyze this schema:\n{schema}"
683
+ }
684
+ ]
685
+
686
  @server.prompt()
687
  async def analyze_file(path: str) -> list[Message]:
688
  content = await read_file(path)