Skip to content

Conversation

@KKonstantinov
Copy link
Contributor

@KKonstantinov KKonstantinov commented Dec 7, 2025

This PR introduces a new Context API for request handlers in the TypeScript SDK, replacing the flat RequestHandlerExtra type with a structured, extensible context system.

The new context provides:

  • A nested structure that organizes context by concern (mcpCtx, requestCtx, taskCtx)
  • Server-specific convenience methods for logging, sampling, and elicitation
  • Type-safe contexts for both server (ServerContext) and client (ClientContext) handlers
  • Extensibility for future additions without breaking the API surface

Motivation and Context

The previous RequestHandlerExtra had several design issues:

1. POJO Creation Overhead

Every incoming request created a new plain JavaScript object with all properties and methods inline:

const fullExtra: RequestHandlerExtra<...> = {
    signal: abortController.signal,
    sessionId: capturedTransport?.sessionId,
    _meta: request.params?._meta,
    sendNotification: async notification => { /* closure */ },
    sendRequest: async (r, resultSchema, options?) => { /* closure */ },
    authInfo: extra?.authInfo,
    requestId: request.id,
    // ... more properties
};

This approach has performance implications:

  • New function closures per request: The sendNotification and sendRequest arrow functions are created as new function objects for every request, each capturing variables from the enclosing scope (request, relatedTaskId, taskStore, this). These closures are heap allocations that add GC pressure.
  • No shared prototype: With classes, methods exist once on the prototype and are shared across all instances. With the POJO approach, function references are stored directly on each instance, increasing per-instance memory footprint.

The new Context API uses class instances with methods on the prototype. While both approaches allocate a new object per request, the class approach avoids per-request function allocations by referencing shared prototype methods instead.

2. Mixed Concerns: Properties Irrelevant to Client or Server

RequestHandlerExtra was a single type used by both Client and Server, but many properties only applied to one side:

Property Relevant To Issue
requestInfo Server only HTTP request details don't exist on client
closeSSEStream Server only SSE stream control is server-side transport concern
closeStandaloneSSEStream Server only Same as above
authInfo Server only Client doesn't authenticate inbound requests from server
taskId, taskStore, taskRequestedTtl Either, but only when tasks enabled Always present but usually undefined

This led to:

  • Confusing API: Client handler authors see closeSSEStream in autocomplete but it's always undefined
  • Excessive optionality: Nearly every property was optional (?), making the type less useful for type checking
  • No type narrowing: No way to express "this is a server context with SSE support"

The new Context API uses separate types (ServerContext, ClientContext) with appropriate properties for each, and nested structures (requestCtx.stream) to group related optional features.

3. Flat Structure Limits Extensibility

Adding new features to RequestHandlerExtra meant:

  • Adding more optional properties to an already crowded type
  • Risk of name collisions with future protocol additions
  • No logical grouping of related functionality

The new nested structure (mcpCtx, requestCtx, taskCtx) provides clear namespaces for different concerns, making it easy to add features without polluting the top-level API.

4. No Convenience Methods

Common operations like logging, sampling, and elicitation required manual construction:

// Old: Manual logging notification
await server.sendLoggingMessage({ level: 'info', data: 'message' });

// New: Convenience method on context
await ctx.loggingNotification.info('message');

The new Context API introduces a clean separation:

  • ctx.mcpCtx - MCP protocol-level context (requestId, method, sessionId, _meta)
  • ctx.requestCtx - Transport/request-level context (signal, authInfo, and server-specific: uri, headers, stream controls)
  • ctx.taskCtx - Task context when tasks are enabled (id, store, requestedTtl)
  • Context methods - sendNotification(), sendRequest() directly on the context
  • Server helpers - loggingNotification, requestSampling(), elicitInput() on ServerContext

How Has This Been Tested?

  • All existing unit tests updated and passing
  • New unit tests added for context functionality in test/integration/test/server/context.test.ts
  • Tests cover:
    • Context property access (mcpCtx, requestCtx, taskCtx)
    • Server context methods (logging, sampling, elicitation)
    • Client context for handling server-initiated requests
    • Task context integration

Breaking Changes

This is a breaking change for code that accesses properties directly on the handler's second parameter:

Before (v1) After (v2)
extra._meta ctx.mcpCtx._meta
extra.sessionId ctx.mcpCtx.sessionId
extra.requestId ctx.mcpCtx.requestId
extra.signal ctx.requestCtx.signal
extra.authInfo ctx.requestCtx.authInfo
extra.sendNotification(...) ctx.sendNotification(...)
extra.sendRequest(...) ctx.sendRequest(...)

Migration: Update property access paths as shown above. The sendNotification and sendRequest methods remain at the top level of the context.

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

Logging from ServerContext

mcpServer.tool('log-example', { message: z.string() }, async ({ message }, ctx) => {
    // Convenience logging methods
    await ctx.loggingNotification.debug('Debug message', { extra: 'data' });
    await ctx.loggingNotification.info('Info message');
    await ctx.loggingNotification.warning('Warning message');
    await ctx.loggingNotification.error('Error message');

    // Or use the generic log method
    await ctx.loggingNotification.log({
        level: 'info',
        data: { message },
        logger: 'my-logger'
    });

    return { content: [{ type: 'text', text: 'ok' }] };
});

Elicitation from ServerContext

mcpServer.tool('reservation-tool', { restaurant: z.string() }, async ({ restaurant }, ctx) => {
    const result = await ctx.elicitInput({
        message: `No tables available at ${restaurant}. Check alternatives?`,
        requestedSchema: {
            type: 'object',
            properties: {
                checkAlternatives: {
                    type: 'boolean',
                    title: 'Check alternative dates'
                }
            },
            required: ['checkAlternatives']
        }
    });

    if (result.action === 'accept' && result.content?.checkAlternatives) {
        // User wants to check alternatives
    }

    return { content: [{ type: 'text', text: 'Done' }] };
});

Sampling from ServerContext

mcpServer.tool('summarize', { text: z.string() }, async ({ text }, ctx) => {
    const response = await ctx.requestSampling({
        messages: [
            {
                role: 'user',
                content: {
                    type: 'text',
                    text: `Please summarize: ${text}`
                }
            }
        ],
        maxTokens: 500
    });

    const summary = response.content.type === 'text' 
        ? response.content.text 
        : 'Unable to generate summary';

    return { content: [{ type: 'text', text: summary }] };
});

Accessing MCP Context

mcpServer.tool('context-aware-tool', {}, async (_args, ctx) => {
    // Access MCP-level context
    console.log('Request ID:', ctx.mcpCtx.requestId);
    console.log('Session ID:', ctx.mcpCtx.sessionId);
    console.log('Method:', ctx.mcpCtx.method);
    console.log('Progress token:', ctx.mcpCtx._meta?.progressToken);

    // Access request-level context
    console.log('Auth info:', ctx.requestCtx.authInfo);
    
    // Server-specific: HTTP request details
    console.log('Request URI:', ctx.requestCtx.uri);
    console.log('Headers:', ctx.requestCtx.headers);

    // Use abort signal for cancellation
    ctx.requestCtx.signal.addEventListener('abort', () => {
        console.log('Request was cancelled');
    });

    return { content: [{ type: 'text', text: 'ok' }] };
});

Task Context (when tasks are enabled)

mcpServer.tool('long-running-task', {}, async (_args, ctx) => {
    if (ctx.taskCtx) {
        // Create a task for long-running operations
        await ctx.taskCtx.store.createTask({
            status: 'working',
            message: 'Processing...'
        });

        console.log('Task ID:', ctx.taskCtx.id);
        console.log('Requested TTL:', ctx.taskCtx.requestedTtl);

        // Update task progress
        await ctx.taskCtx.store.updateTaskStatus(ctx.taskCtx.id, 'working');
    }

    return { content: [{ type: 'text', text: 'Task complete' }] };
});

@KKonstantinov KKonstantinov requested a review from a team as a code owner December 7, 2025 08:26
@pkg-pr-new
Copy link

pkg-pr-new bot commented Dec 7, 2025

Open in StackBlitz

@modelcontextprotocol/client

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

@modelcontextprotocol/server

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

@modelcontextprotocol/express

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

@modelcontextprotocol/hono

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

@modelcontextprotocol/node

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

commit: a5704c5

@KKonstantinov KKonstantinov linked an issue Dec 8, 2025 that may be closed by this pull request
mattzcarey
mattzcarey previously approved these changes Dec 9, 2025
Copy link
Contributor

@mattzcarey mattzcarey left a comment

Choose a reason for hiding this comment

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

big fan

@changeset-bot
Copy link

changeset-bot bot commented Dec 23, 2025

🦋 Changeset detected

Latest commit: a5704c5

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

This PR includes changesets to release 8 packages
Name Type
@modelcontextprotocol/express Patch
@modelcontextprotocol/hono Patch
@modelcontextprotocol/node Patch
@modelcontextprotocol/eslint-config Patch
@modelcontextprotocol/test-integration Patch
@modelcontextprotocol/client Patch
@modelcontextprotocol/server Patch
@modelcontextprotocol/core Patch

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

@KKonstantinov
Copy link
Contributor Author

KKonstantinov commented Dec 23, 2025

Will now have to be a v2 only feature - merged with main - converting back to a draft PR.

Since we can now afford breaking changes, will rework Context and refactor to remove RequestHandlerExtra completely.

Note to self: original context interface was done pre-tasks, need a type generic version with is aware when taskStore, taskId etc. is there so that the end user does not need to check on property existence in tasks handlers

TODO:

  • remove RequestHandlerExtra completely
  • client to also use Context/ContextInterface
  • tasks type generics on Context/ContextInterface
  • README.md update

Edit: All todos done

@KKonstantinov KKonstantinov changed the title Context API in callbacks [v2] Context API in callbacks Dec 23, 2025
@KKonstantinov KKonstantinov added the v2 Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes label Dec 23, 2025
@KKonstantinov KKonstantinov self-assigned this Dec 23, 2025
@KKonstantinov KKonstantinov added the breaking change Will break existing deployments when updated without changes label Dec 23, 2025
@KKonstantinov KKonstantinov marked this pull request as draft December 23, 2025 05:12
@KKonstantinov KKonstantinov marked this pull request as ready for review January 21, 2026 21:52
@KKonstantinov KKonstantinov changed the title [v2] Context API in callbacks v2 Context API in callbacks Jan 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking change Will break existing deployments when updated without changes v2 Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ctx object inside the tool extra for transport/server controls Simplify attribution of elicitation to a tool call

2 participants