Skip to content

MCP Server Part 9: Background callbacks - #3766

Merged
KoolADE85 merged 11 commits into
mcpfrom
feature/mcp-background-callbacks
May 20, 2026
Merged

MCP Server Part 9: Background callbacks#3766
KoolADE85 merged 11 commits into
mcpfrom
feature/mcp-background-callbacks

Conversation

@KoolADE85

Copy link
Copy Markdown
Contributor

Summary

Adds support for MCP Tasks — when an LLM calls a tool backed by a Dash background callback, the tool returns a taskId immediately and the LLM polls for results.

  • New dash/mcp/tasks/ module that lists, starts, and cancels background callbacks per the MCP spec SEP-1686
  • A new tool to trigger background callbacks (for clients that don't yet implement MCP Tasks natively)
  • Background callback tools' descriptions auto-include the polling instructions so the LLM knows how/when to retrieve results
  • Thorough integration tests

@KoolADE85
KoolADE85force-pushed the feature/mcp-server-integration branch 2 times, most recently from 3c39eac to a1ca057CompareMay 8, 2026 22:10
@KoolADE85
KoolADE85force-pushed the feature/mcp-background-callbacks branch from 3e146f8 to 0a82ca0CompareMay 8, 2026 22:11
@KoolADE85
KoolADE85force-pushed the feature/mcp-server-integration branch from a1ca057 to 409be55CompareMay 11, 2026 23:19
@KoolADE85
KoolADE85force-pushed the feature/mcp-background-callbacks branch from fd13290 to 5c45bafCompareMay 11, 2026 23:19
@KoolADE85
KoolADE85force-pushed the feature/mcp-server-integration branch from 409be55 to b788678CompareMay 13, 2026 18:04
@KoolADE85
KoolADE85force-pushed the feature/mcp-background-callbacks branch 2 times, most recently from 1366bd4 to f4254e2CompareMay 14, 2026 17:13
Comment on lines +20 to +21
if TYPE_CHECKING:
from dash.mcp.primitives.tools.callback_adapter import CallbackAdapter

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this necessary?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CallbackAdapter is only used for its type:

def format_callback_response(
response: CallbackExecutionResponse,
callback: CallbackAdapter,
) -> CallToolResult:

and the TYPE_CHECKING constant just prevents circular import errors.



def task_result_to_tool_result(create_task_result: CreateTaskResult) -> CallToolResult:
"""Wrap a CreateTaskResult as a CallToolResult with polling instructions.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahem...

Suggested change
"""WrapaCreateTaskResultasaCallToolResultwithpollinginstructions.
"""
WrapaCreateTaskResultasaCallToolResultwithpollinginstructions.

Comment on lines +29 to +30
"This is a long-running background operation. "
"It returns a taskId immediately. "

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These get joined with newlines, so you could probably skip the ending spaces.

Suggested change
"This is a long-running background operation."
"It returns a taskId immediately."
"This is a long-running background operation."
"It returns a taskId immediately."

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually implicit string concatenation (no commas), so they form a single string (without newlines).

)


def task_result_to_tool_result(create_task_result: CreateTaskResult) -> CallToolResult:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is minor, but CreateTaskResult makes me think that you're creating something. That's not the case though. Is there another name option?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, we are creating a task in MCP parlance, which is just some long-running operation that agents need to wait for.
The name CreateTaskResult comes from the MCP package itself so it's not really ours to rename.

def call_tool(cls, tool_name: str, arguments: dict[str, Any]) -> CallToolResult:
def call_tool(
cls, tool_name: str, arguments: dict[str, Any], task: dict | None = None
) -> CallToolResult | CreateTaskResult:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would CreateTaskResult get returned? In task_result_to_tool_result the result is wrapped in CallToolResult.

tool_name: str,
arguments: dict[str, Any],
task: dict | None = None,
) -> CallToolResult:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could the result here be CreateTaskResult?

Comment threaddash/mcp/tasks/tasks.py
Comment threaddash/mcp/tasks/tasks.py
Comment threaddash/mcp/tasks/tasks.py
Comment on lines +89 to +91
task_status = get_task(task_id)
if task_status.status == "completed":
return get_task_result(task_id)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the task guaranteed to still be available between these two calls? For example, if you have two clients polling for the same task, could one of them grab the result before the other and cause an error?

@KoolADE85
KoolADE85force-pushed the feature/mcp-background-callbacks branch from f4254e2 to 3ef3409CompareMay 19, 2026 15:18
@KoolADE85
KoolADE85 changed the base branch from feature/mcp-server-integration to mcpMay 19, 2026 15:18
robertclausand others added 2 commits May 19, 2026 09:48
- Reorder status checks in get_task: result_ready takes priority over
job_running. A background process can write its result to the cache
before the process fully exits, causing a false "working" status even
after the result is available (fixes test_mcpbg016).
- Annotate response as CallbackExecutionResponse so mypy accepts it as
the argument to _prepare_response and format_callback_response
(fixes Typing Tests CI failure).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@KoolADE85
KoolADE85 merged commit 2cf8f91 into mcpMay 20, 2026
52 of 55 checks passed
Sign up for freeto 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.

3 participants

@KoolADE85@camdecoster@robertclaus