diff --git a/docs/specification/draft/basic/async.mdx b/docs/specification/draft/basic/async.mdx new file mode 100644 index 000000000..20f83c101 --- /dev/null +++ b/docs/specification/draft/basic/async.mdx @@ -0,0 +1,99 @@ +--- +title: Async Tool Calls +--- + +**Protocol Revision**: draft + +The Model Context Protocol (MCP) defines a rigorous protocol for client-server +async call tool requests. + +## 1. Introduction + +### 1.1 Purpose and Scope + +The Model Context Protocol provides asynchronous capabilities at the transport level, +enabling MCP clients to make asynchronous requests to call tools. + +### 1.2 Protocol Requirements + +Asynchronous tool calls is **OPTIONAL** for MCP implementations. When supported: + +- Implementations **MUST** follow established security best practices for their protocol. +- In particular implementations **MUST NOT** use the AsyncToken as a substitute for authorisation or authentication checks. + +### 1.3 Standards Compliance + +- TODO note any relevant standards in this space + +### 2.1 Overview + +#### 2.1.1 Basic usage + +The following diagram shows a basic sequence of request, responses and notifications + +```mermaid +sequenceDiagram + actor Client + actor Server + Client->>Server: CallToolAsyncRequest + Server-->>Client: CallToolAsyncResult + Server-)Client: ProgressNotification (1/2) + Server-)Client: ProgressNotification (2/2) + Client->>Server: GetAsyncResultRequest + Server-->>Client: CallToolResult +``` + +#### 2.1.1 Join usage + +A client may join an existing tool call request. A server MAY send all or partial progress notifications to the client +```mermaid +sequenceDiagram + actor Client + actor Server + Client->>Server: JoinCallToolAsyncRequest + Server-->>Client: CallToolAsyncResult + Server-)Client: ProgressNotification (3/4) + Server-)Client: ProgressNotification (4/4) + Client->>Server: GetAsyncResultRequest + Server-->>Client: CallToolResult +``` + +Multiple clients MAY join an existing tool call request. +```mermaid +sequenceDiagram + actor Client1 + actor Client2 + actor Server + Client1->>Server: CallToolAsyncRequest + Server-->>Client1: CallToolAsyncResult + Server-)Client1: ProgressNotification (1/2) + Client2->>Server: JoinCallToolAsyncRequest + Server-->>Client2: CallToolAsyncResult + Server-)Client1: ProgressNotification (2/2) + Client1->>Server: GetAsyncResultRequest + Server-->>Client1: CallToolResult + Server-)Client2: ProgressNotification (2/2) + Client2->>Server: GetAsyncResultRequest + Server-->>Client2: CallToolResult +``` + +#### 2.1.1 Cancel usage + +A client may cancel any in progress call tool requests. +The server SHOULD notify all clients that have called or joined the async tool call of the cancellation +```mermaid +sequenceDiagram + actor Client1 + actor Client2 + actor Server + Client1->>Server: CallToolAsyncRequest + Server-->>Client1: CallToolAsyncResult + Server-)Client1: ProgressNotification (1/3) + Client2->>Server: JoinCallToolAsyncRequest + Server-->>Client2: CallToolAsyncResult + Server-)Client1: ProgressNotification (2/3) + Server-)Client2: ProgressNotification (2/3) + Client2-)Server: CancelToolAsyncNotification + Server--)Client1: CancelNotification + Server--)Client2: CancelNotification +``` \ No newline at end of file diff --git a/schema/draft/schema.json b/schema/draft/schema.json index 05552b38d..82d5a73b9 100644 --- a/schema/draft/schema.json +++ b/schema/draft/schema.json @@ -20,6 +20,13 @@ }, "type": "object" }, + "AsyncToken": { + "description": "Used by the client to reference a previous async tool call submitted to the server.\n\nThe server SHOULD NOT rely on this for authentication or authorisation purposes", + "type": [ + "string", + "integer" + ] + }, "AudioContent": { "description": "Audio provided to or from an LLM.", "properties": { @@ -92,6 +99,59 @@ ], "type": "object" }, + "CallToolAsyncRequest": { + "description": "Used by the client to call a tool provided by the server and return a token to get the result or cancel the call at a later stage.\n\nThe server SHOULD retain the result of the tool call for the number of seconds specified by keepAlive to enable the client to retrieve the result.\n\nThe server MUST respond with an CallToolAsyncResult.\n\nThe server MAY reduce the number of seconds specified by keepAlive if so the server MUST inform the client of the new keepAlive time in the CallToolAsyncResult.", + "properties": { + "method": { + "const": "tools/async/call", + "type": "string" + }, + "params": { + "properties": { + "arguments": { + "additionalProperties": {}, + "type": "object" + }, + "keepAlive": { + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + }, + "required": [ + "method", + "params" + ], + "type": "object" + }, + "CallToolAsyncResult": { + "description": "Used by the server to provide a reference to the client for a previously submitted async tool call.\n\nThis MUST only be sent as the result of EITHER CallToolAsyncRequest or JoinToolAsyncResultRequest \n\nThe token SHOULD be valid to get or cancel the result for keepAalive seconds from the received time on the server.\n\nThe server SHOULD report the received time in unix time, e.g. seconds elapsed since 00:00:00 UTC on 1 January 1970", + "properties": { + "accepted": { + "description": "Whether the async tool call was accepted for execution on the server.\n\nIf accepted is false the server MAY still respond to GetToolAsyncResultRequest with details of the reason for rejection if so the token MUST be set.", + "type": "boolean" + }, + "keepAlive": { + "description": "The number of seconds that the server should retain results after all sessions \nhave disconnected\n\nMUST be set if accepted is true\n\nMAY be set if accepted is false in order for the client to retrieve an error result", + "type": "integer" + }, + "token": { + "$ref": "#/definitions/AsyncToken", + "description": "The token the client can use to retrieve results, cancel the invocation or rejoin the call in the case of disconnection\n\nMUST be set if accepted is true\n\nMAY be set if accepted is false in order for the client to retrieve the error result" + } + }, + "required": [ + "accepted" + ], + "type": "object" + }, "CallToolRequest": { "description": "Used by the client to invoke a tool provided by the server.", "properties": { @@ -153,6 +213,10 @@ "description": "Whether the tool call ended in an error.\n\nIf not set, this is assumed to be false (the call was successful).\n\nAny errors that originate from the tool SHOULD be reported inside the result\nobject, with `isError` set to true, _not_ as an MCP protocol-level error\nresponse. Otherwise, the LLM would not be able to see that an error occurred\nand self-correct.\n\nHowever, any errors in _finding_ the tool, an error indicating that the\nserver does not support tool calls, or any other exceptional conditions,\nshould be reported as an MCP error response.", "type": "boolean" }, + "isPending": { + "description": "Whether the tool call is pending a result.\n\nThis MUST only be set in response to a GetToolAsyncResultRequest where timeout is set\n\nIf not set, this is assumed to be false (the call was successful).", + "type": "boolean" + }, "structuredContent": { "additionalProperties": {}, "description": "An optional JSON object that represents the structured result of the tool call.", @@ -164,6 +228,31 @@ ], "type": "object" }, + "CancelToolAsyncNotification": { + "description": "Used by the client to cancel a previous async tool call request.\n\nThe request SHOULD still be in-flight, but due to communication latency, it is always possible that this request MAY arrive after the request has already finished.\n\nThis request indicates that the result will be unused, so any associated processing SHOULD cease.\n\nThe server SHOULD send a CancelNotification to any clients that have called or joined the associated async tool call.", + "properties": { + "method": { + "const": "tools/async/cancel", + "type": "string" + }, + "params": { + "properties": { + "token": { + "$ref": "#/definitions/AsyncToken" + } + }, + "required": [ + "token" + ], + "type": "object" + } + }, + "required": [ + "method", + "params" + ], + "type": "object" + }, "CancelledNotification": { "description": "This notification can be sent by either side to indicate that it is cancelling a previously-issued request.\n\nThe request SHOULD still be in-flight, but due to communication latency, it is always possible that this notification MAY arrive after the request has already finished.\n\nThis notification indicates that the result will be unused, so any associated processing SHOULD cease.\n\nA client MUST NOT attempt to cancel its `initialize` request.", "properties": { @@ -239,6 +328,9 @@ { "$ref": "#/definitions/InitializedNotification" }, + { + "$ref": "#/definitions/CancelToolAsyncNotification" + }, { "$ref": "#/definitions/ProgressNotification" }, @@ -252,6 +344,15 @@ { "$ref": "#/definitions/InitializeRequest" }, + { + "$ref": "#/definitions/CallToolAsyncRequest" + }, + { + "$ref": "#/definitions/JoinCallToolAsyncRequest" + }, + { + "$ref": "#/definitions/GetToolAsyncResultRequest" + }, { "$ref": "#/definitions/PingRequest" }, @@ -703,6 +804,35 @@ ], "type": "object" }, + "GetToolAsyncResultRequest": { + "description": "Used by the client to request the result of an async tool call running on a server to be sent to this client.\n\nThe request SHOULD be complete and this request SHOULD be recieved within the keepAlive window, but due to communication latency, it is always possible that this request MAY arrive after the request has already been discarded.\n\nThe server MUST return a CallToolResult with either: the result if the call was successful; an error status if the result cannot be retrieved; or a pending status if the call tool is still in progress.\n\nThe client SHOULD use ProgressNotification progress/total values to determin when to call this method.\n\nThe client MAY call this periodically if regular progress notifications are not recieved.\n\nThe client SHOULD NOT rely solely on polling to retrieve the result.\n\nThe server MAY cancel any in progress tool calls if the client submits too many GetToolAsyncResultRequest to the server.\n\nThe determination of what constitutes too many requests MAY be defined by the server at runtime.\n\nThe server SHOULD send a CancelNotification to the client if the request has been cancelled with a reason.", + "properties": { + "method": { + "const": "tools/async/get", + "type": "string" + }, + "params": { + "properties": { + "timeout": { + "description": "The timeout in seconds for the call tool request timeout.\n\nIf not set the server SHOULD wait indefinitely for a result or until the client session disconnects.\n\nThe server MAY return with an error if the client submits too many requests.", + "type": "integer" + }, + "token": { + "$ref": "#/definitions/AsyncToken" + } + }, + "required": [ + "token" + ], + "type": "object" + } + }, + "required": [ + "method", + "params" + ], + "type": "object" + }, "ImageContent": { "description": "An image provided to or from an LLM.", "properties": { @@ -1031,6 +1161,35 @@ ], "type": "object" }, + "JoinCallToolAsyncRequest": { + "description": "Used by the client to reconnect to or extend the keep alive of a call tool request running on the server.\n\nThe server SHOULD allow multiple clients to join a previous call tool request.\n\nThe request SHOULD still be in-flight, but due to communication latency, it is always possible that this request MAY arrive after the request has already finished.\n\nThe client MAY request a new keepAlive time on the server to extend the amount of time the client has to retrieve the result.\n\nThe async token MUST not be relied on for authorisation and servers SHOULD validate clients have permission to join this tool call via other means.", + "properties": { + "method": { + "const": "tools/async/join", + "type": "string" + }, + "params": { + "properties": { + "keepAlive": { + "description": "The number of seconds to keep results and resources generated by this call tool available on the server", + "type": "integer" + }, + "token": { + "$ref": "#/definitions/AsyncToken" + } + }, + "required": [ + "token" + ], + "type": "object" + } + }, + "required": [ + "method", + "params" + ], + "type": "object" + }, "ListPromptsRequest": { "description": "Sent from the client to request a list of prompts and prompt templates the server has.", "properties": { @@ -1078,7 +1237,7 @@ "type": "object" }, "ListResourceTemplatesRequest": { - "description": "Sent from the client to request a list of resource templates the server has.", + "description": "Sent from the client to request a list of resource templates the server has.\n\nThis SHOULD NOT include any templates to return in progress resources sent as part of a ProgressNotification", "properties": { "method": { "const": "resources/templates/list", @@ -1146,7 +1305,7 @@ "type": "object" }, "ListResourcesResult": { - "description": "The server's response to a resources/list request from the client.", + "description": "The server's response to a resources/list request from the client.\n\nThis SHOULD NOT include any in progress resources sent as part of a ProgressNotification", "properties": { "_meta": { "additionalProperties": {}, @@ -1501,6 +1660,11 @@ "$ref": "#/definitions/ProgressToken", "description": "The progress token which was given in the initial request, used to associate this notification with the request that is proceeding." }, + "resourceUri": { + "description": "An optional resource uri associated with the current progress.\n\nServers SHOULD retain these resources for a reasonable period of time to enable the client\nto retrieve the resource.\n\nIn the case of CallToolAsyncRequest initiated requests this SHOULD be at least the keepAlive\ntime of the tool call.\n\nIn synchronous scenarios the server SHOULD retain these resources at least as long as the\nrequest is in progress and MAY be longer.\n\nIf a request is cancelled the server MAY discard these resources at any time after the cancellation.\n\nThe server SHOULD NOT list in-progress resources in the response to a ListResourcesRequest.", + "format": "uri", + "type": "string" + }, "total": { "description": "Total number of items to process (or total progress required), if known.", "type": "number" @@ -1973,6 +2137,16 @@ "ServerCapabilities": { "description": "Capabilities that a server may support. Known capabilities are defined here, in this schema, but this is not a closed set: any server can define its own, additional capabilities.", "properties": { + "async": { + "description": "Present if the server offers async tool calls.", + "properties": { + "maxKeepAliveTime": { + "description": "The maximum keep alive time in seconds that the server will support for async tool calls.", + "type": "integer" + } + }, + "type": "object" + }, "completions": { "additionalProperties": true, "description": "Present if the server supports argument autocompletion suggestions.", @@ -2080,6 +2254,9 @@ { "$ref": "#/definitions/InitializeResult" }, + { + "$ref": "#/definitions/CallToolAsyncResult" + }, { "$ref": "#/definitions/ListResourcesResult" }, @@ -2327,6 +2504,10 @@ "description": "If true, this tool may interact with an \"open world\" of external\nentities. If false, the tool's domain of interaction is closed.\nFor example, the world of a web search tool is open, whereas that\nof a memory tool is not.\n\nDefault: true", "type": "boolean" }, + "preferAsync": { + "description": "If true, should ideally be called using the async protocol\nas requests are expected to be long running.\n\nDefault: false", + "type": "boolean" + }, "readOnlyHint": { "description": "If true, the tool does not modify its environment.\n\nDefault: false", "type": "boolean" diff --git a/schema/draft/schema.ts b/schema/draft/schema.ts index 0852bad4b..966368608 100644 --- a/schema/draft/schema.ts +++ b/schema/draft/schema.ts @@ -226,6 +226,138 @@ export interface ClientCapabilities { elicitation?: object; } +/** + * Used by the client to call a tool provided by the server and return a token to get the result or cancel the call at a later stage. + * + * The server SHOULD retain the result of the tool call for the number of seconds specified by keepAlive to enable the client to retrieve the result. + * + * The server MUST respond with an CallToolAsyncResult. + * + * The server MAY reduce the number of seconds specified by keepAlive if so the server MUST inform the client of the new keepAlive time in the CallToolAsyncResult. + */ +export interface CallToolAsyncRequest { + method: "tools/async/call"; + params: { + name: string; + arguments?: { [key: string]: unknown }; + keepAlive?: number + }; +} + +/** + * Used by the client to reconnect to or extend the keep alive of a call tool request running on the server. + * + * The server SHOULD allow multiple clients to join a previous call tool request. + * + * The request SHOULD still be in-flight, but due to communication latency, it is always possible that this request MAY arrive after the request has already finished. + * + * The client MAY request a new keepAlive time on the server to extend the amount of time the client has to retrieve the result. + * + * The async token MUST not be relied on for authorisation and servers SHOULD validate clients have permission to join this tool call via other means. + */ +export interface JoinCallToolAsyncRequest { + method: "tools/async/join"; + params: { + token: AsyncToken + /** + * The number of seconds to keep results and resources generated by this call tool available on the server + */ + keepAlive?: number + }; +} + +/** + * Used by the client to cancel a previous async tool call request. + * + * The request SHOULD still be in-flight, but due to communication latency, it is always possible that this request MAY arrive after the request has already finished. + * + * This request indicates that the result will be unused, so any associated processing SHOULD cease. + * + * The server SHOULD send a CancelNotification to any clients that have called or joined the associated async tool call. + */ +export interface CancelToolAsyncNotification { + method: "tools/async/cancel"; + params: { + token: AsyncToken + }; +} + +/** + * Used by the client to request the result of an async tool call running on a server to be sent to this client. + * + * The request SHOULD be complete and this request SHOULD be recieved within the keepAlive window, but due to communication latency, it is always possible that this request MAY arrive after the request has already been discarded. + * + * The server MUST return a CallToolResult with either: the result if the call was successful; an error status if the result cannot be retrieved; or a pending status if the call tool is still in progress. + * + * The client SHOULD use ProgressNotification progress/total values to determin when to call this method. + * + * The client MAY call this periodically if regular progress notifications are not recieved. + * + * The client SHOULD NOT rely solely on polling to retrieve the result. + * + * The server MAY cancel any in progress tool calls if the client submits too many GetToolAsyncResultRequest to the server. + * + * The determination of what constitutes too many requests MAY be defined by the server at runtime. + * + * The server SHOULD send a CancelNotification to the client if the request has been cancelled with a reason. + */ +export interface GetToolAsyncResultRequest { + method: "tools/async/get"; + params: { + token: AsyncToken, + /** + * The timeout in seconds for the call tool request timeout. + * + * If not set the server SHOULD wait indefinitely for a result or until the client session disconnects. + * + * The server MAY return with an error if the client submits too many requests. + */ + timeout?: number + }; +} + +/** + * Used by the client to reference a previous async tool call submitted to the server. + * + * The server SHOULD NOT rely on this for authentication or authorisation purposes + */ +export type AsyncToken = string | number; + +/** + * Used by the server to provide a reference to the client for a previously submitted async tool call. + * + * This MUST only be sent as the result of EITHER CallToolAsyncRequest or JoinToolAsyncResultRequest + * + * The token SHOULD be valid to get or cancel the result for keepAalive seconds from the received time on the server. + * + * The server SHOULD report the received time in unix time, e.g. seconds elapsed since 00:00:00 UTC on 1 January 1970 + */ +export interface CallToolAsyncResult { + /** + * The token the client can use to retrieve results, cancel the invocation or rejoin the call in the case of disconnection + * + * MUST be set if accepted is true + * + * MAY be set if accepted is false in order for the client to retrieve the error result + */ + token?: AsyncToken, + /** + * The number of seconds that the server should retain results after all sessions + * have disconnected + * + * MUST be set if accepted is true + * + * MAY be set if accepted is false in order for the client to retrieve an error result + */ + keepAlive?: number + /** + * Whether the async tool call was accepted for execution on the server. + * + * If accepted is false the server MAY still respond to GetToolAsyncResultRequest with details of the reason for rejection if so the token MUST be set. + */ + accepted: boolean +} + /** * Capabilities that a server may support. Known capabilities are defined here, in this schema, but this is not a closed set: any server can define its own, additional capabilities. */ @@ -273,6 +405,16 @@ export interface ServerCapabilities { */ listChanged?: boolean; }; + + /** + * Present if the server offers async tool calls. + */ + async?: { + /** + * The maximum keep alive time in seconds that the server will support for async tool calls. + */ + maxKeepAliveTime?: number + } } /** @@ -318,6 +460,26 @@ export interface ProgressNotification extends Notification { * An optional message describing the current progress. */ message?: string; + + /** + * An optional resource uri associated with the current progress. + * + * Servers SHOULD retain these resources for a reasonable period of time to enable the client + * to retrieve the resource. + * + * In the case of CallToolAsyncRequest initiated requests this SHOULD be at least the keepAlive + * time of the tool call. + * + * In synchronous scenarios the server SHOULD retain these resources at least as long as the + * request is in progress and MAY be longer. + * + * If a request is cancelled the server MAY discard these resources at any time after the cancellation. + * + * The server SHOULD NOT list in-progress resources in the response to a ListResourcesRequest. + * + * @format uri + */ + resourceUri?: string }; } @@ -350,6 +512,8 @@ export interface ListResourcesRequest extends PaginatedRequest { /** * The server's response to a resources/list request from the client. + * + * This SHOULD NOT include any in progress resources sent as part of a ProgressNotification */ export interface ListResourcesResult extends PaginatedResult { resources: Resource[]; @@ -357,6 +521,8 @@ export interface ListResourcesResult extends PaginatedResult { /** * Sent from the client to request a list of resource templates the server has. + * + * This SHOULD NOT include any templates to return in progress resources sent as part of a ProgressNotification */ export interface ListResourceTemplatesRequest extends PaginatedRequest { method: "resources/templates/list"; @@ -716,6 +882,15 @@ export interface CallToolResult extends Result { * should be reported as an MCP error response. */ isError?: boolean; + + /** + * Whether the tool call is pending a result. + * + * This MUST only be set in response to a GetToolAsyncResultRequest where timeout is set + * + * If not set, this is assumed to be false (the call was successful). + */ + isPending?: boolean; } /** @@ -788,6 +963,14 @@ export interface ToolAnnotations { * Default: true */ openWorldHint?: boolean; + + /** + * If true, should ideally be called using the async protocol + * as requests are expected to be long running. + * + * Default: false + */ + preferAsync?: boolean } /** @@ -1329,12 +1512,16 @@ export type ClientRequest = | SubscribeRequest | UnsubscribeRequest | CallToolRequest + | CallToolAsyncRequest + | JoinCallToolAsyncRequest + | GetToolAsyncResultRequest | ListToolsRequest; export type ClientNotification = | CancelledNotification | ProgressNotification | InitializedNotification + | CancelToolAsyncNotification | RootsListChangedNotification; export type ClientResult = EmptyResult | CreateMessageResult | ListRootsResult | ElicitResult; @@ -1365,4 +1552,5 @@ export type ServerResult = | ListResourcesResult | ReadResourceResult | CallToolResult + | CallToolAsyncResult | ListToolsResult;