Skip to content

Conversation

@MackinnonBuck
Copy link
Collaborator

Summary

Audits and standardizes MCP protocol types for consistency.

Fixes #519

Description

There are several inconsistencies across protocol types, including:

  • init vs. set
  • required vs. default property values
  • Read-only vs. mutable collection properties
  • enum vs. string
  • JsonNode vs. JsonElement vs. IDictionary<string, JsonElement> vs. IDictionary<string, object>

This PR applies the following set of conventions to improve consistency:

Property Mutability

Guideline: Always use set - no init-only properties.

Rationale:

  • If we're going to standardize on either set or init, it's easier to standardize on set because doing so is non-breaking
  • Using init means that if we do want to mutate a property after initialization, we need to clone the its containing object, which can be error-prone and isn't great for perf
  • It's not clear (to me at least) that we actually rely on the immutability of any properties in protocol types

Required Properties

Guideline: Use required when the spec indicates a property is required, unless a clear, safe default exists.

Rationale:

  • Reduces initialization errors when mapping to/from protocol types
  • Aligns better with the MCP schema

Safe defaults include:

  • Empty collections
  • The value returned by calling a type's parameterless constructor

Avoid defaults such as:

  • The empty string ("") - this is rarely the expected value for a "required" property
  • An enum value we arbitrary decide should be the default

Collections

Guideline: Prefer IList<T> / IDictionary<TKey, TValue> over IReadOnlyList<T> / IReadOnlyDictionary<TKey, TValue>.

Rationale:

  • Reduces unnecessary cloning if mutation is needed
  • It's not clear that we actually rely on collections in any protocol types being read-only

Open Questions

The following inconsistencies remain for further discussion:

enum vs. string: properties

The MCP schema defines some properties as unions of string values.

  • enum offers strong typing
  • string is more future-proof (e.g., if new values are added)

Existing examples of each:

  • enum: ContextInclusion, Role
  • string: ElicitResult.Action

JSON-like Structures:

The use of JsonNode, JsonElement, IDictionary<string, JsonElement>, and IDictionary<string, object> varies.

  • These each have different pros and cons, and I'm not sure we necessarily want or need to standardize on a single one.
  • We should define principles for when each is appropriate and align existing types accordingly.

Additional Notes

  • None of these need to be hard and fast rules. If there's a compelling reason to deviate, it should be fine to do so.
  • This PR also addresses a few smaller inconsistencies not listed above.

{
Blob = dataContent.Base64Data.ToString(),
MimeType = dataContent.MediaType,
Uri = string.Empty,
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This was implicitly string.Empty before, but now it's explicit. This PR doesn't introduce a behavioral change here, but we should decide whether the empty string should really be used here, as it's not actually a valid URI. It would also be strange to use dataContent.Uri, because then the data is specified in two places (once in Blob and once in Uri).

Copy link
Collaborator

Choose a reason for hiding this comment

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

then the data is specified in two places

It doesn't sound bad, DataContent.Uri is just $"data:{MediaType};base64,{base64}" while MimeType and Blob are there to access those parts without having to parse the Uri. It would also align with DataCotent.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Realizing that there's also TextResourceContents, I wonder if Uri should be abstract and Blob and Text types should provide an implementation.

public sealed class TextContent
{
    public override string Uri => $"data:text/plain;base64,{Base64Url.EncodeToString(Encoding.UTF8.GetBytes(Text))}"
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

then the data is specified in two places

It doesn't sound bad, DataContent.Uri is just $"data:{MediaType};base64,{base64}" while MimeType and Blob are there to access those parts without having to parse the Uri. It would also align with DataCotent.

The DataContent type in Microsoft.Extensions.AI is different in that the Uri can only be a data URI, and the Data property is [JsonIgnore]d, so the duplication only exists in-memory. However, the MCP BlobResourceContents type has both uri and blob as separate, required properties. This means that if you treat BlobResourceContents.uri as a data URI, you'll be including the data twice in JSON, which would be unfortunate IMO.

The spec also doesn't seem to acknowledge the possibility of the uri property being a data URI (although it doesn't explicitly disallow it). The examples all show file://, and only https://, file://, and git:// are listed as examples of common URI schemes.

So, the purpose of BlobResourceContents.uri seems to be identifying the data, which is different than DataContent.Uri, whose purposes is containing the data itself. This is what creates the dilemma when mapping between these two types.

Realizing that there's also TextResourceContents, I wonder if Uri should be abstract and Blob and Text types should provide an implementation

This would disallow TextResourceContents and BlobResourceContents from having anything other than a data URI, right?

@eiriktsarpalis
Copy link
Member

Guideline: Always use set - no init-only properties.

Rationale:

  • If we're going to standardize on either set or init, it's easier to standardize on set because doing so is non-breaking
  • Using init means that if we do want to mutate a property after initialization, we need to clone the its containing object, which can be error-prone and isn't great for perf
  • It's not clear (to me at least) that we actually rely on the immutability of any properties in protocol types

For context, the presence of init properties is a holdover from when most of the models were record types, where the language supports persistable updates using with expressions. Since we decided to move away from records going from init to set sounds reasonable.

@halter73
Copy link
Contributor

The use of JsonNode, JsonElement, IDictionary<string, JsonElement>, and IDictionary<string, object> varies.

  • These each have different pros and cons, and I'm not sure we necessarily want or need to standardize on a single one.
  • We should define principles for when each is appropriate and align existing types accordingly.

What are our principles around this? I know JsonNode is mutable and JsonElement is not, but why is JsonNode use for params and results and JsonElement or IDictionary<string, JsonElement> seemingly used everywhere else particularly considering these properties are settable? And what's up with IDictionary<string, object> just for experimental capabilities?

@MackinnonBuck
Copy link
Collaborator Author

MackinnonBuck commented Oct 20, 2025

What are our principles around this? I know JsonNode is mutable and JsonElement is not, but why is JsonNode use for params and results and JsonElement or IDictionary<string, JsonElement> seemingly used everywhere else particularly considering these properties are settable? And what's up with IDictionary<string, object> just for experimental capabilities?

Exactly what I'm wondering too 🙂 Maybe we could get an STJ expert to weigh in on this?

Edit: @eiriktsarpalis , do you have any thoughts about this?

Copy link
Contributor

Copilot AI commented Oct 29, 2025

@MackinnonBuck I've opened a new pull request, #923, to work on those changes. Once the pull request is ready, I'll request review from you.

@eiriktsarpalis
Copy link
Member

What are our principles around this? I know JsonNode is mutable and JsonElement is not, but why is JsonNode use for params and results and JsonElement or IDictionary<string, JsonElement> seemingly used everywhere else particularly considering these properties are settable? And what's up with IDictionary<string, object> just for experimental capabilities?

Exactly what I'm wondering too 🙂 Maybe we could get an STJ expert to weigh in on this?

Edit: @eiriktsarpalis , do you have any thoughts about this?

  • JsonElement is a struct with default(JsonElement) representing an invalid value (similar to ImmutableArray<T>). This makes it awkward to use in properties that are optional, typically requiring JsonElement? which adds its own complexities into the mix. This is why I originally opted for JsonNode? to encode parameters, which is a class with null naturally mapping to JSON null.
  • When such concerns aren't present JsonElement is the natural choice since it's more efficient (it's basically just a annotated buffer)..
  • IDictionary<string, object> is used typically in hybrid situations where you want values to both be JsonElement or potentially other .NET values that we are able to serialize into JSON. This is how AIFunction has been designed to work.

* Initial plan

* Add DefaultSamplingMaxTokens property to McpServerOptions

Co-authored-by: MackinnonBuck <[email protected]>

* Add test to verify DefaultSamplingMaxTokens is respected

Co-authored-by: MackinnonBuck <[email protected]>

* Merge test into existing SampleAsync_Messages_Forwards_To_McpServer_SendRequestAsync

Co-authored-by: MackinnonBuck <[email protected]>

* Update src/ModelContextProtocol.Core/Server/McpServerOptions.cs

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: MackinnonBuck <[email protected]>
Co-authored-by: Mackinnon Buck <[email protected]>
@MackinnonBuck
Copy link
Collaborator Author

Thanks for the clarification, @eiriktsarpalis!

I'll leave the properties representing JSON objects as-is for this PR, then. Nothing jumps out at me as obviously using the wrong type.

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.

Audit required / init / abstract / sealed / etc. in protocol types

7 participants