| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
|
Latest commit implements working stream binding to support elicitation and sampling. There's definitely room for refactoring that one 😔 |
Sorry, something went wrong.
|
Added support for configuring the immediate result and made some changes to avoid smuggling parameters through _meta (now we do it through unserialized fields). |
Sorry, something went wrong.
|
If the choice of using async/sync is made by the client, why should we add flavors when defining a tool on the server side? |
Sorry, something went wrong.
Answering my own question... You need to signal in the tool definition which flavor it supports. |
Sorry, something went wrong.
|
Okay. I don't think we should be adding multiple flavors to a tool, I think it makes things more complicated. Also, the spec anyway supports the invocationMode keyword, and not invocationModes, which I think it makes sense. I think a better API is to have a new method e.g. async_tool (although "async" is not the best keyword for this in Python). Is it still possible to find a synonym to "async"? "long-run"? 😅 |
Sorry, something went wrong.
|
|
||
| token: str | ||
| """Server-generated token to use for checking status and retrieving results.""" | ||
| keepAlive: int |
There was a problem hiding this comment.
| keepAlive: int | |
| keep_live: int = Field(alias="keepAlive") |
Sorry, something went wrong.
|
|
||
| name: str | ||
| arguments: dict[str, Any] | None = None | ||
| operation_params: AsyncRequestProperties | None = Field(serialization_alias="operation", default=None) |
There was a problem hiding this comment.
What's the problem with the real name "operation"?
Sorry, something went wrong.
There was a problem hiding this comment.
The base RequestParams type also has a _operation used for association metadata, which needs to be aliased to operation to not be treated as a private/protected field by pyright wherever it gets used.
error: "_operation" is protected and used outside of the class in which it is declared (reportPrivateUsage)
I didn't want to ignore pyright just because of a naming conflict, but now that I'm looking at this again there are only 3 places where we'd need to do so.
Sorry, something went wrong.
On the SEP, we're going to be moving to referring to these uniformly as long-running operations, so an @mcp.long_running would work. Do we want to force tool implementors to make a clean break from existing tools, though? Or are you suggesting that in the case where both modes are desired on a tool in an interim state, they would add both @mcp.tool and @mcp.long_running to the same function? If so, that then requires the tool manager to support adding the same tool more than once, and updating the parameters if long_running is present and a tool is already registered (since the decorators could be applied in either order). |
Sorry, something went wrong.
As for what I understood, since the invokeMode is a string, you can only have a tool that is either "long-running" or "short-running". If that's the case, then a short running tool is different from a long-running tool. So... I'm not sure if I'm missing something, or the spec reflects something different.
I wouldn't like to add two decorators. 🤔 If a tool can be short, and long-running, why the invokeMode is not a boolean? supportsAsync? |
Sorry, something went wrong.
A tool is exactly one or the other from the perspective of a single client. ListTools must show exactly one execution mode. In addition, version negotiation is used to hide long-running tools from clients that don't support them, yet. However, we can still support hybrid tools for backwards-compatibility purposes if the server operator allows it. Essentially, because the core tool implementation is the same @mcp.tool, we can wrap a tool in the functionality it needs to behave in either way within the server SDK, but only allow a client to use one or the other depending on its version. There's a few sections of the SEP which discuss this, but pulling one I think is useful: Old Clients (pre-async support):
// tools/list response (filtered)
{
tools: [
// only supports sync execution
{ name: "search_web", description: "Search the web" },
// only supports sync execution
{ name: "quick_calc", description: "Fast calculation" },
// supports both sync and async - invocationMode field is hidden from old clients
{ name: "get_weather", description: "Get weather" }
// async-capable tools hidden from old clients
]
}
New Clients (async support):
// tools/list response (complete)
{
tools: [
// explicitly only supports sync execution
{ name: "search_web", description: "Search the web", invocationMode: "sync" },
// implicitly only supports sync execution
{ name: "quick_calc", description: "Fast calculation" },
// new clients see the async invocation mode and should assume async-only execution
{ name: "get_weather", description: "Get weather", invocationMode: "async" },
// tool is async-only, and is not shown to old clients
{ name: "deep_analysis", description: "Complex analysis", invocationMode: "async" }
// All tools visible, async capabilities declared
]
}
Note that some tools are present in the "old clients" list despite presenting as LRO-only in the "new clients" list. When an old client invokes a tool represented like this, they won't get the immediate_result response, or even an operation token - the server will simply not return a response until the tool implementation completes.
This was the original idea, actually, but for futureproofing it's been changed to an enum. |
Sorry, something went wrong.
…-sdk into feat/async-tools
|
Alright, there's a lot of small remaining things to address here, particularly around naming (and lots of debugging cruft to clean up), but the SQLite example with both server and client restarts with elicitation now works. Regardless of the exact SEP accepted for LROs, I think there's a useful core implementation here that is likely generalizable. The operation token is essentially used as a routing key for task messages, but as long as there's some globally-unique key, I think something like this would work in several LRO models. |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
This PR implements the required changes for modelcontextprotocol/modelcontextprotocol#1391, which adds asynchronous tool execution.
This is a large PR, and I expect that if the associated SEP is accepted, we might want to break this down into several smaller PRs for SDK reviewers. I tried to generally have separate commits for each step of the implementation, to try and make this easier to review in its current form.
Motivation and Context
Today, most applications integrate with tools in a straightforward but naive manner, choosing to have agents invoke tools synchronously with the conversation instead of allowing agents to multitask where possible. There are a few reasons why we believe this is the case, including the lack of clarity around tool interfaces (single tool or multiple for job tracking), model failures when manually polling on operations, and not having a way to retrieve results with a well-defined TTL, among other problems (described in more detail in the linked issue). Here, we introduce an alternative API that establishes a clear integration path for async job-style use cases that are typically on the order of minutes to hours.
The ultra high-level overview is as follows:
Whether a tool is sync, async, or both (on old/new protocol versions) is defined by tool implementors. This enables remote server operators to control this based on how long each tool is expected to take to execute, rather than potentially serving HTTP requests with widely varying execution times on the same endpoint. This also makes it much more clear to client applications what the "time contract" of a tool is, so that fast tools can still be executed synchronously while allowing long-running tools to be immediately backgrounded.
Usage
Defining an async-compatible tool is just a matter of adjusting the @mcp.tool() decorator to include an invocation_modes parameter, which is a list of "sync" and "async":
If invocation_modes contains "async", the tool is async-compatible and will only be called in async mode by clients on new versions, while if it contains "sync", the tool is sync-compatible and will be called in sync mode if async mode is not supported (a client will never have the option to choose one or the other itself).
Behind the scenes, the SDK handles branching the behavior to either run synchronously (like today) or asynchronously (immediate return with job tracking) depending on if the client version supports async tools yet or not.
To control how long the results are kept for to retrieve with tools/async/result, we can use the keep_alive parameter:
@mcp.tool(invocation_modes=["async", "sync"], keep_alive=30) # retain result for 30s following completionWe can also customize the content returned in the immediate CallToolResult with the immediate_result parameter:
On the client side, we just add the polling and result retrieval like so:
How Has This Been Tested?
Unit tests, integration tests, and new example snippets.
Breaking Changes
Existing users will not need to update their applications to continue using synchronous tool calls. Asynchronous tool calls will require minor code changes that will be documented.
Types of changes
Checklist
Additional context
There were a bunch of decisions in the implementation we may want to discuss further, some of which were due to ambiguity in the proposal (which will be revised again) and some of which were due to working things into the SDK implementation.