mirror of
https://github.com/modelcontextprotocol/servers.git
synced 2026-04-21 21:35:15 +02:00
feat: add async sampling and elicitation tools
Add tools that demonstrate bidirectional MCP tasks where the server sends requests to the client for async execution: - trigger-sampling-request-async: Send sampling request with task params, client creates task and executes LLM call in background, server polls for completion and retrieves result - trigger-elicitation-request-async: Same pattern for user input, useful when user may take time to fill out forms Both tools: - Check client capabilities (tasks.requests.sampling/elicitation) - Accept both CreateTaskResult and direct result responses - Poll tasks/get for status updates - Fetch final result via tasks/result Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,8 @@
|
|||||||
- `toggle-subscriber-updates` (tools/toggle-subscriber-updates.ts): Starts or stops simulated resource update notifications for URIs the invoking session has subscribed to.
|
- `toggle-subscriber-updates` (tools/toggle-subscriber-updates.ts): Starts or stops simulated resource update notifications for URIs the invoking session has subscribed to.
|
||||||
- `trigger-sampling-request` (tools/trigger-sampling-request.ts): Issues a `sampling/createMessage` request to the client/LLM using provided `prompt` and optional generation controls; returns the LLM's response payload.
|
- `trigger-sampling-request` (tools/trigger-sampling-request.ts): Issues a `sampling/createMessage` request to the client/LLM using provided `prompt` and optional generation controls; returns the LLM's response payload.
|
||||||
- `simulate-research-query` (tools/simulate-research-query.ts): Demonstrates MCP Tasks (SEP-1686) with a simulated multi-stage research operation. Accepts `topic` and `ambiguous` parameters. Returns a task that progresses through stages with status updates. If `ambiguous` is true and client supports elicitation, pauses with `input_required` status to gather clarification.
|
- `simulate-research-query` (tools/simulate-research-query.ts): Demonstrates MCP Tasks (SEP-1686) with a simulated multi-stage research operation. Accepts `topic` and `ambiguous` parameters. Returns a task that progresses through stages with status updates. If `ambiguous` is true and client supports elicitation, pauses with `input_required` status to gather clarification.
|
||||||
|
- `trigger-sampling-request-async` (tools/trigger-sampling-request-async.ts): Demonstrates bidirectional tasks where the server sends a sampling request that the client executes as a background task. Server polls for status and retrieves the LLM result when complete. Requires client to support `tasks.requests.sampling.createMessage`.
|
||||||
|
- `trigger-elicitation-request-async` (tools/trigger-elicitation-request-async.ts): Demonstrates bidirectional tasks where the server sends an elicitation request that the client executes as a background task. Server polls while waiting for user input. Requires client to support `tasks.requests.elicitation.create`.
|
||||||
|
|
||||||
## Prompts
|
## Prompts
|
||||||
|
|
||||||
@@ -75,6 +77,26 @@ The server advertises support for MCP Tasks, enabling long-running operations wi
|
|||||||
- `failed`: Task encountered an error
|
- `failed`: Task encountered an error
|
||||||
- `cancelled`: Task was cancelled by client
|
- `cancelled`: Task was cancelled by client
|
||||||
|
|
||||||
### Demo Tool
|
### Demo Tools
|
||||||
|
|
||||||
|
**Server-side tasks (client calls server):**
|
||||||
Use the `simulate-research-query` tool to exercise the full task lifecycle. Set `ambiguous: true` to trigger the `input_required` flow with elicitation.
|
Use the `simulate-research-query` tool to exercise the full task lifecycle. Set `ambiguous: true` to trigger the `input_required` flow with elicitation.
|
||||||
|
|
||||||
|
**Client-side tasks (server calls client):**
|
||||||
|
Use `trigger-sampling-request-async` or `trigger-elicitation-request-async` to demonstrate bidirectional tasks where the server sends requests that the client executes as background tasks. These require the client to advertise `tasks.requests.sampling.createMessage` or `tasks.requests.elicitation.create` capabilities respectively.
|
||||||
|
|
||||||
|
### Bidirectional Task Flow
|
||||||
|
|
||||||
|
MCP Tasks are bidirectional - both server and client can be task executors:
|
||||||
|
|
||||||
|
| Direction | Request Type | Task Executor | Demo Tool |
|
||||||
|
|-----------|--------------|---------------|-----------|
|
||||||
|
| Client -> Server | `tools/call` | Server | `simulate-research-query` |
|
||||||
|
| Server -> Client | `sampling/createMessage` | Client | `trigger-sampling-request-async` |
|
||||||
|
| Server -> Client | `elicitation/create` | Client | `trigger-elicitation-request-async` |
|
||||||
|
|
||||||
|
For client-side tasks:
|
||||||
|
1. Server sends request with task metadata (e.g., `_meta.task.ttl`)
|
||||||
|
2. Client creates task and returns `CreateTaskResult` with `taskId`
|
||||||
|
3. Server polls `tasks/get` for status updates
|
||||||
|
4. When complete, server calls `tasks/result` to retrieve the result
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import { registerToggleSubscriberUpdatesTool } from "./toggle-subscriber-updates
|
|||||||
import { registerTriggerElicitationRequestTool } from "./trigger-elicitation-request.js";
|
import { registerTriggerElicitationRequestTool } from "./trigger-elicitation-request.js";
|
||||||
import { registerTriggerLongRunningOperationTool } from "./trigger-long-running-operation.js";
|
import { registerTriggerLongRunningOperationTool } from "./trigger-long-running-operation.js";
|
||||||
import { registerTriggerSamplingRequestTool } from "./trigger-sampling-request.js";
|
import { registerTriggerSamplingRequestTool } from "./trigger-sampling-request.js";
|
||||||
|
import { registerTriggerSamplingRequestAsyncTool } from "./trigger-sampling-request-async.js";
|
||||||
|
import { registerTriggerElicitationRequestAsyncTool } from "./trigger-elicitation-request-async.js";
|
||||||
import { registerSimulateResearchQueryTool } from "./simulate-research-query.js";
|
import { registerSimulateResearchQueryTool } from "./simulate-research-query.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,4 +47,7 @@ export const registerConditionalTools = (server: McpServer) => {
|
|||||||
registerTriggerSamplingRequestTool(server);
|
registerTriggerSamplingRequestTool(server);
|
||||||
// Task-based research tool (uses experimental tasks API)
|
// Task-based research tool (uses experimental tasks API)
|
||||||
registerSimulateResearchQueryTool(server);
|
registerSimulateResearchQueryTool(server);
|
||||||
|
// Bidirectional task tools - server sends requests that client executes as tasks
|
||||||
|
registerTriggerSamplingRequestAsyncTool(server);
|
||||||
|
registerTriggerElicitationRequestAsyncTool(server);
|
||||||
};
|
};
|
||||||
|
|||||||
242
src/everything/tools/trigger-elicitation-request-async.ts
Normal file
242
src/everything/tools/trigger-elicitation-request-async.ts
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
|
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
// Tool configuration
|
||||||
|
const name = "trigger-elicitation-request-async";
|
||||||
|
const config = {
|
||||||
|
title: "Trigger Async Elicitation Request Tool",
|
||||||
|
description:
|
||||||
|
"Trigger an async elicitation request that the CLIENT executes as a background task. " +
|
||||||
|
"Demonstrates bidirectional MCP tasks where the server sends an elicitation request and " +
|
||||||
|
"the client handles user input asynchronously, allowing the server to poll for completion.",
|
||||||
|
inputSchema: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Poll interval in milliseconds
|
||||||
|
const POLL_INTERVAL = 1000;
|
||||||
|
|
||||||
|
// Maximum poll attempts before timeout (10 minutes for user input)
|
||||||
|
const MAX_POLL_ATTEMPTS = 600;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers the 'trigger-elicitation-request-async' tool.
|
||||||
|
*
|
||||||
|
* This tool demonstrates bidirectional MCP tasks for elicitation:
|
||||||
|
* - Server sends elicitation request to client with task metadata
|
||||||
|
* - Client creates a task and returns CreateTaskResult
|
||||||
|
* - Client prompts user for input (task status: input_required)
|
||||||
|
* - Server polls client's tasks/get endpoint for status
|
||||||
|
* - Server fetches final result from client's tasks/result endpoint
|
||||||
|
*
|
||||||
|
* @param {McpServer} server - The McpServer instance where the tool will be registered.
|
||||||
|
*/
|
||||||
|
export const registerTriggerElicitationRequestAsyncTool = (server: McpServer) => {
|
||||||
|
// Check client capabilities
|
||||||
|
const clientCapabilities = server.server.getClientCapabilities() || {};
|
||||||
|
|
||||||
|
// Client must support elicitation AND tasks.requests.elicitation
|
||||||
|
const clientSupportsElicitation = clientCapabilities.elicitation !== undefined;
|
||||||
|
const clientTasksCapability = clientCapabilities.tasks as {
|
||||||
|
requests?: { elicitation?: { create?: object } };
|
||||||
|
} | undefined;
|
||||||
|
const clientSupportsAsyncElicitation =
|
||||||
|
clientTasksCapability?.requests?.elicitation?.create !== undefined;
|
||||||
|
|
||||||
|
if (clientSupportsElicitation && clientSupportsAsyncElicitation) {
|
||||||
|
server.registerTool(
|
||||||
|
name,
|
||||||
|
config,
|
||||||
|
async (args, extra): Promise<CallToolResult> => {
|
||||||
|
// Create the elicitation request WITH task metadata
|
||||||
|
// Using z.any() schema to avoid complex type matching with _meta
|
||||||
|
const request = {
|
||||||
|
method: "elicitation/create" as const,
|
||||||
|
params: {
|
||||||
|
message: "Please provide inputs for the following fields (async task demo):",
|
||||||
|
requestedSchema: {
|
||||||
|
type: "object" as const,
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
title: "Your Name",
|
||||||
|
type: "string" as const,
|
||||||
|
description: "Your full name",
|
||||||
|
},
|
||||||
|
favoriteColor: {
|
||||||
|
title: "Favorite Color",
|
||||||
|
type: "string" as const,
|
||||||
|
description: "What is your favorite color?",
|
||||||
|
enum: ["Red", "Blue", "Green", "Yellow", "Purple"],
|
||||||
|
},
|
||||||
|
agreeToTerms: {
|
||||||
|
title: "Terms Agreement",
|
||||||
|
type: "boolean" as const,
|
||||||
|
description: "Do you agree to the terms and conditions?",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["name"],
|
||||||
|
},
|
||||||
|
_meta: {
|
||||||
|
task: {
|
||||||
|
ttl: 600000, // 10 minutes (user input may take a while)
|
||||||
|
pollInterval: POLL_INTERVAL,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send the elicitation request
|
||||||
|
// Client may return either:
|
||||||
|
// - ElicitResult (synchronous execution)
|
||||||
|
// - CreateTaskResult (task-based execution with { task } object)
|
||||||
|
const elicitResponse = await extra.sendRequest(
|
||||||
|
request as Parameters<typeof extra.sendRequest>[0],
|
||||||
|
z.union([
|
||||||
|
// CreateTaskResult - client created a task
|
||||||
|
z.object({
|
||||||
|
task: z.object({
|
||||||
|
taskId: z.string(),
|
||||||
|
status: z.string(),
|
||||||
|
pollInterval: z.number().optional(),
|
||||||
|
statusMessage: z.string().optional(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
// ElicitResult - synchronous execution
|
||||||
|
z.object({
|
||||||
|
action: z.string(),
|
||||||
|
content: z.any().optional(),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if client returned CreateTaskResult (has task object)
|
||||||
|
const isTaskResult = 'task' in elicitResponse && elicitResponse.task;
|
||||||
|
if (!isTaskResult) {
|
||||||
|
// Client executed synchronously - return the direct response
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `[SYNC] Client executed synchronously:\n${JSON.stringify(elicitResponse, null, 2)}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskId = elicitResponse.task.taskId;
|
||||||
|
const statusMessages: string[] = [];
|
||||||
|
statusMessages.push(`Task created: ${taskId}`);
|
||||||
|
|
||||||
|
// Poll for task completion
|
||||||
|
let attempts = 0;
|
||||||
|
let taskStatus = elicitResponse.task.status;
|
||||||
|
let taskStatusMessage: string | undefined;
|
||||||
|
|
||||||
|
while (
|
||||||
|
taskStatus !== "completed" &&
|
||||||
|
taskStatus !== "failed" &&
|
||||||
|
taskStatus !== "cancelled" &&
|
||||||
|
attempts < MAX_POLL_ATTEMPTS
|
||||||
|
) {
|
||||||
|
// Wait before polling
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL));
|
||||||
|
attempts++;
|
||||||
|
|
||||||
|
// Get task status from client
|
||||||
|
const pollResult = await extra.sendRequest(
|
||||||
|
{
|
||||||
|
method: "tasks/get",
|
||||||
|
params: { taskId },
|
||||||
|
},
|
||||||
|
z.object({
|
||||||
|
status: z.string(),
|
||||||
|
statusMessage: z.string().optional(),
|
||||||
|
}).passthrough()
|
||||||
|
);
|
||||||
|
|
||||||
|
taskStatus = pollResult.status;
|
||||||
|
taskStatusMessage = pollResult.statusMessage;
|
||||||
|
|
||||||
|
// Only log status changes or every 10 polls to avoid spam
|
||||||
|
if (attempts === 1 || attempts % 10 === 0 || taskStatus !== "input_required") {
|
||||||
|
statusMessages.push(
|
||||||
|
`Poll ${attempts}: ${taskStatus}${taskStatusMessage ? ` - ${taskStatusMessage}` : ""}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for timeout
|
||||||
|
if (attempts >= MAX_POLL_ATTEMPTS) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `[TIMEOUT] Task timed out after ${MAX_POLL_ATTEMPTS} poll attempts\n\nProgress:\n${statusMessages.join("\n")}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for failure/cancellation
|
||||||
|
if (taskStatus === "failed" || taskStatus === "cancelled") {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `[${taskStatus.toUpperCase()}] ${taskStatusMessage || "No message"}\n\nProgress:\n${statusMessages.join("\n")}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the final result
|
||||||
|
const result = await extra.sendRequest(
|
||||||
|
{
|
||||||
|
method: "tasks/result",
|
||||||
|
params: { taskId },
|
||||||
|
},
|
||||||
|
z.any()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Format the elicitation result
|
||||||
|
const content: CallToolResult["content"] = [];
|
||||||
|
|
||||||
|
if (result.action === "accept" && result.content) {
|
||||||
|
content.push({
|
||||||
|
type: "text",
|
||||||
|
text: `[COMPLETED] User provided the requested information!`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const userData = result.content as Record<string, unknown>;
|
||||||
|
const lines = [];
|
||||||
|
if (userData.name) lines.push(`- Name: ${userData.name}`);
|
||||||
|
if (userData.favoriteColor) lines.push(`- Favorite Color: ${userData.favoriteColor}`);
|
||||||
|
if (userData.agreeToTerms !== undefined) lines.push(`- Agreed to terms: ${userData.agreeToTerms}`);
|
||||||
|
|
||||||
|
content.push({
|
||||||
|
type: "text",
|
||||||
|
text: `User inputs:\n${lines.join("\n")}`,
|
||||||
|
});
|
||||||
|
} else if (result.action === "decline") {
|
||||||
|
content.push({
|
||||||
|
type: "text",
|
||||||
|
text: `[DECLINED] User declined to provide the requested information.`,
|
||||||
|
});
|
||||||
|
} else if (result.action === "cancel") {
|
||||||
|
content.push({
|
||||||
|
type: "text",
|
||||||
|
text: `[CANCELLED] User cancelled the elicitation dialog.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include progress and raw result for debugging
|
||||||
|
content.push({
|
||||||
|
type: "text",
|
||||||
|
text: `\nProgress:\n${statusMessages.join("\n")}\n\nRaw result: ${JSON.stringify(result, null, 2)}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { content };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
214
src/everything/tools/trigger-sampling-request-async.ts
Normal file
214
src/everything/tools/trigger-sampling-request-async.ts
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
|
import {
|
||||||
|
CallToolResult,
|
||||||
|
CreateMessageRequest,
|
||||||
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
// Tool input schema
|
||||||
|
const TriggerSamplingRequestAsyncSchema = z.object({
|
||||||
|
prompt: z.string().describe("The prompt to send to the LLM"),
|
||||||
|
maxTokens: z
|
||||||
|
.number()
|
||||||
|
.default(100)
|
||||||
|
.describe("Maximum number of tokens to generate"),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tool configuration
|
||||||
|
const name = "trigger-sampling-request-async";
|
||||||
|
const config = {
|
||||||
|
title: "Trigger Async Sampling Request Tool",
|
||||||
|
description:
|
||||||
|
"Trigger an async sampling request that the CLIENT executes as a background task. " +
|
||||||
|
"Demonstrates bidirectional MCP tasks where the server sends a request and the client " +
|
||||||
|
"executes it asynchronously, allowing the server to poll for progress and results.",
|
||||||
|
inputSchema: TriggerSamplingRequestAsyncSchema,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Poll interval in milliseconds
|
||||||
|
const POLL_INTERVAL = 1000;
|
||||||
|
|
||||||
|
// Maximum poll attempts before timeout
|
||||||
|
const MAX_POLL_ATTEMPTS = 60;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers the 'trigger-sampling-request-async' tool.
|
||||||
|
*
|
||||||
|
* This tool demonstrates bidirectional MCP tasks:
|
||||||
|
* - Server sends sampling request to client with task metadata
|
||||||
|
* - Client creates a task and returns CreateTaskResult
|
||||||
|
* - Server polls client's tasks/get endpoint for status
|
||||||
|
* - Server fetches final result from client's tasks/result endpoint
|
||||||
|
*
|
||||||
|
* @param {McpServer} server - The McpServer instance where the tool will be registered.
|
||||||
|
*/
|
||||||
|
export const registerTriggerSamplingRequestAsyncTool = (server: McpServer) => {
|
||||||
|
// Check client capabilities
|
||||||
|
const clientCapabilities = server.server.getClientCapabilities() || {};
|
||||||
|
|
||||||
|
// Client must support sampling AND tasks.requests.sampling
|
||||||
|
const clientSupportsSampling = clientCapabilities.sampling !== undefined;
|
||||||
|
const clientTasksCapability = clientCapabilities.tasks as {
|
||||||
|
requests?: { sampling?: { createMessage?: object } };
|
||||||
|
} | undefined;
|
||||||
|
const clientSupportsAsyncSampling =
|
||||||
|
clientTasksCapability?.requests?.sampling?.createMessage !== undefined;
|
||||||
|
|
||||||
|
if (clientSupportsSampling && clientSupportsAsyncSampling) {
|
||||||
|
server.registerTool(
|
||||||
|
name,
|
||||||
|
config,
|
||||||
|
async (args, extra): Promise<CallToolResult> => {
|
||||||
|
const validatedArgs = TriggerSamplingRequestAsyncSchema.parse(args);
|
||||||
|
const { prompt, maxTokens } = validatedArgs;
|
||||||
|
|
||||||
|
// Create the sampling request WITH task metadata
|
||||||
|
// The _meta.task field signals to the client that this should be executed as a task
|
||||||
|
const request: CreateMessageRequest & { params: { _meta?: { task: { ttl: number; pollInterval: number } } } } = {
|
||||||
|
method: "sampling/createMessage",
|
||||||
|
params: {
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: {
|
||||||
|
type: "text",
|
||||||
|
text: `Resource ${name} context: ${prompt}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
systemPrompt: "You are a helpful test server.",
|
||||||
|
maxTokens,
|
||||||
|
temperature: 0.7,
|
||||||
|
_meta: {
|
||||||
|
task: {
|
||||||
|
ttl: 300000, // 5 minutes
|
||||||
|
pollInterval: POLL_INTERVAL,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send the sampling request
|
||||||
|
// Client may return either:
|
||||||
|
// - CreateMessageResult (synchronous execution)
|
||||||
|
// - CreateTaskResult (task-based execution with { task } object)
|
||||||
|
const samplingResponse = await extra.sendRequest(
|
||||||
|
request,
|
||||||
|
z.union([
|
||||||
|
// CreateTaskResult - client created a task
|
||||||
|
z.object({
|
||||||
|
task: z.object({
|
||||||
|
taskId: z.string(),
|
||||||
|
status: z.string(),
|
||||||
|
pollInterval: z.number().optional(),
|
||||||
|
statusMessage: z.string().optional(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
// CreateMessageResult - synchronous execution
|
||||||
|
z.object({
|
||||||
|
role: z.string(),
|
||||||
|
content: z.any(),
|
||||||
|
model: z.string(),
|
||||||
|
stopReason: z.string().optional(),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if client returned CreateTaskResult (has task object)
|
||||||
|
const isTaskResult = 'task' in samplingResponse && samplingResponse.task;
|
||||||
|
if (!isTaskResult) {
|
||||||
|
// Client executed synchronously - return the direct response
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `[SYNC] Client executed synchronously:\n${JSON.stringify(samplingResponse, null, 2)}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskId = samplingResponse.task.taskId;
|
||||||
|
const statusMessages: string[] = [];
|
||||||
|
statusMessages.push(`Task created: ${taskId}`);
|
||||||
|
|
||||||
|
// Poll for task completion
|
||||||
|
let attempts = 0;
|
||||||
|
let taskStatus = samplingResponse.task.status;
|
||||||
|
let taskStatusMessage: string | undefined;
|
||||||
|
|
||||||
|
while (
|
||||||
|
taskStatus !== "completed" &&
|
||||||
|
taskStatus !== "failed" &&
|
||||||
|
taskStatus !== "cancelled" &&
|
||||||
|
attempts < MAX_POLL_ATTEMPTS
|
||||||
|
) {
|
||||||
|
// Wait before polling
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL));
|
||||||
|
attempts++;
|
||||||
|
|
||||||
|
// Get task status from client
|
||||||
|
const pollResult = await extra.sendRequest(
|
||||||
|
{
|
||||||
|
method: "tasks/get",
|
||||||
|
params: { taskId },
|
||||||
|
},
|
||||||
|
z.object({
|
||||||
|
status: z.string(),
|
||||||
|
statusMessage: z.string().optional(),
|
||||||
|
}).passthrough()
|
||||||
|
);
|
||||||
|
|
||||||
|
taskStatus = pollResult.status;
|
||||||
|
taskStatusMessage = pollResult.statusMessage;
|
||||||
|
statusMessages.push(
|
||||||
|
`Poll ${attempts}: ${taskStatus}${taskStatusMessage ? ` - ${taskStatusMessage}` : ""}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for timeout
|
||||||
|
if (attempts >= MAX_POLL_ATTEMPTS) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `[TIMEOUT] Task timed out after ${MAX_POLL_ATTEMPTS} poll attempts\n\nProgress:\n${statusMessages.join("\n")}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for failure/cancellation
|
||||||
|
if (taskStatus === "failed" || taskStatus === "cancelled") {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `[${taskStatus.toUpperCase()}] ${taskStatusMessage || "No message"}\n\nProgress:\n${statusMessages.join("\n")}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the final result
|
||||||
|
const result = await extra.sendRequest(
|
||||||
|
{
|
||||||
|
method: "tasks/result",
|
||||||
|
params: { taskId },
|
||||||
|
},
|
||||||
|
z.any()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Return the result with status history
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `[COMPLETED] Async sampling completed!\n\n**Progress:**\n${statusMessages.join("\n")}\n\n**Result:**\n${JSON.stringify(result, null, 2)}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user