Jeremiah Lowin commited on
Commit
1978cbf
·
unverified ·
2 Parent(s): 4b951b92c6ad11

Merge pull request #177 from strawgate/McpRegisterable-example

Browse files
.github/workflows/run-tests.yml CHANGED
@@ -60,4 +60,3 @@ jobs:
60
 
61
  - name: Run tests
62
  run: uv run pytest -vv
63
- if: ${{ !(github.event.pull_request.head.repo.fork) }}
 
60
 
61
  - name: Run tests
62
  run: uv run pytest -vv
 
.pre-commit-config.yaml CHANGED
@@ -27,4 +27,3 @@ repos:
27
  hooks:
28
  - id: pyright-pretty
29
  files: ^src/|^tests/
30
- exclude: ^examples/
 
27
  hooks:
28
  - id: pyright-pretty
29
  files: ^src/|^tests/
 
src/contrib/README.md ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # FastMCP Contrib Modules
2
+
3
+ This directory holds community-contributed modules for FastMCP. These modules extend FastMCP's functionality but are not officially maintained by the core team.
4
+
5
+ **Guarantees:**
6
+ * Modules in `contrib` may have different testing requirements or stability guarantees compared to the core library.
7
+ * Changes to the core FastMCP library might break modules in `contrib` without explicit warnings in the main changelog.
8
+
9
+ Use these modules at your own discretion. Contributions are welcome, but please include tests and documentation.
src/contrib/mcp_mixin/README.md ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MCP Mixin
2
+
3
+ This module provides the `MCPMixin` base class and associated decorators (`@mcp_tool`, `@mcp_resource`, `@mcp_prompt`).
4
+
5
+ It allows developers to easily define classes whose methods can be registered as tools, resources, or prompts with a `FastMCP` server instance using the `register_all()`, `register_tools()`, `register_resources()`, or `register_prompts()` methods provided by the mixin.
6
+
7
+ ## Usage
8
+
9
+ Inherit from `MCPMixin` and use the decorators on the methods you want to register.
10
+
11
+ ```python
12
+ from fastmcp import FastMCP
13
+ from contrib.mcp_mixin.mcp_mixin import MCPMixin, mcp_tool, mcp_resource
14
+
15
+ class MyComponent(MCPMixin):
16
+ @mcp_tool(name="my_tool", description="Does something cool.")
17
+ def tool_method(self):
18
+ return "Tool executed!"
19
+
20
+ @mcp_resource(uri="component://data")
21
+ def resource_method(self):
22
+ return {"data": "some data"}
23
+
24
+ mcp_server = FastMCP()
25
+ component = MyComponent()
26
+
27
+ # Register all decorated methods with a prefix
28
+ # Useful if you will have multiple instantiated objects of the same class
29
+ # and want to avoid name collisions.
30
+ component.register_all(mcp_server, prefix="my_comp")
31
+
32
+ # Register without a prefix
33
+ # component.register_all(mcp_server)
34
+
35
+ # Now 'my_comp_my_tool' tool and 'my_comp+component://data' resource are registered (if prefix used)
36
+ # Or 'my_tool' and 'component://data' are registered (if no prefix used)
37
+ ```
38
+
39
+ The `prefix` argument in registration methods is optional. If omitted, methods are registered with their original decorated names/URIs. Individual separators (`tools_separator`, `resources_separator`, `prompts_separator`) can also be provided to `register_all` to change the separator for specific types.
src/contrib/mcp_mixin/example.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Sample code for FastMCP using MCPMixin."""
2
+
3
+ import asyncio
4
+
5
+ from contrib.mcp_mixin.mcp_mixin import (
6
+ MCPMixin,
7
+ mcp_prompt,
8
+ mcp_resource,
9
+ mcp_tool,
10
+ )
11
+ from fastmcp import FastMCP
12
+
13
+ mcp = FastMCP()
14
+
15
+
16
+ class Sample(MCPMixin):
17
+ def __init__(self, name):
18
+ self.name = name
19
+
20
+ @mcp_tool()
21
+ def first_tool(self):
22
+ """First tool description."""
23
+ return f"Executed tool {self.name}."
24
+
25
+ @mcp_resource(uri="test://test")
26
+ def first_resource(self):
27
+ """First resource description."""
28
+ return f"Executed resource {self.name}."
29
+
30
+ @mcp_prompt()
31
+ def first_prompt(self):
32
+ """First prompt description."""
33
+ return f"here's a prompt! {self.name}."
34
+
35
+
36
+ first_sample = Sample("First")
37
+ second_sample = Sample("Second")
38
+
39
+ first_sample.register_all(mcp_server=mcp, prefix="first")
40
+ second_sample.register_all(mcp_server=mcp, prefix="second")
41
+
42
+
43
+ async def list_components():
44
+ print("MCP Server running with registered components...")
45
+ print("Tools:", list(await mcp.get_tools()))
46
+ print("Resources:", list(await mcp.get_resources()))
47
+ print("Prompts:", list(await mcp.get_prompts()))
48
+
49
+
50
+ if __name__ == "__main__":
51
+ asyncio.run(list_components())
52
+ mcp.run()
src/contrib/mcp_mixin/mcp_mixin.py ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Provides a base mixin class and decorators for easy registration of class methods with FastMCP."""
2
+
3
+ from collections.abc import Callable
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ if TYPE_CHECKING:
7
+ from fastmcp.server import FastMCP
8
+
9
+ _MCP_REGISTRATION_TOOL_ATTR = "_mcp_tool_registration"
10
+ _MCP_REGISTRATION_RESOURCE_ATTR = "_mcp_resource_registration"
11
+ _MCP_REGISTRATION_PROMPT_ATTR = "_mcp_prompt_registration"
12
+
13
+ _DEFAULT_SEPARATOR_TOOL = "_"
14
+ _DEFAULT_SEPARATOR_RESOURCE = "+"
15
+ _DEFAULT_SEPARATOR_PROMPT = "_"
16
+
17
+
18
+ def mcp_tool(
19
+ name: str | None = None,
20
+ description: str | None = None,
21
+ tags: set[str] | None = None,
22
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
23
+ """Decorator to mark a method as an MCP tool for later registration."""
24
+
25
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
26
+ call_args = {
27
+ "name": name or func.__name__,
28
+ "description": description,
29
+ "tags": tags,
30
+ }
31
+ call_args = {k: v for k, v in call_args.items() if v is not None}
32
+ setattr(func, _MCP_REGISTRATION_TOOL_ATTR, call_args)
33
+ return func
34
+
35
+ return decorator
36
+
37
+
38
+ def mcp_resource(
39
+ uri: str,
40
+ *,
41
+ name: str | None = None,
42
+ description: str | None = None,
43
+ mime_type: str | None = None,
44
+ tags: set[str] | None = None,
45
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
46
+ """Decorator to mark a method as an MCP resource for later registration."""
47
+
48
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
49
+ call_args = {
50
+ "uri": uri,
51
+ "name": name or func.__name__,
52
+ "description": description,
53
+ "mime_type": mime_type,
54
+ "tags": tags,
55
+ }
56
+ call_args = {k: v for k, v in call_args.items() if v is not None}
57
+
58
+ setattr(func, _MCP_REGISTRATION_RESOURCE_ATTR, call_args)
59
+
60
+ return func
61
+
62
+ return decorator
63
+
64
+
65
+ def mcp_prompt(
66
+ name: str | None = None,
67
+ description: str | None = None,
68
+ tags: set[str] | None = None,
69
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
70
+ """Decorator to mark a method as an MCP prompt for later registration."""
71
+
72
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
73
+ call_args = {
74
+ "name": name or func.__name__,
75
+ "description": description,
76
+ "tags": tags,
77
+ }
78
+
79
+ call_args = {k: v for k, v in call_args.items() if v is not None}
80
+
81
+ setattr(func, _MCP_REGISTRATION_PROMPT_ATTR, call_args)
82
+ return func
83
+
84
+ return decorator
85
+
86
+
87
+ class MCPMixin:
88
+ """Base mixin class for objects that can register tools, resources, and prompts
89
+ with a FastMCP server instance using decorators.
90
+
91
+ This mixin provides methods like `register_all`, `register_tools`, etc.,
92
+ which iterate over the methods of the inheriting class, find methods
93
+ decorated with `@mcp_tool`, `@mcp_resource`, or `@mcp_prompt`, and
94
+ register them with the provided FastMCP server instance.
95
+ """
96
+
97
+ def _get_methods_to_register(self, registration_type: str):
98
+ """Retrieves all methods marked for a specific registration type."""
99
+ return [
100
+ (
101
+ getattr(self, method_name),
102
+ getattr(getattr(self, method_name), registration_type).copy(),
103
+ )
104
+ for method_name in dir(self)
105
+ if callable(getattr(self, method_name))
106
+ and hasattr(getattr(self, method_name), registration_type)
107
+ ]
108
+
109
+ def register_tools(
110
+ self,
111
+ mcp_server: "FastMCP",
112
+ prefix: str | None = None,
113
+ separator: str = _DEFAULT_SEPARATOR_TOOL,
114
+ ) -> None:
115
+ """Registers all methods marked with @mcp_tool with the FastMCP server.
116
+
117
+ Args:
118
+ mcp_server: The FastMCP server instance to register tools with.
119
+ prefix: Optional prefix to prepend to tool names. If provided, the
120
+ final name will be f"{prefix}{separator}{original_name}".
121
+ separator: The separator string used between prefix and original name.
122
+ Defaults to '_'.
123
+ """
124
+ for method, registration_info in self._get_methods_to_register(
125
+ _MCP_REGISTRATION_TOOL_ATTR
126
+ ):
127
+ if prefix:
128
+ registration_info["name"] = (
129
+ f"{prefix}{separator}{registration_info['name']}"
130
+ )
131
+ mcp_server.add_tool(fn=method, **registration_info)
132
+
133
+ def register_resources(
134
+ self,
135
+ mcp_server: "FastMCP",
136
+ prefix: str | None = None,
137
+ separator: str = _DEFAULT_SEPARATOR_RESOURCE,
138
+ ) -> None:
139
+ """Registers all methods marked with @mcp_resource with the FastMCP server.
140
+
141
+ Args:
142
+ mcp_server: The FastMCP server instance to register resources with.
143
+ prefix: Optional prefix to prepend to resource names and URIs. If provided,
144
+ the final name will be f"{prefix}{separator}{original_name}" and the
145
+ final URI will be f"{prefix}{separator}{original_uri}".
146
+ separator: The separator string used between prefix and original name/URI.
147
+ Defaults to '+'.
148
+ """
149
+ for method, registration_info in self._get_methods_to_register(
150
+ _MCP_REGISTRATION_RESOURCE_ATTR
151
+ ):
152
+ if prefix:
153
+ registration_info["name"] = (
154
+ f"{prefix}{separator}{registration_info['name']}"
155
+ )
156
+ registration_info["uri"] = (
157
+ f"{prefix}{separator}{registration_info['uri']}"
158
+ )
159
+ mcp_server.add_resource_fn(fn=method, **registration_info)
160
+
161
+ def register_prompts(
162
+ self,
163
+ mcp_server: "FastMCP",
164
+ prefix: str | None = None,
165
+ separator: str = _DEFAULT_SEPARATOR_PROMPT,
166
+ ) -> None:
167
+ """Registers all methods marked with @mcp_prompt with the FastMCP server.
168
+
169
+ Args:
170
+ mcp_server: The FastMCP server instance to register prompts with.
171
+ prefix: Optional prefix to prepend to prompt names. If provided, the
172
+ final name will be f"{prefix}{separator}{original_name}".
173
+ separator: The separator string used between prefix and original name.
174
+ Defaults to '_'.
175
+ """
176
+ for method, registration_info in self._get_methods_to_register(
177
+ _MCP_REGISTRATION_PROMPT_ATTR
178
+ ):
179
+ if prefix:
180
+ registration_info["name"] = (
181
+ f"{prefix}{separator}{registration_info['name']}"
182
+ )
183
+ mcp_server.add_prompt(fn=method, **registration_info)
184
+
185
+ def register_all(
186
+ self,
187
+ mcp_server: "FastMCP",
188
+ prefix: str | None = None,
189
+ tool_separator: str = _DEFAULT_SEPARATOR_TOOL,
190
+ resource_separator: str = _DEFAULT_SEPARATOR_RESOURCE,
191
+ prompt_separator: str = _DEFAULT_SEPARATOR_PROMPT,
192
+ ) -> None:
193
+ """Registers all marked tools, resources, and prompts with the server.
194
+
195
+ This method calls `register_tools`, `register_resources`, and `register_prompts`
196
+ internally, passing the provided prefix and separators.
197
+
198
+ Args:
199
+ mcp_server: The FastMCP server instance to register with.
200
+ prefix: Optional prefix applied to all registered items unless overridden
201
+ by a specific separator argument.
202
+ tool_separator: Separator for tool names (defaults to '_').
203
+ resource_separator: Separator for resource names/URIs (defaults to '+').
204
+ prompt_separator: Separator for prompt names (defaults to '_').
205
+ """
206
+ self.register_tools(mcp_server, prefix=prefix, separator=tool_separator)
207
+ self.register_resources(mcp_server, prefix=prefix, separator=resource_separator)
208
+ self.register_prompts(mcp_server, prefix=prefix, separator=prompt_separator)
tests/contrib/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # This file makes Python treat the directory as a package.
tests/contrib/test_mcp_mixin.py ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for the MCPMixin class."""
2
+
3
+ import pytest
4
+
5
+ from contrib.mcp_mixin.mcp_mixin import (
6
+ _DEFAULT_SEPARATOR_PROMPT,
7
+ _DEFAULT_SEPARATOR_RESOURCE,
8
+ _DEFAULT_SEPARATOR_TOOL,
9
+ MCPMixin,
10
+ mcp_prompt,
11
+ mcp_resource,
12
+ mcp_tool,
13
+ )
14
+ from fastmcp import FastMCP
15
+
16
+
17
+ class TestMCPMixin:
18
+ """Test suite for MCPMixin functionality."""
19
+
20
+ def test_initialization(self):
21
+ """Test that a class inheriting MCPMixin can be initialized."""
22
+
23
+ class MyMixin(MCPMixin):
24
+ pass
25
+
26
+ instance = MyMixin()
27
+ assert instance is not None
28
+
29
+ # --- Tool Registration Tests ---
30
+ @pytest.mark.parametrize(
31
+ "prefix, separator, expected_key, unexpected_key",
32
+ [
33
+ (
34
+ None,
35
+ _DEFAULT_SEPARATOR_TOOL,
36
+ "sample_tool",
37
+ f"None{_DEFAULT_SEPARATOR_TOOL}sample_tool",
38
+ ),
39
+ (
40
+ "pref",
41
+ _DEFAULT_SEPARATOR_TOOL,
42
+ f"pref{_DEFAULT_SEPARATOR_TOOL}sample_tool",
43
+ "sample_tool",
44
+ ),
45
+ (
46
+ "pref",
47
+ "-",
48
+ "pref-sample_tool",
49
+ f"pref{_DEFAULT_SEPARATOR_TOOL}sample_tool",
50
+ ),
51
+ ],
52
+ ids=["No prefix", "Default separator", "Custom separator"],
53
+ )
54
+ async def test_tool_registration(
55
+ self, prefix, separator, expected_key, unexpected_key
56
+ ):
57
+ """Test tool registration with prefix and separator variations."""
58
+ mcp = FastMCP()
59
+
60
+ class MyToolMixin(MCPMixin):
61
+ @mcp_tool()
62
+ def sample_tool(self):
63
+ pass
64
+
65
+ instance = MyToolMixin()
66
+ instance.register_tools(mcp, prefix=prefix, separator=separator)
67
+
68
+ registered_tools = await mcp.get_tools()
69
+ assert expected_key in registered_tools
70
+ assert unexpected_key not in registered_tools
71
+
72
+ @pytest.mark.parametrize(
73
+ "prefix, separator, expected_uri_key, expected_name, unexpected_uri_key",
74
+ [
75
+ (
76
+ None,
77
+ _DEFAULT_SEPARATOR_RESOURCE,
78
+ "test://resource",
79
+ "sample_resource",
80
+ f"None{_DEFAULT_SEPARATOR_RESOURCE}test://resource",
81
+ ),
82
+ (
83
+ "pref",
84
+ _DEFAULT_SEPARATOR_RESOURCE,
85
+ f"pref{_DEFAULT_SEPARATOR_RESOURCE}test://resource",
86
+ f"pref{_DEFAULT_SEPARATOR_RESOURCE}sample_resource",
87
+ "test://resource",
88
+ ),
89
+ (
90
+ "pref",
91
+ "fff",
92
+ "prefffftest://resource",
93
+ "preffffsample_resource",
94
+ f"pref{_DEFAULT_SEPARATOR_RESOURCE}test://resource",
95
+ ),
96
+ ],
97
+ ids=["No prefix", "Default separator", "Custom separator"],
98
+ )
99
+ async def test_resource_registration(
100
+ self, prefix, separator, expected_uri_key, expected_name, unexpected_uri_key
101
+ ):
102
+ """Test resource registration with prefix and separator variations."""
103
+ mcp = FastMCP()
104
+
105
+ class MyResourceMixin(MCPMixin):
106
+ @mcp_resource(uri="test://resource")
107
+ def sample_resource(self):
108
+ pass
109
+
110
+ instance = MyResourceMixin()
111
+ instance.register_resources(mcp, prefix=prefix, separator=separator)
112
+
113
+ registered_resources = await mcp.get_resources()
114
+ assert expected_uri_key in registered_resources
115
+ assert registered_resources[expected_uri_key].name == expected_name
116
+ assert unexpected_uri_key not in registered_resources
117
+
118
+ @pytest.mark.parametrize(
119
+ "prefix, separator, expected_name, unexpected_name",
120
+ [
121
+ (
122
+ None,
123
+ _DEFAULT_SEPARATOR_PROMPT,
124
+ "sample_prompt",
125
+ f"None{_DEFAULT_SEPARATOR_PROMPT}sample_prompt",
126
+ ),
127
+ (
128
+ "pref",
129
+ _DEFAULT_SEPARATOR_PROMPT,
130
+ f"pref{_DEFAULT_SEPARATOR_PROMPT}sample_prompt",
131
+ "sample_prompt",
132
+ ),
133
+ (
134
+ "pref",
135
+ ":",
136
+ "pref:sample_prompt",
137
+ f"pref{_DEFAULT_SEPARATOR_PROMPT}sample_prompt",
138
+ ),
139
+ ],
140
+ ids=["No prefix", "Default separator", "Custom separator"],
141
+ )
142
+ async def test_prompt_registration(
143
+ self, prefix, separator, expected_name, unexpected_name
144
+ ):
145
+ """Test prompt registration with prefix and separator variations."""
146
+ mcp = FastMCP()
147
+
148
+ class MyPromptMixin(MCPMixin):
149
+ @mcp_prompt()
150
+ def sample_prompt(self):
151
+ pass
152
+
153
+ instance = MyPromptMixin()
154
+ instance.register_prompts(mcp, prefix=prefix, separator=separator)
155
+
156
+ prompts = await mcp.get_prompts()
157
+ assert expected_name in prompts
158
+ assert unexpected_name not in prompts
159
+
160
+ async def test_register_all_no_prefix(self):
161
+ """Test register_all method registers all types without a prefix."""
162
+ mcp = FastMCP()
163
+
164
+ class MyFullMixin(MCPMixin):
165
+ @mcp_tool()
166
+ def tool_all(self):
167
+ pass
168
+
169
+ @mcp_resource(uri="res://all")
170
+ def resource_all(self):
171
+ pass
172
+
173
+ @mcp_prompt()
174
+ def prompt_all(self):
175
+ pass
176
+
177
+ instance = MyFullMixin()
178
+ instance.register_all(mcp)
179
+
180
+ tools = await mcp.get_tools()
181
+ resources = await mcp.get_resources()
182
+ prompts = await mcp.get_prompts()
183
+
184
+ assert "tool_all" in tools
185
+ assert "res://all" in resources
186
+ assert "prompt_all" in prompts
187
+
188
+ async def test_register_all_with_prefix_default_separators(self):
189
+ """Test register_all method registers all types with a prefix and default separators."""
190
+ mcp = FastMCP()
191
+
192
+ class MyFullMixinPrefixed(MCPMixin):
193
+ @mcp_tool()
194
+ def tool_all_p(self):
195
+ pass
196
+
197
+ @mcp_resource(uri="res://all_p")
198
+ def resource_all_p(self):
199
+ pass
200
+
201
+ @mcp_prompt()
202
+ def prompt_all_p(self):
203
+ pass
204
+
205
+ instance = MyFullMixinPrefixed()
206
+ instance.register_all(mcp, prefix="all")
207
+
208
+ tools = await mcp.get_tools()
209
+ resources = await mcp.get_resources()
210
+ prompts = await mcp.get_prompts()
211
+
212
+ assert f"all{_DEFAULT_SEPARATOR_TOOL}tool_all_p" in tools
213
+ assert f"all{_DEFAULT_SEPARATOR_RESOURCE}res://all_p" in resources
214
+ assert f"all{_DEFAULT_SEPARATOR_PROMPT}prompt_all_p" in prompts
215
+
216
+ async def test_register_all_with_prefix_custom_separators(self):
217
+ """Test register_all method registers all types with a prefix and custom separators."""
218
+ mcp = FastMCP()
219
+
220
+ class MyFullMixinCustomSep(MCPMixin):
221
+ @mcp_tool()
222
+ def tool_cust(self):
223
+ pass
224
+
225
+ @mcp_resource(uri="res://cust")
226
+ def resource_cust(self):
227
+ pass
228
+
229
+ @mcp_prompt()
230
+ def prompt_cust(self):
231
+ pass
232
+
233
+ instance = MyFullMixinCustomSep()
234
+ instance.register_all(
235
+ mcp,
236
+ prefix="cust",
237
+ tool_separator="-",
238
+ resource_separator="::",
239
+ prompt_separator=".",
240
+ )
241
+
242
+ tools = await mcp.get_tools()
243
+ resources = await mcp.get_resources()
244
+ prompts = await mcp.get_prompts()
245
+
246
+ assert "cust-tool_cust" in tools
247
+ assert "cust::res://cust" in resources
248
+ assert "cust.prompt_cust" in prompts
249
+
250
+ # Check default separators weren't used
251
+ assert f"cust{_DEFAULT_SEPARATOR_TOOL}tool_cust" not in tools
252
+ assert f"cust{_DEFAULT_SEPARATOR_RESOURCE}res://cust" not in resources
253
+ assert f"cust{_DEFAULT_SEPARATOR_PROMPT}prompt_cust" not in prompts