Jeremiah Lowin commited on
Commit
6da0e6b
·
1 Parent(s): 92533b3

Permit empty prefixes

Browse files
src/fastmcp/prompts/prompt_manager.py CHANGED
@@ -11,20 +11,23 @@ class PromptManager(BasePromptManager):
11
  Adds ability to import prompts from other managers with prefixed names.
12
  """
13
 
14
- def import_prompts(self, manager: "PromptManager", prefix: str) -> None:
 
 
15
  """
16
  Import all prompts from another PromptManager with prefixed names.
17
 
18
  Args:
19
  manager: Another PromptManager instance to import prompts from
20
  prefix: Prefix to add to prompt names. The resulting prompt name will
21
- be in the format "{prefix}/{original_name}"
22
- For example, with prefix "weather" and prompt "forecast_prompt",
 
23
  the imported prompt would be available as "weather/forecast_prompt"
24
  """
25
  for name, prompt in manager._prompts.items():
26
  # Create prefixed name - we keep the original name in the Prompt object
27
- prefixed_name = f"{prefix}/{name}"
28
 
29
  # Log the import
30
  logger.debug(f"Importing prompt with name {name} as {prefixed_name}")
 
11
  Adds ability to import prompts from other managers with prefixed names.
12
  """
13
 
14
+ def import_prompts(
15
+ self, manager: "PromptManager", prefix: str | None = None
16
+ ) -> None:
17
  """
18
  Import all prompts from another PromptManager with prefixed names.
19
 
20
  Args:
21
  manager: Another PromptManager instance to import prompts from
22
  prefix: Prefix to add to prompt names. The resulting prompt name will
23
+ be in the format "{prefix}{original_name}" if prefix is provided,
24
+ otherwise the original name is used.
25
+ For example, with prefix "weather/" and prompt "forecast_prompt",
26
  the imported prompt would be available as "weather/forecast_prompt"
27
  """
28
  for name, prompt in manager._prompts.items():
29
  # Create prefixed name - we keep the original name in the Prompt object
30
+ prefixed_name = f"{prefix}{name}" if prefix else name
31
 
32
  # Log the import
33
  logger.debug(f"Importing prompt with name {name} as {prefixed_name}")
src/fastmcp/resources/resource_manager.py CHANGED
@@ -10,20 +10,25 @@ logger = logging.getLogger(__name__)
10
  class ResourceManager(BaseResourceManager):
11
  """ResourceManager that adds methods to import resources from other managers."""
12
 
13
- def import_resources(self, manager: "ResourceManager", prefix: str) -> None:
 
 
14
  """Import resources from another resource manager.
15
 
16
- Resources are imported with a prefixed URI. For example, if a resource has
17
- URI "data://users" and you import it with prefix "app", the imported resource
18
- will have URI "app+data://users".
 
19
 
20
  Args:
21
  manager: The ResourceManager to import from
22
- prefix: A prefix to apply to the resource URIs
 
 
23
  """
24
  for uri, resource in manager._resources.items():
25
  # Create prefixed URI and copy the resource with the new URI
26
- prefixed_uri = f"{prefix}+{uri}"
27
 
28
  # Log the import
29
  logger.debug(f"Importing resource with URI {uri} as {prefixed_uri}")
@@ -31,20 +36,27 @@ class ResourceManager(BaseResourceManager):
31
  # Store directly in resources dictionary
32
  self._resources[prefixed_uri] = resource
33
 
34
- def import_templates(self, manager: "ResourceManager", prefix: str) -> None:
 
 
35
  """Import resource templates from another resource manager.
36
 
37
- Templates are imported with a prefixed URI template. For example, if a template has
38
- URI template "data://users/{id}" and you import it with prefix "app", the
39
- imported template will have URI template "app+data://users/{id}".
 
40
 
41
  Args:
42
  manager: The ResourceManager to import templates from
43
- prefix: A prefix to apply to the template URIs
 
 
44
  """
45
  for uri_template, template in manager._templates.items():
46
  # Create prefixed URI template and copy the template with the new URI template
47
- prefixed_uri_template = f"{prefix}+{uri_template}"
 
 
48
 
49
  # Log the import
50
  logger.debug(
 
10
  class ResourceManager(BaseResourceManager):
11
  """ResourceManager that adds methods to import resources from other managers."""
12
 
13
+ def import_resources(
14
+ self, manager: "ResourceManager", prefix: str | None = None
15
+ ) -> None:
16
  """Import resources from another resource manager.
17
 
18
+ Resources are imported with a prefixed URI if a prefix is provided. For example,
19
+ if a resource has URI "data://users" and you import it with prefix "app+", the
20
+ imported resource will have URI "app+data://users". If no prefix is provided,
21
+ the original URI is used.
22
 
23
  Args:
24
  manager: The ResourceManager to import from
25
+ prefix: A prefix to apply to the resource URIs, including the delimiter.
26
+ For example, "app+" would result in URIs like "app+data://users".
27
+ If None, the original URI is used.
28
  """
29
  for uri, resource in manager._resources.items():
30
  # Create prefixed URI and copy the resource with the new URI
31
+ prefixed_uri = f"{prefix}{uri}" if prefix else uri
32
 
33
  # Log the import
34
  logger.debug(f"Importing resource with URI {uri} as {prefixed_uri}")
 
36
  # Store directly in resources dictionary
37
  self._resources[prefixed_uri] = resource
38
 
39
+ def import_templates(
40
+ self, manager: "ResourceManager", prefix: str | None = None
41
+ ) -> None:
42
  """Import resource templates from another resource manager.
43
 
44
+ Templates are imported with a prefixed URI template if a prefix is provided.
45
+ For example, if a template has URI template "data://users/{id}" and you import
46
+ it with prefix "app+", the imported template will have URI template
47
+ "app+data://users/{id}". If no prefix is provided, the original URI template is used.
48
 
49
  Args:
50
  manager: The ResourceManager to import templates from
51
+ prefix: A prefix to apply to the template URIs, including the delimiter.
52
+ For example, "app+" would result in URI templates like "app+data://users/{id}".
53
+ If None, the original URI template is used.
54
  """
55
  for uri_template, template in manager._templates.items():
56
  # Create prefixed URI template and copy the template with the new URI template
57
+ prefixed_uri_template = (
58
+ f"{prefix}{uri_template}" if prefix else uri_template
59
+ )
60
 
61
  # Log the import
62
  logger.debug(
src/fastmcp/server/server.py CHANGED
@@ -1,4 +1,4 @@
1
- from typing import Any, Dict
2
 
3
  import mcp.server.fastmcp
4
  import mcp.types
@@ -9,6 +9,9 @@ from fastmcp.server.context import Context
9
  from fastmcp.tools.tool_manager import ToolManager
10
  from fastmcp.utilities.logging import get_logger
11
 
 
 
 
12
  logger = get_logger(__name__)
13
 
14
 
@@ -62,20 +65,21 @@ class FastMCP(mcp.server.fastmcp.FastMCP):
62
  # Mount the app in the list of mounted apps
63
  self._mounted_apps[prefix] = app
64
 
65
- # Import tools from the mounted app
66
- self._tool_manager.import_tools(app._tool_manager, prefix)
67
-
68
- # Import resources from the mounted app
69
- self._resource_manager.import_resources(app._resource_manager, prefix)
70
 
71
- # Import resource templates
72
- self._resource_manager.import_templates(app._resource_manager, prefix)
 
 
73
 
74
- # Import prompts
75
- self._prompt_manager.import_prompts(app._prompt_manager, prefix)
 
76
 
77
  logger.info(f"Mounted app with prefix '{prefix}'")
78
- logger.debug(f"Imported tools with prefix '{prefix}/'")
79
- logger.debug(f"Imported resources with prefix '{prefix}+'")
80
- logger.debug(f"Imported templates with prefix '{prefix}+'")
81
- logger.debug(f"Imported prompts with prefix '{prefix}/'")
 
1
+ from typing import TYPE_CHECKING, Any, Dict
2
 
3
  import mcp.server.fastmcp
4
  import mcp.types
 
9
  from fastmcp.tools.tool_manager import ToolManager
10
  from fastmcp.utilities.logging import get_logger
11
 
12
+ if TYPE_CHECKING:
13
+ pass
14
+
15
  logger = get_logger(__name__)
16
 
17
 
 
65
  # Mount the app in the list of mounted apps
66
  self._mounted_apps[prefix] = app
67
 
68
+ # Import tools from the mounted app with / delimiter
69
+ tool_prefix = f"{prefix}/"
70
+ self._tool_manager.import_tools(app._tool_manager, tool_prefix)
 
 
71
 
72
+ # Import resources and templates from the mounted app with + delimiter
73
+ resource_prefix = f"{prefix}+"
74
+ self._resource_manager.import_resources(app._resource_manager, resource_prefix)
75
+ self._resource_manager.import_templates(app._resource_manager, resource_prefix)
76
 
77
+ # Import prompts with / delimiter
78
+ prompt_prefix = f"{prefix}/"
79
+ self._prompt_manager.import_prompts(app._prompt_manager, prompt_prefix)
80
 
81
  logger.info(f"Mounted app with prefix '{prefix}'")
82
+ logger.debug(f"Imported tools with prefix '{tool_prefix}'")
83
+ logger.debug(f"Imported resources with prefix '{resource_prefix}'")
84
+ logger.debug(f"Imported templates with prefix '{resource_prefix}'")
85
+ logger.debug(f"Imported prompts with prefix '{prompt_prefix}'")
src/fastmcp/tools/tool_manager.py CHANGED
@@ -12,19 +12,22 @@ class ToolManager(mcp.server.fastmcp.tools.ToolManager):
12
  Adds ability to import tools from other managers with prefixed names.
13
  """
14
 
15
- def import_tools(self, tool_manager: "ToolManager", prefix: str) -> None:
 
 
16
  """
17
  Import all tools from another ToolManager with prefixed names.
18
 
19
  Args:
20
  tool_manager: Another ToolManager instance to import tools from
21
- prefix: Prefix to add to tool names. The resulting tool name will
22
- be in the format "{prefix}/{original_name}"
23
- For example, with prefix "weather" and tool "forecast",
 
24
  the imported tool would be available as "weather/forecast"
25
  """
26
  for name, tool in tool_manager._tools.items():
27
- prefixed_name = f"{prefix}/{name}"
28
 
29
  # Create a shallow copy of the tool with the prefixed name
30
  copied_tool = Tool.from_function(
 
12
  Adds ability to import tools from other managers with prefixed names.
13
  """
14
 
15
+ def import_tools(
16
+ self, tool_manager: "ToolManager", prefix: str | None = None
17
+ ) -> None:
18
  """
19
  Import all tools from another ToolManager with prefixed names.
20
 
21
  Args:
22
  tool_manager: Another ToolManager instance to import tools from
23
+ prefix: Prefix to add to tool names, including the delimiter.
24
+ The resulting tool name will be in the format "{prefix}{original_name}"
25
+ if prefix is provided, otherwise the original name is used.
26
+ For example, with prefix "weather/" and tool "forecast",
27
  the imported tool would be available as "weather/forecast"
28
  """
29
  for name, tool in tool_manager._tools.items():
30
+ prefixed_name = f"{prefix}{name}" if prefix else name
31
 
32
  # Create a shallow copy of the tool with the prefixed name
33
  copied_tool = Tool.from_function(
tests/prompts/test_prompt_manager.py CHANGED
@@ -44,7 +44,7 @@ def test_import_prompts():
44
  target_manager = PromptManager()
45
 
46
  # Import prompts from source to target
47
- prefix = "nlp"
48
  target_manager.import_prompts(source_manager, prefix)
49
 
50
  # Verify prompts were imported with prefixes
@@ -109,7 +109,7 @@ def test_import_prompts_with_duplicates():
109
  target_manager._prompts["common"] = target_prompt
110
 
111
  # Import prompts with prefix
112
- prefix = "external"
113
  target_manager.import_prompts(source_manager, prefix)
114
 
115
  # Verify both prompts exist in target manager
@@ -146,10 +146,10 @@ def test_import_prompts_with_nested_prefixes():
146
  first_manager._prompts["analyze"] = original_prompt
147
 
148
  # Import to second manager with prefix
149
- second_manager.import_prompts(first_manager, "text")
150
 
151
  # Import from second to third with another prefix
152
- third_manager.import_prompts(second_manager, "ai")
153
 
154
  # Verify the nested prefixing
155
  assert "text/analyze" in second_manager._prompts
 
44
  target_manager = PromptManager()
45
 
46
  # Import prompts from source to target
47
+ prefix = "nlp/"
48
  target_manager.import_prompts(source_manager, prefix)
49
 
50
  # Verify prompts were imported with prefixes
 
109
  target_manager._prompts["common"] = target_prompt
110
 
111
  # Import prompts with prefix
112
+ prefix = "external/"
113
  target_manager.import_prompts(source_manager, prefix)
114
 
115
  # Verify both prompts exist in target manager
 
146
  first_manager._prompts["analyze"] = original_prompt
147
 
148
  # Import to second manager with prefix
149
+ second_manager.import_prompts(first_manager, "text/")
150
 
151
  # Import from second to third with another prefix
152
+ third_manager.import_prompts(second_manager, "ai/")
153
 
154
  # Verify the nested prefixing
155
  assert "text/analyze" in second_manager._prompts
tests/resources/test_resource_manager.py CHANGED
@@ -39,7 +39,7 @@ def test_import_resources():
39
  target_manager = ResourceManager()
40
 
41
  # Import resources from source to target
42
- prefix = "data"
43
  target_manager.import_resources(source_manager, prefix)
44
 
45
  # Verify resources were imported with prefixes
@@ -126,7 +126,7 @@ def test_import_templates():
126
  target_manager = ResourceManager()
127
 
128
  # Import templates from source to target
129
- prefix = "shop"
130
  target_manager.import_templates(source_manager, prefix)
131
 
132
  # Verify templates were imported with prefixes
@@ -212,7 +212,7 @@ def test_import_multiple_resource_types():
212
  target_manager = ResourceManager()
213
 
214
  # Import both resources and templates
215
- prefix = "test"
216
  target_manager.import_resources(source_manager, prefix)
217
  target_manager.import_templates(source_manager, prefix)
218
 
 
39
  target_manager = ResourceManager()
40
 
41
  # Import resources from source to target
42
+ prefix = "data+"
43
  target_manager.import_resources(source_manager, prefix)
44
 
45
  # Verify resources were imported with prefixes
 
126
  target_manager = ResourceManager()
127
 
128
  # Import templates from source to target
129
+ prefix = "shop+"
130
  target_manager.import_templates(source_manager, prefix)
131
 
132
  # Verify templates were imported with prefixes
 
212
  target_manager = ResourceManager()
213
 
214
  # Import both resources and templates
215
+ prefix = "test+"
216
  target_manager.import_resources(source_manager, prefix)
217
  target_manager.import_templates(source_manager, prefix)
218
 
tests/tools/test_tool_manager.py CHANGED
@@ -23,7 +23,7 @@ def test_import_tools():
23
  target_manager = ToolManager()
24
 
25
  # Import tools from source to target
26
- prefix = "source"
27
  target_manager.import_tools(source_manager, prefix)
28
 
29
  # Verify tools were imported with prefixes
@@ -65,7 +65,7 @@ def test_tool_duplicate_behavior():
65
  ) # Pre-create with the prefixed name
66
 
67
  # Import tools from source to target
68
- target_manager.import_tools(source_manager, "source")
69
 
70
  # The original tool in the target manager is replaced by the imported one
71
  assert target_manager._tools["source/common_tool"].fn.__name__ == source_fn.__name__
@@ -89,8 +89,8 @@ def test_import_tools_with_multiple_prefixes():
89
 
90
  # Create target manager and import from both sources
91
  main_manager = ToolManager()
92
- main_manager.import_tools(weather_manager, "weather")
93
- main_manager.import_tools(news_manager, "news")
94
 
95
  # Verify tools were imported with correct prefixes
96
  assert "weather/forecast" in main_manager._tools
 
23
  target_manager = ToolManager()
24
 
25
  # Import tools from source to target
26
+ prefix = "source/"
27
  target_manager.import_tools(source_manager, prefix)
28
 
29
  # Verify tools were imported with prefixes
 
65
  ) # Pre-create with the prefixed name
66
 
67
  # Import tools from source to target
68
+ target_manager.import_tools(source_manager, "source/")
69
 
70
  # The original tool in the target manager is replaced by the imported one
71
  assert target_manager._tools["source/common_tool"].fn.__name__ == source_fn.__name__
 
89
 
90
  # Create target manager and import from both sources
91
  main_manager = ToolManager()
92
+ main_manager.import_tools(weather_manager, "weather/")
93
+ main_manager.import_tools(news_manager, "news/")
94
 
95
  # Verify tools were imported with correct prefixes
96
  assert "weather/forecast" in main_manager._tools