fastmcp 2.0.0__py3-none-any.whl → 2.1.1__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.
@@ -1,12 +1,16 @@
1
1
  """Resource manager functionality."""
2
2
 
3
+ import inspect
4
+ import re
3
5
  from collections.abc import Callable
4
6
  from typing import Any
5
7
 
6
8
  from pydantic import AnyUrl
7
9
 
8
- from fastmcp.resources.base import Resource
9
- from fastmcp.resources.templates import ResourceTemplate
10
+ from fastmcp.exceptions import ResourceError
11
+ from fastmcp.resources import FunctionResource, Resource
12
+ from fastmcp.resources.template import ResourceTemplate
13
+ from fastmcp.settings import DuplicateBehavior
10
14
  from fastmcp.utilities.logging import get_logger
11
15
 
12
16
  logger = get_logger(__name__)
@@ -15,21 +19,91 @@ logger = get_logger(__name__)
15
19
  class ResourceManager:
16
20
  """Manages FastMCP resources."""
17
21
 
18
- def __init__(self, warn_on_duplicate_resources: bool = True):
22
+ def __init__(self, duplicate_behavior: DuplicateBehavior = DuplicateBehavior.WARN):
19
23
  self._resources: dict[str, Resource] = {}
20
24
  self._templates: dict[str, ResourceTemplate] = {}
21
- self.warn_on_duplicate_resources = warn_on_duplicate_resources
25
+ self.duplicate_behavior = duplicate_behavior
22
26
 
23
- def add_resource(self, resource: Resource) -> Resource:
24
- """Add a resource to the manager.
27
+ def add_resource_or_template_from_fn(
28
+ self,
29
+ fn: Callable[..., Any],
30
+ uri: str,
31
+ name: str | None = None,
32
+ description: str | None = None,
33
+ mime_type: str | None = None,
34
+ tags: set[str] | None = None,
35
+ ) -> Resource | ResourceTemplate:
36
+ """Add a resource or template to the manager from a function.
25
37
 
26
38
  Args:
27
- resource: A Resource instance to add
39
+ fn: The function to register as a resource or template
40
+ uri: The URI for the resource or template
41
+ name: Optional name for the resource or template
42
+ description: Optional description of the resource or template
43
+ mime_type: Optional MIME type for the resource or template
44
+ tags: Optional set of tags for categorizing the resource or template
45
+
46
+ Returns:
47
+ The added resource or template. If a resource or template with the same URI already exists,
48
+ returns the existing resource or template.
49
+ """
50
+ # Check if this should be a template
51
+ has_uri_params = "{" in uri and "}" in uri
52
+ has_func_params = bool(inspect.signature(fn).parameters)
53
+
54
+ if has_uri_params and has_func_params:
55
+ return self.add_template_from_fn(
56
+ fn, uri, name, description, mime_type, tags
57
+ )
58
+ elif not has_uri_params and not has_func_params:
59
+ return self.add_resource_from_fn(
60
+ fn, uri, name, description, mime_type, tags
61
+ )
62
+ else:
63
+ raise ValueError(
64
+ "Invalid resource or template definition due to a "
65
+ "mismatch between URI parameters and function parameters."
66
+ )
67
+
68
+ def add_resource_from_fn(
69
+ self,
70
+ fn: Callable[..., Any],
71
+ uri: str,
72
+ name: str | None = None,
73
+ description: str | None = None,
74
+ mime_type: str | None = None,
75
+ tags: set[str] | None = None,
76
+ ) -> Resource:
77
+ """Add a resource to the manager from a function.
78
+
79
+ Args:
80
+ fn: The function to register as a resource
81
+ uri: The URI for the resource
82
+ name: Optional name for the resource
83
+ description: Optional description of the resource
84
+ mime_type: Optional MIME type for the resource
85
+ tags: Optional set of tags for categorizing the resource
28
86
 
29
87
  Returns:
30
88
  The added resource. If a resource with the same URI already exists,
31
89
  returns the existing resource.
32
90
  """
91
+ resource = FunctionResource(
92
+ uri=AnyUrl(uri),
93
+ name=name,
94
+ description=description,
95
+ mime_type=mime_type or "text/plain",
96
+ fn=fn,
97
+ tags=tags or set(),
98
+ )
99
+ return self.add_resource(resource)
100
+
101
+ def add_resource(self, resource: Resource) -> Resource:
102
+ """Add a resource to the manager.
103
+
104
+ Args:
105
+ resource: A Resource instance to add
106
+ """
33
107
  logger.debug(
34
108
  "Adding resource",
35
109
  extra={
@@ -40,28 +114,78 @@ class ResourceManager:
40
114
  )
41
115
  existing = self._resources.get(str(resource.uri))
42
116
  if existing:
43
- if self.warn_on_duplicate_resources:
117
+ if self.duplicate_behavior == DuplicateBehavior.WARN:
44
118
  logger.warning(f"Resource already exists: {resource.uri}")
45
- return existing
119
+ self._resources[str(resource.uri)] = resource
120
+ elif self.duplicate_behavior == DuplicateBehavior.REPLACE:
121
+ self._resources[str(resource.uri)] = resource
122
+ elif self.duplicate_behavior == DuplicateBehavior.ERROR:
123
+ raise ValueError(f"Resource already exists: {resource.uri}")
124
+ elif self.duplicate_behavior == DuplicateBehavior.IGNORE:
125
+ pass
46
126
  self._resources[str(resource.uri)] = resource
47
127
  return resource
48
128
 
49
- def add_template(
129
+ def add_template_from_fn(
50
130
  self,
51
131
  fn: Callable[..., Any],
52
132
  uri_template: str,
53
133
  name: str | None = None,
54
134
  description: str | None = None,
55
135
  mime_type: str | None = None,
136
+ tags: set[str] | None = None,
56
137
  ) -> ResourceTemplate:
57
- """Add a template from a function."""
138
+ """Create a template from a function."""
139
+
140
+ # Validate that URI params match function params
141
+ uri_params = set(re.findall(r"{(\w+)}", uri_template))
142
+ func_params = set(inspect.signature(fn).parameters.keys())
143
+
144
+ if uri_params != func_params:
145
+ raise ValueError(
146
+ f"Mismatch between URI parameters {uri_params} "
147
+ f"and function parameters {func_params}"
148
+ )
149
+
58
150
  template = ResourceTemplate.from_function(
59
151
  fn,
60
152
  uri_template=uri_template,
61
153
  name=name,
62
154
  description=description,
63
155
  mime_type=mime_type,
156
+ tags=tags,
157
+ )
158
+ return self.add_template(template)
159
+
160
+ def add_template(self, template: ResourceTemplate) -> ResourceTemplate:
161
+ """Add a template to the manager.
162
+
163
+ Args:
164
+ template: A ResourceTemplate instance to add
165
+
166
+ Returns:
167
+ The added template. If a template with the same URI already exists,
168
+ returns the existing template.
169
+ """
170
+ logger.debug(
171
+ "Adding resource",
172
+ extra={
173
+ "uri": template.uri_template,
174
+ "type": type(template).__name__,
175
+ "resource_name": template.name,
176
+ },
64
177
  )
178
+ existing = self._templates.get(str(template.uri_template))
179
+ if existing:
180
+ if self.duplicate_behavior == DuplicateBehavior.WARN:
181
+ logger.warning(f"Resource already exists: {template.uri_template}")
182
+ self._templates[str(template.uri_template)] = template
183
+ elif self.duplicate_behavior == DuplicateBehavior.REPLACE:
184
+ self._templates[str(template.uri_template)] = template
185
+ elif self.duplicate_behavior == DuplicateBehavior.ERROR:
186
+ raise ValueError(f"Resource already exists: {template.uri_template}")
187
+ elif self.duplicate_behavior == DuplicateBehavior.IGNORE:
188
+ pass
65
189
  self._templates[template.uri_template] = template
66
190
  return template
67
191
 
@@ -82,7 +206,7 @@ class ResourceManager:
82
206
  except Exception as e:
83
207
  raise ValueError(f"Error creating resource from template: {e}")
84
208
 
85
- raise ValueError(f"Unknown resource: {uri}")
209
+ raise ResourceError(f"Unknown resource: {uri}")
86
210
 
87
211
  def list_resources(self) -> list[Resource]:
88
212
  """List all registered resources."""
@@ -114,11 +238,11 @@ class ResourceManager:
114
238
  # Create prefixed URI and copy the resource with the new URI
115
239
  prefixed_uri = f"{prefix}{uri}" if prefix else uri
116
240
 
117
- # Log the import
118
- logger.debug(f"Importing resource with URI {uri} as {prefixed_uri}")
241
+ new_resource = resource.copy(updates=dict(uri=prefixed_uri))
119
242
 
120
243
  # Store directly in resources dictionary
121
- self._resources[prefixed_uri] = resource
244
+ self.add_resource(new_resource)
245
+ logger.debug(f'Imported resource "{uri}" as "{prefixed_uri}"')
122
246
 
123
247
  def import_templates(
124
248
  self, manager: "ResourceManager", prefix: str | None = None
@@ -142,10 +266,12 @@ class ResourceManager:
142
266
  f"{prefix}{uri_template}" if prefix else uri_template
143
267
  )
144
268
 
145
- # Log the import
146
- logger.debug(
147
- f"Importing resource template with URI {uri_template} as {prefixed_uri_template}"
269
+ new_template = template.copy(
270
+ updates=dict(uri_template=prefixed_uri_template)
148
271
  )
149
272
 
150
273
  # Store directly in templates dictionary
151
- self._templates[prefixed_uri_template] = template
274
+ self.add_template(new_template)
275
+ logger.debug(
276
+ f'Imported template "{uri_template}" as "{prefixed_uri_template}"'
277
+ )
@@ -5,11 +5,13 @@ from __future__ import annotations
5
5
  import inspect
6
6
  import re
7
7
  from collections.abc import Callable
8
- from typing import Any
8
+ from typing import Annotated, Any
9
9
 
10
- from pydantic import BaseModel, Field, TypeAdapter, validate_call
10
+ from pydantic import BaseModel, BeforeValidator, Field, TypeAdapter, validate_call
11
+ from typing_extensions import Self
11
12
 
12
13
  from fastmcp.resources.types import FunctionResource, Resource
14
+ from fastmcp.utilities.types import _convert_set_defaults
13
15
 
14
16
 
15
17
  class ResourceTemplate(BaseModel):
@@ -20,10 +22,13 @@ class ResourceTemplate(BaseModel):
20
22
  )
21
23
  name: str = Field(description="Name of the resource")
22
24
  description: str | None = Field(description="Description of what the resource does")
25
+ tags: Annotated[set[str], BeforeValidator(_convert_set_defaults)] = Field(
26
+ default_factory=set, description="Tags for the resource"
27
+ )
23
28
  mime_type: str = Field(
24
29
  default="text/plain", description="MIME type of the resource content"
25
30
  )
26
- fn: Callable[..., Any] = Field(exclude=True)
31
+ fn: Callable[..., Any]
27
32
  parameters: dict[str, Any] = Field(
28
33
  description="JSON schema for function parameters"
29
34
  )
@@ -36,6 +41,7 @@ class ResourceTemplate(BaseModel):
36
41
  name: str | None = None,
37
42
  description: str | None = None,
38
43
  mime_type: str | None = None,
44
+ tags: set[str] | None = None,
39
45
  ) -> ResourceTemplate:
40
46
  """Create a template from a function."""
41
47
  func_name = name or fn.__name__
@@ -55,6 +61,7 @@ class ResourceTemplate(BaseModel):
55
61
  mime_type=mime_type or "text/plain",
56
62
  fn=fn,
57
63
  parameters=parameters,
64
+ tags=tags or set(),
58
65
  )
59
66
 
60
67
  def matches(self, uri: str) -> dict[str, Any] | None:
@@ -80,6 +87,19 @@ class ResourceTemplate(BaseModel):
80
87
  description=self.description,
81
88
  mime_type=self.mime_type,
82
89
  fn=lambda: result, # Capture result in closure
90
+ tags=self.tags,
83
91
  )
84
92
  except Exception as e:
85
93
  raise ValueError(f"Error creating resource from template: {e}")
94
+
95
+ def copy(self, updates: dict[str, Any] | None = None) -> Self:
96
+ """Copy the resource template with optional updates."""
97
+ data = self.model_dump()
98
+ if updates:
99
+ data.update(updates)
100
+ return type(self)(**data)
101
+
102
+ def __eq__(self, other: object) -> bool:
103
+ if not isinstance(other, ResourceTemplate):
104
+ return False
105
+ return self.model_dump() == other.model_dump()
@@ -13,7 +13,7 @@ import pydantic.json
13
13
  import pydantic_core
14
14
  from pydantic import Field, ValidationInfo
15
15
 
16
- from fastmcp.resources.base import Resource
16
+ from fastmcp.resources.resource import Resource
17
17
 
18
18
 
19
19
  class TextResource(Resource):
@@ -49,7 +49,7 @@ class FunctionResource(Resource):
49
49
  - other types will be converted to JSON
50
50
  """
51
51
 
52
- fn: Callable[[], Any] = Field(exclude=True)
52
+ fn: Callable[[], Any]
53
53
 
54
54
  async def read(self) -> str | bytes:
55
55
  """Read the resource by calling the wrapped function."""
fastmcp/server/context.py CHANGED
@@ -118,7 +118,7 @@ class Context(BaseModel, Generic[ServerSessionT, LifespanContextT]):
118
118
  assert self._fastmcp is not None, (
119
119
  "Context is not available outside of a request"
120
120
  )
121
- return await self._fastmcp.read_resource(uri)
121
+ return await self._fastmcp._mcp_read_resource(uri)
122
122
 
123
123
  async def log(
124
124
  self,
fastmcp/server/openapi.py CHANGED
@@ -12,7 +12,7 @@ from pydantic.networks import AnyUrl
12
12
 
13
13
  from fastmcp.resources import Resource, ResourceTemplate
14
14
  from fastmcp.server.server import FastMCP
15
- from fastmcp.tools.base import Tool
15
+ from fastmcp.tools.tool import Tool
16
16
  from fastmcp.utilities import openapi
17
17
  from fastmcp.utilities.func_metadata import func_metadata
18
18
  from fastmcp.utilities.logging import get_logger
@@ -115,6 +115,7 @@ class OpenAPITool(Tool):
115
115
  parameters: dict[str, Any],
116
116
  fn_metadata: Any,
117
117
  is_async: bool = True,
118
+ tags: set[str] = set(),
118
119
  ):
119
120
  super().__init__(
120
121
  name=name,
@@ -124,6 +125,7 @@ class OpenAPITool(Tool):
124
125
  fn_metadata=fn_metadata,
125
126
  is_async=is_async,
126
127
  context_kwarg="context", # Default context keyword argument
128
+ tags=tags,
127
129
  )
128
130
  self._client = client
129
131
  self._route = route
@@ -242,12 +244,14 @@ class OpenAPIResource(Resource):
242
244
  name: str,
243
245
  description: str,
244
246
  mime_type: str = "application/json",
247
+ tags: set[str] = set(),
245
248
  ):
246
249
  super().__init__(
247
250
  uri=AnyUrl(uri), # Convert string to AnyUrl
248
251
  name=name,
249
252
  description=description,
250
253
  mime_type=mime_type,
254
+ tags=tags,
251
255
  )
252
256
  self._client = client
253
257
  self._route = route
@@ -332,6 +336,7 @@ class OpenAPIResourceTemplate(ResourceTemplate):
332
336
  name: str,
333
337
  description: str,
334
338
  parameters: dict[str, Any],
339
+ tags: set[str] = set(),
335
340
  ):
336
341
  super().__init__(
337
342
  uri_template=uri_template,
@@ -339,6 +344,7 @@ class OpenAPIResourceTemplate(ResourceTemplate):
339
344
  description=description,
340
345
  fn=self._create_resource_fn,
341
346
  parameters=parameters,
347
+ tags=tags,
342
348
  )
343
349
  self._client = client
344
350
  self._route = route
@@ -405,6 +411,7 @@ class OpenAPIResourceTemplate(ResourceTemplate):
405
411
  description=self.description
406
412
  or f"Resource for {self._route.path}", # Provide default if None
407
413
  mime_type="application/json", # Default, will be updated when read
414
+ tags=set(self._route.tags or []),
408
415
  )
409
416
 
410
417
 
@@ -525,10 +532,13 @@ class FastMCPOpenAPI(FastMCP):
525
532
  parameters=combined_schema,
526
533
  fn_metadata=func_metadata(_openapi_passthrough),
527
534
  is_async=True,
535
+ tags=set(route.tags or []),
528
536
  )
529
537
  # Register the tool by directly assigning to the tools dictionary
530
538
  self._tool_manager._tools[tool_name] = tool
531
- logger.debug(f"Registered TOOL: {tool_name} ({route.method} {route.path})")
539
+ logger.debug(
540
+ f"Registered TOOL: {tool_name} ({route.method} {route.path}) with tags: {route.tags}"
541
+ )
532
542
 
533
543
  def _create_openapi_resource(self, route: openapi.HTTPRoute, operation_id: str):
534
544
  """Creates and registers an OpenAPIResource with enhanced description."""
@@ -550,11 +560,12 @@ class FastMCPOpenAPI(FastMCP):
550
560
  uri=resource_uri,
551
561
  name=resource_name,
552
562
  description=enhanced_description,
563
+ tags=set(route.tags or []),
553
564
  )
554
565
  # Register the resource by directly assigning to the resources dictionary
555
566
  self._resource_manager._resources[str(resource.uri)] = resource
556
567
  logger.debug(
557
- f"Registered RESOURCE: {resource_uri} ({route.method} {route.path})"
568
+ f"Registered RESOURCE: {resource_uri} ({route.method} {route.path}) with tags: {route.tags}"
558
569
  )
559
570
 
560
571
  def _create_openapi_template(self, route: openapi.HTTPRoute, operation_id: str):
@@ -594,11 +605,12 @@ class FastMCPOpenAPI(FastMCP):
594
605
  name=template_name,
595
606
  description=enhanced_description,
596
607
  parameters=template_params_schema,
608
+ tags=set(route.tags or []),
597
609
  )
598
610
  # Register the template by directly assigning to the templates dictionary
599
611
  self._resource_manager._templates[uri_template_str] = template
600
612
  logger.debug(
601
- f"Registered TEMPLATE: {uri_template_str} ({route.method} {route.path})"
613
+ f"Registered TEMPLATE: {uri_template_str} ({route.method} {route.path}) with tags: {route.tags}"
602
614
  )
603
615
 
604
616
  async def call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
fastmcp/server/proxy.py CHANGED
@@ -1,15 +1,15 @@
1
1
  from typing import Any, cast
2
2
 
3
3
  import mcp.types
4
- from mcp.types import BlobResourceContents, PromptMessage, TextResourceContents
4
+ from mcp.types import BlobResourceContents, TextResourceContents
5
5
 
6
6
  import fastmcp
7
7
  from fastmcp.client import Client
8
- from fastmcp.prompts import Prompt
8
+ from fastmcp.prompts import Message, Prompt
9
9
  from fastmcp.resources import Resource, ResourceTemplate
10
10
  from fastmcp.server.context import Context
11
11
  from fastmcp.server.server import FastMCP
12
- from fastmcp.tools.base import Tool
12
+ from fastmcp.tools.tool import Tool
13
13
  from fastmcp.utilities.func_metadata import func_metadata
14
14
  from fastmcp.utilities.logging import get_logger
15
15
 
@@ -40,11 +40,15 @@ class ProxyTool(Tool):
40
40
  async def run(
41
41
  self, arguments: dict[str, Any], context: Context | None = None
42
42
  ) -> Any:
43
+ # the client context manager will swallow any exceptions inside a TaskGroup
44
+ # so we return the raw result and raise an exception ourselves
43
45
  async with self._client:
44
- result = await self._client.call_tool(self.name, arguments)
46
+ result = await self._client.call_tool(
47
+ self.name, arguments, _return_raw_result=True
48
+ )
45
49
  if result.isError:
46
50
  raise ValueError(cast(mcp.types.TextContent, result.content[0]).text)
47
- return result.content[0]
51
+ return result.content
48
52
 
49
53
 
50
54
  class ProxyResource(Resource):
@@ -73,12 +77,12 @@ class ProxyResource(Resource):
73
77
 
74
78
  async with self._client:
75
79
  result = await self._client.read_resource(self.uri)
76
- if isinstance(result.contents[0], TextResourceContents):
77
- return result.contents[0].text
78
- elif isinstance(result.contents[0], BlobResourceContents):
79
- return result.contents[0].blob
80
+ if isinstance(result[0], TextResourceContents):
81
+ return result[0].text
82
+ elif isinstance(result[0], BlobResourceContents):
83
+ return result[0].blob
80
84
  else:
81
- raise ValueError(f"Unsupported content type: {type(result.contents[0])}")
85
+ raise ValueError(f"Unsupported content type: {type(result[0])}")
82
86
 
83
87
 
84
88
  class ProxyTemplate(ResourceTemplate):
@@ -103,20 +107,20 @@ class ProxyTemplate(ResourceTemplate):
103
107
  async with self._client:
104
108
  result = await self._client.read_resource(uri)
105
109
 
106
- if isinstance(result.contents[0], TextResourceContents):
107
- value = result.contents[0].text
108
- elif isinstance(result.contents[0], BlobResourceContents):
109
- value = result.contents[0].blob
110
+ if isinstance(result[0], TextResourceContents):
111
+ value = result[0].text
112
+ elif isinstance(result[0], BlobResourceContents):
113
+ value = result[0].blob
110
114
  else:
111
- raise ValueError(f"Unsupported content type: {type(result.contents[0])}")
115
+ raise ValueError(f"Unsupported content type: {type(result[0])}")
112
116
 
113
117
  return ProxyResource(
114
118
  client=self._client,
115
119
  uri=uri,
116
120
  name=self.name,
117
121
  description=self.description,
118
- mime_type=result.contents[0].mimeType,
119
- contents=result.contents,
122
+ mime_type=result[0].mimeType,
123
+ contents=result,
120
124
  _value=value,
121
125
  )
122
126
 
@@ -138,10 +142,10 @@ class ProxyPrompt(Prompt):
138
142
  fn=_proxy_passthrough,
139
143
  )
140
144
 
141
- async def render(self, arguments: dict[str, Any]) -> list[PromptMessage]:
145
+ async def render(self, arguments: dict[str, Any]) -> list[Message]:
142
146
  async with self._client:
143
147
  result = await self._client.get_prompt(self.name, arguments)
144
- return result.messages
148
+ return [Message(role=m.role, content=m.content) for m in result.messages]
145
149
 
146
150
 
147
151
  class FastMCPProxy(FastMCP):
@@ -177,15 +181,15 @@ class FastMCPProxy(FastMCP):
177
181
 
178
182
  async with client:
179
183
  # Register proxies for client tools
180
- tools_result = await client.list_tools()
181
- for tool in tools_result.tools:
184
+ tools = await client.list_tools()
185
+ for tool in tools:
182
186
  tool_proxy = await ProxyTool.from_client(client, tool)
183
187
  server._tool_manager._tools[tool_proxy.name] = tool_proxy
184
188
  logger.debug(f"Created proxy for tool: {tool_proxy.name}")
185
189
 
186
190
  # Register proxies for client resources
187
- resources_result = await client.list_resources()
188
- for resource in resources_result.resources:
191
+ resources = await client.list_resources()
192
+ for resource in resources:
189
193
  resource_proxy = await ProxyResource.from_client(client, resource)
190
194
  server._resource_manager._resources[str(resource_proxy.uri)] = (
191
195
  resource_proxy
@@ -193,8 +197,8 @@ class FastMCPProxy(FastMCP):
193
197
  logger.debug(f"Created proxy for resource: {resource_proxy.uri}")
194
198
 
195
199
  # Register proxies for client resource templates
196
- templates_result = await client.list_resource_templates()
197
- for template in templates_result.resourceTemplates:
200
+ templates = await client.list_resource_templates()
201
+ for template in templates:
198
202
  template_proxy = await ProxyTemplate.from_client(client, template)
199
203
  server._resource_manager._templates[template_proxy.uri_template] = (
200
204
  template_proxy
@@ -204,8 +208,8 @@ class FastMCPProxy(FastMCP):
204
208
  )
205
209
 
206
210
  # Register proxies for client prompts
207
- prompts_result = await client.list_prompts()
208
- for prompt in prompts_result.prompts:
211
+ prompts = await client.list_prompts()
212
+ for prompt in prompts:
209
213
  prompt_proxy = await ProxyPrompt.from_client(client, prompt)
210
214
  server._prompt_manager._prompts[prompt_proxy.name] = prompt_proxy
211
215
  logger.debug(f"Created proxy for prompt: {prompt_proxy.name}")