Skip to content

SEP-1391: Long-Running Operations #1391

Description

@bzsurbhi

Preamble

Title: Asynchronous Tool Execution
Author: Surbhi Bansal, Luca Chang
Status: Proposal
Type: Standards Track
Created: 2025-08-26

Abstract

This SEP improves support for long-running operations (LRO) in the Model Context Protocol (MCP). It introduces a modified tool call operation with token-based execution tracking, with version negotiation as a means of supporting old and new tool call semantics on the same RPC method. Clients pass a unique operation token to represent a task, and servers manage background execution and serve operation statuses and results to client requests with the operation token provided. By splitting a tool call into multiple request-response methods, this avoids the need for persistent connections between the client and server to support long-running tools. This also enables tool results to be retrieved multiple times, enabling call-now, fetch-later execution and improved resilience to connection faults.

Motivation

The current MCP specification only supports single request-response tool execution, which creates significant limitations for real-world applications. While host applications or tool implementors could theoretically implement custom long-running operation handling themselves, this approach is fundamentally inadequate:

Why Host-Level Solutions Are Insufficient:

  1. Lack of Standardization: This is a common use case that many MCP servers require and are already duplicating effort to support (see customer use cases below). Server implementers want standardized approaches for supporting this to avoid redundant and error-prone efforts.
  2. Model Behavior Inconsistency: Model-driven solutions rely heavily on inconsistent behavior and prompt engineering to decide when and if to poll at all, as opposed to having a deterministic, protocol-defined construct for long-running operations. With the proposed solution, host applications will have a reliable mechanism for immediately backgrounding long-running tool calls and waking the model up later to handle their results.
  3. Limited Solutions for Timeouts: There is no standardized way to handle operations that exceed typical request timeouts across all tools. Servers need to impose timeouts to avoid resource exhaustion, but these cannot always be configured in a way that generalizes across all tools, or even across combinations of parameters in a single tool. Rather than being limited by timeouts in the network stack, this enables tools to execute for arbitrary lengths of time without holding a single connection open, and without relying on clients to terminate connections proactively.

This SEP addresses these limitations by introducing long-running operation capabilities while preserving all existing functionality.

Customer Use Cases Requiring Long-Running Operation Support

The current single request-response execution model prevents MCP from supporting common use cases that require extended processing time, on the scale of minutes to hours:

1. Healthcare & Life Sciences Data Analysis

Challenge: Amazon's customers in the healthcare and life sciences industry are attempting to use MCP to wrap existing computational tools to analyze molecular properties and predict drug interactions, processing hundreds of thousands of data points per job from chemical libraries through multiple inference models simultaneously.

Duration: Small molecule analysis requires 30-60 minutes; large molecule simulations with complex predictions take several hours.

Current Workaround: Not yet determined.

Impact: Cannot integrate with real-time research workflows, prevents interactive drug discovery platforms, and blocks automated research pipelines. These customers are looking for best practices for long-running tool calls and have noted the lack of support in MCP as a concern. If these customers do not have a solution for long-running tool calls, they will likely forego MCP and continue using their existing platforms.

Ideal: Some form of push notification system to avoid blocking their agents on long analyses, with concurrent long-running tool calls as an answer for operations executing in the range of a few minutes.

2. Enterprise Automation Platforms

Challenge: Amazon’s large enterprise customers are looking to develop internal MCP platforms to automate SDLC processes across their organizations, extending to sales, customer service, legal, HR, and cross-divisional teams. They have noted they have long-running agent and agent-tool interactions beyond typical timeouts, supporting complex business process automation.

Duration: Processing time ranges from minutes to hours depending on the task.

Current Workaround: Not yet determined. Considering an application-level system outside of MCP backed by webhooks.

Impact: Limitations related to the host doing synchronous request-response execution prevent complex business process automation and limit sophisticated multi-step operations. These customers want to dispatch processes concurrently and collect their results later, and are noting the lack of async tool calls as a concern — and are considering involved application-level notification systems as a possible workaround.

Ideal: Built-in mechanisms for managing concurrent work to avoid needing to implement notification systems specific to their own tool conventions themselves.

3. Code Migration Workflows

Challenge: Amazon has automated code migration and transformation tools to perform upgrades across its own codebases and those of external customers, and is attempting to wrap those tools in MCP servers. These migrations analyze dependencies, transform code to avoid deprecated runtime features, and validate changes across multiple repositories.

Duration: Processing time ranges from minutes to hours depending on migration scope, complexity, and validation requirements. Large enterprise workloads require extensive testing cycles.

Current Workaround: Developers implement manual tracking by splitting a job into create and get tools, forcing models to manage state and repeatedly poll for completion.

Impact: Poor developer experience due to needing to replicate this hand-rolled long-running operation mechanism across many tools. One team had to debug an issue where the model would hallucinate job names if it hadn’t listed them first. Validating that this does not happen across many tools in a large toolset is time-consuming and error-prone.

Ideal: Support SDK-controlled polling by default to support pushing a tool to the background and avoiding blocking other tasks in the chat session. The team needs the same pattern across many tools in their MCP servers, and wants a common solution across them.

4. Test Execution Platforms

Challenge: Amazon’s internal test infrastructure executes comprehensive test suites including thousands of test cases, integration tests across services, and performance benchmarks. They have built an MCP server wrapping this existing infrastructure.

Duration: Test runs can execute for hours in some services, especially for full regression testing suites.

Current Workaround: For streaming test logs, the MCP server exposes a tool that can read a range of log lines, as it cannot effectively notify the client when the execution is complete. There is not yet any workaround for executing test runs.

Impact: Cannot run a test suite and stream its logs simultaneously without a single hours-long tool call, which would time out on either the client or the server. This prevents agents from looking into test failures in an incomplete test run until the entire test suite has completed, potentially hours later.

Ideal: Support application-driven long-running tool calls, so a client can be notified when a long-running tool is complete.

5. Deep Research

Challenge: Deep research tools spawn multiple research agents to gather and summarize information about topics, going through several rounds of search and conversation turns internally to produce a final result for the caller application.

Duration: 3-5 minutes per research topic.

Current Workaround: The research tool can be split into separate tools to create a report job and get the status/result of that job later.

Impact: When using this in clients like Claude Desktop, which has a 4 minute MCP tool call timeout, tool calls will time out unpredictably if some agents take longer than usual to complete. After splitting the tool into create/get steps, the LLM runs into issues calling the get tool repeatedly — instead, it sometimes calls the get tool once before ending its conversation turn, claiming to be "waiting" before calling the tool again. It cannot resume until receiving a new user message. This also complicates expiration times, as it is not possible to predict when the client will retrieve the result when this occurs. It is possible to work around this by adding a wait tool for the model, but this prevents the model from doing anything else concurrently.

Ideal: Support backgrounding a tool call with a deterministic way to notify the model when a result is ready, so it can be immediately retrieved and the tool result can be deleted. This cannot be done generically today in any client application.

6. Agent-to-Agent Communication (Multi-Agent Systems)

Challenge: One of Amazon’s internal multi-agent systems for customer question answering faces scenarios where agents require significant processing time for complex reasoning, research, or analysis. When agents communicate through MCP, slow agents cause cascading delays throughout this system.

Duration: Several minutes for complex tasks.

Current Workaround: Not yet determined.

Impact: Synchronous communication creates cascading delays, prevents parallel agent processing, and degrades system responsiveness for other time-sensitive interactions.

Ideal: Notifications of some kind to allow agents to perform other work concurrently and get notified once long-running tasks complete.

These use cases demonstrate that long-running tool execution is not a theoretical need but a practical requirement for real MCP deployments in production environments.

Integration with Existing Architectures

Many production systems already provide async workflow execution capabilities with built-in state tracking, monitoring, and management. This proposal enables MCP servers to expose these existing APIs directly rather than forcing them into synchronous wrappers.

Benefits for Existing Async Systems:

  1. Leverage Existing State Management: Systems like Step Functions, Kubernetes, and CI/CD platforms already maintain execution state, logs, and results. MCP servers can use these systems' native identifiers (ARNs, job names, run IDs) as MCP tokens.
  2. Preserve Native Monitoring: Existing monitoring, alerting, and observability tools continue to work unchanged. The async execution happens within the existing system's context.
  3. Reduce Implementation Overhead: Server implementers don't need to build new state management, persistence, or monitoring infrastructure. They can focus on the MCP protocol mapping.

This approach simplifies integration with existing workflows and allows workflow services to continue to manage their own state, rather than making MCP servers that do nothing but poll on other services.

Specification

Core Approach: Extend Existing Tool Call with Token-Based Tracking

This proposal introduces an extension of the existing tools/call RPC call method to support long-running operations.

Async tools MUST be marked with an invocationMode field to differentiate them. SDKs MUST NOT show LRO-only tools or the invocationMode field to clients on old protocol versions.

SDKs MAY support declaring tools that support both non-LRO and LRO execution. In these cases, clients on old protocol versions will be able to see the tool definition without the invocationMode field, and will continue to call it with non-LRO semantics. Clients on new protocol versions will see the tool definition with the invocationMode field, and therefore MUST call the tool with LRO semantics.

When issuing a CallToolRequest for a long-running tool call on a supported protocol version, the client MUST provide a unique operation.token to represent the operation across requests. The server MUST validate that this token is unique before accepting new operations. This token SHOULD be scoped to the particular authorization context or session it originated from, to avoid granting unauthorized access to tool results to third parties.

The CallToolResult MAY include immediate results in its content fields to immediately provide to the client (e.g. to guide subsequent model behavior).

The result of a long-running operation does not need to be retained by the server indefinitely; the client MAY specify an operation.keepAlive value representing the time it needs the final tool result to be available for after the tool call reaches the "completed" or "failed" status. Servers MAY override this duration with a suitable duration independently, and MUST return its final value in the operation.keepAlive field in the CallToolResult. An operation.keepAlive of null represents an unlimited duration.

When an operation’s operation.keepAlive duration has elapsed, the server SHOULD move the operation into the "failed" status. Servers are not obligated to retain operation metadata indefinitely, and MAY delete the operation’s metadata at any time after the operation’s operation.keepAlive duration has elapsed, regardless of the observed state of the operation.

Servers using session-based transports SHOULD NOT delete active sessions until all operation.keepAlive durations have elapsed, unless the client requests the termination of its session. Upon session termination, servers MAY choose to interrupt ongoing async tool calls and delete any associated status, if operation.token was session-bound.

Servers MAY include an operation.pollFrequency in the immediate CallToolResult, representing (in seconds) how often clients are expected to check for the operation status. Clients SHOULD respect this value when polling for results.

Receivers of notifications/cancelled notifications for the request ID associated with the original CallToolRequest SHOULD ignore any subsequent requests using the associated token and immediately move the operation to the "canceled" status. Servers are not obligated to retain operation metadata indefinitely, and MAY delete the operation’s metadata at any time after the operation has been moved to the "canceled" status.

If a server sends the client a request or notification associated with a long-running operation, the request/notification MUST contain an _operation.token associated with the originating operation and move it into the "input_required" status. In the case of requests, the client MUST respond with the same _operation.token for association. Upon completing the associated request, the server MAY move the operation out of the "input_required" status, unless other associated requests are still pending.

An ongoing LRO can be queried with a new tools/async/status RPC call, which accepts a token representing the original CallToolRequest. When a GetOperationStatusRequest is issued, the server MUST respond with a GetOperationStatusResult containing one of the following statuses:

  • "submitted": The operation has been submitted, but has not yet begun execution.
  • "working": The operation is incomplete.
  • "input_required": The server is expecting a response from the client for e.g. elicitation or sampling.
  • "completed": The operation completed successfully.
  • "canceled": The operation was canceled. This is a terminal status.
  • "failed": The operation completed unsuccessfully. The error field SHOULD be set to the failure reason. This is a terminal status.
  • "unknown": Fallback state to denote unexpected conditions. This SHOULD be considered a terminal status.

When an asynchronous tool call is in the "completed" status, its result can be queried with a new tools/async/result RPC call, accepting a token representing the CallToolResult. When a GetOperationPayloadRequest is issued and the corresponding token matches a valid "completed" operation, the server MUST respond with a GetOperationPayloadResult, which will contain the final CallToolResult.

Modified Base Request

interface Request {
  method: string;
  params?: {
    _meta?: {
      progressToken?: ProgressToken;
      [key: string]: unknown;
    };
    /**
     * Async operation parameters, only used when a request is sent during an asynchronous tool call.
     */
    _operation?: {
      /**
       * The token associated with the originating asynchronous tool call.
       */
      token: string;
    };
    [key: string]: unknown;
  };
}

Modified Base Notification

interface Notification {
  method: string;
  params?: {
    _meta?: { [key: string]: unknown };
    /**
     * Async operation parameters, only used when a notification is sent during an asynchronous tool call.
     */
    _operation?: {
      /**
       * The token associated with the originating long-running operation tool call.
       */
      token: string;
    };
    [key: string]: unknown;
  };
}

Modified Base Result

interface Result {
  _meta?: { [key: string]: unknown };
  /**
   * Async operation parameters, only used when a result is sent in response to a request with operation parameters.
   */
  _operation?: {
    /**
     * The token associated with the originating long-running operation tool call.
     */
    token: string;
  };
  [key: string]: unknown;
}

Modified Tool Call Request

interface CallToolRequest {
  method: "tools/call";
  params: {
    name: string;
    arguments?: { [key: string]: unknown };
    // May be present in async tool call requests. Does not need to be provided.
    operation?: {
      /**
       * Client-generated token to use for tracking the operation.
       */
      token: string;
      /**
       * Number of seconds the client wants the result to be kept available upon completion.
       */
      keepAlive?: number;
    };
  };
}

Response Types

Modified Tool Call Response

type OperationStatus = "submitted" | "working" | "completed" | "canceled" | "failed" | "input_required" | "unknown";

interface CallToolResult {
  content: ToolContent[];
  isError?: boolean;
  
  // Will always be present in async tool call responses.
  operation?: {
    /**
     * Number of seconds the result will be kept available upon completion.
     */
    keepAlive: number | null;
    /**
     * Number of seconds the server suggests the client wait between status checks.
     */
    pollFrequency?: number;
    /**
     * Initial status of the async operation.
     */
    status: OperationStatus;
  };
}

Async Operation Management

Status Checking

interface GetOperationStatusRequest {
  method: "tools/async/status";
  params: {
    token: string;
  };
}

interface GetOperationStatusResult {
  /**
   * Current status of the async operation.
   */
  status: OperationStatus;
  /**
   * Error message if status is "failed".
   */
  error?: string;
}

Result Retrieval

interface GetOperationPayloadRequest {
  method: "tools/async/result";
  params: {
    token: string;
  };
}

interface GetOperationPayloadResult {
  /**
   * The result of the tool call.
   */
  result: CallToolResult["params"];
}

Enhanced Tool Definitions

interface Tool {
  name: string;
  description: string;
  inputSchema: JSONSchema;
  
  // NEW: Optional async capability declaration
  invocationMode?: "sync" | "async";
}

Tool Examples

const tools = [
  {
    name: "quick_calculation",
    description: "Perform fast mathematical calculations",
    inputSchema: { /* ... */ }
    // No invocationMode field = sync only
  },
  {
    name: "search_web",
    description: "Search the web",
    inputSchema: { /* ... */ },
    // Explicitly declaring sync-only execution
    invocationMode: "sync"
  },
  {
    name: "analyze_dataset", 
    description: "Analyze large datasets",
    inputSchema: { /* ... */ },
    // Supports async execution, may or may not support sync execution on old clients.
    // Clients that can see this property should assume async-only execution
    // (see backwards-compatibility notes).
    invocationMode: "async"
  },
];

Execution Flow

  1. Tool Discovery: Client calls tools/list (existing behavior, no filtering needed)
  2. Sync Path (Old Clients): Client calls tools/call, server executes synchronously and returns result immediately
  3. Async Path (New Clients): Client calls tools/call, server starts background execution and returns token
  4. Client Polling: Client polls tools/async/status until status is "completed", "canceled", "failed", or "unknown"
  5. Result Retrieval: Client calls tools/async/result to get final result
  6. Cleanup: Server cleans up operation data after result retrieval or expiration

Example Usage

Synchronous Execution (Existing Behavior)

const result = await client.request({
  method: "tools/call",
  params: {
    name: "quick_calculation",
    arguments: { x: 5, y: 10 },
  },
});
// Returns: CallToolResult immediately

// (Not shown) Send the result to the conversation...

Asynchronous Execution (New)

// Call tool asynchronously using dedicated async RPC call
const asyncResponse = await client.request({
  method: "tools/call", // Reuses existing RPC call
  params: {
    request: {
      name: "expensive_analysis",
      arguments: { dataset: "large_file.csv" },
    },
    // New property
    operation: {
      keepAlive: 3600,
    },
  },
});
// Returns: CallToolResult { content: [ TextContent("started analysis...") ], operation: { token: "abc123", keepAlive: 3600, pollFrequency: 5 } }

// (Not shown) Send the initial result to the conversation...

// Poll for completion deterministically
while (true) {
  const status = await client.request({
    method: "tools/async/status",
    params: { token: asyncResponse.operation.token },
  });
  
  if (status.status === "completed") {
    const result = await client.request({
        method: "tools/async/result",
        params: { token: asyncResponse.operation.token },
    });
    
    // (Not shown) Send the result to the conversation...
    
    break;
  }
  await sleep(1000 * asyncResponse.operation.pollFrequency ?? 1);
}

Error Handling

Servers MUST return standard JSON-RPC errors for the following protocol error cases:

  • Long-running operation called by an outdated client: -32600 (Invalid request)
  • Long-running operation called with an invalid or nonexistent token: -32602 (Invalid params)
  • tools/call called with a token that was already used to start a different operation: -32602 (Invalid params)
  • Internal errors: -32603 (Internal error)

Servers SHOULD provide an informative error message to describe the cause of any errors. For example, a server that fails to look up an operation by its ID MAY return the following error:

{
  "jsonrpc": "2.0",
  "id": 70,
  "error": {
    "code": -32602,
    "message": "Failed to retrieve operation: Operation not found"
  }
}

Likewise, a server that attempts to retrieve an expired operation MAY return the following error:

{
  "jsonrpc": "2.0",
  "id": 70,
  "error": {
    "code": -32602,
    "message": "Failed to retrieve operation: Operation has expired"
  }
}

Note that servers are not obligated to retain operation metadata indefinitely, and it is compliant behavior for a server to return a “not-found” error should it purge an expired operation.

Rationale

Design Decision: Extend Existing RPC Call

The decision to extend the existing RPC call rather than introducing a new, dedicated tool call type was made for the following key reasons:

API Clarity:

  • Maintaining a single tools/call RPC call maintains a single abstraction for all tool calls, regardless of invocation mode
  • Avoids divergence between non-LRO and LRO tool call data models

Host Application Simplicity:

  • Automatically filtering out LRO tools for old client versions enables older clients to continue using servers without errors
  • Maintaining the same data models enables retaining existing tool call handling paths rather than building out parallel ones
  • Predictable workflow of tool call and polling can be wrapped in a convenience method by clients for simpler host application integration

Alternative Designs Considered

Webhooks:

MCP could provide a way for clients to pass a webhook to the server, which would receive notifications and the tool result asynchronously. This would be especially useful for tasks with a distant time horizon, on the order of hours or longer.

Rejected because:

  • For remote servers or containerized local servers, this potentially requires a public endpoint for the MCP server to invoke. This is nontrivial to implement securely, especially in a desktop host application.
  • This approach may be pursued in a separate proposal as a specialized approach suitable for very-long-running operations.

Application-Level Job IDs:

Server developers can implement similar functionality on top of synchronous tool calls today:

// immediately return after tool call
CallToolResult { jobId: "00000000-0000-0000-0000-000000000000" }

// model polls in chat session
CallTool { name: "tool", arguments: { jobId: "00000000-0000-0000-0000-000000000000" } }

Rejected because:

  • This is a common use case that many MCP servers require and are already duplicating effort to support (see customer use cases above). Server implementers want standardized approaches for supporting this to avoid redundant and error-prone efforts.
  • This relies heavily on inconsistent model behavior and prompt engineering to decide when and if to poll at all, as opposed to having a deterministic, protocol-defined construct for asynchronous calls. With the proposed solution, host applications will have a reliable mechanism for immediately backgrounding long-running tool calls and waking the model up later to handle their results.
  • Client-configured timeouts and timeouts in network gateways can interrupt legitimate long-running operations before completion.

Backward Compatibility

This SEP introduces no backward incompatibilities. All existing MCP functionality remains completely unchanged:

Compatibility Guarantees

  • Existing methods unchanged: tools/call, tools/list, and all other existing methods continue to work exactly as before
  • Tool definitions backward compatible: The optional async field is additive - existing tool definitions work unchanged (controlled by protocol version negotiation)
  • Client compatibility: Existing clients continue to work without any modifications
  • Server compatibility: Servers can implement async support incrementally without affecting existing functionality

Key compatibility aspects:

  • Version detection: Servers detect client async support via protocol version or feature flags
  • Automatically filtered tool lists: Old clients can only see tools that support sync execution — SDKs may expose methods to allow an async tool to be invoked synchronously if server owners wish to support this
  • Graceful degradation: Old clients continue with existing sync functionality in sync-only and hybrid tools
  • No breaking changes: Existing tools and workflows remain unaffected, and the async field is not included for clients on old versions
  • Gradual adoption: New clients see enhanced tool capabilities when ready

Client Experience by Version

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
  ]
}

Future Work

This async tool execution foundation is designed to evolve naturally into more sophisticated workflow capabilities. The following extensions are planned for future SEPs:

Task-Based Workflows

The token-based async execution can be extended to support multi-operation workflows through task coordination:

Server-Initiated Multi-Agent Flows

The task infrastructure enables server-initiated workflows for multi-agent scenarios:

// Future: Server creates task for multi-agent workflow
async function handleExternalEvent(event, connectedClient) {
  const taskId = await server.createTask("Process Support Ticket");

  // Server executes operations within task
  const analyzeToken = await server.callToolAsync("analyze_ticket", {
    ticket: event.ticket,
    _meta: { taskId },
  });

  const classifyToken = await server.callToolAsync("classify_urgency", {
    ticket: event.ticket,
    _meta: { taskId },
  });

  // Notify client about server-initiated workflow, providing tokens
  // for individual steps
  connectedClient.notify("tasks/created", {
    taskId,
    initiator: "server",
    operations: [analyzeToken, classifyToken],
  });
}

Task Management Methods

Future task-based workflows would introduce additional methods for client monitoring:

// Task status monitoring (client-facing)
interface GetTaskStatusRequest {
  method: "tasks/status";
  params: { taskId: string };
}

interface GetTaskStatusResult {
  taskId: string;
  status: "active" | "completed" | "failed";
  operations: {
    token: string;
    name: string;
    status: "pending" | "running" | "completed" | "failed";
    dependencies: string[];
  }[];
  progress: { completed: number; total: number };
}

// Task notifications (server-initiated)
interface TaskCreatedNotification {
  method: "notifications/tasks/created";
  params: {
    taskId: string;
    initiator: "server";
    description?: string;
    operations: string[]; // Array of operation tokens
  };
}

interface TaskCompletedNotification {
  method: "notifications/tasks/completed";
  params: {
    taskId: string;
    status: "completed" | "failed";
    finalResults?: any;
  };
}

Benefits of This Foundation

By establishing async tool execution first, future task-based workflows will benefit from:

  • Proven async execution patterns - token management, polling, result retrieval
  • Resource management - cleanup, expiration, and resource limits
  • Client library support - existing async infrastructure can be extended

This foundation ensures that complex workflow capabilities can be added without disrupting the simple async execution patterns that most applications need.

Triggers and Event-Driven Workflows

Beyond async execution and task-based workflows, the MCP ecosystem will benefit from Triggers - a webhook-like mechanism for event-driven tool execution. While async execution addresses concurrency within persistent connections, Triggers will enable:

  • Event-driven activation: Tools that respond to external events without requiring persistent client connections
  • Scheduled execution: Time-based tool activation for monitoring, maintenance, and periodic tasks
  • Cross-session workflows: Operations that span multiple client sessions or survive client disconnections

The async foundation established in this SEP provides the execution primitives that Triggers will leverage for background operation management.

Activity

  1. changed the title [-]SEP: Asynchronous Tool Execution[/-] [+]SEP-1391: Asynchronous Tool Execution[/+] on Aug 26, 2025
  2. mikekistler commented on Aug 28, 2025

    @mikekistler

    I really like this design. I think it has many strengths including:

    • Complete independence of transport-level features like streams.
    • Strong focus on backward compatibility.
    • Relative simplicity, while allowing for additional features to be added later.

    A few concerns I have:

    • In a few places, the SEP makes requirements on the “SDKs”, e.g.

      SDKs MUST NOT show async-only tools or the async field to clients on old protocol versions.

      I think the MCP Spec should describe the behavior of clients and servers, which the SDKs then comply to.

    • The SEP currently makes no mention of whether or how async tool calls can utilize elicitation. Maybe this is expected to “just work”, but if so I think it should be stated.

    • Lack of support for progress notifications is a concern but could be mitigated by allowing the ToolsAsyncStatusResult to contain progress information.

    • Also missing from this SEP is support for streaming/partial results. I don’t think it is necessary to include this but it would be nice to mention it in the Future Work section and possibly outline some potential approaches.

    • It should also be possible to query the result of an asynchronous tool call when its status is “FAILED”, as the tool call can return error information in the ToolContent — just as it can today for synchronous tool calls.

    • I think this SEP depends on characteristics of sessions that are not currently defined in the spec, but should be. In particular, sessions should be able to survive disconnect and reconnect to the server.

    I also have a quibble with the assertion that

    The current MCP specification only supports synchronous tool execution

    I don't believe this is true, and for that reason I think that calling these new tool calls "async" is problematic. But naming is hard, so I won't dwell on this point too much.

  3. mikekistler commented on Aug 31, 2025

    @mikekistler

    One other detail that should be addressed: How to handle tools with "structured outputs"? Do "async" tools document that they return a CallToolAsyncResult (because they do) or do they document the shape of the actual result? I think the latter could cause breaking changes in some clients, but the former negates the benefits of structured output (unless some other means is introduced to communicate the result schema).

  4. PederHP commented on Sep 2, 2025

    @PederHP
    Member

    This will always require custom host level implementation as current LLM APIs fundamentally do not have async tool support, in the sense that they can agentically poll for completion.

    That's not to say that async tools is not a good feature and that a solution isn't needed to the challenges you describe, but any protocol defined async mechanism should have very strong guidance on how client hosts should implement this.

    Should the model be given a special SDK-bound tool to perform the polling? Should the host invoke the model at specific intervals to give it a chance to poll? Should the host poll independently of the model?

    What message type is used to return the final result to the model? A user message? Tool results in many models operate a different trust level than than user messages (ie post-training establishes these as different, I believe OpenAI even documents the different trust level). This means that some models may get confused about a tool result arriving in a later message. This can be solved by binding a special tool and doing a forced tool_choice invocation which puts the result into the expected context flow of messages (mostly).

    I think if there is no guidance, this will be a feature which is wildly inconsistent across SDKs and libraries and which will confuse many developers, as they might easily expect the models to be able to natively do async tool calls.

    Your proposal is VERY strong in describing the protocol-level mechanisms and flows - but I am worried the protocol is becoming increasingly detached from the reality of LLM integrations, and that implementations will fragment and developers get confused about what is host responsibility and what is model controlled. Structured output has already done this to a small degree (by adding a feature that is not natively supported by LLM APIs).

    I believe maintaining a clear path to implementation and making it very clear to developers what they need to implement, what the protocol provides, and what the models are capable of (at least to the extent possible) is very important to the continued health of the protocol ecosystem.

    In other words, we'd want client host developers to be aware that they have to decide how to compose their context in a way that presents the async result to the model in the right way, allows for agentic polling (if relevant), whether to inject in the next inference loop iteration or trigger one, whether they need to block incoming injections during certain agentic operations, etc. And ideally with rich samples and guidance in the SDKs.

    Also note that passing async: true to the provider api will fail as they have strict validation. Hosts or sdks will need to lift this into the description field. Tools schemas are not extensible at the api level, so the spec should include guidance on this.

  5. 000-000-000-000-000 commented on Sep 5, 2025

    @000-000-000-000-000
    Contributor

    Important concerns to address:

    1. Backwards compatability needs double click on how it will work to ensure that old clients have an easy way to talk to new servers w/o a lot of heavy lifting on servers. Current proposal is: servers that 'accept' a version downgrade are expected to filter out async tools.
    2. A lot of agents want to invoke the async status check as a tool. We need to figure out how we are going to make this simple.

    Useful doc: https://docs.google.com/document/d/1nvaK7fHX5czTr2aDE-RtMckUjbWMxSbK1FG0Gnk0izY/edit?tab=t.0#heading=h.mzviwb4vjk02

  6. mikekistler commented on Sep 6, 2025

    @mikekistler

    I haven't thought this through completely, but I wonder if we need the concept of "async" tools at all. Instead, we can simply use the RPC message id, which already must be unique within a session, rather than the token returned from the async tool call as the "id" for tool status and result calls. Then we could extend these to support all tool calls, and that would provide a "polling" alternative to progress notifications.

  7. ggoodman commented on Sep 10, 2025

    @ggoodman
    Contributor

    Nit: In the enumeration of polled states, you have:

    status: "PENDING" | "COMPLETED" | "FAILED" | "EXPIRED";

    How will servers distinguish "EXPIRED" calls from non-existent calls? Assuming an expired call is purged from durable storage, this distinction might be unreasonably difficult to make.

    I'd suggest dropping that enumeration option and clarifying what JSON-RPC-level error would look like for tokens whose referenced state is not found.

  8. ggoodman commented on Sep 10, 2025

    @ggoodman
    Contributor

    What is the value of separating the polling method from the query of the final state? Are there some ugly trade-offs in combining these two methods that I'm not seeing?

  9. ggoodman commented on Sep 10, 2025

    @ggoodman
    Contributor

    Instead of async?: boolean, might I suggest something along the lines of

    interface Tool {
      name: string;
      description: string;
      inputSchema: JSONSchema;
    
      // NEW: Optional async capability declaration
      invocationModes?: InvocationMode[];
    }
    
    type InvocationMode = "sync" | "async";

    The name 'invocation mode' is just a straw man. The key part is staying open to future invocation modes without having a boolean matrix explosion.

    Side-note: I kept the feedback into distinct messages for the distinct ideas. I also want to mention that I love the idea of this proposal.

  10. 72 remaining items

  11. LucaButBoring commented on Oct 10, 2025

    @LucaButBoring
    Contributor

    While the current specification does not use the word idempotency explicitly, they do require “Unlike base JSON-RPC, the ID MUST NOT be null” and “The request ID MUST NOT have been previously used by the requestor within the same session”. Given that, we have used the session ID + the request ID for idempotency successfully; I think spelling this out explicitly in the specification would be a great addition!

    Apologies if I sound like a broken record, but the server can use the session ID + request ID for idempotency, which is exactly what we do for this situation in our MCP servers.

    Is that actually allowed under the current specification? If the client can only use a request ID once, it would follow that it cannot reuse the same request ID again for any reason, including retrying a request. The client also only knows if a request has been accepted once it receives a response. It would be noncompliant to use request IDs like that, if I'm reading the spec correctly.

    Do you have specific user stories in mind for when a user would want an operation to be “dispatch[ed] from one invocation and get its result from another”, or “transfer an operation between sessions”?

    As a trivial one: I start a conversation on one device, and then I either move to another or just restart my application. That at the very least requires either splitting the invocations or doing server-side reverse-proxying like Claude Desktop does. As for sessions, one goal here is to simply not require sessions for this (acknowledging e.g. #1372 and #1442). Not requiring sessions reduces the amount of information necessary to restore an operation - I don't need to maintain request IDs, session IDs, and an arbitrary number of stream pointers, I can just (at a minimum) keep track of the operation token.

    If we don't require sessions, then being able to transfer an operation between sessions is mostly just a side-effect.

    Our interpretation is that the specification has the concept of sessions to create a natural mapping to a conversation, e.g., in Claude, ChatGPT, VS Code, etc. So I’m having a hard time coming up with user stories where I’d want to say something like, “remember the other day in conversation XYZ where I asked you to go book me a vacation, what’s the status of that?” But maybe! Of course, if I want to do that, I see no reason why I shouldn't be able to do that for all operations. Said another way, how am I as a user to know that some MCP operations I can recover across conversations vs which ones I can't?

    I disagree that the intent of sessions is to map to a conversation - sessions are both optional and sHTTP-specific, and the old SSE transport didn't need them at all. I think it's a mistake to treat sessions as more than a transport-specific concept irrelevant to the rest of the spec.

    As a user, you would not know which MCP operations could be recovered or even be aware that they can be, but the client would use operation tokens under the hood to retrieve tool results across requests, which the host application could then choose how to expose (the need for docs on integration patterns has been noted, though I've explained a few previously). I would like to see that used to avoid blocking a conversation on a single tool call without LLM polling, but it could be used very simply to just e.g. move between devices without a reverse-proxy in the mix, keeping the operation as blocking in a conversation.

    I wouldn't generally expect them to be used for cross-conversation tool calls, but that would technically be allowed (you would need a tool exposed to the model to look up conversations and then also retrieve operation tokens from that and pass them to the client).

    If we have some really strong user stories for this then it really begs the question: should we be simplifying by removing sessions all together and requiring that an application-level request ID is globally unique?

    That is compelling, though the request ID change would be fully backwards-incompatible due to request IDs not being globally-unique today, which could not be gated based on e.g. protocol versioning. That's actually part of what #1612 is proposing (despite the name), and I'm very interested in seeing where that goes - I think it would be a good thing if it gets accepted.

    That is pretty unsettling as an MCP client and server implementor, because it adds significant complexity for me to implement, especially if the current specification + SEP-1335 can already support all of these examples.

    Let’s say that both SEP-1335 and this SEP get accepted. Can I as an MCP server / tool implementor assume that MCP clients will correctly implement the spec as proposed by SEP-1335 and therefore I can perform my long running operations idempotently and safely following just that? Or will I have to make our MCP servers look at the InitializeRequest.clientInfo.name and start doing special casing that is reminiscent of the User-Agent drama that has plagued the web forever? I hope not the latter! I hope we can have a single way to do things, for both short running and long running operations.

    As it turns out today, VS Code does not fully implement the specification around resumability and redelivery and we’ve already had to special case it in our MCP servers. Rather than giving VS Code an entirely new way to implement things, I’d love to see it, and the industry in general, converge on a single way to do things, instead of potentially causing further fragmentation and partially implemented clients; that’s less productive for everyone.

    I'd need clarification on what you mean by this - #1335 only changes SSE stream semantics, it doesn't change what happens when the client sends a POST request, if I'm reading it correctly. I am also extremely interested in what VSC is doing that is noncompliant, as that should imply that the TS SDK itself is noncompliant, which Claude Code and others also use.

  12. benh commented on Oct 10, 2025

    @benh

    Would you be kind enough to share a few words about the SDKs used or custom code developed to make this work in practice in a multi-node environment. I made an earlier comment in the thread on the point about Last-Event-Id. Intuitively, the SSE channels model of POST, Fail, Resume on GET feels... a bit untidy but not unworkable.

    Most definitely. The custom code I'll reference here comes from durable-mcp, which is built on top of the official Python SDK. Here's the relevant things we've done to make this work:

    • We have a custom StreamableHTTPASGIApp: official vs durable-mcp

      This enables us to route session messages appropriately across nodes. Even when doing local development every durable-mcp app is run across multiple Python processes to simulate how it runs in the cloud (by default we run 4 processes but you can configure this with --partitions=N).

      It's also where we special case VS Code's interpretation of the spec from our own. For VS Code we send all messages, regardless of which request "stream" they're actually a part of, on the single GET stream. See discussion on SEP-1335 for more on this.

    • We do use the official StreamableHTTPServerTransport though, nothing custom there, but we do have our own concrete implementation of the official EventStore abstract base class.

      Of note here the official Python SDK effectively uses request ID as a stream ID. This is not sufficiently distinct because most clients use incrementing integers for request IDs, so our EventStore has to also take into account the current session ID, in order to construct event IDs that we can map back to the appropriate stream. I definitely think there is room to improve both the specification and the official Python SDK implementation in this regard (amongst other things that we're trying to take to relevant issues or SEPs and therefore am not enumerating here).

    • On the client side we’ve created our own connect and reconnect helpers that wrap the official streamablehttp_client.

      These allow us to reconnect after a failure and ensure we don’t start using request IDs from 0 again! FWIW, this is another area of the official Python SDK I’d love to see improved.

    It's also worth bearing in mind that STDIO servers don't have quite the same integration/threading model (e.g. both Mcp-Session-Id and Last-Event-Id are transport specific) - so in cases of interruption I think we need to clear up what's expected.

    100% agree. And this should be cleared up regardless of whether the operation is short or long running, because nothing precludes a short running operation from e.g., having side-effects.

  13. jonathanhefner commented on Oct 10, 2025

    @jonathanhefner
    Member

    I'd need clarification on what you mean by this - #1335 only changes SSE stream semantics, it doesn't change what happens when the client sends a POST request, if I'm reading it correctly.

    I believe @benh means that with SEP-1335 servers may disconnect at will, so an LRO tool does not have to maintain a long-running connection (as I mentioned in #1391 (comment)). Indeed, an LRO tool could open an SSE stream, send an initial event ID to prime the client, and then immediately disconnect. The client could then use the event ID to resume the stream after the retry interval had elapsed, and repeat that process until it receives the tool call result.

    That said, I understand that some existing infrastructure is not designed with SSE streams in mind. I have put together a proposal to allow servers to avoid SSE streams when serving long-running requests, including supporting elicitation and sampling, and not requiring sessions: https://docs.google.com/document/d/1j7JzIdGL91JesCzKzgrTtoFdHEyDPRjArXoqRwjo6QA/edit

    I intend to submit an SEP soon, but I would like to gather more feedback first. (Feel free to leave a comment on the doc or in the Discord thread.)

  14. benh commented on Oct 11, 2025

    @benh

    Is that actually allowed under the current specification? If the client can only use a request ID once, it would follow that it cannot reuse the same request ID again for any reason, including retrying a request. The client also only knows if a request has been accepted once it receives a response. It would be noncompliant to use request IDs like that, if I'm reading the spec correctly.

    The fact that the client cannot retry is exactly the deficiency that I believe needs to be addressed in the spec. So to clarify: no, our clients are not retrying the request, they are just trying to resume the stream to get the response, which has problems. And to restate what I posted before: I think there is a relatively straightforward improvement to the specification that can be made to fix this along the lines of “allowing a client to retry with the same request ID and some indicator that it is a retry”, but I’ll leave that discussion to SEP-1335.

    We have used the combination of session ID + request ID on the server for any idempotent retrying, as that combination is unique and should not exist again.

    I disagree that the intent of sessions is to map to a conversation - sessions are both optional and sHTTP-specific, and the old SSE transport didn't need them at all. I think it's a mistake to treat sessions as more than a transport-specific concept irrelevant to the rest of the spec.

    It does make me wonder why they introduced sessions at all then? Our interpretation may very well be wrong (any previous specification authors care to chime in?) That being said, it is very useful to be able to scope calls into some kind of grouping related to the conversation they are having, as highlighted recently here.

    I am also extremely interested in what VSC is doing that is noncompliant, as that should imply that the TS SDK itself is noncompliant, which Claude Code and others also use.

    When an HTTP connection terminates where VS Code was waiting for a response to a request, it doesn't resume that "stream" using a Last-Event-ID. In fact, it doesn't resume the stream at all! See here.

    However, VS Code will happily take responses on what it calls the "backchannel", i.e., as outlined in this part of the spec. Our read of the spec is that we should not be sending stream specific messages on that GET / "backchannel", but when we do that for VS Code it all works. 🤷‍♂️

    IMO the immediate concerns of the specification should be addressing these possible divergences, so we don't find ourselves following down the User-Agent footsteps.

  15. LucaButBoring commented on Oct 13, 2025

    @LucaButBoring
    Contributor

    Ah, I wasn't aware VS Code had its own entirely-custom MCP client implementation - that reads like a bug that should be fixed (cc: @connor4312). In particular, it seems to go directly against this clause about how the non-resumed GET stream is used:

    The server MUST NOT send a JSON-RPC response on the stream unless resuming a stream associated with a previous client request.

    This SEP does not resolve ambiguities around how sHTTP streams should be used, and stream selection does deserve a dedicated SEP to clarify it.


    @benh

    The fact that the client cannot retry is exactly the deficiency that I believe needs to be addressed in the spec. So to clarify: no, our clients are not retrying the request, they are just trying to resume the stream to get the response, which has problems. And to restate what I posted before: I think there is a relatively straightforward improvement to the specification that can be made to fix this along the lines of “allowing a client to retry with the same request ID and some indicator that it is a retry”, but I’ll leave that discussion to SEP-1335.

    This is valid, actually - and in hindsight, I think I've let this SEP discussion conflate LROs as proposed with the general idea of long-running operations as a concept (this was also a problem when we were calling it "async"). This SEP does have side-effects that benefit scaling, but those were not the intended effect of the SEP as a whole - and it was a mistake on my part to use those as points, because that burdens this SEP with the orthogonal concerns of the transport discussion space, where there are better proposed solutions for e.g. avoiding long-running connections and sessions (#1335, #1442, #1597, etc.). Transport-level solutions can mitigate the challenges associated with scaling MCP servers that need to perform long-running tool calls, but that does not make them easier to work with within the protocol, across all transports.

    In particular:

    1. Even with transport-level improvements for long-running RPC calls, clients need a way to know if a tool is expected to return quickly or not to have branching behavior in e.g. a chat UX. If this was the beginning and end of the problem, an annotation would be sufficient for this, as @mikekistler has noted. However, I think there's more to it than that.
    2. Even if a client knows that a tool is long-running, it cannot know the status of an ongoing tool call that it has deferred a result for. SEP-1597: HTTP REST Transport for Scalability, Fault Tolerance, and many other features #1597 solves this by introducing endpoints to retrieve and advance a tool call's state, but sHTTP has no equivalent mechanism. SEP-1335: Address Streamable HTTP transport issues #1335 might be a solution for sHTTP, but the point is that every transport (existing and proposed) needs an independent mechanism for this, and the intent is always for the transport itself to abstract that behavior. This becomes relevant for stdio because it has no extension points for equivalent functionality - the reason this matters for stdio at all is because it is relevant for clients to be able to check the status of an LRO even if there is an active stream for it.

    If an LRO has stopped sending progress notifications, for example, I as either a client or a host application don't know if this is because the tool call got lost or if it's because the tool is just doing something expensive and can't send notifications, yet. The transport can poll as many times as it wants, but all that could tell me is whether or not there are new messages available, not if I should expect to ever receive a result in the future. Even if it could tell me that: in a transport-level solution, that is intentionally abstracted from me, necessitating application-level RPC methods for me to query a call's state in a transport-agnostic way. Once we've changed the semantics of LROs versus non-LROs, I think that is a significant-enough change to warrant communicating LROs as a ListTools-level property with special behavior, and not just adding an annotation.

    We still rely on the underlying transport semantics to actually deliver messages from the server to the client, but that is a distinct and separate concern from knowing what the state of the tool call itself is.

    This is also why we need to introduce a dedicated operation token for idempotency over LROs, as sessions only exist in sHTTP and are optional, and request IDs on their own are not guaranteed to be unique. Even if #1372 or #1442 is accepted, session IDs will continue to be optional. Regarding idempotency, that matters in stdio as well, both because network-related faults are not the only case where a retry is desirable and due to cases like mcp-remote, where proxies are being used and retry conditions may vary between actors.

    I want to distinguish that from concerns about session management and stream selection in sHTTP - operation tokens could be used as stream keys (I do this in the Python implementation), but they do not need to be, and this SEP should make no statements clarifying or further-confusing how sessions or SSE streams are managed. Those are problems unique to sHTTP, and dedicated SEPs should update the guidance here, or propose a successor to it altogether (as #1597 does).

    I will adjust the SEP framing again to explicitly exclude sHTTP concerns as being in-scope. The principal goal of this SEP is to make it as easy as possible to integrate with existing async workflows without needing to poll on the server (within the tool implementation - not looking at the transport) or resort to model-driven polling.

  16. jonathanhefner commented on Oct 13, 2025

    @jonathanhefner
    Member

    This becomes relevant for stdio because it has no extension points for equivalent functionality - the reason this matters for stdio at all is because it is relevant for clients to be able to check the status of an LRO even if there is an active stream for it.

    Why does this need an entirely new category of tools though? What is stopping us from adding a "get status" method that works with any tool?

    It seems like the missing piece is a unique identifier that the client can use to retrieve the status (even without a session). As proposed in #1391 (comment), why not attach that identifier to a notification instead of a tool call result? It could be a progress notification or a brand new type of notification. The notification could even be sent automatically by the SDK on tool start in order to transparently support all tools.

    This is also why we need to introduce a dedicated operation token for idempotency over LROs, as sessions only exist in sHTTP and are optional, and request IDs on their own are not guaranteed to be unique. [...] Regarding idempotency, that matters in stdio as well, both because network-related faults are not the only case where a retry is desirable and due to cases like mcp-remote, where proxies are being used and retry conditions may vary between actors.

    I think what you're saying is that we need a data-layer idempotency token for any tool call or potentially non-idempotent request, correct? If so, I agree. (To be clear: I do not think it makes sense to limit such a token to LRO tools.)

  17. LucaButBoring commented on Oct 14, 2025

    @LucaButBoring
    Contributor

    Why does this need an entirely new category of tools though? What is stopping us from adding a "get status" method that works with any tool?

    We could do this, but then there's no concept of an immediate result, so it's unclear if a call was accepted or not (the transport layer knows this in sHTTP but not stdio; the data layer does not know this).

    It seems like the missing piece is a unique identifier that the client can use to retrieve the status (even without a session). As proposed in #1391 (comment), why not attach that identifier to a notification instead of a tool call result? It could be a progress notification or a brand new type of notification. The notification could even be sent automatically by the SDK on tool start in order to transparently support all tools.

    Making it part of some sort of response makes it more reliable than a notification, for the same reason - a notification may never come, I as a client do not know when to expect it, and even if I did, that complicates request management if I wanted to e.g. block until I get that confirmation. I would need to return from e.g. call_tool not just when I receive a response, but alternatively only after I get that notification.

    It also prevents choosing to actively request and receive a tool result at a later time (the keepalive logic), as that relies on the status coming from the server to establish the final negotiated keepalive duration, so the only meaningful value for backfilling existing tools would be 0, which is a nonsensical value for result retrieval purposes.

    I think what you're saying is that we need a data-layer idempotency token for any tool call or potentially non-idempotent request, correct? If so, I agree. (To be clear: I do not think it makes sense to limit such a token to LRO tools.)

    Yes, and that's a fair callout, I think in isolation, the operation token doesn't need to be bound to LROs specifically - I can adjust that.

  18. LucaButBoring commented on Oct 14, 2025

    @LucaButBoring
    Contributor

    @benh

    That being said, it is very useful to be able to scope calls into some kind of grouping related to the conversation they are having, as highlighted recently here.

    For reference, what server is this? Interested in reading this in detail to understand this independently of this SEP.

  19. benh commented on Oct 14, 2025

    @benh

    For reference, what server is this? Interested in reading this in detail to understand this independently of this SEP.

    Apps SDK: https://discord.gg/DwazBXG58B

    That link was in the #questions channel. Hope that helps!

  20. jonathanhefner commented on Oct 14, 2025

    @jonathanhefner
    Member

    Making it part of some sort of response makes it more reliable than a notification, for the same reason - a notification may never come, I as a client do not know when to expect it, and even if I did, that complicates request management if I wanted to e.g. block until I get that confirmation.

    The tool definition could include an annotation indicating it will send a notification, so the client would know to expect it. Current SDK implementations aside, how is blocking until you get a JSON-RPC notification different than blocking until you get a JSON-RPC response?

    In the LRO meeting earlier, someone asserted that some servers don't support sending notifications. I do not fully understand that claim. A notification is just information that a tool emits. If a tool can emit its status (which we are say it will), then it should be able to emit a notification. A notification message might be stored differently than a status string, but the principle is the same. Could someone explain the issue in more detail?


    An alternative approach is to have the client specify the token used for the "get status" method, similar to how the client specifies a progress token. We currently don't stipulate a particular format for progress tokens, but we could say that a "get status" token should be sufficiently random (e.g., UUID-ish).

    So if a tool advertises (via an annotation) that it supports "get status", the client can include a "get status" token in the request, and subsequently call the "get status" method with the "get status" token. No notification (or tool result) necessary.


    Another alternative is to actually use the progress token for this purpose, but only when specified in a particular format. For example, "modelcontextprotocol.io/request/${uuid}" (bikesheddable). With this approach, the "get status" method could work for any tool (no annotation needed), and could also return the most recent progress information.

  21. LucaButBoring commented on Oct 16, 2025

    @LucaButBoring
    Contributor

    Current SDK implementations aside, how is blocking until you get a JSON-RPC notification different than blocking until you get a JSON-RPC response?

    The SDK implementation was actually what I had in mind - it's difficult to model this, but not actually impossible, so I'll set that aside.

    In the LRO meeting earlier, someone asserted that some servers don't support sending notifications. I do not fully understand that claim. A notification is just information that a tool emits.

    I wasn't the one who said this, but just providing my thoughts: I think this (once again) comes back to conflating LROs with transport concerns. The only reason a server would not support sending notifications in general is if the server uses sHTTP but doesn't support SSE streams, and that concern is out-of-scope for this SEP. Separately, it is true that most things that use notifications are represented as capabilities, but that doesn't mean that supporting notifications in general is optional.

    If a tool can emit its status (which we are say it will), then it should be able to emit a notification. A notification message might be stored differently than a status string, but the principle is the same.

    I will note that this isn't the same thing - statuses as defined by this proposal are not emitted in any sense. A status is a property of an LRO, and it will be present on the LRO metadata that the client actively requests through a JSON-RPC request.

    The tool definition could include an annotation indicating it will send a notification, so the client would know to expect it.

    With this stipulation in place, I see how this can work, and I don't have any concrete opposition to it.

    I would certainly prefer for this not to rely on SSE in its sHTTP implementation, but that is again not a refutation that is relevant to this proposal.

    An alternative approach is to have the client specify the token used for the "get status" method, similar to how the client specifies a progress token. We currently don't stipulate a particular format for progress tokens, but we could say that a "get status" token should be sufficiently random (e.g., UUID-ish).

    That is actually how things work in this proposal (operation tokens must be globally-unique to be idempotent), but it's not a workaround on its own, as a client-specified token alone doesn't enable the server to return the negotiated keepalive or the suggested polling frequency.

    However, thinking about it more, we could move this information into GetOperationStatusResult, which does address that concern. I'm leaning towards this solution, actually - it'd make the whole flow much cleaner.

    Another alternative is to actually use the progress token for this purpose, but only when specified in a particular format.

    😭 (really want to avoid special-casing existing constructs like this)


    As an aside, following discussions with @mikekistler and others (and @dsp-ant has independently suggested this) we'll be renaming this again from "LROs" to "Tasks," as that's much more obvious about the intent and does not confuse this with transport SEP discussion.

    In the proposal's "future work", we talk about "tasks" as something this can be extended to, but I'm actually going to rephrase that to "client-dictated tasks" and turn that into an alternative, not future work. One concept associated with the idea of "tasks" as a general concept is composability (subtasks), which that future work section was really meant to discuss, but that might be a straightforward extension of this SEP's form of tasks already, though I still need to think about potential designs.

    It'd probably be possible to have "dependent tool calls" as a model for subtasks and subdivide input_required further to accommodate that, but I'd need to reason about the flow to be sure - you'd need to do something special (possibly a more exotic something special from what we've already proposed) on the host application side to make sure the tool requests and responses are sequenced correctly. Leaning towards a follow-up SEP for that as I want to make sure this solves the most basic problems on its own.

  22. LucaButBoring commented on Oct 17, 2025

    @LucaButBoring
    Contributor

    Just a heads-up: I'll be publishing a superseding SEP for the rewritten version of this, as it'll be significantly changed in content, despite the protocol operations being similar. That will be done by EOD Monday at the very latest (trying to finish this much sooner, though).

  23. LucaButBoring commented on Oct 20, 2025

    @LucaButBoring
    Contributor

    Alright, thank you everyone for your inputs on this SEP! As noted previously, multiple rounds of discussion have revealed both shortcomings of some technical details of this proposal in addition to scope concerns related to this proposal adopting transport-related language and attempting to straddle both protocol layers, despite being largely unrelated to transports at a functional level.

    I'm superseding this SEP with SEP-1686, which isolates the scope of the proposal to the data layer of the protocol (RPC methods etc.). It also addresses some longtime/original concerns with this proposal around limiting tasks to tool calls and introducing a strict bifurcation between "sync" and "async" (using these terms loosely) tool calls in general. #1686 tweaks how tasks are initiated in a way that happens to generalize them to all JSON-RPC requests, hopefully resolving this concern.

    I invite everyone to review the latest revision and continue discussion there. Tagging @bzsurbhi to close.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

SEPproposalSEP proposal without a sponsor.

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions