Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions docs/McpClient-Feedback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
ModelContextProtocol.Protocol.Implementation name is too generic. Lots of things in this namespace named poorly. That might be OK if users don't have to deal with them, but it looks like they do.


ModelClient.CompleteAsync - naming seems off. Maybe "GetCompletion"



static ModelClient.CreateSamplingHandler - why is this factory method on ModelClient??? Maybe should be an extension method.



ModelClient - Enumerate\* vs List\* - why have these redundant methods that return the same thing just one wrapping. Can we just return IAE and have folks materialize that?



ModelClient.EnumerateToolsAsync - why accept JsonSerializerOptions on this call, it doesn't feel like something tied to enumerating tools. Would it make more sense to make this a property on the client?



SetLoggingLevel(LogLevel, CancellationToken) vs SetLoggingLevel(LoggingLevel, CancellationToken) -- WHY? Don't make these overloads, instead choose one then make conversions on the types.
Copy link
Contributor

Choose a reason for hiding this comment

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

LoggingLevel is the MCP type, LogLevel is the M.E.Logging type. Why shouldn't these be overloads? LoggingLevel has more values than LogLevel, so anyone with a LogLevel would be forced to call ToLoggingLevel as part of calling this. Why is that better than having it "just work"?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

With an enum like this aren't folks directly calling it with a literal? Why make them choose which one to use? The reason I wrote this was because I saw both in intellisense and wondered which I should use and why they were different. Having both sets the precedent for adding more. If we choose one as the currency of this type it establishes a pattern without any promise of more.

Copy link
Contributor

Choose a reason for hiding this comment

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

With an enum like this aren't folks directly calling it with a literal?

I'd expect it would more commonly be read from configuration or otherwise be dynamic rather than being hardcoded to a literal at the call site.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will scan for any other public surface that uses MCP primitives where we might have an ME/MEAI primitive as well.




SubscribeToResourceAsync - how are the notifications delivered? This method confused me as I don't understand what it's supposed to do. Might just need to research more.
Copy link
Contributor

Choose a reason for hiding this comment

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

MCP has a notifications mechanism. The client can set up a notification handler for specific notifications.

public abstract IAsyncDisposable RegisterNotificationHandler(string method, Func<JsonRpcNotification, CancellationToken, ValueTask> handler);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This probably needs better docs to connect the two methods at least. I wonder if naming could be improved to make it more obvious that the methods are complimentary.

I think I understand what to pass SubscribeToResourceAsync - that's the resource "path", but no clue what I should pass for method to RegisterNotificationHandler based on those docs. Is it just any of these

and the the combination of methods subscribed + resource subscribed to determines when the handler is invoked?

It's unusual that this method has subscribe+unsubscribe, yet RegisterNotificationHandler just returns an IAsyncDisposable. Seems inconsistent.

I also see tests that are specifying Handlers when creating the client.

Copy link
Contributor

Choose a reason for hiding this comment

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

This probably needs better docs to connect the two methods at least.

We need lots more docs but it does seem this should be high on the list.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Could make Subscribe return an IAsyncDisposable. That could unsubscribe.
Could add an overload to Subscribe that takes a handler and registers it. The dispose from that would both unsubscribe and unregister.
@ericstj to file an issue



TextContentBlock doesn't override ToString -- to get any of the data returned I need to get at protocol types, which feels wrong since those are all "raw" and not designed surface area.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jeffhandley -- here's an example of one of the "need to use Protocols" to do a basic thing. I didn't do a deep scrub of everything, but this one was a very basic scenario

Copy link
Contributor

Choose a reason for hiding this comment

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

What is this proposing? Not exposing any of the of the protocol types in various APIs?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To use these we have to downcast. There is no DebuggerDisplay to help folks understand what they have in the IDE. We could make these more closely match the pattern used in AIContent. @ericstj to file issue - DebuggerDisplay and ToString.


Same is true for TextResourceContents. The entire content model seems pretty rough - might be better to unify on MEAI content types.
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't believe relying only on the MEAI types is viable. They're good for passing a 90% representation around, but they will never be full-fidelity with every provider.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, that's fair. It would put pressure on us to add more types to MEAI if we didn't support the scenario, but it could create a common set of things that the ecosystem might build upon. We can always do that later I suppose. Perhaps we can build conversion extensions now.

Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps we can build conversion extensions now.

We have those, e.g.

public static AIContent? ToAIContent(this ContentBlock content)

public static AIContent ToAIContent(this ResourceContents content)

public static ContentBlock ToContentBlock(this AIContent content)

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 see - do we want to tell folks this is the preferred way to deal with content? I presume we still want the MCP content types to work (without using protocol types).

Copy link
Contributor

Choose a reason for hiding this comment

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

I see - do we want to tell folks this is the preferred way to deal with content?

In what context? If you're just coding against the MCP library directly, you'd typically use the MCP types. You'd use the conversion utilities when needing to interoperate across library boundaries. For example, the McpClientTool's AIFunction.InvokeAsync override converts the MCP types to AIContent so that other components in an IChatClient pipeline can consume the content readily.




Odd that ResourceContents is plural, but ContentBlock is not. Why do we even need to different sets of types for these?



Similarly ReadResourceResult has Contents, while CallToolResult has Content, but both are ILists.



BlobResourceContents exposes content as a normal string, which means it encoded the UTF-8 to a string, creating work for GC. Instead it should keep the UTF8 contents, and lazily decode that from base64 UTF8 to bytes (either as stream, or byte array). The same problem exists with ContentBlock types, where base-64 data is exposed as Unicode strings.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This seems like something we control and ought to resolve before going stable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ericstj to open an issue for properties that don't force folks to decode a string. Expose raw, and lazy byte[]



Prompts expose arguments as types, whereas tools expose arguments as json schema.
Copy link
Contributor

Choose a reason for hiding this comment

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

Tools in MCP expose JSON schema. Prompts do not.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, but both are describing typing of arguments on the same McpClient - it feels unusual that they have different technologies for the same/similar task.

Copy link
Contributor

Choose a reason for hiding this comment

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

Can you elaborate? What do you mean by "Prompts expose arguments as types"? There's no type information for prompts in the MCP specification, e.g.

public sealed class PromptArgument : IBaseMetadata
{
/// <inheritdoc />
[JsonPropertyName("name")]
public required string Name { get; set; }
/// <inheritdoc />
[JsonPropertyName("title")]
public string? Title { get; set; }
/// <summary>
/// Gets or sets a human-readable description of the argument's purpose and expected values.
/// </summary>
/// <remarks>
/// This description helps developers understand what information should be provided
/// for this argument and how it will affect the generated prompt.
/// </remarks>
[JsonPropertyName("description")]
public string? Description { get; set; }
/// <summary>
/// Gets or sets a value that indicates whether this argument must be provided when requesting the prompt.
/// </summary>
/// <remarks>
/// When set to <see langword="true"/>, the client must include this argument when making a <see cref="RequestMethods.PromptsGet"/> request.
/// If a required argument is missing, the server should respond with an error.
/// </remarks>
[JsonPropertyName("required")]
public bool? Required { get; set; }
}
. In contrast, tools in the MCP specification expose an input schema for all parameters, e.g.
public JsonElement InputSchema
. They're different in the MCP library because they're different in the MCP spec/protocol.




Loading