Skip to content

Progress notifications? #461

Open
Open
@olalonde

Description

@olalonde

I have a mcp server with a tool that can take several minutes to complete... Right now mcp clients usually consider it has timed out but it would be good if I was either 1) able to report progress on the task to avoid getting timed out 2) able to immediately return a message indicating the result will be returned later as a notification. Not sure which approach is recommended for that type of tool.

server.tool(
  "askHuman",
  {
    question: z.string().describe("The question to ask a human worker"),
    reward: z
      .string()
      .default("0.05")
      .describe("The reward amount in USD (default: $0.05)"),
    title: z.string().optional().describe("Title for the HIT (optional)"),
    description: z
      .string()
      .optional()
      .describe("Description for the HIT (optional)"),
    hitValiditySeconds: z
      .number()
      .default(3600)
      .describe("Time until the HIT expires in seconds (default: 1 hour)")
  },
  async ({
    question,
    reward,
    title,
    description,
    hitValiditySeconds,
  }) => {
    try {
      // Create HIT parameters
      // For GitHub Pages, use the direct HTML page URL
      // Default to local server if GITHUB_PAGES_URL is not set
      let formUrl;

      // Always use the GitHub Pages URL
      formUrl = new URL(FORM_SERVER_URL);

      // Add question and callback parameters
      formUrl.searchParams.append("question", encodeURIComponent(question));

      // If a callback URL is provided, add it to the form URL
      if (process.env.CALLBACK_URL) {
        formUrl.searchParams.append("callbackUrl", process.env.CALLBACK_URL);
      }

      const params = {
        Title: title || "Answer a question from an AI assistant",
        Description:
          description ||
          "Please provide your human perspective on this question",
        Question: `
          <ExternalQuestion xmlns="http://mechanicalturk.amazonaws.com/AWSMechanicalTurkDataSchemas/2006-07-14/ExternalQuestion.xsd">
            <ExternalURL>${formUrl.toString()}</ExternalURL>
            <FrameHeight>600</FrameHeight>
          </ExternalQuestion>
        `,
        Reward: reward,
        MaxAssignments: 1,
        AssignmentDurationInSeconds: hitValiditySeconds,
        LifetimeInSeconds: hitValiditySeconds,
        AutoApprovalDelayInSeconds: 86400, // Auto-approve after 24 hours
      };

      // Create the HIT
      const createResult = await mturkClient.send(new CreateHITCommand(params));
      const hitId = createResult.HIT?.HITId;

      if (!hitId) {
        throw new Error("Failed to create HIT");
      }

      // Poll for results
      let assignment = null;
      const startTime = Date.now();
      const maxWaitTime = hitValiditySeconds * 1000;
      const pollInterval = 5000; // Poll every 5 seconds

      while (Date.now() - startTime < maxWaitTime) {
        const listAssignmentsResponse = await mturkClient.send(
          new ListAssignmentsForHITCommand({
            HITId: hitId,
            AssignmentStatuses: ["Submitted", "Approved"],
          }),
        );

        if (
          listAssignmentsResponse.Assignments &&
          listAssignmentsResponse.Assignments.length > 0
        ) {
          assignment = listAssignmentsResponse.Assignments[0];
          break;
        }

        // Wait before polling again
        await new Promise((resolve) => setTimeout(resolve, pollInterval));
      }

      // Return results
      if (assignment && assignment.AssignmentId) {
        // Auto-approve the assignment
        try {
          await mturkClient.send(
            new ApproveAssignmentCommand({
              AssignmentId: assignment.AssignmentId,
              RequesterFeedback: "Thank you for your response!",
            }),
          );
        } catch (approveError) {
          console.error("Error approving assignment:", approveError);
          // Continue with the response even if approval fails
        }

        if (assignment.Answer) {
          // Parse XML answer (simplified - in production, use an XML parser)
          const answerText = assignment.Answer.replace(
            /<\?xml.*?\?>/,
            "",
          ).replace(
            /<Answer>.*?<QuestionIdentifier>.*?<\/QuestionIdentifier>.*?<FreeText>(.*?)<\/FreeText>.*?<\/Answer>/s,
            "$1",
          );

          return {
            content: [
              {
                type: "text",
                text: `Human response: ${answerText}`,
              },
            ],
          };
        } else {
          return {
            content: [
              {
                type: "text",
                text: `Assignment received but answer format was invalid. Assignment ID: ${assignment.AssignmentId}, HIT ID: ${hitId}`,
              },
            ],
          };
        }
      } else {
        return {
          content: [
            {
              type: "text",
              text: `No response received within the maximum wait time. Your question is still available for workers on MTurk. HIT ID: ${hitId} - You can check its status later with the checkHITStatus tool.`,
            },
          ],
        };
      }
    } catch (error) {
      console.error("Error in askHuman tool:", error);
      return {
        content: [
          {
            type: "text",
            text: `Error: ${error instanceof Error ? error.message : String(error)}`,
          },
        ],
      };
    }
  },
);

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions