Skip to content

Commit 4fa9ebd

Browse files
authored
Merge pull request #101 from golf-mcp/asch/resource-templates
feat[core]: implement FastMCP 2.11+ resource template support
2 parents 5066562 + 69d6b8c commit 4fa9ebd

File tree

2 files changed

+116
-44
lines changed

2 files changed

+116
-44
lines changed

src/golf/core/builder.py

Lines changed: 100 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,21 @@ def _get_transport_config(self, transport_type: str) -> dict:
496496

497497
return config
498498

499+
def _is_resource_template(self, component: ParsedComponent) -> bool:
500+
"""Check if a resource component is a template (has URI parameters).
501+
502+
Args:
503+
component: The parsed component to check
504+
505+
Returns:
506+
True if the resource has URI parameters, False otherwise
507+
"""
508+
return (
509+
component.type == ComponentType.RESOURCE
510+
and component.parameters is not None
511+
and len(component.parameters) > 0
512+
)
513+
499514
def _generate_server(self) -> None:
500515
"""Generate the main server entry point."""
501516
server_file = self.output_dir / "server.py"
@@ -514,7 +529,7 @@ def _generate_server(self) -> None:
514529
imports = [
515530
"from fastmcp import FastMCP",
516531
"from fastmcp.tools import Tool",
517-
"from fastmcp.resources import Resource",
532+
"from fastmcp.resources import Resource, ResourceTemplate",
518533
"from fastmcp.prompts import Prompt",
519534
"import os",
520535
"import sys",
@@ -654,12 +669,20 @@ def _generate_server(self) -> None:
654669
registration += f".with_annotations({component.annotations})"
655670
registration += "\nmcp.add_tool(_tool)"
656671
elif component_type == ComponentType.RESOURCE:
657-
registration += (
658-
f"\n_resource = Resource.from_function(_wrapped_func, "
659-
f'uri="{component.uri_template}", name="{component.name}", '
660-
f'description="{component.docstring or ""}")\n'
661-
f"mcp.add_resource(_resource)"
662-
)
672+
if self._is_resource_template(component):
673+
registration += (
674+
f"\n_template = ResourceTemplate.from_function(_wrapped_func, "
675+
f'uri_template="{component.uri_template}", name="{component.name}", '
676+
f'description="{component.docstring or ""}")\n'
677+
f"mcp.add_template(_template)"
678+
)
679+
else:
680+
registration += (
681+
f"\n_resource = Resource.from_function(_wrapped_func, "
682+
f'uri="{component.uri_template}", name="{component.name}", '
683+
f'description="{component.docstring or ""}")\n'
684+
f"mcp.add_resource(_resource)"
685+
)
663686
else: # PROMPT
664687
registration += (
665688
f"\n_prompt = Prompt.from_function(_wrapped_func, "
@@ -692,12 +715,20 @@ def _generate_server(self) -> None:
692715
registration += f".with_annotations({component.annotations})"
693716
registration += "\nmcp.add_tool(_tool)"
694717
elif component_type == ComponentType.RESOURCE:
695-
registration += (
696-
f"\n_resource = Resource.from_function(_wrapped_func, "
697-
f'uri="{component.uri_template}", name="{component.name}", '
698-
f'description="{component.docstring or ""}")\n'
699-
f"mcp.add_resource(_resource)"
700-
)
718+
if self._is_resource_template(component):
719+
registration += (
720+
f"\n_template = ResourceTemplate.from_function(_wrapped_func, "
721+
f'uri_template="{component.uri_template}", name="{component.name}", '
722+
f'description="{component.docstring or ""}")\n'
723+
f"mcp.add_template(_template)"
724+
)
725+
else:
726+
registration += (
727+
f"\n_resource = Resource.from_function(_wrapped_func, "
728+
f'uri="{component.uri_template}", name="{component.name}", '
729+
f'description="{component.docstring or ""}")\n'
730+
f"mcp.add_resource(_resource)"
731+
)
701732
else: # PROMPT
702733
registration += (
703734
f"\n_prompt = Prompt.from_function(_wrapped_func, "
@@ -737,33 +768,64 @@ def _generate_server(self) -> None:
737768
registration += "\nmcp.add_tool(_tool)"
738769

739770
elif component_type == ComponentType.RESOURCE:
740-
registration = f"# Register the resource '{component.name}' from {full_module_path}"
741-
742-
# Use the entry_function if available, otherwise try the
743-
# export variable
744-
if hasattr(component, "entry_function") and component.entry_function:
745-
registration += (
746-
f"\n_resource = Resource.from_function("
747-
f"{full_module_path}.{component.entry_function}, "
748-
f'uri="{component.uri_template}"'
749-
)
750-
else:
751-
registration += (
752-
f"\n_resource = Resource.from_function("
753-
f"{full_module_path}.export, "
754-
f'uri="{component.uri_template}"'
771+
if self._is_resource_template(component):
772+
registration = (
773+
f"# Register the resource template '{component.name}' from {full_module_path}"
755774
)
756775

757-
# Add the name parameter
758-
registration += f', name="{component.name}"'
759-
760-
# Add description from docstring
761-
if component.docstring:
762-
# Escape any quotes in the docstring
763-
escaped_docstring = component.docstring.replace('"', '\\"')
764-
registration += f', description="{escaped_docstring}"'
765-
766-
registration += ")\nmcp.add_resource(_resource)"
776+
# Use the entry_function if available, otherwise try the
777+
# export variable
778+
if hasattr(component, "entry_function") and component.entry_function:
779+
registration += (
780+
f"\n_template = ResourceTemplate.from_function("
781+
f"{full_module_path}.{component.entry_function}, "
782+
f'uri_template="{component.uri_template}"'
783+
)
784+
else:
785+
registration += (
786+
f"\n_template = ResourceTemplate.from_function("
787+
f"{full_module_path}.export, "
788+
f'uri_template="{component.uri_template}"'
789+
)
790+
791+
# Add the name parameter
792+
registration += f', name="{component.name}"'
793+
794+
# Add description from docstring
795+
if component.docstring:
796+
# Escape any quotes in the docstring
797+
escaped_docstring = component.docstring.replace('"', '\\"')
798+
registration += f', description="{escaped_docstring}"'
799+
800+
registration += ")\nmcp.add_template(_template)"
801+
else:
802+
registration = f"# Register the resource '{component.name}' from {full_module_path}"
803+
804+
# Use the entry_function if available, otherwise try the
805+
# export variable
806+
if hasattr(component, "entry_function") and component.entry_function:
807+
registration += (
808+
f"\n_resource = Resource.from_function("
809+
f"{full_module_path}.{component.entry_function}, "
810+
f'uri="{component.uri_template}"'
811+
)
812+
else:
813+
registration += (
814+
f"\n_resource = Resource.from_function("
815+
f"{full_module_path}.export, "
816+
f'uri="{component.uri_template}"'
817+
)
818+
819+
# Add the name parameter
820+
registration += f', name="{component.name}"'
821+
822+
# Add description from docstring
823+
if component.docstring:
824+
# Escape any quotes in the docstring
825+
escaped_docstring = component.docstring.replace('"', '\\"')
826+
registration += f', description="{escaped_docstring}"'
827+
828+
registration += ")\nmcp.add_resource(_resource)"
767829

768830
else: # PROMPT
769831
registration = f"# Register the prompt '{component.name}' from {full_module_path}"

src/golf/core/builder_telemetry.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def generate_component_registration_with_telemetry(
2929
entry_function: str,
3030
docstring: str = "",
3131
uri_template: str = None,
32+
is_template: bool = False,
3233
) -> str:
3334
"""Generate component registration code with telemetry instrumentation.
3435
@@ -39,6 +40,7 @@ def generate_component_registration_with_telemetry(
3940
entry_function: Entry function name
4041
docstring: Component description
4142
uri_template: URI template for resources (optional)
43+
is_template: Whether the resource is a template (has URI parameters)
4244
4345
Returns:
4446
Python code string for registering the component with instrumentation
@@ -56,12 +58,20 @@ def generate_component_registration_with_telemetry(
5658

5759
elif component_type == "resource":
5860
wrapped_func = f"instrument_resource({func_ref}, '{uri_template}')"
59-
return (
60-
f"_resource = Resource.from_function({wrapped_func}, "
61-
f'uri="{uri_template}", name="{component_name}", '
62-
f'description="{escaped_docstring}")\n'
63-
f"mcp.add_resource(_resource)"
64-
)
61+
if is_template:
62+
return (
63+
f"_resource = ResourceTemplate.from_function({wrapped_func}, "
64+
f'uri_template="{uri_template}", name="{component_name}", '
65+
f'description="{escaped_docstring}")\n'
66+
f"mcp.add_template(_resource)"
67+
)
68+
else:
69+
return (
70+
f"_resource = Resource.from_function({wrapped_func}, "
71+
f'uri="{uri_template}", name="{component_name}", '
72+
f'description="{escaped_docstring}")\n'
73+
f"mcp.add_resource(_resource)"
74+
)
6575

6676
elif component_type == "prompt":
6777
wrapped_func = f"instrument_prompt({func_ref}, '{component_name}')"

0 commit comments

Comments
 (0)