fastmcp 2.2.3__tar.gz → 2.2.4__tar.gz

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.
Files changed (144) hide show
  1. {fastmcp-2.2.3 → fastmcp-2.2.4}/PKG-INFO +2 -2
  2. {fastmcp-2.2.3 → fastmcp-2.2.4}/README.md +1 -1
  3. {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/servers/resources.mdx +8 -1
  4. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/cli/cli.py +36 -3
  5. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/resources/template.py +1 -1
  6. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/server/openapi.py +13 -59
  7. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/utilities/func_metadata.py +16 -4
  8. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/resources/test_resource_template.py +77 -0
  9. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/server/test_openapi.py +137 -56
  10. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/server/test_server.py +73 -0
  11. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/utilities/test_func_metadata.py +68 -0
  12. {fastmcp-2.2.3 → fastmcp-2.2.4}/.cursor/rules/core-mcp-objects.mdc +0 -0
  13. {fastmcp-2.2.3 → fastmcp-2.2.4}/.github/ISSUE_TEMPLATE/bug.yml +0 -0
  14. {fastmcp-2.2.3 → fastmcp-2.2.4}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  15. {fastmcp-2.2.3 → fastmcp-2.2.4}/.github/ISSUE_TEMPLATE/enhancement.yml +0 -0
  16. {fastmcp-2.2.3 → fastmcp-2.2.4}/.github/release.yml +0 -0
  17. {fastmcp-2.2.3 → fastmcp-2.2.4}/.github/workflows/publish.yml +0 -0
  18. {fastmcp-2.2.3 → fastmcp-2.2.4}/.github/workflows/run-static.yml +0 -0
  19. {fastmcp-2.2.3 → fastmcp-2.2.4}/.github/workflows/run-tests.yml +0 -0
  20. {fastmcp-2.2.3 → fastmcp-2.2.4}/.gitignore +0 -0
  21. {fastmcp-2.2.3 → fastmcp-2.2.4}/.pre-commit-config.yaml +0 -0
  22. {fastmcp-2.2.3 → fastmcp-2.2.4}/LICENSE +0 -0
  23. {fastmcp-2.2.3 → fastmcp-2.2.4}/Windows_Notes.md +0 -0
  24. {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/assets/demo-inspector.png +0 -0
  25. {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/clients/client.mdx +0 -0
  26. {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/clients/transports.mdx +0 -0
  27. {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/docs.json +0 -0
  28. {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/getting-started/installation.mdx +0 -0
  29. {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/getting-started/quickstart.mdx +0 -0
  30. {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/getting-started/welcome.mdx +0 -0
  31. {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/patterns/composition.mdx +0 -0
  32. {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/patterns/contrib.mdx +0 -0
  33. {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/patterns/decorating-methods.mdx +0 -0
  34. {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/patterns/fastapi.mdx +0 -0
  35. {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/patterns/openapi.mdx +0 -0
  36. {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/patterns/proxy.mdx +0 -0
  37. {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/servers/context.mdx +0 -0
  38. {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/servers/fastmcp.mdx +0 -0
  39. {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/servers/prompts.mdx +0 -0
  40. {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/servers/tools.mdx +0 -0
  41. {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/snippets/version-badge.mdx +0 -0
  42. {fastmcp-2.2.3 → fastmcp-2.2.4}/docs/style.css +0 -0
  43. {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/complex_inputs.py +0 -0
  44. {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/desktop.py +0 -0
  45. {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/echo.py +0 -0
  46. {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/memory.py +0 -0
  47. {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/mount_example.py +0 -0
  48. {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/readme-quickstart.py +0 -0
  49. {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/sampling.py +0 -0
  50. {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/screenshot.py +0 -0
  51. {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/simple_echo.py +0 -0
  52. {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/smart_home/README.md +0 -0
  53. {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/smart_home/pyproject.toml +0 -0
  54. {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/smart_home/src/smart_home/__init__.py +0 -0
  55. {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/smart_home/src/smart_home/__main__.py +0 -0
  56. {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/smart_home/src/smart_home/hub.py +0 -0
  57. {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/smart_home/src/smart_home/lights/__init__.py +0 -0
  58. {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/smart_home/src/smart_home/lights/hue_utils.py +0 -0
  59. {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/smart_home/src/smart_home/lights/server.py +0 -0
  60. {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/smart_home/src/smart_home/py.typed +0 -0
  61. {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/smart_home/src/smart_home/settings.py +0 -0
  62. {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/smart_home/uv.lock +0 -0
  63. {fastmcp-2.2.3 → fastmcp-2.2.4}/examples/text_me.py +0 -0
  64. {fastmcp-2.2.3 → fastmcp-2.2.4}/justfile +0 -0
  65. {fastmcp-2.2.3 → fastmcp-2.2.4}/pyproject.toml +0 -0
  66. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/__init__.py +0 -0
  67. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/cli/__init__.py +0 -0
  68. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/cli/claude.py +0 -0
  69. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/client/__init__.py +0 -0
  70. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/client/base.py +0 -0
  71. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/client/client.py +0 -0
  72. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/client/roots.py +0 -0
  73. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/client/sampling.py +0 -0
  74. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/client/transports.py +0 -0
  75. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/contrib/README.md +0 -0
  76. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/contrib/bulk_tool_caller/README.md +0 -0
  77. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/contrib/bulk_tool_caller/__init__.py +0 -0
  78. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/contrib/bulk_tool_caller/bulk_tool_caller.py +0 -0
  79. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/contrib/bulk_tool_caller/example.py +0 -0
  80. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/contrib/mcp_mixin/README.md +0 -0
  81. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/contrib/mcp_mixin/__init__.py +0 -0
  82. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/contrib/mcp_mixin/example.py +0 -0
  83. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/contrib/mcp_mixin/mcp_mixin.py +0 -0
  84. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/exceptions.py +0 -0
  85. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/prompts/__init__.py +0 -0
  86. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/prompts/prompt.py +0 -0
  87. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/prompts/prompt_manager.py +0 -0
  88. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/py.typed +0 -0
  89. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/resources/__init__.py +0 -0
  90. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/resources/resource.py +0 -0
  91. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/resources/resource_manager.py +0 -0
  92. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/resources/types.py +0 -0
  93. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/server/__init__.py +0 -0
  94. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/server/context.py +0 -0
  95. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/server/proxy.py +0 -0
  96. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/server/server.py +0 -0
  97. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/settings.py +0 -0
  98. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/tools/__init__.py +0 -0
  99. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/tools/tool.py +0 -0
  100. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/tools/tool_manager.py +0 -0
  101. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/utilities/__init__.py +0 -0
  102. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/utilities/decorators.py +0 -0
  103. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/utilities/logging.py +0 -0
  104. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/utilities/openapi.py +0 -0
  105. {fastmcp-2.2.3 → fastmcp-2.2.4}/src/fastmcp/utilities/types.py +0 -0
  106. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/__init__.py +0 -0
  107. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/cli/test_run.py +0 -0
  108. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/client/__init__.py +0 -0
  109. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/client/test_client.py +0 -0
  110. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/client/test_roots.py +0 -0
  111. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/client/test_sampling.py +0 -0
  112. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/conftest.py +0 -0
  113. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/contrib/__init__.py +0 -0
  114. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/contrib/test_bulk_tool_caller.py +0 -0
  115. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/contrib/test_mcp_mixin.py +0 -0
  116. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/prompts/__init__.py +0 -0
  117. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/prompts/test_base.py +0 -0
  118. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/prompts/test_prompt_manager.py +0 -0
  119. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/resources/__init__.py +0 -0
  120. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/resources/test_file_resources.py +0 -0
  121. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/resources/test_function_resources.py +0 -0
  122. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/resources/test_resource_manager.py +0 -0
  123. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/resources/test_resources.py +0 -0
  124. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/server/__init__.py +0 -0
  125. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/server/test_file_server.py +0 -0
  126. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/server/test_import_server.py +0 -0
  127. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/server/test_lifespan.py +0 -0
  128. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/server/test_mount.py +0 -0
  129. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/server/test_proxy.py +0 -0
  130. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/server/test_run_server.py +0 -0
  131. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/test_servers/fastmcp_server.py +0 -0
  132. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/test_servers/sse.py +0 -0
  133. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/test_servers/stdio.py +0 -0
  134. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/tools/__init__.py +0 -0
  135. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/tools/test_tool_manager.py +0 -0
  136. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/utilities/__init__.py +0 -0
  137. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/utilities/openapi/__init__.py +0 -0
  138. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/utilities/openapi/conftest.py +0 -0
  139. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/utilities/openapi/test_openapi.py +0 -0
  140. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/utilities/openapi/test_openapi_advanced.py +0 -0
  141. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/utilities/openapi/test_openapi_fastapi.py +0 -0
  142. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/utilities/test_decorated_function.py +0 -0
  143. {fastmcp-2.2.3 → fastmcp-2.2.4}/tests/utilities/test_logging.py +0 -0
  144. {fastmcp-2.2.3 → fastmcp-2.2.4}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastmcp
3
- Version: 2.2.3
3
+ Version: 2.2.4
4
4
  Summary: The fast, Pythonic way to build MCP servers.
5
5
  Project-URL: Homepage, https://gofastmcp.com
6
6
  Project-URL: Repository, https://github.com/jlowin/fastmcp
@@ -141,7 +141,7 @@ FastMCP aims to be:
141
141
  ### Servers
142
142
  - **Create** servers with minimal boilerplate using intuitive decorators
143
143
  - **Proxy** existing servers to modify configuration or transport
144
- - **Compose** servers by into complex applications
144
+ - **Compose** servers into complex applications
145
145
  - **Generate** servers from OpenAPI specs or FastAPI objects
146
146
 
147
147
  ### Clients
@@ -112,7 +112,7 @@ FastMCP aims to be:
112
112
  ### Servers
113
113
  - **Create** servers with minimal boilerplate using intuitive decorators
114
114
  - **Proxy** existing servers to modify configuration or transport
115
- - **Compose** servers by into complex applications
115
+ - **Compose** servers into complex applications
116
116
  - **Generate** servers from OpenAPI specs or FastAPI objects
117
117
 
118
118
  ### Clients
@@ -251,13 +251,18 @@ With these two templates defined, clients can request a variety of resources:
251
251
 
252
252
  <VersionBadge version="2.2.3" />
253
253
 
254
+ <Warning>
255
+ Please note: the Model Context Protocol URI standard follows RFC 6570, which does not include support for wildcard parameters. FastMCP extends the template syntax to support wildcards (`{param*}`), and because template matching happens entirely in the FastMCP server, it is not expected that these wildcards will cause compatibility issues with other MCP implementations. However, this can not be guaranteed.
256
+ </Warning>
257
+
254
258
  Resource templates support wildcard parameters that can match multiple path segments. While standard parameters (`{param}`) only match a single path segment and don't cross "/" boundaries, wildcard parameters (`{param*}`) can capture multiple segments including slashes. Wildcards capture all subsequent path segments *up until* the defined part of the URI template (whether literal or another parameter). This allows you to have multiple wildcard parameters in a single URI template.
255
259
 
256
- ```python
260
+ ```python {15, 23}
257
261
  from fastmcp import FastMCP
258
262
 
259
263
  mcp = FastMCP(name="DataServer")
260
264
 
265
+
261
266
  # Standard parameter only matches one segment
262
267
  @mcp.resource("files://{filename}")
263
268
  def get_file(filename: str) -> str:
@@ -265,6 +270,7 @@ def get_file(filename: str) -> str:
265
270
  # Will only match files://<single-segment>
266
271
  return f"File content for: {filename}"
267
272
 
273
+
268
274
  # Wildcard parameter can match multiple segments
269
275
  @mcp.resource("path://{filepath*}")
270
276
  def get_path_content(filepath: str) -> str:
@@ -272,6 +278,7 @@ def get_path_content(filepath: str) -> str:
272
278
  # Can match path://docs/server/resources.mdx
273
279
  return f"Content at path: {filepath}"
274
280
 
281
+
275
282
  # Mixing standard and wildcard parameters
276
283
  @mcp.resource("repo://{owner}/{path*}/template.py")
277
284
  def get_template_file(owner: str, path: str) -> dict:
@@ -223,6 +223,27 @@ def dev(
223
223
  help="Additional packages to install",
224
224
  ),
225
225
  ] = [],
226
+ inspector_version: Annotated[
227
+ str | None,
228
+ typer.Option(
229
+ "--inspector-version",
230
+ help="Version of the MCP Inspector to use",
231
+ ),
232
+ ] = None,
233
+ ui_port: Annotated[
234
+ int | None,
235
+ typer.Option(
236
+ "--ui-port",
237
+ help="Port for the MCP Inspector UI",
238
+ ),
239
+ ] = None,
240
+ server_port: Annotated[
241
+ int | None,
242
+ typer.Option(
243
+ "--server-port",
244
+ help="Port for the MCP Inspector Proxy server",
245
+ ),
246
+ ] = None,
226
247
  ) -> None:
227
248
  """Run a MCP server with the MCP Inspector."""
228
249
  file, server_object = _parse_file_path(file_spec)
@@ -234,6 +255,8 @@ def dev(
234
255
  "server_object": server_object,
235
256
  "with_editable": str(with_editable) if with_editable else None,
236
257
  "with_packages": with_packages,
258
+ "ui_port": ui_port,
259
+ "server_port": server_port,
237
260
  },
238
261
  )
239
262
 
@@ -243,7 +266,11 @@ def dev(
243
266
  if hasattr(server, "dependencies"):
244
267
  with_packages = list(set(with_packages + server.dependencies))
245
268
 
246
- uv_cmd = _build_uv_command(file_spec, with_editable, with_packages)
269
+ env_vars = {}
270
+ if ui_port:
271
+ env_vars["CLIENT_PORT"] = str(ui_port)
272
+ if server_port:
273
+ env_vars["SERVER_PORT"] = str(server_port)
247
274
 
248
275
  # Get the correct npx command
249
276
  npx_cmd = _get_npx_command()
@@ -254,13 +281,19 @@ def dev(
254
281
  )
255
282
  sys.exit(1)
256
283
 
284
+ inspector_cmd = "@modelcontextprotocol/inspector"
285
+ if inspector_version:
286
+ inspector_cmd += f"@{inspector_version}"
287
+
288
+ uv_cmd = _build_uv_command(file_spec, with_editable, with_packages)
289
+
257
290
  # Run the MCP Inspector command with shell=True on Windows
258
291
  shell = sys.platform == "win32"
259
292
  process = subprocess.run(
260
- [npx_cmd, "@modelcontextprotocol/inspector"] + uv_cmd,
293
+ [npx_cmd, inspector_cmd] + uv_cmd,
261
294
  check=True,
262
295
  shell=shell,
263
- env=dict(os.environ.items()), # Convert to list of tuples for env update
296
+ env=dict(os.environ.items()) | env_vars,
264
297
  )
265
298
  sys.exit(process.returncode)
266
299
  except subprocess.CalledProcessError as e:
@@ -95,7 +95,7 @@ class ResourceTemplate(BaseModel):
95
95
  raise ValueError("You must provide a name for lambda functions")
96
96
 
97
97
  # Validate that URI params match function params
98
- uri_params = set(re.findall(r"{(\w+)}", uri_template))
98
+ uri_params = set(re.findall(r"{(\w+)(?:\*)?}", uri_template))
99
99
  if not uri_params:
100
100
  raise ValueError("URI template must contain at least one parameter")
101
101
 
@@ -257,7 +257,7 @@ class OpenAPIResource(Resource):
257
257
  self._client = client
258
258
  self._route = route
259
259
 
260
- async def read(self) -> str:
260
+ async def read(self) -> str | bytes:
261
261
  """Fetch the resource data by making an HTTP request."""
262
262
  try:
263
263
  # Extract path parameters from the URI if present
@@ -297,15 +297,16 @@ class OpenAPIResource(Resource):
297
297
  # Raise for 4xx/5xx responses
298
298
  response.raise_for_status()
299
299
 
300
- # Return response content based on mime type
301
- if self.mime_type == "application/json":
302
- try:
303
- return response.json()
304
- except (json.JSONDecodeError, ValueError):
305
- # Fallback to returning the text
306
- return response.text
307
- else:
300
+ # Determine content type and return appropriate format
301
+ content_type = response.headers.get("content-type", "").lower()
302
+
303
+ if "application/json" in content_type:
304
+ result = response.json()
305
+ return json.dumps(result)
306
+ elif any(ct in content_type for ct in ["text/", "application/xml"]):
308
307
  return response.text
308
+ else:
309
+ return response.content
309
310
 
310
311
  except httpx.HTTPStatusError as e:
311
312
  # Handle HTTP errors (4xx, 5xx)
@@ -343,59 +344,13 @@ class OpenAPIResourceTemplate(ResourceTemplate):
343
344
  uri_template=uri_template,
344
345
  name=name,
345
346
  description=description,
346
- fn=self._create_resource_fn,
347
+ fn=lambda **kwargs: None,
347
348
  parameters=parameters,
348
349
  tags=tags,
349
350
  )
350
351
  self._client = client
351
352
  self._route = route
352
353
 
353
- async def _create_resource_fn(self, **kwargs):
354
- """Create a resource with parameters."""
355
- # Prepare the path with parameters
356
- path = self._route.path
357
- for param_name, param_value in kwargs.items():
358
- path = path.replace(f"{{{param_name}}}", str(param_value))
359
-
360
- try:
361
- response = await self._client.request(
362
- method=self._route.method,
363
- url=path,
364
- timeout=30.0, # Default timeout
365
- )
366
-
367
- # Raise for 4xx/5xx responses
368
- response.raise_for_status()
369
-
370
- # Determine the mime type from the response
371
- content_type = response.headers.get("content-type", "application/json")
372
- mime_type = content_type.split(";")[0].strip()
373
-
374
- # Return the appropriate data
375
- if mime_type == "application/json":
376
- try:
377
- return response.json()
378
- except (json.JSONDecodeError, ValueError):
379
- return response.text
380
- else:
381
- return response.text
382
-
383
- except httpx.HTTPStatusError as e:
384
- error_message = (
385
- f"HTTP error {e.response.status_code}: {e.response.reason_phrase}"
386
- )
387
- try:
388
- error_data = e.response.json()
389
- error_message += f" - {error_data}"
390
- except (json.JSONDecodeError, ValueError):
391
- if e.response.text:
392
- error_message += f" - {e.response.text}"
393
-
394
- raise ValueError(error_message)
395
-
396
- except httpx.RequestError as e:
397
- raise ValueError(f"Request error: {str(e)}")
398
-
399
354
  async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
400
355
  """Create a resource with the given parameters."""
401
356
  # Generate a URI for this resource instance
@@ -409,9 +364,8 @@ class OpenAPIResourceTemplate(ResourceTemplate):
409
364
  route=self._route,
410
365
  uri=uri,
411
366
  name=f"{self.name}-{'-'.join(uri_parts)}",
412
- description=self.description
413
- or f"Resource for {self._route.path}", # Provide default if None
414
- mime_type="application/json", # Default, will be updated when read
367
+ description=self.description or f"Resource for {self._route.path}",
368
+ mime_type="application/json",
415
369
  tags=set(self._route.tags or []),
416
370
  )
417
371
 
@@ -7,7 +7,15 @@ from typing import (
7
7
  ForwardRef,
8
8
  )
9
9
 
10
- from pydantic import BaseModel, ConfigDict, Field, WithJsonSchema, create_model
10
+ from pydantic import (
11
+ BaseModel,
12
+ ConfigDict,
13
+ Field,
14
+ TypeAdapter,
15
+ ValidationError,
16
+ WithJsonSchema,
17
+ create_model,
18
+ )
11
19
  from pydantic._internal._typing_extra import eval_type_backport
12
20
  from pydantic.fields import FieldInfo
13
21
  from pydantic_core import PydanticUndefined
@@ -80,14 +88,18 @@ class FuncMetadata(BaseModel):
80
88
  dicts (JSON objects) as JSON strings, which can be pre-parsed here.
81
89
  """
82
90
  new_data = data.copy() # Shallow copy
83
- for field_name, _field_info in self.arg_model.model_fields.items():
91
+ for field_name, field_info in self.arg_model.model_fields.items():
84
92
  if field_name not in data.keys():
85
93
  continue
86
94
  if isinstance(data[field_name], str):
87
95
  try:
88
96
  pre_parsed = json.loads(data[field_name])
89
- except json.JSONDecodeError:
90
- continue # Not JSON - skip
97
+
98
+ # Check if the pre_parsed value is valid for the field
99
+ validator = TypeAdapter(field_info.annotation)
100
+ validator.validate_python(pre_parsed)
101
+ except (json.JSONDecodeError, ValidationError):
102
+ continue # Not JSON or invalid for the field
91
103
  if isinstance(pre_parsed, str | int | float):
92
104
  # This is likely that the raw value is e.g. `"hello"` which we
93
105
  # Should really be parsed as '"hello"' in Python - but if we parse
@@ -297,6 +297,66 @@ class TestResourceTemplate:
297
297
  content = await resource.read()
298
298
  assert content == "hello"
299
299
 
300
+ async def test_wildcard_param_can_create_resource(self):
301
+ """Test that wildcard parameters are valid."""
302
+
303
+ def identity(path: str) -> str:
304
+ return path
305
+
306
+ template = ResourceTemplate.from_function(
307
+ fn=identity,
308
+ uri_template="test://{path*}.py",
309
+ name="test",
310
+ )
311
+
312
+ assert await template.create_resource(
313
+ "test://path/to/test.py",
314
+ {"path": "path/to/test.py"},
315
+ )
316
+
317
+ async def test_wildcard_param_matches(self):
318
+ def identify(path: str) -> str:
319
+ return path
320
+
321
+ template = ResourceTemplate.from_function(
322
+ fn=identify,
323
+ uri_template="test://src/{path*}.py",
324
+ name="test",
325
+ )
326
+ # Valid match
327
+ params = template.matches("test://src/path/to/test.py")
328
+ assert params == {"path": "path/to/test"}
329
+
330
+ async def test_multiple_wildcard_params(self):
331
+ """Test that multiple wildcard parameters are valid."""
332
+
333
+ def identity(path: str, path2: str) -> str:
334
+ return f"{path}/{path2}"
335
+
336
+ template = ResourceTemplate.from_function(
337
+ fn=identity,
338
+ uri_template="test://{path*}/xyz/{path2*}",
339
+ name="test",
340
+ )
341
+
342
+ params = template.matches("test://path/to/xyz/abc")
343
+ assert params == {"path": "path/to", "path2": "abc"}
344
+
345
+ async def test_wildcard_param_with_regular_param(self):
346
+ """Test that a wildcard parameter can be used with a regular parameter."""
347
+
348
+ def identity(prefix: str, path: str) -> str:
349
+ return f"{prefix}/{path}"
350
+
351
+ template = ResourceTemplate.from_function(
352
+ fn=identity,
353
+ uri_template="test://{prefix}/{path*}",
354
+ name="test",
355
+ )
356
+
357
+ params = template.matches("test://src/path/to/test.py")
358
+ assert params == {"prefix": "src", "path": "path/to/test.py"}
359
+
300
360
 
301
361
  class TestMatchUriTemplate:
302
362
  """Test match_uri_template function."""
@@ -443,3 +503,20 @@ class TestMatchUriTemplate:
443
503
  uri_template = "test://a/{x}{y}"
444
504
  result = match_uri_template(uri=uri, uri_template=uri_template)
445
505
  assert result is None
506
+
507
+ @pytest.mark.parametrize(
508
+ "uri, expected_params",
509
+ [
510
+ ("file://abc/xyz.py", {"path": "xyz"}),
511
+ ("file://abc/x/y/z.py", {"path": "x/y/z"}),
512
+ ("file://abc/x/y/z/.py", {"path": "x/y/z/"}),
513
+ ("file://abc/x/y/z.md", None),
514
+ ("file://x/y/z.txt", None),
515
+ ],
516
+ )
517
+ def test_match_uri_template_with_non_slash_suffix(
518
+ self, uri: str, expected_params: dict[str, str]
519
+ ):
520
+ uri_template = "file://abc/{path*}.py"
521
+ result = match_uri_template(uri=uri, uri_template=uri_template)
522
+ assert result == expected_params