Skip to content

MCP Server Part 9: Background callbacks#3766

Open
KoolADE85 wants to merge 7 commits into
feature/mcp-server-integrationfrom
feature/mcp-background-callbacks
Open

MCP Server Part 9: Background callbacks#3766
KoolADE85 wants to merge 7 commits into
feature/mcp-server-integrationfrom
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 KoolADE85 force-pushed the feature/mcp-server-integration branch 2 times, most recently from 3c39eac to a1ca057 Compare May 8, 2026 22:10
@KoolADE85 KoolADE85 force-pushed the feature/mcp-background-callbacks branch from 3e146f8 to 0a82ca0 Compare May 8, 2026 22:11
@KoolADE85 KoolADE85 force-pushed the feature/mcp-server-integration branch from a1ca057 to 409be55 Compare May 11, 2026 23:19
@KoolADE85 KoolADE85 force-pushed the feature/mcp-background-callbacks branch from fd13290 to 5c45baf Compare May 11, 2026 23:19
@KoolADE85 KoolADE85 force-pushed the feature/mcp-server-integration branch from 409be55 to b788678 Compare May 13, 2026 18:04
@KoolADE85 KoolADE85 force-pushed the feature/mcp-background-callbacks branch from 01e8744 to 1366bd4 Compare May 13, 2026 18:06
@KoolADE85 KoolADE85 force-pushed the feature/mcp-background-callbacks branch from 1366bd4 to f4254e2 Compare May 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?



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
"""Wrap a CreateTaskResult as a CallToolResult with polling instructions.
"""
Wrap a CreateTaskResult as a CallToolResult with polling instructions.

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."

)


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?

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 thread dash/mcp/tasks/tasks.py
Comment on lines +29 to +30
if adapter is None:
return None
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.

Does an error need to be returned here?

Comment thread dash/mcp/tasks/tasks.py
from dash.mcp.types import MCPError


def parse_task_id(task_id: str) -> tuple[str, str, str, datetime]:
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 you add error handling here in the event that task_id is malformed?

Comment thread dash/mcp/tasks/tasks.py
raise MCPError("No background callback manager configured.")

app = get_app()
adapter = app.mcp_callback_map.find_by_tool_name(tool_name)
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 this ever be None? If so, then 117 would throw an error.

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?

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.

2 participants