The primary goal is to allow a server to execute complex tool logic that requires multiple interactions with the client (like elicitation or LLM sampling) without maintaining any state between requests. This is achieved by serializing the saga's state into an opaque token that is passed back and forth.
- Server Benefits: Simplifies server implementation, enhances scalability, and improves resilience. A server can be restarted without losing the progress of an in-flight tool call.
- Client Responsibilities: The client acts as the state holder, responsible for driving the saga forward by responding to server requests and passing the state token back.
Two new methods are proposed: tools/callSaga
to initiate the process and tools/resumeSaga
to continue it.
This is the entry point for a stateless tool call saga. It is sent from the client to the server and is analogous to the existing tools/call
request.
- Method:
tools/callSaga
- Params:
name
:string
- The name of the tool to invoke.arguments
:object
- The initial arguments for the tool.
Sent by the client to continue a suspended saga. It provides the results of the steps requested by the server in the previous turn.
- Method:
tools/resumeSaga
- Params:
sagaToken
:string
- The opaque state token received from the server in the lastToolCallSagaResult
.completedSteps
:SagaStepResult[]
- An array containing the results for all steps that have been completed in the saga thus far. This ensures the server receives the full context on every resume call.
To support this flow, several new data structures are required.
This is a new type of result, returned by the server when a tool call cannot be completed immediately and requires further action from the client.
state
:'suspended'
- A literal string indicating the saga is paused.sagaToken
:string
- An opaque, server-generated token. This token encapsulates the entire state of the server-side saga logic. The client MUST return this token verbatim in the subsequenttools/resumeSaga
request. For security, servers SHOULD sign or encrypt this token to prevent tampering.steps
:SagaStep[]
- An array of one or more actions the client needs to perform.
Represents a single action the client must take. Each step has a unique stepId
for correlation.
-
ElicitationStep
:type
:'elicitation'
stepId
:string
params
: The parameters for anelicitation/create
request (e.g.,message
,requestedSchema
).
-
SamplingStep
:type
:'sampling'
stepId
:string
params
: The parameters for asampling/createMessage
request (e.g.,messages
,modelPreferences
).
Sent by the client within the tools/resumeSaga
request to provide the outcome of a specific step.
stepId
:string
- The ID of the step this result corresponds to.result
: The result object from the completed step (e.g., anElicitResult
or aCreateMessageResult
).error
: An error object if the step failed on the client side (e.g., the user rejected the elicitation).
Here is the step-by-step flow based on your example, using the proposed new structures.
1. Client Initiates the Tool Call
The client starts the saga.
// Client -> Server
{
"jsonrpc": "2.0",
"id": "client-req-1",
"method": "tools/callSaga",
"params": {
"name": "complex_tool",
"arguments": { "initial_arg": "value" }
}
}
2. Server Suspends for Elicitation
The server's logic determines it needs user input. It suspends the saga and sends back an elicitation request.
// Server -> Client
{
"jsonrpc": "2.0",
"id": "client-req-1",
"result": {
"state": "suspended",
"sagaToken": "opaque-server-state-token-v1",
"steps": [
{
"type": "elicitation",
"stepId": "step-elicitation-A",
"params": {
"message": "Please provide the deployment target:",
"requestedSchema": {
"type": "object",
"properties": { "target": { "type": "string" } },
"required": ["target"]
}
}
}
]
}
}
3. Client Resumes with Elicitation Result
The client performs the elicitation, gets the data from the user, and resumes the saga, providing the result for the first step.
// Client -> Server
{
"jsonrpc": "2.0",
"id": "client-req-2",
"method": "tools/resumeSaga",
"params": {
"sagaToken": "opaque-server-state-token-v1",
"completedSteps": [
{
"stepId": "step-elicitation-A",
"result": {
"action": "accept",
"content": { "target": "production" }
}
}
]
}
}
4. Server Suspends for LLM Sampling
The server processes the elicitation result and now needs to sample an LLM. It generates a new state token and requests the sampling step.
// Server -> Client
{
"jsonrpc": "2.0",
"id": "client-req-2",
"result": {
"state": "suspended",
"sagaToken": "opaque-server-state-token-v2",
"steps": [
{
"type": "sampling",
"stepId": "step-sampling-B",
"params": {
"messages": [
{
"role": "user",
"content": { "type": "text", "text": "Is deploying to 'production' safe right now?" }
}
],
"maxTokens": 100
}
}
]
}
}
5. Client Resumes with All Completed Step Results
The client performs the sampling and resumes the saga again. This time, it includes the results from both the elicitation and the sampling steps to provide the server with full context.
// Client -> Server
{
"jsonrpc": "2.0",
"id": "client-req-3",
"method": "tools/resumeSaga",
"params": {
"sagaToken": "opaque-server-state-token-v2",
"completedSteps": [
{
"stepId": "step-elicitation-A",
"result": {
"action": "accept",
"content": { "target": "production" }
}
},
{
"stepId": "step-sampling-B",
"result": {
"role": "assistant",
"content": { "type": "text", "text": "Yes, all systems are green." },
"model": "client-side-llm-v2"
}
}
]
}
}
6. Server Completes the Tool Call
The server now has all the information it needs. It completes the business logic and returns a final CallToolResult
. The saga is now concluded.
// Server -> Client
{
"jsonrpc": "2.0",
"id": "client-req-3",
"result": {
"content": [
{ "type": "text", "text": "Deployment to production initiated successfully based on confirmation." }
]
}
}
// In mcp-schema.ts
// New Request types
export interface CallToolSagaRequest extends Request {
method: 'tools/callSaga';
params: {
name: string;
arguments?: { [key: string]: unknown };
};
}
export interface ResumeToolSagaRequest extends Request {
method: 'tools/resumeSaga';
params: {
sagaToken: string;
completedSteps: SagaStepResult[];
};
}
// New Result type for suspended sagas
export interface ToolCallSagaResult extends Result {
state: 'suspended';
sagaToken: string;
steps: SagaStep[];
}
// Union type for different saga steps
export type SagaStep = ElicitationStep | SamplingStep;
export interface ElicitationStep {
type: 'elicitation';
stepId: string;
params: ElicitRequest['params']; // Reuse existing ElicitRequest params
}
export interface SamplingStep {
type: 'sampling';
stepId: string;
params: CreateMessageRequest['params']; // Reuse existing CreateMessageRequest params
}
// Structure for returning step results
export interface SagaStepResult {
stepId: string;
result?: ElicitResult | CreateMessageResult;
error?: { code: number; message: string; data?: unknown };
}
// Final result remains the same
// type CallToolResult = ...