-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Added RFC 6570 complaint form style query expansion as optional param… #427
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for working on this!
This PR makes good progress toward RFC 6570 support by adding form-style query expansion.
There are a few suggestions:
- Type Conversion for Query Parameters. Query parameters from URLs are always strings, but the code doesn't convert them to match function parameter
types.
Example of the problem:
@server.resource("resource://items/{category}{?limit}")
def get_items(category: str, limit: int = 10) -> str:
# When called with resource://items/books?limit=20
# limit will be string "20" not int 20
return f"Items in {category}, limit: {limit} (type: {type(limit)})"
Suggested fix: Add type conversion based on function annotations in
- Handle Edge Cases in Query Parsing. The current implementation might not handle edge cases properly:
- Empty query values (?param=)
- URL-encoded values (?name=John%20Doe)
-
Add Tests for Type Conversion Scenarios
-
Consider Documenting Query Parameter Behavior
@ihrpr thanks for the feedback during the fn call with params result = self.fn(**params) can i use call_fn_with_arg_validation
|
@ihrpr the pydantic validate_call already takes care of the compatible type conversion based on annontated parameters for query parameters and raises validation error for incompatible types added a decorator which check if the validation error is raised by optional parameters and ignores them so they fall back to default values to keep it compliant added relevant tests covering these cases for the cases in query parsing the empty values and url encoded parameters are handled by parse_qs added relevant tests covering these |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apologies for the delay in coming back to this - there seem to be some failing tests & merge conflicts, would you be able to resolve?
I'll prioritize responding quickly if you have the bandwidth to update the PR here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @codecracker007 thank you for this contribution.
I think this PR is heading in the right direction in terms of adding support for optional params on resources. However, to lower the maintenance burden as well as adhering to a "fail fast" principle, we shouldn't do the automatic retry logic when clients provide invalid optional params and fallback to the default.
If clients pass in invalid optional params, we should just fail like we would have without this retry logic.
return [TextContent(type="text", text=result)] | ||
|
||
|
||
def use_defaults_on_optional_validation_error( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should fail fast if an optional parameter was provided with a non-matching type.
I think we should completely remove this as well as the call to it in templates.py
|
||
if uri_params != func_params: | ||
raise ValueError( | ||
f"Mismatch between URI parameters {uri_params} and function parameters {func_params}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: removing this validation makes sense because we now support optional params.
validated_fn = validate_call(original_fn) | ||
|
||
# Then, apply our decorator to handle default fallback for optional params | ||
final_fn = use_defaults_on_optional_validation_error(validated_fn) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove, we should fail fast and not silently when invalid params are provided by clients.
|
||
# self.fn is now multiply-decorated: | ||
# 1. validate_call for coercion/validation | ||
# 2. our new decorator for default fallback on optional param validation err |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
don't use a fallback, just fail if the optional params are supplied incorrectly
with pytest.raises(Exception): # Specific exception type may vary | ||
await session.read_resource(AnyUrl("resource://users/123/invalid")) # Invalid template | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thank you for adding significant test coverage - we'll want to update these to catch expected validation errors rather than the fallback logic.
) -> ResourceTemplate: | ||
"""Create a template from a function.""" | ||
func_name = name or fn.__name__ | ||
original_fn = fn |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
original_fn
, fn
, final_fn
- this gets quite confusing.
I think if we remove the use_defaults_on_optional_validation_error
we shouldn't need any of these and can just stick to fn
Don't we want something more FastAPI-ish? Like |
Something like this? from typing import Annotated
@mcp.resource("search://{category}/{id}")
def search(
category: Annotated[str, Path()],
id: Annotated[int, Path()],
format: Annotated[str, Query()] = "json",
limit: Annotated[int, Query()] = 10
):
... Note: there's also #1439 which is touching the same area so we'd need some coordination cc: @beaterblank |
For |
OK thanks, so more like this? from typing import Annotated
@mcp.resource("search://{category}/{id}")
def search(
category: str,
id: int,
format: Annotated[str, Query(default="json")],
limit: Annotated[int, Query(default=10)],
):
... |
Yes, using
I also noticed an issue in my PR: I’m currently inferring types only from the URI, but this should be done based on the function signature value's types instead. can reuse the code from this PR for that. Additionally, since both features modify the same function in different ways, we should refactor the logic into smaller, more focused functions. That will make the codebase cleaner and easier to extend in the future. At this point, I could either merge this PR into mine and resolve the conflicts properly, or do it the other way around. merging this into #1439 |
Can close this PR merged into #1439 . |
Add RFC 6570 support for optional parameters as form-style query expansions
Motivation and Context
Adds optional parameter support with RFC 6570 compliance as form-style query expansions. This change addresses issue #378, .
Breaking Changes
None
Types of changes
Checklist