Skip to content

Conversation

@ccreighton-apptio
Copy link
Contributor

@ccreighton-apptio ccreighton-apptio commented Sep 22, 2025

Adds definitions for params? on all Requests that avoid overwriting extended interfaces, while still allowing base models to be extended with custom functionality, implementing SEP-1319 in the process. DOES NOT FIX allowance for arbitrary params in 7 models as this is a "breaking" change.

Please consider #1692 instead, as it fixes what I believe is unintentional behavior that allows arbitrary params in 7 models.

Motivation and Context

In #1284, it's suggested to use _meta on tools/list & other requests, but this isn't currently possible.

The cleanest approach, IMO, to solving that issue involves implementing SEP-1319.

While doing so, I believe it would make sense to "bite the bullet" on a breaking change to what I believe is unintentional behavior present in the 7 models which were not affected by _meta erasure. THIS PR DOES NOT DO THIS. Please consider #1692 instead.

Please note that the introduction of non-arbitrary params to any of these 7 models would also be a breaking change in the future unless support for arbitrary params is retained at that time. In the event someone has already made "unofficial" use of the support for arbitrary params, even defining a non-arbitrary param while retaining support for other arbitrary params would also be a potentially breaking change. As a result, I believe it is best to address this issue sooner than later.

How Has This Been Tested?

Defined a sample ListToolsRequest to ensure that both _meta and cursor were supported params:

const listToolsRequest: ListToolsRequest = {
  id: 'foo',
  jsonrpc: "2.0",
  method: "tools/list",
  params: {
    cursor: 'next',
    badKey: 'typescript error here - invalid key',
    _meta: {
      any: 'no typescript error here',
      thing: {
        complex: 'object'
      }
    }
  }
}

Defined a sample PingRequest to ensure that arbitrary params are STILL allowed:

const pingRequest: PingRequest = {
  id: "foo",
  jsonrpc: "2.0",
  method: "ping",
  params: {
    _meta: {
      "anything": ["in", "here"]
    },
    arbitraryKey: 'NO typescript error here - the spec allows this. Depending on the server implementation, this request might:\n' +
      '      - Fail - but hopefully not since the spec allows this\n' +
      '      - Succeed, with this param ignored\n' +
      '      - Succeed, with this param being handled in some unofficial manner',
  },
}

Breaking Changes

No, but IMO it should. Please consider #1692 instead.

Please see further discussion here, and follow-up here.

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

Please consider #1692 instead.

Copy link
Contributor Author

@ccreighton-apptio ccreighton-apptio left a comment

Choose a reason for hiding this comment

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

Per the comments in #1284 I believe it was intended that progressToken and _meta be available on all Request types. I'm certainly open to other approaches / names / naming conventions, but believe this matches the original intent.

[key: string]: unknown;
};
progressToken?: ProgressToken;
[key: string]: unknown;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Breaking change: this was previously present both inside _meta and outside it. My assumption was that the intent was to show that other interfaces might add properties to params. In practice, such types overwrote this definition, so it only applied to PingRequest and ListRootsRequest which utilized the base definition.

If desired, this could be added back, but if I've understood the intent properly, I don't think that's actually desired behavior.

Copy link
Member

Choose a reason for hiding this comment

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

I think (read: speculate) the intent was to allow MCP to be extended with unofficial RPC methods that merely have to conform to the Request interface, which supports any properties in params. So I think we should leave this in.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the initial review! I'm not 100% sure I follow this though. I'll put this back, but would like to make sure I understand how this delivers on that intent (or not?).

In the "test" object from the description, badKey fails type checking, but introducing arbitrary params means the test object should instead look something like:

const listToolsRequest: ListToolsRequest = {
  id: 'foo',
  jsonrpc: "2.0",
  method: "tools/list",
  params: {
    cursor: 'next',
    arbitraryKey: 'NO typescript error here - the spec allows this. Depending on the server implementation, this request might:\n' +
      '      - Fail - but hopefully not since the spec allows this\n' +
      '      - Succeed, with this param ignored\n' +
      '      - Succeed, with this param being handled in some unofficial manner',
    _meta: {
      any: 'no typescript error here',
      thing: {
        complex: 'object'
      }
    }
  }
}

Is the intent that the Succeed, with this param being handled in some unofficial manner case be allowed for the tools/list method? If so, I misunderstood what you meant by allow MCP to be extended with unofficial RPC methods, but the way I interpreted that was more like:

If I wanted to extend MCP with an unofficial RPC method, I think I would need to introduce some new method definition which would have a params definition of its own. As part of that definition, I would define the params it takes similar to how the spec does, and my definition would extend RequestParams with definitions for the specific params needed. For example, I might want to introduce a tools/call/cancel method, which extends existing tool calling functionality to enable requesting cancellation a prior tool call request.

To support this, I want a params definition that lets me specify the id of the Request that wish to cancel:

export interface CancelCallToolRequestParams extends RequestParams {
  /**
   * The id of the previously sent tool call request which cancellation is being requested for.
   */
  id: RequestId;
}

And then define the cancellation request which uses it:

export interface CancelCallToolRequest extends JSONRPCRequest {
  method: "tools/call/cancel";
  params: CancelCallToolRequestParams;
}

This enables me to send cancellation requests which specify the id of the prior request, along with the standard RequestParams, but not arbitrary params.

Which of these understandings is more in line with the intent behind:

allow MCP to be extended with unofficial RPC methods

Copy link
Member

Choose a reason for hiding this comment

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

If I wanted to extend MCP with an unofficial RPC method, I think I would need to introduce some new method definition which would have a params definition of its own. As part of that definition, I would define the params it takes similar to how the spec does, and my definition would extend RequestParams with definitions for the specific params needed.

My thinking (again, speculation) is that while a custom server / client might validate against a new request type that has its own dedicated params type, the SDK or other intermediary code would still validate against the Request type. So Request.params would need to allow arbitrary key-values in order to support that.

Is the intent that the Succeed, with this param being handled in some unofficial manner case be allowed for the tools/list method?

No, I don't think so. So perhaps I rescind my suggestion about renaming BaseRequestParams, and instead we could have both BaseRequestParams and RequestParams types.

All *RequestParams types would extend BaseRequestParams, which would define _meta. Additionally, the RequestParams type, which would be the type for Request.params, would allow arbitrary key-values. That way, we preserve backward compatibility.

Does that make sense?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

IIUC, as described, that will introduce TypeScript errors like:

2430: Interface InitializeRequest incorrectly extends interface JSONRPCRequest
  Types of property params are incompatible.
    Type InitializeRequestParams is not assignable to type RequestParams
      Index signature for type string is missing in type InitializeRequestParams

Copy link
Contributor Author

Choose a reason for hiding this comment

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

How about?

/** @internal */
export interface Request {
  method: string;
  params?: RequestParams | { [key: string]: unknown };
}

This allows arbitrary params on Request without imposing that requirement on RequestParams and avoids re-introducing the Base nomenclature.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've pushed a commit with this proposed change & addressing this same issue with notifications, but if there's an alternate route I should pursue that doesn't cause type errors, I can certainly revisit this.

Copy link
Member

Choose a reason for hiding this comment

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

IIUC, as described, that will introduce TypeScript errors like:

Ah, looks like that's due to microsoft/TypeScript#15300. TL;DR: if we used type aliases instead of interfaces, it would work.

How about?

/** @internal */
export interface Request {
  method: string;
  params?: RequestParams | { [key: string]: unknown };
}

That works, though the JSON schema ends up being:

{
    "Request": {
        "properties": {
            "method": {
                "type": "string"
            },
            "params": {
                "anyOf": [
                    {
                        "$ref": "#/definitions/RequestParams"
                    },
                    {
                        "additionalProperties": {},
                        "type": "object"
                    }
                ]
            }
        },
        "required": [
            "method"
        ],
        "type": "object"
    }
}

Which might be fine?

Another option is simply defining Request as:

/** @internal */
export interface Request {
  method: string;
  params?: { [key: string]: any };
}

Then JSON schema ends up as:

{
    "Request": {
        "properties": {
            "method": {
                "type": "string"
            },
            "params": {
                "additionalProperties": {},
                "type": "object"
            }
        },
        "required": [
            "method"
        ],
        "type": "object"
    }
}

Which feels slightly more accurate (in a nitpicky way).

I generally prefer unknown to any, but since we are contending with TypeScript's design decision more so than type safety, I'm inclined to just go with any. Thoughts?

Copy link
Contributor Author

@ccreighton-apptio ccreighton-apptio Oct 6, 2025

Choose a reason for hiding this comment

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

Done!

E: I will leave this convo unresolved for visibility in future reviews, since it concerns the behavior called out as breaking in the description.

Copy link
Member

@jonathanhefner jonathanhefner left a comment

Choose a reason for hiding this comment

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

There is some overlap here with #1318, which addresses SEP-1319.

I think there is value in this refactoring / fix, and, being a refactoring / fix, it's not clear to me that an SEP would actually be necessary.

Anyway, 👍 from me.

[key: string]: unknown;
};
progressToken?: ProgressToken;
[key: string]: unknown;
Copy link
Member

Choose a reason for hiding this comment

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

I think (read: speculate) the intent was to allow MCP to be extended with unofficial RPC methods that merely have to conform to the Request interface, which supports any properties in params. So I think we should leave this in.

@ccreighton-apptio
Copy link
Contributor Author

ccreighton-apptio commented Oct 6, 2025

@jonathanhefner thanks again for reviewing! For easy diff visibility, all changes are in a 2nd commit. Please see my comment about my concerns/understanding regarding arbitrary params & unofficial RPC methods.

After we're certain that arbitrary params keys are desirable, I'd be happy to open a separate PR with changes for notifications unless you'd really like to see them addressed in this one.

jonathanhefner
jonathanhefner previously approved these changes Oct 7, 2025
Copy link
Member

@jonathanhefner jonathanhefner left a comment

Choose a reason for hiding this comment

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

Excellent! Thank you, @ccreighton-apptio! ❤️

Comment on lines 102 to 110
params?: RequestParams;
}

/**
* A notification which does not expect a response.
*/
export interface JSONRPCNotification extends Notification {
jsonrpc: typeof JSONRPC_VERSION;
params?: NotificationParams;

This comment was marked as outdated.

*/
export interface PingRequest extends JSONRPCRequest {
method: "ping";
params?: RequestParams
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Per @jonathanhefner 's comment here, and my response, after updating Request and Notification to allow extension using params?: { [key: string]: any };, the 7 models listed as having "breaking" changes in the description weren't actually experiencing any breaking changes.

If getting this merged is reliant on backing out the breaking change, I can do that, but in the meantime I've updated the PR to actually make that breaking change again, because I don't believe the behavior it breaks is intended.

Without this breaking change, the following is a valid PingRequest:

const pingRequest: PingRequest = {
  id: "foo",
  jsonrpc: "2.0",
  method: "ping",
  params: {
    _meta: {
      "anything": ["in", "here"]
    },
    "but also": "anything",
    "in": {
      "here": "too",
    }
  },
}

Copy link
Member

Choose a reason for hiding this comment

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

Depending on how the core maintainers lean, it might be better to make a separate PR based on top of this PR, and move the last commit over to the new PR. (That way, the fix for those 7 types can be evaluated separately.)

Copy link
Contributor Author

@ccreighton-apptio ccreighton-apptio Oct 21, 2025

Choose a reason for hiding this comment

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

😞

In the interest of not blocking an accept+merge of this PR in the event that the breaking change is unacceptable to the core maintainers, I have opened #1692 with the breaking change and removed the commit from this PR. The description has been updated to reflect that there are no longer any breaking changes in this PR.

While the GHA checks are currently failing, I believe this error:

The job was not started because your account is locked due to a billing issue.

is concerning modelcontextprotocol, and not my account. I've opened a PR that targets the main branch in my account to demonstrate that the workflows are passing: ccreighton-apptio#2

E: rebased since GHA workflows seem to be happier now

@ccreighton-apptio ccreighton-apptio changed the title Ensure _meta is available on all requests (#1284) SEP-1319: Decouple Request Payloads + _meta erasure fix (#1284) - PLEASE consider #1692 instead! Oct 21, 2025
@dsp-ant
Copy link
Member

dsp-ant commented Nov 4, 2025

Closing in favor of #1692

@dsp-ant dsp-ant closed this Nov 4, 2025
auto-merge was automatically disabled November 4, 2025 21:49

Pull request was closed

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.

Missing _meta in schema.json for CallToolRequest

4 participants