Skip to content

mcp: expose request options through InvokeOptions - #1988

Open
leeweisern wants to merge 1 commit into
UsefulSoftwareCo:mainfrom
leeweisern:mcp-invoke-request-options
Open

leeweisern wants to merge 1 commit into
UsefulSoftwareCo:mainfrom
leeweisern:mcp-invoke-request-options

Conversation

@leeweisern

Copy link
Copy Markdown

MCP tool calls are capped at the client SDK's 60s DEFAULT_REQUEST_TIMEOUT_MSEC; a caller has no way to extend it, so long-running tools (deploys, builds, batch jobs) always die at the one-minute mark.

Where the options flow

executor.execute(address, args, options)
  InvokeOptions { timeoutMs, maxTotalTimeoutMs, resetTimeoutOnProgress, onProgress }
    plugin.invokeTool({ invokeOptions })          # already threaded
      invokeMcpTool
        client.callTool(args, requestOptions)   # was: callTool(args)

invokeOptions already reaches plugin.invokeTool untouched; only the MCP plugin reads the new fields. Other plugins (openapi, graphql) keep ignoring them.

New surface

interface InvokeOptions {
  onElicitation?: OnElicitation;
  /** Per-request timeout, ms. Omit → SDK default (60s). */
  timeoutMs?: number;
  /** Hard cap on the whole request, including progress-extended time. */
  maxTotalTimeoutMs?: number;
  /** Reset the request timeout on each progress notification. */
  resetTimeoutOnProgress?: boolean;
  /** Called per `notifications/progress`; supplying it requests progress. */
  onProgress?: (p: InvocationProgress) => void;
}

Mapped to RequestOptions (timeout / maxTotalTimeout / resetTimeoutOnProgress / onprogress) in packages/plugins/mcp/src/sdk/invoke.ts. signal is deliberately not exposed: Effect interruption already abandons the fiber, and a caller-held AbortSignal would have to outlive the pooled connection lease.

Progress-reset

sequenceDiagram
    participant C as caller
    participant E as executor
    participant S as MCP server
    C->>E: execute(addr, args, { timeoutMs: 60_000,<br/>resetTimeoutOnProgress: true })
    E->>S: tools/call
    S-->>E: notifications/progress (t+50s)
    Note over E: request timer resets
    S-->>E: notifications/progress (t+100s)
    Note over E: request timer resets
    S-->>E: result (t+140s)
    E-->>C: result
Loading

Design choice

Generic fields on InvokeOptions rather than a transport-specific requestOptions passthrough: callers shouldn't name MCP internals to say "let this run longer", only the MCP plugin consumes them today, and a future HTTP plugin can map the same fields onto its own timeouts.

Verification

  • vitest run src/sdk/invoke.test.ts — 9/9 pass, including three new cases: options map onto callTool RequestOptions, onprogress routes to onProgress, and omitting invokeOptions passes no RequestOptions (SDK defaults untouched).
  • vitest run src/sdk/elicitation.test.ts — 6/6 pass (pooled-connection path still works).
  • vitest run src/executor.test.ts src/promise.test.ts (core/sdk) — 35/35 pass.
  • tsgo --noEmit on packages/plugins/mcp and packages/core/sdk — clean (pre-existing lint suggestions only).
  • oxlint --deny-warnings and oxfmt on touched files — clean.

MCP tool calls are capped at the client SDK's 60s default request
timeout with no way for a caller to extend it. Long-running tools —
deployments, builds, batch operations — fail at the one-minute mark.

Add four fields to InvokeOptions (timeoutMs, maxTotalTimeoutMs,
resetTimeoutOnProgress, onProgress) and map them onto the MCP SDK's
per-request RequestOptions inside the mcp plugin's callTool hop.
InvokeOptions already threads from executor.execute() through
plugin.invokeTool, so this stays additive: callers that set nothing
keep the SDK defaults, and other plugins ignore fields they don't
consume.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant