-
Notifications
You must be signed in to change notification settings - Fork 235
Resources-enhancements #257
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Resources-enhancements #257
Conversation
The failing test appears to indicate that it will sometimes fail accidentally. I don't appear to be able to requeue the tests for evaluation. @PederHP or @stephentoub, would you be able to look into this? The test is in // wait a bit to validate we don't receive another. this is best effort only;
// false negatives are possible.
await Assert.ThrowsAsync<TimeoutException>(() => receivedNotification.Task.WaitAsync(TimeSpan.FromSeconds(1), TestContext.Current.CancellationToken)); I would expect that this test should be broadcasting the notifications it expects to intercept instead of random sampling leading to indeterminate output. |
That's #228 |
This is for addressing #72. I've implemented a portion of the issue that can be extended to support the entire feature request. |
Nice stuff. Will be a lot simpler to work with Resources like that! |
/// <returns>The <see cref="IMcpServerBuilder"/> instance.</returns> | ||
public static IMcpServerBuilder WithResource( | ||
this IMcpServerBuilder builder, | ||
IFileInfo fileInfo) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't it critically important to have a MIME type for a resource? Otherwise the client doesn't know how to interpret the bytes. And IFileInfo doesn't provide any information about the media type (for a limited set of known file extensions we might be able to infer it, but it would require a hardcoded list and would likely fall off a cliff).
/// <param name="builder">The builder instance.</param> | ||
/// <param name="fileInfo">The file info of the resource.</param> | ||
/// <returns>The <see cref="IMcpServerBuilder"/> instance.</returns> | ||
public static IMcpServerBuilder WithResource( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't have singular versions for tools or prompts. I don't think we should start having singular versions for resources, either. We can later add singular versions for all of them if it proves worthwhile, but with collection expressions, you can wrap any instance with [...]
and have it work with the plural versions.
/// <returns>The <see cref="IMcpServerBuilder"/> instance.</returns> | ||
public static IMcpServerBuilder WithResources( | ||
this IMcpServerBuilder builder, | ||
params IEnumerable<IFileInfo> fileInfos) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we should have such IFileInfo overloads, both for the mime type issue, but also because it's not clear exactly when or how we'd be reading the data from the file. Would every request require re-reading the data? If not, what is the caching policy? Would we be required to subscribe to file change notifications in order to raise resource changed notifications? Maybe at some point we'd want to sign up for all that, but I'd prefer if we do, for it to be via creating McpServerResource instances, with Create methods that are configurable with various details.
@@ -61,4 +61,9 @@ public class ToolsCapability | |||
/// </remarks> | |||
[JsonIgnore] | |||
public McpServerPrimitiveCollection<McpServerTool>? ToolCollection { get; set; } | |||
McpServerPrimitiveCollection<McpServerTool>? IListCapability<McpServerTool>.Collection |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
McpServerPrimitiveCollection<McpServerTool>? IListCapability<McpServerTool>.Collection | |
McpServerPrimitiveCollection<McpServerTool>? IListCapability<McpServerTool>.Collection |
@@ -64,32 +62,16 @@ public McpServer(ITransport transport, McpServerOptions options, ILoggerFactory? | |||
SetCompletionHandler(options); | |||
SetPingHandler(); | |||
|
|||
var capabilities = options.Capabilities; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
var capabilities = options.Capabilities; | |
var capabilities = options.Capabilities; | |
var isMissingListResourceHandlers = originalListResourcesHandler is not { } && listResourceTemplatesHandler is not { }; | ||
if (resourceCollection is not { IsEmpty: false } && (isMissingListResourceHandlers || readResourceHandler is not { })) | ||
{ | ||
throw new McpException("Resources capability was enabled, but ListResources, ListResourceTemplates, and/or ReadResource handlers were not specified."); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand this logic. Why is "is missing" true if both originalListResourcesHandler and listResourceTemplatesHandler are non-null?
// https://modelcontextprotocol.io/specification/2024-11-05/server/tools#capabilities | ||
// Look to spec for guidance on ListChanged over collection existance. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
?
// https://modelcontextprotocol.io/specification/2024-11-05/server/tools#capabilities | ||
// Look to spec for guidance on ListChanged over collection existance. | ||
if (capability?.Collection is { } collection) | ||
//&& capability.ListChanged is true) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
?
/// </remarks> | ||
/// <param name="disposeAction">The action to execute when this object is disposed.</param> | ||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="disposeAction"/> is null.</exception> | ||
public sealed class Disposable(Action disposeAction) : IDisposable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not needed, and it certainly doesn't need to be public.
@@ -32,6 +29,7 @@ internal sealed class McpServer : McpEndpoint, IMcpServer | |||
/// rather than a nullable to be able to manipulate it atomically. | |||
/// </remarks> | |||
private StrongBox<LoggingLevel>? _loggingLevel; | |||
private readonly List<Disposable> _disposables = []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can just be List<Action>
void ChangedDelegate(object? sender, EventArgs e) | ||
=> _ = this.SendNotificationAsync(methodName); | ||
collection.Changed += ChangedDelegate; | ||
_disposables.Add(new(() => collection.Changed -= ChangedDelegate)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_disposables.Add(new(() => collection.Changed -= ChangedDelegate)); | |
_disposables.Add(() => collection.Changed -= ChangedDelegate); |
/// <summary> | ||
/// Represents a resource that the server supports. | ||
/// </summary> | ||
public class McpServerResource : IMcpServerPrimitive |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
McpServerTool and McpServerPrompt are both abstract, with abstract members. This should be the same.
|
||
/// <inheritdoc /> | ||
public string Name => ProtocolResource.Name; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should have members for being invoked and returning the relevant data, as is the case for McpServerPrompt/Tool.
Added DI builder enhancements to resources to emulate registration of other MCP list primitives (like tools and prompts).
Motivation and Context
Enhances the api to provide a more consistent way to add resources.
How Has This Been Tested?
I wrote additional tests to cover the cases.
Breaking Changes
Yes. ReadResourceRequest marked its property as nullable instead of required. This was a bug because the spec specified it as not optional. That is changed in the PR.
Types of changes
Checklist
Additional context
Simplified registration of other MCP primitives for future commits.