-
Notifications
You must be signed in to change notification settings - Fork 1.6k
v2 draft - RFC on features refactor
#1426
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
KKonstantinov
wants to merge
26
commits into
modelcontextprotocol:main
Choose a base branch
from
KKonstantinov:feature/v2-protocol-refactor
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
v2 draft - RFC on features refactor
#1426
KKonstantinov
wants to merge
26
commits into
modelcontextprotocol:main
from
KKonstantinov:feature/v2-protocol-refactor
+11,465
−2,795
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
🦋 Changeset detectedLatest commit: caa3164 The changes in this PR will be included in the next version bump. This PR includes changesets to release 8 packages
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 |
@modelcontextprotocol/client
@modelcontextprotocol/server
@modelcontextprotocol/express
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Note: This PR is a
draftand is to be perceived as a discussion point and a potential "pick list" / "wish list" of features for refactor. Features in this PR may or may not come in, or may come in a different shape or form.V2 Protocol Refactor: Structured Context, Middleware, and Registry Systems
This PR introduces a comprehensive architectural refactor of the MCP TypeScript SDK, establishing cleaner separation of concerns, a structured context API for request handlers, and an Express-style middleware system.
Summary
Architecture Changes (Internal/Contributors)
API Changes (External/Users)
ctxobject (breaking change)onError/onProtocolErrorMotivation and Context
Problem: Scattered Context and Limited Extensibility
The previous SDK architecture had several limitations:
Fragmented Request Context: Tool/resource/prompt handlers received an
extraparameter containing request info, but accessing related capabilities (logging, elicitation, sampling) required keeping references to the server instance and manually correlating request IDs.No Middleware Support: Cross-cutting concerns like logging, authentication, and rate limiting had to be implemented ad-hoc in each handler, leading to code duplication.
Monolithic Protocol Class: The
Protocolclass (1,667 lines) contained all message handling, timeout management, progress tracking, and task support in one file. It also mixed client-specific code (e.g.,getTask(),listTasks(),cancelTask()methods) with server-specific code (e.g., task request handlers registered conditionally), violating separation of concerns—the abstraction leaked by assuming knowledge of who was using it.No Runtime Registry Access: Registered tools, resources, and prompts could only be accessed by saving the return value of
registerTool()/registerResource()/registerPrompt(). There was no way to query all registered items, filter by enabled/disabled status, or perform bulk operations without maintaining external references.Inconsistent Error Handling:
McpErrorwas used for three semantically different purposes:-32700 ParseError)-32001 RequestTimeout,-32000 ConnectionClosed)This made it impossible to distinguish "this error code is locked per spec" from "user can customize this code" from "this error should never be sent over the wire."
Solution: Layered Architecture with Structured Context
graph TB subgraph "High-Level API" McpServer["McpServer"] Builder["McpServerBuilder"] end subgraph "Middleware Layer" MW["MiddlewareManager"] UnivMW["Universal Middleware"] TypeMW["Type-Specific Middleware"] PerItemMW["Per-Item Middleware"] end subgraph "Registry Layer" ToolReg["ToolRegistry"] ResReg["ResourceRegistry"] PromptReg["PromptRegistry"] end subgraph "Context Layer" ServerCtx["ServerContext"] BaseCtx["BaseContext"] McpCtx["McpContext"] ReqCtx["RequestContext"] TaskCtx["TaskContext"] end subgraph "Protocol Layer" Server["Server"] Protocol["Protocol"] Plugins["ProtocolPlugins"] end subgraph "Transport Layer" Transport["Transport"] end Builder --> McpServer McpServer --> MW McpServer --> ToolReg McpServer --> ResReg McpServer --> PromptReg McpServer --> Server MW --> UnivMW MW --> TypeMW MW --> PerItemMW Server --> Protocol Protocol --> Plugins Protocol --> Transport ServerCtx --> BaseCtx BaseCtx --> McpCtx BaseCtx --> ReqCtx BaseCtx --> TaskCtxArchitecture Changes (Internal/Contributors)
These changes affect the SDK's internal structure and are most relevant for contributors and those wanting to understand how the SDK works.
1. Internal Plugin System
The Protocol class has been decomposed via an internal plugin system—the most significant architectural change in this PR:
graph LR subgraph "Protocol Plugins (Internal)" TP[TaskPlugin<br/>Message queueing] TCP[TaskClientPlugin<br/>Client-side tasks] PM[ProgressManager<br/>Progress tracking] TM[TimeoutManager<br/>Request timeouts] end Protocol --> TP Protocol --> TCP Protocol --> PM Protocol --> TMPlugins hook into the request/response lifecycle without modifying core Protocol logic:
install()- Register handlers during setuponRequest()/onRequestResult()- Intercept request processingshouldRouteMessage()/routeMessage()- Custom message routing (e.g., task queueing)onBuildHandlerContext()- Contribute context fields (e.g., task context)Why plugins instead of a monolithic Protocol?
Plugin usage example:
2. Error Hierarchy
A structured error hierarchy distinguishes errors by their semantics:
graph TD subgraph "Crosses the Wire (JSON-RPC)" PE[ProtocolError<br/>Locked codes: -32700, -32600, etc.] UE[User Error<br/>Plain Error thrown in handlers<br/>Customizable via onError callback] end subgraph "Local Errors (SDK-side only)" SE[SdkError] StateE[StateError<br/>NOT_CONNECTED, ALREADY_CONNECTED] CapE[CapabilityError<br/>CAPABILITY_NOT_SUPPORTED] TransE[TransportError<br/>CONNECTION_FAILED, TIMEOUT, SEND_FAILED] ValE[ValidationError<br/>INVALID_SCHEMA, INVALID_REQUEST] end SE --> StateE SE --> CapE SE --> TransE SE --> ValEError types:
ProtocolError: SDK-generated protocol violations with spec-mandated codes (e.g.,-32700 ParseError,-32602 InvalidParams). Users can also throwProtocolErrordirectly to signal a specific locked code.Error: User-thrown errors in handlers become JSON-RPC errors. Customize the response (code, message, data) via theonErrorcallback.SdkErrorsubclasses: Local errors that never cross the wire—they are rejected/thrown locally.Why this matters internally:
ProtocolError→ locked code,onProtocolErrorcan only customize message/dataError→ customizable code/message/data viaonError3. Registry System & Class-Based Entities
Tools, resources, and prompts are now managed by
ToolRegistry,ResourceRegistry, andPromptRegistryclasses. Each registered item is a class instance (RegisteredTool,RegisteredResource,RegisteredPrompt) with private fields.Before (POJOs with public properties):
After (classes with private fields):
Runtime registry access:
Why registries & class-based entities?
#enabled,#name) prevent bypassing notification callbacksdisableAll(),enableAll()for maintenance windows, feature flagsMcpServer4. Context Layer
Structured context objects flow through the request lifecycle:
Why structured context?
onBuildHandlerContext()5. New Components
TimeoutManagerProtocolProgressManagerProtocolHandlerRegistryProtocolTaskPluginProtocolviausePlugin()TaskClientPluginProtocolviausePlugin()ToolRegistryMcpServerResourceRegistryMcpServerPromptRegistryMcpServerMiddlewareManagerMcpServerClientMiddlewareManagerClientAPI Changes (External/Users)
These changes affect the SDK's public API and are most relevant for developers building MCP servers and clients.
1. Structured Context Object (Breaking)
Handlers now receive a typed
ctxobject with clear namespacing:2. Middleware System
Express/Koa-style middleware for cross-cutting concerns:
sequenceDiagram participant Client participant Universal as Universal MW participant TypeMW as Tool MW participant PerItem as Per-Tool MW participant Handler Client->>Universal: Request Universal->>Universal: Pre-processing Universal->>TypeMW: next() TypeMW->>TypeMW: Pre-processing TypeMW->>PerItem: next() PerItem->>PerItem: Auth check PerItem->>Handler: next() Handler-->>PerItem: Result PerItem-->>TypeMW: Result TypeMW->>TypeMW: Post-processing TypeMW-->>Universal: Result Universal->>Universal: Post-processing Universal-->>Client: ResponseWhy middleware?
3. Builder Pattern
Fluent API for declarative server configuration:
Why builder pattern?
4. Registry Access & Bulk Operations
Registries are now accessible at runtime without saving individual references:
Why registry access?
disableAll(),enableAll()for maintenance windows, feature flagsgetEnabled(),getDisabled()for status-based queries5. Error Handler Callbacks
Customize error responses sent to clients:
Key differences between handlers:
onErroronProtocolError{ code?, message?, data? }{ message?, data? }'tool'|'resource'|'prompt''protocol'ErrorContext provided to handlers:
Server:
Client:
Return value options:
undefined/void- Use default error handlingstring- Use as error message{ code?, message?, data? }- Customize error responseError- Wrap and use as errorWhy error handler callbacks?
6. Type-Safe Event System
Cross-platform event emitter for observability:
Why a custom event system?
EventEmitterdoesn't work in browsers or edge runtimes; this implementation works everywhereanytypes or string-based lookupson()instead of requiring manualoff()callsAvailable events (Protocol-level):
connection:opened{ sessionId }connection:closed{ sessionId, reason? }error{ error, context? }Pre-defined event maps for future expansion:
McpServerEvents- Tool/resource/prompt registration, connection lifecycleMcpClientEvents- Tool calls, results, connection lifecycle7. Content Helpers
New utility functions for creating tool result content:
Why content helpers?
ContentBlockobjectsHow Has This Been Tested?
TODO:
Breaking Changes
1. Tool/Resource/Prompt Callback Signatures
Before:
After:
The second parameter is now a config object, and the callback receives
ctxinstead ofextra.2. Context Object Structure
Before:
After:
3. Server-Initiated Requests from Handlers
Before:
After:
4. Deprecated Method Signatures Removed
The legacy overloaded signatures for
.tool(),.resource(),.prompt()that accepted positional arguments have been removed. Use the config object pattern instead.Migration Guide
Types of changes
Checklist
Additional Context
Architecture Decisions
Why structured context over flat extras?
Why internal plugins instead of public API?
Why Express-style middleware?
(ctx, next) => { ... next() ... }patternNew Exports
Future Work (not done in this PR)
Split / Abstract away Transport Classes
Problem: WebStandardStreamableHTTPServerTransport (994 lines) handles too many concerns.
Solution: Split into focused / interfaced componsens (e.g. HttpRequestRouter, SessionManager, ResumabilityManager, SecurityMiddleware)
Lifecycle Hooks
Problem: No hooks for server/client lifecycle events that run once (not per-connection).
State Introspection
Problem: Beyond getTool()/getResource()/getPrompt() retrieval, users may want deeper inspection for debugging and admin endpoints.
Debug Mode
Problem: No built-in verbose logging for SDK internals.
Solution: MCP_DEBUG environment variable that enables verbose internal logging for:
Performance Hooks
Problem: No built-in instrumentation points for internal metrics/tracing.
Solution: Internal hooks at key points:
Fixed: Local Timeout/ConnectionClosed Errors Now Use
TransportErrorPrevious Issue: The SDK previously used
McpError/ProtocolErrorwith code-32001(RequestTimeout) and-32000(ConnectionClosed) for local errors that never cross the wire. This was semantically inconsistent.Resolution: These errors now use
TransportError(anSdkErrorsubclass):ProtocolError(RequestTimeout)TransportError.requestTimeout()ProtocolError(ConnectionClosed)TransportError.connectionClosed()Why this is correct:
ProtocolErroris semantically defined as "protocol errors that cross the wire as JSON-RPC error responses"notifications/cancelledfor timeouts, not an error responseTransportErrorproperly represents local transport-layer issuesStatus: ✅ Fixed in this PR