Skip to content

v2 - RFC Extract Tasks out of protocol.ts into TaskManager#1673

Merged
felixweinberger merged 20 commits intomodelcontextprotocol:mainfrom
KKonstantinov:feature/extract-tasks-in-task-manager-trigger-on-capability
Mar 25, 2026
Merged

v2 - RFC Extract Tasks out of protocol.ts into TaskManager#1673
felixweinberger merged 20 commits intomodelcontextprotocol:mainfrom
KKonstantinov:feature/extract-tasks-in-task-manager-trigger-on-capability

Conversation

@KKonstantinov
Copy link
Copy Markdown
Contributor

@KKonstantinov KKonstantinov commented Mar 12, 2026

Based on #1449

Extract all task orchestration logic (creation, polling, queuing, routing) from the monolithic Protocol class into a dedicated TaskManager class that implements a new ProtocolModule interface. TaskManager is only instantiated when capabilities.tasks is declared, keeping Protocol lean for non-task use cases.

Motivation and Context

The Protocol class had grown to ~1800 lines, with roughly half dedicated to task management. This made it difficult to reason about, test, and extend independently. Extracting tasks into TaskManager achieves:

  • Separation of concerns: Protocol handles JSON-RPC message routing; TaskManager handles task lifecycle
  • Conditional initialization: Task infrastructure is only created when tasks capability is declared
  • Extensibility: The new ProtocolModule interface allows future modules to hook into request/response/notification lifecycle without modifying Protocol
  • Consolidated task context: Handler extra/ctx task fields are grouped under a single task object instead of scattered top-level properties

How Has This Been Tested?

  • All 1239 tests pass across all packages (pnpm test:all)
  • pnpm typecheck:all passes (except a pre-existing unrelated type error in examples/client)
  • Core protocol tests refactored to test TaskManager independently
  • Integration tests updated to properly declare tasks capability when using experimental.tasks.* APIs
  • Task-augmented request flows (elicitation, sampling) tested with InMemoryTaskStore on both client and server

Breaking Changes

Before After
ProtocolOptions.taskStore capabilities.tasks.taskStore (on ClientOptions / ServerOptions)
ProtocolOptions.taskMessageQueue capabilities.tasks.taskMessageQueue (on ClientOptions / ServerOptions)
Protocol.assertTaskCapability() (abstract) Removed — passed as callback in TaskManagerOptions
Protocol.assertTaskHandlerCapability() (abstract) Removed — passed as callback in TaskManagerOptions

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

New files:

  • packages/core/src/shared/taskManager.tsTaskManager class implementing ProtocolModule
  • packages/core/src/shared/protocolModule.tsProtocolModule interface and lifecycle types

Architecture: Protocol now maintains a _modules array. During request/response/notification processing, it delegates to each registered module in order. TaskManager is the first (and currently only) module. The interface is designed so additional modules can be added without further changes to Protocol.

Test fixes: Integration tests for createMessageStream and elicitInputStream were updated to declare tasks capability on the server, since experimental.tasks.* methods correctly require it. The createMessageStream task-related tests were also restructured with shared beforeEach/afterEach to eliminate repeated inline server creation.

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 12, 2026

🦋 Changeset detected

Latest commit: 0e9dd77

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@modelcontextprotocol/core Minor
@modelcontextprotocol/client Minor
@modelcontextprotocol/server Minor
@modelcontextprotocol/node Major
@modelcontextprotocol/express Major
@modelcontextprotocol/hono Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Mar 12, 2026

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/client@1673

@modelcontextprotocol/server

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/server@1673

@modelcontextprotocol/express

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/express@1673

@modelcontextprotocol/hono

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/hono@1673

@modelcontextprotocol/node

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/node@1673

commit: 0e9dd77

@KKonstantinov
Copy link
Copy Markdown
Contributor Author

KKonstantinov commented Mar 15, 2026

Just leaving this thought process here. I wonder if looping through _modules just to support multiple modules (where we have only one right now) is premature abstraction/generalization here. This could fall back to direct calling of the task module now (while keeping it running only if the task capability is declared), and if additional modules are added in the future, this could probably be done in a 15 minute change/iteration. Wondering if it is worth the indirection today - probably not. Any new "module" I imagine would be a significant feature coming from the protocol spec and not a random SDK-level feature. Thus, it would have to be carefully implemented and reviewed anyway; and the looping of the _modules etc. could be added at that point.

The real two benefits would be - tasks code runs runtime only if tasks is declared as a capability; ProtocolModule interface and Protocol Module could still exist if we removed the multi-module speculative abstraction; Still holds value in defining the contract, but Protocol would call the module directly rather than looping. Right now reading through the loop of _modules and seems like added cognitive complexity - but wanted to see how it'd look like.

Leaving it as-is right now, but happy to remove that

Copy link
Copy Markdown

@mcp-claude mcp-claude bot left a comment

Choose a reason for hiding this comment

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

Review commit 3b7c0fdc.

Copy link
Copy Markdown
Contributor

@felixweinberger felixweinberger left a comment

Choose a reason for hiding this comment

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

The extraction itself looks good - the TaskManager internals are solid. The issue is the ProtocolModule, I'm not sure we have enough justification to extract this. "Tasks" is really three things: server handlers, client polling, and the send-or-queue routing interceptor. The first two extract cleanly; the third is where the bugs @claude found come from.

I think we should drop ProtocolModule and wire TaskManager via direct composition with specific call sites — closer to what #1449 did. A NullTaskManager means requestStream works without tasks capability, and config flows through naturally.

Re: extensions (SEP-2133) — I considered whether ProtocolModule could be the foundation for an extension runtime, but most extensions just need handler registration, not lifecycle interception. We can add a module system later if real patterns demand it, I think it's going to be very hard to predict the shape of potential extensions. I think it's better to avoid further abstraction and build them once we know what the shape should be rather than guess.

…lity' of github.com:KKonstantinov/typescript-sdk into feature/extract-tasks-in-task-manager-trigger-on-capability
@felixweinberger felixweinberger marked this pull request as ready for review March 19, 2026 12:06
@felixweinberger felixweinberger requested a review from a team as a code owner March 19, 2026 12:06
@felixweinberger
Copy link
Copy Markdown
Contributor

@claude review

Copy link
Copy Markdown

@claude claude bot left a comment

Choose a reason for hiding this comment

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

No new bugs found and previous issues appear addressed, but this is a 19-file architectural refactoring of core Protocol infrastructure with breaking changes -- worth a human confirming the design direction (NullTaskManager pattern, TaskManagerHost interface, direct composition) aligns with the team's vision.

Extended reasoning...

Overview

This PR extracts ~500 lines of task orchestration logic from the monolithic Protocol class into a dedicated TaskManager class. It touches 19 files across core, client, server, and integration test packages. A new NullTaskManager subclass provides passthrough behavior when tasks capability is not configured. The ProtocolModule abstraction loop from earlier revisions has been replaced with direct composition (Protocol holds a single _taskManager reference), addressing felixweinberger's architectural feedback.

Security risks

No security concerns. The changes are internal refactoring of task lifecycle management. No auth, crypto, or permission-related code is modified. The Zod validation regression (registerHandler bypassing schema validation) has been fixed -- registerHandler now calls schema.parse(request) before delegating to the handler.

Level of scrutiny

This warrants thorough human review. It refactors the core Protocol class that underpins all MCP communication, introduces a new class hierarchy (TaskManager / NullTaskManager), changes the public API surface (taskStore/taskMessageQueue moved into capabilities.tasks), and has documented breaking changes. The previous bug reports (asymmetric sendRequest/sendNotification gating, dropped config options, resource leak on assertTaskCapability throw, streaming regression) all appear to have been addressed in the current revision.

Other factors

  • All 5 previously reported bugs appear fixed: (1) defaultTaskPollInterval/maxTaskQueueSize are now extracted via extractTaskManagerOptions and forwarded correctly, (2) sendRequest wrapping now gates on relatedTaskId like sendNotification, (3) streaming works without tasks via NullTaskManager, (4) registerHandler includes Zod validation, (5) processOutboundRequest is wrapped in try/catch for cleanup on synchronous throw.
  • The NullTaskManager.processOutboundNotification drops relatedTask metadata from _meta, but this is acceptable since tasks aren't configured in that path.
  • Tests were updated extensively (many capabilities: {} changed to capabilities: { tasks: {...} }) which is expected given the new requirement for explicit task capability declaration.
  • The author's own comment about premature abstraction with _modules looping appears resolved -- the code now uses direct composition as felixweinberger suggested.

@modelcontextprotocol modelcontextprotocol deleted a comment from claude bot Mar 24, 2026
@felixweinberger
Copy link
Copy Markdown
Contributor

@claude review

Copy link
Copy Markdown

@claude claude bot left a comment

Choose a reason for hiding this comment

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

All five previously reported bugs appear to be fixed, and the architectural feedback (direct composition instead of generic ProtocolModule, NullTaskManager for streaming without tasks capability) has been addressed. However, this is a large architectural refactoring (19 files, ~900-line new TaskManager, breaking API changes) touching the core protocol layer, so it warrants human sign-off before merging.

Extended reasoning...

Overview

This PR extracts ~500 lines of task orchestration logic from the monolithic Protocol class into a dedicated TaskManager class. Key changes:

  • New TaskManager (and NullTaskManager no-op) in packages/core/src/shared/taskManager.ts
  • taskStore/taskMessageQueue/defaultTaskPollInterval/maxTaskQueueSize moved from ProtocolOptions into capabilities.tasks on ClientOptions/ServerOptions
  • ExperimentalClientTasks and ExperimentalServerTasks now delegate to TaskManager directly instead of unsafe casts to internal Protocol methods
  • Protocol now holds a single _taskManager field with direct calls (no generic module loop)
  • 19 files changed across core, client, server, examples, and integration tests

Security risks

No security-sensitive changes. The PR does not touch auth, crypto, permissions, or input sanitization beyond what was already in place. The Zod schema validation for task request handlers was initially dropped but has been restored in registerHandler.

Level of scrutiny

This PR warrants high scrutiny:

  • It refactors the core protocol layer that all MCP communication flows through
  • It introduces breaking API changes (task config location, handler context shape)
  • The TaskManager is a new ~900-line class that takes over critical request/response routing logic
  • The interaction between NullTaskManager and TaskManager across inbound/outbound request paths is subtle

Other factors

  • All five bugs previously found by the bug hunting system have been addressed: config forwarding, sendRequest gating asymmetry, streaming without tasks capability, schema validation in registerHandler, and handler/timeout leak on assertTaskCapability throw
  • felixweinberger's architectural feedback (direct composition instead of generic ProtocolModule, NullTaskManager approach) has been incorporated
  • All inline review comments are marked as resolved
  • Tests were updated extensively, though some streaming tests now declare tasks capability that may not strictly be needed (minor test quality concern)
  • The changeset documents the breaking changes but a human should verify completeness against the actual API surface changes

…lity

Restores the assertTaskHandlerCapability check that was inadvertently
skipped when the server has no tasks capability configured. On main,
this check ran unconditionally in Protocol._onrequest; after extraction
it moved behind the validateInbound callback which NullTaskManager
wasn't providing.
…r-on-capability

Resolves conflicts in experimental/tasks/{client,server}.ts by
combining main's non-generic getTaskResult signature (hardcoded
GetTaskPayloadResultSchema) with this PR's _module call pattern
(no unsafe casts).
isTaskAugmentedRequestParams returns true for any valid params since
the task field is optional in the schema. Check request.params.task
presence to correctly detect task-augmented requests.
@felixweinberger
Copy link
Copy Markdown
Contributor

@claude review

Copy link
Copy Markdown

@claude claude bot left a comment

Choose a reason for hiding this comment

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

No new bugs found and all five previously-reported issues appear resolved. However, this is a 19-file architectural refactor extracting ~500 lines from Protocol into TaskManager with breaking API changes — it needs human sign-off on the design decisions (NullTaskManager pattern, direct composition vs modules, capability type structure).

Extended reasoning...

Overview

This PR extracts task orchestration (creation, polling, queuing, routing) from the monolithic Protocol class (~1800 lines) into a dedicated TaskManager class with a NullTaskManager no-op variant. It touches 19 files across core, client, server, and integration test packages. The breaking changes move taskStore, taskMessageQueue, defaultTaskPollInterval, and maxTaskQueueSize from ProtocolOptions to capabilities.tasks.

Security risks

No security concerns identified. The refactor preserves the same trust boundaries — task handlers still validate via Zod schemas (now added to registerHandler), and capability assertions are delegated to the same abstract methods on Protocol subclasses.

Level of scrutiny

This requires high scrutiny. It restructures how Protocol integrates with task management, introduces a new TaskManagerHost interface, and changes the public API surface for both Client and Server constructors. The NullTaskManager pattern (suggested by felixweinberger referencing PR #1449) is a key architectural decision. The ProtocolModule abstraction was dropped in favor of direct composition per reviewer feedback — these are design-level choices that warrant human judgment.

Other factors

All five bugs from the prior mcp-claude[bot] review have been addressed in the current code:

  1. ClientTasksCapabilityWithRuntime now uses & TaskManagerOptions (full intersection, not Pick)
  2. sendRequest wrapping in processInboundRequest now gates on relatedTaskId (matching sendNotification)
  3. _module getter no longer throws — returns taskManager which is always available via NullTaskManager
  4. registerHandler now applies Zod schema validation before delegating to handlers
  5. processOutboundRequest is wrapped in try/catch with proper handler/timeout cleanup

The test suite updates (adding capabilities: { tasks: {...} } to many tests) reflect the intentional requirement that streaming APIs go through TaskManager, which is valid given the NullTaskManager fallback handles non-task streaming correctly.

@felixweinberger
Copy link
Copy Markdown
Contributor

@claude review

Copy link
Copy Markdown

@claude claude bot left a comment

Choose a reason for hiding this comment

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

All five bugs from my March 17 review have been addressed, and the architecture was updated to direct composition per feedback. No new bugs found. However, this is a 19-file refactoring of core protocol infrastructure with breaking API changes and a new 900-line TaskManager -- it warrants a human sign-off on the architecture and migration surface.

Extended reasoning...

Overview

This PR extracts ~900 lines of task orchestration logic from Protocol into a dedicated TaskManager class. It touches 19 files across core, client, server, and integration tests. The key architectural change is that Protocol now holds a TaskManager (or NullTaskManager) directly, delegating task lifecycle operations through a TaskManagerHost interface. Configuration moved from ProtocolOptions to capabilities.tasks on ClientOptions/ServerOptions.

Bug fix verification

All five issues I identified on March 17 are resolved:

  • defaultTaskPollInterval/maxTaskQueueSize: Now included in ClientTasksCapabilityWithRuntime and ServerTasksCapabilityWithRuntime via & TaskManagerOptions, and correctly extracted by extractTaskManagerOptions() (taskManager.ts:207-211).
  • sendRequest asymmetry: processInboundRequest (taskManager.ts:724) now gates on relatedTaskId first, matching sendNotification behavior. Falls back to taskContext wrapping when no relatedTaskId.
  • Streaming regression: ExperimentalClientTasks._module (client.ts:55) now returns this._client.taskManager which is always available (NullTaskManager when unconfigured). NullTaskManager inherits requestStream() which handles the non-task fallback path.
  • Zod validation: registerHandler (protocol.ts:377-382) now calls schema.parse(request) before delegating to the handler.
  • Handler/timeout leak: _requestWithSchema (protocol.ts:888-903) now wraps processOutboundRequest in try/catch that cleans up _responseHandlers, _progressHandlers, and timeout on synchronous throw.

Security risks

No new security risks identified. The Zod validation fix actually improves input validation for task handlers. The registerHandler now validates incoming requests the same way setRequestHandler does.

Level of scrutiny

This PR requires high scrutiny:

  • Core infrastructure: Changes to Protocol._requestWithSchema, _onrequest, _onresponse, and notification affect every request/response/notification in the SDK.
  • Breaking changes: Configuration migration from ProtocolOptions to capabilities.tasks affects all existing task users.
  • New 900-line file: taskManager.ts contains the full task lifecycle implementation and needs careful review for correctness of state management, message queuing, and polling logic.
  • Architecture decision: Direct composition vs module interface was updated per reviewer feedback, but should be confirmed.

Other factors

  • The NullTaskManager.processOutboundNotification drops relatedTask metadata from options (line 890-894), but this is benign since no relatedTask metadata should flow through when tasks are not configured.
  • Extensive test updates (10+ test files) provide good coverage of the migration.
  • The changeset correctly documents all four breaking changes.
  • This is based on PR #1449 and the reviewer has provided substantive architectural feedback that appears to have been incorporated.

@felixweinberger felixweinberger merged commit 462c3fc into modelcontextprotocol:main Mar 25, 2026
14 checks passed
@github-actions github-actions bot mentioned this pull request Mar 25, 2026
felixweinberger added a commit to KKonstantinov/typescript-sdk that referenced this pull request Mar 25, 2026
- protocol.ts imports: keep modelcontextprotocol#1673's reduced list, apply this PR's
  ../types/index.js path
- taskManager.ts: update import paths to ../types/index.js
- exports/public/index.ts: move RequestTaskStore, TaskContext,
  TaskRequestOptions from protocol.js to taskManager.js, add
  TaskManagerOptions export
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