Jeremiah Lowin commited on
Commit
8d16d26
·
unverified ·
1 Parent(s): 1076983

Ensure resource + template names are properly prefixed when importing/mounting (#1423)

Browse files
docs/servers/composition.mdx CHANGED
@@ -83,10 +83,12 @@ When you call `await main_mcp.import_server(subserver, prefix={whatever})`:
83
 
84
  1. **Tools**: All tools from `subserver` are added to `main_mcp` with names prefixed using `{prefix}_`.
85
  - `subserver.tool(name="my_tool")` becomes `main_mcp.tool(name="{prefix}_my_tool")`.
86
- 2. **Resources**: All resources are added with URIs prefixed in the format `protocol://{prefix}/path`.
87
- - `subserver.resource(uri="data://info")` becomes `main_mcp.resource(uri="data://{prefix}/info")`.
 
88
  3. **Resource Templates**: Templates are prefixed similarly to resources.
89
- - `subserver.resource(uri="data://{id}")` becomes `main_mcp.resource(uri="data://{prefix}/{id}")`.
 
90
  4. **Prompts**: All prompts are added with names prefixed using `{prefix}_`.
91
  - `subserver.prompt(name="my_prompt")` becomes `main_mcp.prompt(name="{prefix}_my_prompt")`.
92
 
@@ -196,7 +198,7 @@ When mounting is configured:
196
  3. **Prefixed Access**: The parent server uses prefixes to route requests to the mounted server.
197
  4. **Delegation**: Requests for components matching the prefix are delegated to the mounted server at runtime.
198
 
199
- The same prefixing rules apply as with `import_server` for naming tools, resources, templates, and prompts.
200
 
201
  <Tip>
202
  The `prefix` parameter is optional. If omitted, components are mounted without modification.
 
83
 
84
  1. **Tools**: All tools from `subserver` are added to `main_mcp` with names prefixed using `{prefix}_`.
85
  - `subserver.tool(name="my_tool")` becomes `main_mcp.tool(name="{prefix}_my_tool")`.
86
+ 2. **Resources**: All resources are added with both URIs and names prefixed.
87
+ - URI: `subserver.resource(uri="data://info")` becomes `main_mcp.resource(uri="data://{prefix}/info")`.
88
+ - Name: `resource.name` becomes `"{prefix}_{resource.name}"`.
89
  3. **Resource Templates**: Templates are prefixed similarly to resources.
90
+ - URI: `subserver.resource(uri="data://{id}")` becomes `main_mcp.resource(uri="data://{prefix}/{id}")`.
91
+ - Name: `template.name` becomes `"{prefix}_{template.name}"`.
92
  4. **Prompts**: All prompts are added with names prefixed using `{prefix}_`.
93
  - `subserver.prompt(name="my_prompt")` becomes `main_mcp.prompt(name="{prefix}_my_prompt")`.
94
 
 
198
  3. **Prefixed Access**: The parent server uses prefixes to route requests to the mounted server.
199
  4. **Delegation**: Requests for components matching the prefix are delegated to the mounted server at runtime.
200
 
201
+ The same prefixing rules apply as with `import_server` for naming tools, resources, templates, and prompts. This includes prefixing both the URIs/keys and the names of resources and templates for better identification in multi-server configurations.
202
 
203
  <Tip>
204
  The `prefix` parameter is optional. If omitted, components are mounted without modification.
src/fastmcp/prompts/prompt_manager.py CHANGED
@@ -69,8 +69,8 @@ class PromptManager:
69
  child_dict = {p.key: p for p in child_results}
70
  if mounted.prefix:
71
  for prompt in child_dict.values():
72
- prefixed_prompt = prompt.with_key(
73
- f"{mounted.prefix}_{prompt.key}"
74
  )
75
  all_prompts[prefixed_prompt.key] = prefixed_prompt
76
  else:
 
69
  child_dict = {p.key: p for p in child_results}
70
  if mounted.prefix:
71
  for prompt in child_dict.values():
72
+ prefixed_prompt = prompt.model_copy(
73
+ key=f"{mounted.prefix}_{prompt.key}"
74
  )
75
  all_prompts[prefixed_prompt.key] = prefixed_prompt
76
  else:
src/fastmcp/resources/resource_manager.py CHANGED
@@ -101,8 +101,11 @@ class ResourceManager:
101
  prefixed_uri = add_resource_prefix(
102
  uri, mounted.prefix, mounted.resource_prefix_format
103
  )
104
- # Create a copy of the resource with the prefixed key
105
- prefixed_resource = resource.with_key(prefixed_uri)
 
 
 
106
  all_resources[prefixed_uri] = prefixed_resource
107
  else:
108
  all_resources.update(child_resources)
@@ -149,8 +152,11 @@ class ResourceManager:
149
  prefixed_uri_template = add_resource_prefix(
150
  uri_template, mounted.prefix, mounted.resource_prefix_format
151
  )
152
- # Create a copy of the template with the prefixed key
153
- prefixed_template = template.with_key(prefixed_uri_template)
 
 
 
154
  all_templates[prefixed_uri_template] = prefixed_template
155
  else:
156
  all_templates.update(child_dict)
@@ -273,7 +279,7 @@ class ResourceManager:
273
  Args:
274
  resource: A Resource instance to add. The resource's .key attribute
275
  will be used as the storage key. To overwrite it, call
276
- Resource.with_key() before calling this method.
277
  """
278
  existing = self._resources.get(resource.key)
279
  if existing:
@@ -322,7 +328,7 @@ class ResourceManager:
322
  Args:
323
  template: A ResourceTemplate instance to add. The template's .key attribute
324
  will be used as the storage key. To overwrite it, call
325
- ResourceTemplate.with_key() before calling this method.
326
 
327
  Returns:
328
  The added template. If a template with the same URI already exists,
 
101
  prefixed_uri = add_resource_prefix(
102
  uri, mounted.prefix, mounted.resource_prefix_format
103
  )
104
+ # Create a copy of the resource with the prefixed key and name
105
+ prefixed_resource = resource.model_copy(
106
+ update={"name": f"{mounted.prefix}_{resource.name}"},
107
+ key=prefixed_uri,
108
+ )
109
  all_resources[prefixed_uri] = prefixed_resource
110
  else:
111
  all_resources.update(child_resources)
 
152
  prefixed_uri_template = add_resource_prefix(
153
  uri_template, mounted.prefix, mounted.resource_prefix_format
154
  )
155
+ # Create a copy of the template with the prefixed key and name
156
+ prefixed_template = template.model_copy(
157
+ update={"name": f"{mounted.prefix}_{template.name}"},
158
+ key=prefixed_uri_template,
159
+ )
160
  all_templates[prefixed_uri_template] = prefixed_template
161
  else:
162
  all_templates.update(child_dict)
 
279
  Args:
280
  resource: A Resource instance to add. The resource's .key attribute
281
  will be used as the storage key. To overwrite it, call
282
+ Resource.model_copy(key=new_key) before calling this method.
283
  """
284
  existing = self._resources.get(resource.key)
285
  if existing:
 
328
  Args:
329
  template: A ResourceTemplate instance to add. The template's .key attribute
330
  will be used as the storage key. To overwrite it, call
331
+ ResourceTemplate.model_copy(key=new_key) before calling this method.
332
 
333
  Returns:
334
  The added template. If a template with the same URI already exists,
src/fastmcp/server/server.py CHANGED
@@ -1891,7 +1891,7 @@ class FastMCP(Generic[LifespanResultT]):
1891
  # Import tools from the server
1892
  for key, tool in (await server.get_tools()).items():
1893
  if prefix:
1894
- tool = tool.with_key(f"{prefix}_{key}")
1895
  self._tool_manager.add_tool(tool)
1896
 
1897
  # Import resources and templates from the server
@@ -1900,7 +1900,9 @@ class FastMCP(Generic[LifespanResultT]):
1900
  resource_key = add_resource_prefix(
1901
  key, prefix, self.resource_prefix_format
1902
  )
1903
- resource = resource.with_key(resource_key)
 
 
1904
  self._resource_manager.add_resource(resource)
1905
 
1906
  for key, template in (await server.get_resource_templates()).items():
@@ -1908,13 +1910,15 @@ class FastMCP(Generic[LifespanResultT]):
1908
  template_key = add_resource_prefix(
1909
  key, prefix, self.resource_prefix_format
1910
  )
1911
- template = template.with_key(template_key)
 
 
1912
  self._resource_manager.add_template(template)
1913
 
1914
  # Import prompts from the server
1915
  for key, prompt in (await server.get_prompts()).items():
1916
  if prefix:
1917
- prompt = prompt.with_key(f"{prefix}_{key}")
1918
  self._prompt_manager.add_prompt(prompt)
1919
 
1920
  if prefix:
 
1891
  # Import tools from the server
1892
  for key, tool in (await server.get_tools()).items():
1893
  if prefix:
1894
+ tool = tool.model_copy(key=f"{prefix}_{key}")
1895
  self._tool_manager.add_tool(tool)
1896
 
1897
  # Import resources and templates from the server
 
1900
  resource_key = add_resource_prefix(
1901
  key, prefix, self.resource_prefix_format
1902
  )
1903
+ resource = resource.model_copy(
1904
+ update={"name": f"{prefix}_{resource.name}"}, key=resource_key
1905
+ )
1906
  self._resource_manager.add_resource(resource)
1907
 
1908
  for key, template in (await server.get_resource_templates()).items():
 
1910
  template_key = add_resource_prefix(
1911
  key, prefix, self.resource_prefix_format
1912
  )
1913
+ template = template.model_copy(
1914
+ update={"name": f"{prefix}_{template.name}"}, key=template_key
1915
+ )
1916
  self._resource_manager.add_template(template)
1917
 
1918
  # Import prompts from the server
1919
  for key, prompt in (await server.get_prompts()).items():
1920
  if prefix:
1921
+ prompt = prompt.model_copy(key=f"{prefix}_{key}")
1922
  self._prompt_manager.add_prompt(prompt)
1923
 
1924
  if prefix:
src/fastmcp/tools/tool_manager.py CHANGED
@@ -75,7 +75,9 @@ class ToolManager:
75
  child_dict = {t.key: t for t in child_results}
76
  if mounted.prefix:
77
  for tool in child_dict.values():
78
- prefixed_tool = tool.with_key(f"{mounted.prefix}_{tool.key}")
 
 
79
  all_tools[prefixed_tool.key] = prefixed_tool
80
  else:
81
  all_tools.update(child_dict)
 
75
  child_dict = {t.key: t for t in child_results}
76
  if mounted.prefix:
77
  for tool in child_dict.values():
78
+ prefixed_tool = tool.model_copy(
79
+ key=f"{mounted.prefix}_{tool.key}"
80
+ )
81
  all_tools[prefixed_tool.key] = prefixed_tool
82
  else:
83
  all_tools.update(child_dict)
src/fastmcp/utilities/components.py CHANGED
@@ -91,12 +91,27 @@ class FastMCPComponent(FastMCPBaseModel):
91
 
92
  return meta or None
93
 
94
- def with_key(self, key: str) -> Self:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  # `model_copy` has an `update` parameter but it doesn't work for certain private attributes
96
  # https://github.com/pydantic/pydantic/issues/12116
97
- # So we manually set the private attribute here instead
98
- copy = self.model_copy()
99
- copy._key = key
 
100
  return copy
101
 
102
  def __eq__(self, other: object) -> bool:
 
91
 
92
  return meta or None
93
 
94
+ def model_copy(
95
+ self,
96
+ *,
97
+ update: dict[str, Any] | None = None,
98
+ deep: bool = False,
99
+ key: str | None = None,
100
+ ) -> Self:
101
+ """
102
+ Create a copy of the component.
103
+
104
+ Args:
105
+ update: A dictionary of fields to update.
106
+ deep: Whether to deep copy the component.
107
+ key: The key to use for the copy.
108
+ """
109
  # `model_copy` has an `update` parameter but it doesn't work for certain private attributes
110
  # https://github.com/pydantic/pydantic/issues/12116
111
+ # So we manually set the private attribute here instead, such as _key
112
+ copy = super().model_copy(update=update, deep=deep)
113
+ if key is not None:
114
+ copy._key = key
115
  return copy
116
 
117
  def __eq__(self, other: object) -> bool:
tests/resources/test_resource_manager.py CHANGED
@@ -469,8 +469,8 @@ class TestCustomResourceKeys:
469
  fn=get_data,
470
  )
471
 
472
- # Use with_key to create a new resource with the custom key
473
- resource_with_custom_key = resource.with_key(custom_key)
474
  manager.add_resource(resource_with_custom_key)
475
 
476
  # Resource should be accessible via custom key
@@ -496,8 +496,8 @@ class TestCustomResourceKeys:
496
  name="test_template",
497
  )
498
 
499
- # Use with_key to create a new template with the custom key
500
- template_with_custom_key = template.with_key(custom_key)
501
  manager.add_template(template_with_custom_key)
502
 
503
  # Template should be accessible via custom key
@@ -523,8 +523,8 @@ class TestCustomResourceKeys:
523
  fn=get_data,
524
  )
525
 
526
- # Use with_key to create a new resource with the custom key
527
- resource_with_custom_key = resource.with_key(custom_key)
528
  manager.add_resource(resource_with_custom_key)
529
 
530
  # Should be retrievable by the custom key
@@ -552,8 +552,8 @@ class TestCustomResourceKeys:
552
  name="custom_greeter",
553
  )
554
 
555
- # Use with_key to create a new template with the custom key
556
- template_with_custom_key = template.with_key(custom_key)
557
  manager.add_template(template_with_custom_key)
558
 
559
  # Using a URI that matches the custom key pattern
 
469
  fn=get_data,
470
  )
471
 
472
+ # Use model_copy to create a new resource with the custom key
473
+ resource_with_custom_key = resource.model_copy(key=custom_key)
474
  manager.add_resource(resource_with_custom_key)
475
 
476
  # Resource should be accessible via custom key
 
496
  name="test_template",
497
  )
498
 
499
+ # Use model_copy to create a new template with the custom key
500
+ template_with_custom_key = template.model_copy(key=custom_key)
501
  manager.add_template(template_with_custom_key)
502
 
503
  # Template should be accessible via custom key
 
523
  fn=get_data,
524
  )
525
 
526
+ # Use model_copy to create a new resource with the custom key
527
+ resource_with_custom_key = resource.model_copy(key=custom_key)
528
  manager.add_resource(resource_with_custom_key)
529
 
530
  # Should be retrievable by the custom key
 
552
  name="custom_greeter",
553
  )
554
 
555
+ # Use model_copy to create a new template with the custom key
556
+ template_with_custom_key = template.model_copy(key=custom_key)
557
  manager.add_template(template_with_custom_key)
558
 
559
  # Using a URI that matches the custom key pattern
tests/server/test_import_server.py CHANGED
@@ -605,3 +605,41 @@ async def test_import_conflict_resolution_with_prefix():
605
 
606
  result = await client.call_tool("api_shared_tool", {})
607
  assert result.data == "Second app tool"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
605
 
606
  result = await client.call_tool("api_shared_tool", {})
607
  assert result.data == "Second app tool"
608
+
609
+
610
+ async def test_import_server_resource_name_prefixing():
611
+ """Test that resource names are prefixed when using import_server."""
612
+ # Create a sub-server with a resource
613
+ sub_server = FastMCP("SubServer")
614
+
615
+ @sub_server.resource("resource://test_resource")
616
+ def test_resource() -> str:
617
+ return "Test content"
618
+
619
+ # Create main server and import sub-server with prefix
620
+ main_server = FastMCP("MainServer")
621
+ await main_server.import_server(sub_server, prefix="imported")
622
+
623
+ # Get resources and verify name prefixing
624
+ resources = await main_server.get_resources()
625
+ resource = resources["resource://imported/test_resource"]
626
+ assert resource.name == "imported_test_resource"
627
+
628
+
629
+ async def test_import_server_resource_template_name_prefixing():
630
+ """Test that resource template names are prefixed when using import_server."""
631
+ # Create a sub-server with a resource template
632
+ sub_server = FastMCP("SubServer")
633
+
634
+ @sub_server.resource("resource://data/{item_id}")
635
+ def data_template(item_id: str) -> str:
636
+ return f"Data for {item_id}"
637
+
638
+ # Create main server and import sub-server with prefix
639
+ main_server = FastMCP("MainServer")
640
+ await main_server.import_server(sub_server, prefix="imported")
641
+
642
+ # Get resource templates and verify name prefixing
643
+ templates = await main_server.get_resource_templates()
644
+ template = templates["resource://imported/data/{item_id}"]
645
+ assert template.name == "imported_data_template"
tests/server/test_mount.py CHANGED
@@ -967,3 +967,55 @@ class TestAsProxyKwarg:
967
  # in the present implementation the sub server will be invoked 3 times
968
  # to call its tool
969
  assert lifespan_check.count("start") >= 2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
967
  # in the present implementation the sub server will be invoked 3 times
968
  # to call its tool
969
  assert lifespan_check.count("start") >= 2
970
+
971
+
972
+ class TestResourceNamePrefixing:
973
+ """Test that resource and resource template names get prefixed when mounted."""
974
+
975
+ async def test_resource_name_prefixing(self):
976
+ """Test that resource names are prefixed when mounted."""
977
+
978
+ # Create a sub-app with a resource
979
+ sub_app = FastMCP("SubApp")
980
+
981
+ @sub_app.resource("resource://my_resource")
982
+ def my_resource() -> str:
983
+ return "Resource content"
984
+
985
+ # Create main app and mount sub-app with prefix
986
+ main_app = FastMCP("MainApp")
987
+ main_app.mount(sub_app, "prefix")
988
+
989
+ # Get resources from main app
990
+ resources = await main_app.get_resources()
991
+
992
+ # Should have prefixed key (using path format: resource://prefix/resource_name)
993
+ assert "resource://prefix/my_resource" in resources
994
+
995
+ # The resource name should also be prefixed
996
+ resource = resources["resource://prefix/my_resource"]
997
+ assert resource.name == "prefix_my_resource"
998
+
999
+ async def test_resource_template_name_prefixing(self):
1000
+ """Test that resource template names are prefixed when mounted."""
1001
+
1002
+ # Create a sub-app with a resource template
1003
+ sub_app = FastMCP("SubApp")
1004
+
1005
+ @sub_app.resource("resource://user/{user_id}")
1006
+ def user_template(user_id: str) -> str:
1007
+ return f"User {user_id} data"
1008
+
1009
+ # Create main app and mount sub-app with prefix
1010
+ main_app = FastMCP("MainApp")
1011
+ main_app.mount(sub_app, "prefix")
1012
+
1013
+ # Get resource templates from main app
1014
+ templates = await main_app.get_resource_templates()
1015
+
1016
+ # Should have prefixed key (using path format: resource://prefix/template_uri)
1017
+ assert "resource://prefix/user/{user_id}" in templates
1018
+
1019
+ # The template name should also be prefixed
1020
+ template = templates["resource://prefix/user/{user_id}"]
1021
+ assert template.name == "prefix_user_template"
tests/tools/test_tool_manager.py CHANGED
@@ -834,8 +834,8 @@ class TestCustomToolNames:
834
  # Create a tool with a specific name
835
  tool = Tool.from_function(fn, name="my_tool")
836
  manager = ToolManager()
837
- # Use with_key to create a new tool with the custom key
838
- tool_with_custom_key = tool.with_key("proxy_tool")
839
  manager.add_tool(tool_with_custom_key)
840
  # The tool is accessible under the key
841
  stored = await manager.get_tool("proxy_tool")
 
834
  # Create a tool with a specific name
835
  tool = Tool.from_function(fn, name="my_tool")
836
  manager = ToolManager()
837
+ # Use model_copy to create a new tool with the custom key
838
+ tool_with_custom_key = tool.model_copy(key="proxy_tool")
839
  manager.add_tool(tool_with_custom_key)
840
  # The tool is accessible under the key
841
  stored = await manager.get_tool("proxy_tool")
tests/utilities/test_components.py CHANGED
@@ -123,9 +123,9 @@ class TestFastMCPComponent:
123
  result = component.get_meta(include_fastmcp_meta=False)
124
  assert result is None
125
 
126
- def test_with_key_creates_copy_with_new_key(self, basic_component):
127
- """Test that with_key creates a copy with a new key."""
128
- new_component = basic_component.with_key("new_key")
129
  assert new_component.key == "new_key"
130
  assert new_component.name == basic_component.name
131
  assert new_component is not basic_component # Should be a copy
@@ -290,8 +290,8 @@ class TestMirroredComponent:
290
  # Test key property
291
  assert mirrored_component.key == "mirrored"
292
 
293
- # Test with_key
294
- with_key = mirrored_component.with_key("new_key")
295
  assert with_key.key == "new_key"
296
 
297
  # Test get_meta
@@ -353,8 +353,8 @@ class TestEdgeCasesAndIntegration:
353
  component = FastMCPComponent(name="test", meta=complex_meta)
354
  assert component.meta == complex_meta
355
 
356
- def test_with_key_preserves_all_attributes(self):
357
- """Test that with_key preserves all component attributes."""
358
  component = FastMCPComponent(
359
  name="test",
360
  title="Title",
@@ -363,7 +363,7 @@ class TestEdgeCasesAndIntegration:
363
  meta={"key": "value"},
364
  enabled=False,
365
  )
366
- new_component = component.with_key("new_key")
367
 
368
  assert new_component.name == component.name
369
  assert new_component.title == component.title
@@ -389,3 +389,50 @@ class TestEdgeCasesAndIntegration:
389
  assert original.name == "original"
390
  assert copy1.name == "copy1"
391
  assert copy2.name == "copy2"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  result = component.get_meta(include_fastmcp_meta=False)
124
  assert result is None
125
 
126
+ def test_model_copy_creates_copy_with_new_key(self, basic_component):
127
+ """Test that model_copy with key creates a copy with a new key."""
128
+ new_component = basic_component.model_copy(key="new_key")
129
  assert new_component.key == "new_key"
130
  assert new_component.name == basic_component.name
131
  assert new_component is not basic_component # Should be a copy
 
290
  # Test key property
291
  assert mirrored_component.key == "mirrored"
292
 
293
+ # Test model_copy with key
294
+ with_key = mirrored_component.model_copy(key="new_key")
295
  assert with_key.key == "new_key"
296
 
297
  # Test get_meta
 
353
  component = FastMCPComponent(name="test", meta=complex_meta)
354
  assert component.meta == complex_meta
355
 
356
+ def test_model_copy_with_key_preserves_all_attributes(self):
357
+ """Test that model_copy with key preserves all component attributes."""
358
  component = FastMCPComponent(
359
  name="test",
360
  title="Title",
 
363
  meta={"key": "value"},
364
  enabled=False,
365
  )
366
+ new_component = component.model_copy(key="new_key")
367
 
368
  assert new_component.name == component.name
369
  assert new_component.title == component.title
 
389
  assert original.name == "original"
390
  assert copy1.name == "copy1"
391
  assert copy2.name == "copy2"
392
+
393
+ def test_model_copy_with_update_and_key(self):
394
+ """Test that model_copy works with both update dict and key parameter."""
395
+ component = FastMCPComponent(
396
+ name="test",
397
+ title="Original Title",
398
+ description="Original Description",
399
+ tags=["tag1"],
400
+ enabled=True,
401
+ )
402
+
403
+ # Test with both update and key
404
+ updated_component = component.model_copy(
405
+ update={"title": "New Title", "description": "New Description"},
406
+ key="new_key",
407
+ )
408
+
409
+ assert updated_component.name == "test" # Not in update, unchanged
410
+ assert updated_component.title == "New Title" # Updated
411
+ assert updated_component.description == "New Description" # Updated
412
+ assert updated_component.tags == {"tag1"} # Not in update, unchanged
413
+ assert updated_component.enabled is True # Not in update, unchanged
414
+ assert updated_component.key == "new_key" # Custom key set
415
+
416
+ # Original should be unchanged
417
+ assert component.title == "Original Title"
418
+ assert component.description == "Original Description"
419
+ assert component.key == "test" # Uses name as key
420
+
421
+ def test_model_copy_deep_parameter(self):
422
+ """Test that model_copy respects the deep parameter."""
423
+ nested_dict = {"nested": {"value": 1}}
424
+ component = FastMCPComponent(name="test", meta=nested_dict)
425
+
426
+ # Shallow copy (default)
427
+ shallow_copy = component.model_copy()
428
+ assert shallow_copy.meta is not None
429
+ assert component.meta is not None
430
+ shallow_copy.meta["nested"]["value"] = 2
431
+ assert component.meta["nested"]["value"] == 2 # Original affected
432
+
433
+ # Deep copy
434
+ component.meta["nested"]["value"] = 1 # Reset
435
+ deep_copy = component.model_copy(deep=True)
436
+ assert deep_copy.meta is not None
437
+ deep_copy.meta["nested"]["value"] = 3
438
+ assert component.meta["nested"]["value"] == 1 # Original unaffected