fastmcp 2.2.9__py3-none-any.whl → 2.3.0rc1__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/server/server.py CHANGED
@@ -16,17 +16,10 @@ import anyio
16
16
  import httpx
17
17
  import pydantic
18
18
  import uvicorn
19
- from mcp.server.auth.middleware.auth_context import AuthContextMiddleware
20
- from mcp.server.auth.middleware.bearer_auth import (
21
- BearerAuthBackend,
22
- RequireAuthMiddleware,
23
- )
24
19
  from mcp.server.auth.provider import OAuthAuthorizationServerProvider
25
20
  from mcp.server.lowlevel.helper_types import ReadResourceContents
26
21
  from mcp.server.lowlevel.server import LifespanResultT
27
22
  from mcp.server.lowlevel.server import Server as MCPServer
28
- from mcp.server.session import ServerSession
29
- from mcp.server.sse import SseServerTransport
30
23
  from mcp.server.stdio import stdio_server
31
24
  from mcp.types import (
32
25
  AnyFunction,
@@ -42,127 +35,31 @@ from mcp.types import ResourceTemplate as MCPResourceTemplate
42
35
  from mcp.types import Tool as MCPTool
43
36
  from pydantic import AnyUrl
44
37
  from starlette.applications import Starlette
45
- from starlette.middleware import Middleware
46
- from starlette.middleware.authentication import AuthenticationMiddleware
47
38
  from starlette.requests import Request
48
39
  from starlette.responses import Response
49
- from starlette.routing import Mount, Route
50
- from starlette.types import Receive, Scope, Send
40
+ from starlette.routing import Route
51
41
 
52
- import fastmcp
42
+ import fastmcp.server
53
43
  import fastmcp.settings
54
44
  from fastmcp.exceptions import NotFoundError, ResourceError
55
45
  from fastmcp.prompts import Prompt, PromptManager
56
46
  from fastmcp.prompts.prompt import PromptResult
57
47
  from fastmcp.resources import Resource, ResourceManager
58
48
  from fastmcp.resources.template import ResourceTemplate
49
+ from fastmcp.server.http import create_sse_app
59
50
  from fastmcp.tools import ToolManager
60
51
  from fastmcp.tools.tool import Tool
52
+ from fastmcp.utilities.cache import TimedCache
61
53
  from fastmcp.utilities.decorators import DecoratedFunction
62
- from fastmcp.utilities.http import RequestMiddleware
63
54
  from fastmcp.utilities.logging import configure_logging, get_logger
64
55
 
65
56
  if TYPE_CHECKING:
66
57
  from fastmcp.client import Client
67
- from fastmcp.server.context import Context
68
58
  from fastmcp.server.openapi import FastMCPOpenAPI
69
59
  from fastmcp.server.proxy import FastMCPProxy
70
60
 
71
61
  logger = get_logger(__name__)
72
62
 
73
- NOT_FOUND = object()
74
-
75
-
76
- class MountedServer:
77
- def __init__(
78
- self,
79
- prefix: str,
80
- server: FastMCP,
81
- tool_separator: str | None = None,
82
- resource_separator: str | None = None,
83
- prompt_separator: str | None = None,
84
- ):
85
- if tool_separator is None:
86
- tool_separator = "_"
87
- if resource_separator is None:
88
- resource_separator = "+"
89
- if prompt_separator is None:
90
- prompt_separator = "_"
91
-
92
- _validate_resource_prefix(f"{prefix}{resource_separator}")
93
-
94
- self.server = server
95
- self.prefix = prefix
96
- self.tool_separator = tool_separator
97
- self.resource_separator = resource_separator
98
- self.prompt_separator = prompt_separator
99
-
100
- async def get_tools(self) -> dict[str, Tool]:
101
- tools = await self.server.get_tools()
102
- return {
103
- f"{self.prefix}{self.tool_separator}{key}": tool
104
- for key, tool in tools.items()
105
- }
106
-
107
- async def get_resources(self) -> dict[str, Resource]:
108
- resources = await self.server.get_resources()
109
- return {
110
- f"{self.prefix}{self.resource_separator}{key}": resource
111
- for key, resource in resources.items()
112
- }
113
-
114
- async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
115
- templates = await self.server.get_resource_templates()
116
- return {
117
- f"{self.prefix}{self.resource_separator}{key}": template
118
- for key, template in templates.items()
119
- }
120
-
121
- async def get_prompts(self) -> dict[str, Prompt]:
122
- prompts = await self.server.get_prompts()
123
- return {
124
- f"{self.prefix}{self.prompt_separator}{key}": prompt
125
- for key, prompt in prompts.items()
126
- }
127
-
128
- def match_tool(self, key: str) -> bool:
129
- return key.startswith(f"{self.prefix}{self.tool_separator}")
130
-
131
- def strip_tool_prefix(self, key: str) -> str:
132
- return key.removeprefix(f"{self.prefix}{self.tool_separator}")
133
-
134
- def match_resource(self, key: str) -> bool:
135
- return key.startswith(f"{self.prefix}{self.resource_separator}")
136
-
137
- def strip_resource_prefix(self, key: str) -> str:
138
- return key.removeprefix(f"{self.prefix}{self.resource_separator}")
139
-
140
- def match_prompt(self, key: str) -> bool:
141
- return key.startswith(f"{self.prefix}{self.prompt_separator}")
142
-
143
- def strip_prompt_prefix(self, key: str) -> str:
144
- return key.removeprefix(f"{self.prefix}{self.prompt_separator}")
145
-
146
-
147
- class TimedCache:
148
- def __init__(self, expiration: datetime.timedelta):
149
- self.expiration = expiration
150
- self.cache: dict[Any, tuple[Any, datetime.datetime]] = {}
151
-
152
- def set(self, key: Any, value: Any) -> None:
153
- expires = datetime.datetime.now() + self.expiration
154
- self.cache[key] = (value, expires)
155
-
156
- def get(self, key: Any) -> Any:
157
- value = self.cache.get(key)
158
- if value is not None and value[1] > datetime.datetime.now():
159
- return value[0]
160
- else:
161
- return NOT_FOUND
162
-
163
- def clear(self) -> None:
164
- self.cache.clear()
165
-
166
63
 
167
64
  @asynccontextmanager
168
65
  async def default_lifespan(server: FastMCP) -> AsyncIterator[Any]:
@@ -249,7 +146,8 @@ class FastMCP(Generic[LifespanResultT]):
249
146
  "is specified"
250
147
  )
251
148
  self._auth_server_provider = auth_server_provider
252
- self._custom_starlette_routes: list[Route] = []
149
+
150
+ self._additional_http_routes: list[Route] = []
253
151
  self.dependencies = self.settings.dependencies
254
152
 
255
153
  # Set up MCP protocol handlers
@@ -270,30 +168,36 @@ class FastMCP(Generic[LifespanResultT]):
270
168
  return self._mcp_server.instructions
271
169
 
272
170
  async def run_async(
273
- self, transport: Literal["stdio", "sse"] | None = None, **transport_kwargs: Any
171
+ self,
172
+ transport: Literal["stdio", "sse", "streamable-http"] | None = None,
173
+ **transport_kwargs: Any,
274
174
  ) -> None:
275
175
  """Run the FastMCP server asynchronously.
276
176
 
277
177
  Args:
278
- transport: Transport protocol to use ("stdio" or "sse")
178
+ transport: Transport protocol to use ("stdio", "sse", or "streamable-http")
279
179
  """
280
180
  if transport is None:
281
181
  transport = "stdio"
282
- if transport not in ["stdio", "sse"]:
182
+ if transport not in ["stdio", "sse", "streamable-http"]:
283
183
  raise ValueError(f"Unknown transport: {transport}")
284
184
 
285
185
  if transport == "stdio":
286
186
  await self.run_stdio_async(**transport_kwargs)
287
- else: # transport == "sse"
187
+ elif transport == "sse":
288
188
  await self.run_sse_async(**transport_kwargs)
189
+ else: # transport == "streamable-http"
190
+ await self.run_streamable_http_async(**transport_kwargs)
289
191
 
290
192
  def run(
291
- self, transport: Literal["stdio", "sse"] | None = None, **transport_kwargs: Any
193
+ self,
194
+ transport: Literal["stdio", "sse", "streamable-http"] | None = None,
195
+ **transport_kwargs: Any,
292
196
  ) -> None:
293
197
  """Run the FastMCP server. Note this is a synchronous function.
294
198
 
295
199
  Args:
296
- transport: Transport protocol to use ("stdio" or "sse")
200
+ transport: Transport protocol to use ("stdio", "sse", or "streamable-http")
297
201
  """
298
202
  logger.info(f'Starting server "{self.name}"...')
299
203
 
@@ -309,23 +213,9 @@ class FastMCP(Generic[LifespanResultT]):
309
213
  self._mcp_server.get_prompt()(self._mcp_get_prompt)
310
214
  self._mcp_server.list_resource_templates()(self._mcp_list_resource_templates)
311
215
 
312
- def get_context(self) -> Context[ServerSession, LifespanResultT]:
313
- """
314
- Returns a Context object. Note that the context will only be valid
315
- during a request; outside a request, most methods will error.
316
- """
317
-
318
- try:
319
- request_context = self._mcp_server.request_context
320
- except LookupError:
321
- request_context = None
322
- from fastmcp.server.context import Context
323
-
324
- return Context(request_context=request_context, fastmcp=self)
325
-
326
216
  async def get_tools(self) -> dict[str, Tool]:
327
217
  """Get all registered tools, indexed by registered key."""
328
- if (tools := self._cache.get("tools")) is NOT_FOUND:
218
+ if (tools := self._cache.get("tools")) is self._cache.NOT_FOUND:
329
219
  tools = {}
330
220
  for server in self._mounted_servers.values():
331
221
  server_tools = await server.get_tools()
@@ -336,7 +226,7 @@ class FastMCP(Generic[LifespanResultT]):
336
226
 
337
227
  async def get_resources(self) -> dict[str, Resource]:
338
228
  """Get all registered resources, indexed by registered key."""
339
- if (resources := self._cache.get("resources")) is NOT_FOUND:
229
+ if (resources := self._cache.get("resources")) is self._cache.NOT_FOUND:
340
230
  resources = {}
341
231
  for server in self._mounted_servers.values():
342
232
  server_resources = await server.get_resources()
@@ -347,7 +237,9 @@ class FastMCP(Generic[LifespanResultT]):
347
237
 
348
238
  async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
349
239
  """Get all registered resource templates, indexed by registered key."""
350
- if (templates := self._cache.get("resource_templates")) is NOT_FOUND:
240
+ if (
241
+ templates := self._cache.get("resource_templates")
242
+ ) is self._cache.NOT_FOUND:
351
243
  templates = {}
352
244
  for server in self._mounted_servers.values():
353
245
  server_templates = await server.get_resource_templates()
@@ -360,7 +252,7 @@ class FastMCP(Generic[LifespanResultT]):
360
252
  """
361
253
  List all available prompts.
362
254
  """
363
- if (prompts := self._cache.get("prompts")) is NOT_FOUND:
255
+ if (prompts := self._cache.get("prompts")) is self._cache.NOT_FOUND:
364
256
  prompts = {}
365
257
  for server in self._mounted_servers.values():
366
258
  server_prompts = await server.get_prompts()
@@ -400,7 +292,7 @@ class FastMCP(Generic[LifespanResultT]):
400
292
  def decorator(
401
293
  func: Callable[[Request], Awaitable[Response]],
402
294
  ) -> Callable[[Request], Awaitable[Response]]:
403
- self._custom_starlette_routes.append(
295
+ self._additional_http_routes.append(
404
296
  Route(
405
297
  path,
406
298
  endpoint=func,
@@ -458,43 +350,46 @@ class FastMCP(Generic[LifespanResultT]):
458
350
  self, key: str, arguments: dict[str, Any]
459
351
  ) -> list[TextContent | ImageContent | EmbeddedResource]:
460
352
  """Call a tool by name with arguments."""
461
- if self._tool_manager.has_tool(key):
462
- context = self.get_context()
463
- result = await self._tool_manager.call_tool(key, arguments, context=context)
464
353
 
465
- else:
466
- for server in self._mounted_servers.values():
467
- if server.match_tool(key):
468
- new_key = server.strip_tool_prefix(key)
469
- result = await server.server._mcp_call_tool(new_key, arguments)
470
- break
354
+ with fastmcp.server.context.Context(fastmcp=self):
355
+ if self._tool_manager.has_tool(key):
356
+ result = await self._tool_manager.call_tool(key, arguments)
357
+
471
358
  else:
472
- raise NotFoundError(f"Unknown tool: {key}")
473
- return result
359
+ for server in self._mounted_servers.values():
360
+ if server.match_tool(key):
361
+ new_key = server.strip_tool_prefix(key)
362
+ result = await server.server._mcp_call_tool(new_key, arguments)
363
+ break
364
+ else:
365
+ raise NotFoundError(f"Unknown tool: {key}")
366
+ return result
474
367
 
475
368
  async def _mcp_read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
476
369
  """
477
370
  Read a resource by URI, in the format expected by the low-level MCP
478
371
  server.
479
372
  """
480
- if self._resource_manager.has_resource(uri):
481
- context = self.get_context()
482
- resource = await self._resource_manager.get_resource(uri, context=context)
483
- try:
484
- content = await resource.read(context=context)
485
- return [
486
- ReadResourceContents(content=content, mime_type=resource.mime_type)
487
- ]
488
- except Exception as e:
489
- logger.error(f"Error reading resource {uri}: {e}")
490
- raise ResourceError(str(e))
491
- else:
492
- for server in self._mounted_servers.values():
493
- if server.match_resource(str(uri)):
494
- new_uri = server.strip_resource_prefix(str(uri))
495
- return await server.server._mcp_read_resource(new_uri)
373
+ with fastmcp.server.context.Context(fastmcp=self):
374
+ if self._resource_manager.has_resource(uri):
375
+ resource = await self._resource_manager.get_resource(uri)
376
+ try:
377
+ content = await resource.read()
378
+ return [
379
+ ReadResourceContents(
380
+ content=content, mime_type=resource.mime_type
381
+ )
382
+ ]
383
+ except Exception as e:
384
+ logger.error(f"Error reading resource {uri}: {e}")
385
+ raise ResourceError(str(e))
496
386
  else:
497
- raise NotFoundError(f"Unknown resource: {uri}")
387
+ for server in self._mounted_servers.values():
388
+ if server.match_resource(str(uri)):
389
+ new_uri = server.strip_resource_prefix(str(uri))
390
+ return await server.server._mcp_read_resource(new_uri)
391
+ else:
392
+ raise NotFoundError(f"Unknown resource: {uri}")
498
393
 
499
394
  async def _mcp_get_prompt(
500
395
  self, name: str, arguments: dict[str, Any] | None = None
@@ -504,19 +399,19 @@ class FastMCP(Generic[LifespanResultT]):
504
399
  MCP server.
505
400
 
506
401
  """
507
- if self._prompt_manager.has_prompt(name):
508
- context = self.get_context()
509
- prompt_result = await self._prompt_manager.render_prompt(
510
- name, arguments=arguments or {}, context=context
511
- )
512
- return prompt_result
513
- else:
514
- for server in self._mounted_servers.values():
515
- if server.match_prompt(name):
516
- new_key = server.strip_prompt_prefix(name)
517
- return await server.server._mcp_get_prompt(new_key, arguments)
402
+ with fastmcp.server.context.Context(fastmcp=self):
403
+ if self._prompt_manager.has_prompt(name):
404
+ prompt_result = await self._prompt_manager.render_prompt(
405
+ name, arguments=arguments or {}
406
+ )
407
+ return prompt_result
518
408
  else:
519
- raise NotFoundError(f"Unknown prompt: {name}")
409
+ for server in self._mounted_servers.values():
410
+ if server.match_prompt(name):
411
+ new_key = server.strip_prompt_prefix(name)
412
+ return await server.server._mcp_get_prompt(new_key, arguments)
413
+ else:
414
+ raise NotFoundError(f"Unknown prompt: {name}")
520
415
 
521
416
  def add_tool(
522
417
  self,
@@ -823,14 +718,17 @@ class FastMCP(Generic[LifespanResultT]):
823
718
  host: str | None = None,
824
719
  port: int | None = None,
825
720
  log_level: str | None = None,
721
+ path: str | None = None,
722
+ message_path: str | None = None,
826
723
  uvicorn_config: dict | None = None,
827
724
  ) -> None:
828
725
  """Run the server using SSE transport."""
829
726
  uvicorn_config = uvicorn_config or {}
830
- # the SSE app hangs even when a signal is sent, so we disable the timeout to make it possible to close immediately.
831
- # see https://github.com/jlowin/fastmcp/issues/296
727
+ # the SSE app hangs even when a signal is sent, so we disable the
728
+ # timeout to make it possible to close immediately. see
729
+ # https://github.com/jlowin/fastmcp/issues/296
832
730
  uvicorn_config.setdefault("timeout_graceful_shutdown", 0)
833
- app = RequestMiddleware(self.sse_app())
731
+ app = self.sse_app(path=path, message_path=message_path)
834
732
 
835
733
  config = uvicorn.Config(
836
734
  app,
@@ -842,107 +740,63 @@ class FastMCP(Generic[LifespanResultT]):
842
740
  server = uvicorn.Server(config)
843
741
  await server.serve()
844
742
 
845
- def sse_app(self) -> Starlette:
743
+ def sse_app(
744
+ self,
745
+ path: str | None = None,
746
+ message_path: str | None = None,
747
+ ) -> Starlette:
846
748
  """Return an instance of the SSE server app."""
847
- from starlette.middleware import Middleware
848
- from starlette.routing import Mount, Route
849
-
850
- # Set up auth context and dependencies
851
-
852
- sse = SseServerTransport(self.settings.message_path)
853
-
854
- async def handle_sse(scope: Scope, receive: Receive, send: Send):
855
- # Add client ID from auth context into request context if available
749
+ return create_sse_app(
750
+ server=self,
751
+ message_path=message_path or self.settings.message_path,
752
+ sse_path=path or self.settings.sse_path,
753
+ auth_server_provider=self._auth_server_provider,
754
+ auth_settings=self.settings.auth,
755
+ debug=self.settings.debug,
756
+ additional_routes=self._additional_http_routes,
757
+ )
856
758
 
857
- async with sse.connect_sse(
858
- scope,
859
- receive,
860
- send,
861
- ) as streams:
862
- await self._mcp_server.run(
863
- streams[0],
864
- streams[1],
865
- self._mcp_server.create_initialization_options(),
866
- )
867
- return Response()
868
-
869
- # Create routes
870
- routes: list[Route | Mount] = []
871
- middleware: list[Middleware] = []
872
- required_scopes = []
873
-
874
- # Add auth endpoints if auth provider is configured
875
- if self._auth_server_provider:
876
- assert self.settings.auth
877
- from mcp.server.auth.routes import create_auth_routes
878
-
879
- required_scopes = self.settings.auth.required_scopes or []
880
-
881
- middleware = [
882
- # extract auth info from request (but do not require it)
883
- Middleware(
884
- AuthenticationMiddleware,
885
- backend=BearerAuthBackend(
886
- provider=self._auth_server_provider,
887
- ),
888
- ),
889
- # Add the auth context middleware to store
890
- # authenticated user in a contextvar
891
- Middleware(AuthContextMiddleware),
892
- ]
893
- routes.extend(
894
- create_auth_routes(
895
- provider=self._auth_server_provider,
896
- issuer_url=self.settings.auth.issuer_url,
897
- service_documentation_url=self.settings.auth.service_documentation_url,
898
- client_registration_options=self.settings.auth.client_registration_options,
899
- revocation_options=self.settings.auth.revocation_options,
900
- )
901
- )
759
+ def streamable_http_app(self, path: str | None = None) -> Starlette:
760
+ """Return an instance of the StreamableHTTP server app."""
761
+ from fastmcp.server.http import create_streamable_http_app
762
+
763
+ return create_streamable_http_app(
764
+ server=self,
765
+ streamable_http_path=path or self.settings.streamable_http_path,
766
+ event_store=None,
767
+ auth_server_provider=self._auth_server_provider,
768
+ auth_settings=self.settings.auth,
769
+ json_response=self.settings.json_response,
770
+ stateless_http=self.settings.stateless_http,
771
+ debug=self.settings.debug,
772
+ additional_routes=self._additional_http_routes,
773
+ )
902
774
 
903
- # When auth is not configured, we shouldn't require auth
904
- if self._auth_server_provider:
905
- # Auth is enabled, wrap the endpoints with RequireAuthMiddleware
906
- routes.append(
907
- Route(
908
- self.settings.sse_path,
909
- endpoint=RequireAuthMiddleware(handle_sse, required_scopes),
910
- methods=["GET"],
911
- )
912
- )
913
- routes.append(
914
- Mount(
915
- self.settings.message_path,
916
- app=RequireAuthMiddleware(sse.handle_post_message, required_scopes),
917
- )
918
- )
919
- else:
920
- # Auth is disabled, no need for RequireAuthMiddleware
921
- # Since handle_sse is an ASGI app, we need to create a compatible endpoint
922
- async def sse_endpoint(request: Request) -> None:
923
- # Convert the Starlette request to ASGI parameters
924
- await handle_sse(request.scope, request.receive, request._send) # type: ignore[reportPrivateUsage]
775
+ async def run_streamable_http_async(
776
+ self,
777
+ host: str | None = None,
778
+ port: int | None = None,
779
+ log_level: str | None = None,
780
+ path: str | None = None,
781
+ uvicorn_config: dict | None = None,
782
+ ) -> None:
783
+ """Run the server using StreamableHTTP transport."""
784
+ uvicorn_config = uvicorn_config or {}
785
+ uvicorn_config.setdefault("timeout_graceful_shutdown", 0)
925
786
 
926
- routes.append(
927
- Route(
928
- self.settings.sse_path,
929
- endpoint=sse_endpoint,
930
- methods=["GET"],
931
- )
932
- )
933
- routes.append(
934
- Mount(
935
- self.settings.message_path,
936
- app=sse.handle_post_message,
937
- )
938
- )
939
- # mount these routes last, so they have the lowest route matching precedence
940
- routes.extend(self._custom_starlette_routes)
787
+ app = self.streamable_http_app(path=path)
941
788
 
942
- # Create Starlette app with routes and middleware
943
- return Starlette(
944
- debug=self.settings.debug, routes=routes, middleware=middleware
789
+ config = uvicorn.Config(
790
+ app,
791
+ host=host or self.settings.host,
792
+ port=port or self.settings.port,
793
+ log_level=log_level or self.settings.log_level.lower(),
794
+ # lifespan is required for streamable http
795
+ lifespan="on",
796
+ **uvicorn_config,
945
797
  )
798
+ server = uvicorn.Server(config)
799
+ await server.serve()
946
800
 
947
801
  def mount(
948
802
  self,
@@ -1145,3 +999,74 @@ def _validate_resource_prefix(prefix: str) -> None:
1145
999
  raise ValueError(
1146
1000
  f"Resource prefix or separator would result in an invalid resource URI: {e}"
1147
1001
  )
1002
+
1003
+
1004
+ class MountedServer:
1005
+ def __init__(
1006
+ self,
1007
+ prefix: str,
1008
+ server: FastMCP,
1009
+ tool_separator: str | None = None,
1010
+ resource_separator: str | None = None,
1011
+ prompt_separator: str | None = None,
1012
+ ):
1013
+ if tool_separator is None:
1014
+ tool_separator = "_"
1015
+ if resource_separator is None:
1016
+ resource_separator = "+"
1017
+ if prompt_separator is None:
1018
+ prompt_separator = "_"
1019
+
1020
+ _validate_resource_prefix(f"{prefix}{resource_separator}")
1021
+
1022
+ self.server = server
1023
+ self.prefix = prefix
1024
+ self.tool_separator = tool_separator
1025
+ self.resource_separator = resource_separator
1026
+ self.prompt_separator = prompt_separator
1027
+
1028
+ async def get_tools(self) -> dict[str, Tool]:
1029
+ tools = await self.server.get_tools()
1030
+ return {
1031
+ f"{self.prefix}{self.tool_separator}{key}": tool
1032
+ for key, tool in tools.items()
1033
+ }
1034
+
1035
+ async def get_resources(self) -> dict[str, Resource]:
1036
+ resources = await self.server.get_resources()
1037
+ return {
1038
+ f"{self.prefix}{self.resource_separator}{key}": resource
1039
+ for key, resource in resources.items()
1040
+ }
1041
+
1042
+ async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
1043
+ templates = await self.server.get_resource_templates()
1044
+ return {
1045
+ f"{self.prefix}{self.resource_separator}{key}": template
1046
+ for key, template in templates.items()
1047
+ }
1048
+
1049
+ async def get_prompts(self) -> dict[str, Prompt]:
1050
+ prompts = await self.server.get_prompts()
1051
+ return {
1052
+ f"{self.prefix}{self.prompt_separator}{key}": prompt
1053
+ for key, prompt in prompts.items()
1054
+ }
1055
+
1056
+ def match_tool(self, key: str) -> bool:
1057
+ return key.startswith(f"{self.prefix}{self.tool_separator}")
1058
+
1059
+ def strip_tool_prefix(self, key: str) -> str:
1060
+ return key.removeprefix(f"{self.prefix}{self.tool_separator}")
1061
+
1062
+ def match_resource(self, key: str) -> bool:
1063
+ return key.startswith(f"{self.prefix}{self.resource_separator}")
1064
+
1065
+ def strip_resource_prefix(self, key: str) -> str:
1066
+ return key.removeprefix(f"{self.prefix}{self.resource_separator}")
1067
+
1068
+ def match_prompt(self, key: str) -> bool:
1069
+ return key.startswith(f"{self.prefix}{self.prompt_separator}")
1070
+
1071
+ def strip_prompt_prefix(self, key: str) -> str:
1072
+ return key.removeprefix(f"{self.prefix}{self.prompt_separator}")