Skip to content

Conversation

KKonstantinov
Copy link
Contributor

@KKonstantinov KKonstantinov commented Oct 6, 2025

This is a draft PR which aims to introduce convenience / quality of life improvements to the Typescript SDK and also bring it closer to the Python SDK, which already has implemented this, in a similar fashion.

This is a draft PR, feedback is more than welcome.

It adds a Context utility (with the aim to be backwards-compatible, so anyone using RequestHandlerExtra will not have breaking changes), which exposes extra convenience methods and could be extended further down the line.

Motivation and Context

It aims to explore the concept of a backwards-compatible Context utility class being passed in callbacks on the server-side. The Context utility class provides convenience methods and can be further extended in the future. It provides access to methods like logging, sampling, elicitation (and others could be added) and one can call them from the callbacks without having access to the Server or McpServer instances.

It mimics to a large extent on how Python SDK works.

Additionally, Context has a composed class inside it, RequestContext (again, similar to how PythonSDK), which holds all the metadata, and adds on top of it the lifespanContext data. lifespanContext is a convenience / quality of life utility as implemented in the PythonSDK, which allows to pass any data to the McpServer, and access it back from the callback, by doing extra.requestContext.lifespanContext - this could be database connections or anything that the user wants to live through the whole lifespan of the server. This feature required some type generics to be added so that the .lifespanContext can have type safety when used from the callbacks.

TBD. Once any discussions have been had and if this gets accepted, documentation needs to be updated additionally in this PR to reflect the changes.

How Has This Been Tested?

Additional unit tests have been added.
Existing unit tests work with no changes.
Tests when callback is tied to RequestHandlerExtra.

Breaking Changes

No breaking changes. More tests could be added to ensure it.

The additions affect only server-side. Client-side should remain the same.

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

The changes follow existing concepts of the PythonSDK, applied in a similar fashion (to the extent that the libraries differ or similar in various parts of them) to Typescript.

INotes:

  • If we can afford breaking changes, would definitely restructure/remove things out of Context, but currently it needs to implement RequestHandlerExtra to keep backward compatibility
  • All the RequestHandlerExtra data actually resides inside RequestContext with the intent of moving it solely in there when we can afford breaking changes, leaving us with discrete data in Context and RequestContext.
  • Currently, its implemented in a non-breaking way, which leads to the extra getters on Context

Lifespan Context example

 const mcpServer = new McpServer(
            { name: 'ctx-test', version: '1.0' },
            {
                lifespan: {
                    userId: 'user-123',
                    attempt: 0
                }
            }
        );
        const client = new Client({ name: 'ctx-client', version: '1.0' });

        // Register a tool that reads/writes typed lifespanContext
        mcpServer.tool('lifespan-tool', { name: z.string() }, async (_args: { name: string }, extra) => {
            // Demonstrate type-safe access to lifespanContext
            const lc = extra.requestCtx.lifespanContext;

            // Typed writes
            lc.userId = 'user-123';
            lc.attempt = lc.attempt + 1;

            // Typed read
            const userId = lc.userId;

            return {
                content: [
                    {
                        type: 'text' as const,
                        text: JSON.stringify({ userId, attempt: lc.attempt })
                    }
                ]
            };
        });

Logging Utility methods

       // The .debug, .info, .warning, .error methods require a string message, and take optional extra data
       // The generic .log method follows the out of the box schema
        mcpServer.tool('ctx-log-test', { name: z.string() }, async (_args: { name: string }, extra) => {
            // Debug message with extra log data and a related session ID
            await extra.debug('Test message', { test: 'test' }, 'sample-session-id');
            // Info message with extra log data and a related session ID
            await extra.info('Test message', { test: 'test' }, 'sample-session-id');
            // Warning message with extra log data and a related session ID
            await extra.warning('Test message', { test: 'test' }, 'sample-session-id');
            // Error message with extra log data and a related session ID
            await extra.error('Test message', { test: 'test' }, 'sample-session-id');

            // Generic .log method which supports all levels, and "data" could be anything (not just a string)
            await extra.log({
                level,
                data: 'Test message',
                logger: 'test-logger-namespace'
            }, 'sample-session-id');

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

Elicit from Context

mcpServer.tool(
            'elicit-through-ctx-tool',
            {
                restaurant: z.string(),
                date: z.string(),
                partySize: z.number()
            },
            async ({ restaurant, date, partySize }, extra) => {
                // Check availability
                const available = await checkAvailability(restaurant, date, partySize);

                if (!available) {
                    // Ask user if they want to try alternative dates
                    const result = await extra.elicit({ // **Calling .elicit from Context here
                        message: `No tables available at ${restaurant} on ${date}. Would you like to check alternative dates?`,
                        requestedSchema: {
                            type: 'object',
                            properties: {
                                checkAlternatives: {
                                    type: 'boolean',
                                    title: 'Check alternative dates',
                                    description: 'Would you like me to check other dates?'
                                },
                                flexibleDates: {
                                    type: 'string',
                                    title: 'Date flexibility',
                                    description: 'How flexible are your dates?',
                                    enum: ['next_day', 'same_week', 'next_week'],
                                    enumNames: ['Next day', 'Same week', 'Next week']
                                }
                            },
                            required: ['checkAlternatives']
                        }
                    });
......

Sampling from Context

mcpServer.registerTool(
            'summarize',
            {
                title: 'Text Summarizer',
                description: 'Summarize any text using an LLM',
                inputSchema: {
                    text: z.string().describe('Text to summarize')
                },
                outputSchema: { summary: z.string() }
            },
            async ({ text }, extra) => {
                // Call the LLM through MCP sampling through Context
                const response = await extra.requestSampling({
                    messages: [
                        {
                            role: 'user',
                            content: {
                                type: 'text',
                                text: `Please summarize the following text concisely:\n\n${text}`
                            }
                        }
                    ],
                    maxTokens: 500
                });
        
                const summary = response.content.type === 'text' ? response.content.text : 'Unable to generate summary';

@KKonstantinov KKonstantinov requested a review from a team as a code owner October 6, 2025 14:10
@KKonstantinov
Copy link
Contributor Author

Think also the examples in this case should have the extra parameter no longer be extra, but ctx

@KKonstantinov
Copy link
Contributor Author

cc @ihrpr

@KKonstantinov KKonstantinov marked this pull request as draft October 8, 2025 05:56
@KKonstantinov KKonstantinov marked this pull request as ready for review October 8, 2025 05:56
Copy link
Contributor

@ihrpr ihrpr left a comment

Choose a reason for hiding this comment

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

Than you for working on this!

I would be very careful about Python SDK to Typescript SDK quality of life features porting, as many paradigms do not translate well to Typescript. I have concern about lifespan:

  1. Python's async context managers don't translate well to TypeScript/JavaScript
  2. Existing patterns - TypeScript/Node.js already has established patterns for dependency injection and lifecycle management
  3. Memory management - JavaScript's garbage collection and event loop model make this pattern unnecessary and potentially problematic

The TypeScript SDK should stick to idiomatic Typescript/Javascript patterns like:

  • Constructor injection for dependencies
  • Module-level state management
  • cleanup in disconnect handlers

I think I would also love to see in this PR how it will be used and what improvements we can get, probably adding a few code examples would be useful to have in the PR

@KKonstantinov
Copy link
Contributor Author

Thanks for the comments.

I can add examples in commits themselves (e.g. in the examples/ folder), they will be pretty much the same as in this PR's description.

On your comments:

  1. Python's async context managers don't translate well to TypeScript/JavaScript
  • This hasn't ported Python's async context managers.
  • Lifespan context is something I was debating on myself, we could remove it potentially all together, although I do see some value in it.
  1. Existing patterns - TypeScript/Node.js already has established patterns for dependency injection and lifecycle management

OK, what in the PR contradicts that? We are simply adding an utility, need some clarification/example on this point.

  1. Memory management - JavaScript's garbage collection and event loop model make this pattern unnecessary and potentially problematic
  • I assume this comment is about lifespan context - that's for stuff that would live throughout the full lifespan, e.g. external dependencies etc. We could remove lifespan context if we think it's not that useful. People should be aware the fact it's "lifespan" means it lives throughout all requests and isn't garbage collected at end of request.
  • On the Context object/class itself, especially if we do not have lifespan_context, it will get garbage collected. I do believe it will be garbage collected even if it holds references to something like a DB connection in lifespan_context though, but I need to double check it. If we think we don't need lifespan_context, then this isn't a concern (although I believe even with it, Context will get GC-ed at end at request)

The TypeScript SDK should stick to idiomatic Typescript/Javascript patterns like:

Agree, but let me know what parts of the PR you see as non-conformant with these points.

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