Skip to content

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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

Tyler-R-Kendrick
Copy link
Contributor

@Tyler-R-Kendrick Tyler-R-Kendrick commented Apr 9, 2025

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

  • 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

Simplified registration of other MCP primitives for future commits.

Sorry, something went wrong.

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
…-sdk into resources-enhancements
@Tyler-R-Kendrick Tyler-R-Kendrick marked this pull request as ready for review April 10, 2025 14:28
@Tyler-R-Kendrick
Copy link
Contributor Author

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 ClientIntegrationTests on line 312.
It reads:

        // 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.

@stephentoub
Copy link
Contributor

That's #228

@Tyler-R-Kendrick
Copy link
Contributor Author

Tyler-R-Kendrick commented Apr 10, 2025

This is for addressing #72. I've implemented a portion of the issue that can be extended to support the entire feature request.
I believe this implementation minimizes the necessary public types exposed through the API.

@alainkaiser
Copy link

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)
Copy link
Contributor

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(
Copy link
Contributor

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)
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 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
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
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;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
var capabilities = options.Capabilities;
var capabilities = options.Capabilities;

Comment on lines +206 to +210
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.");
}
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 understand this logic. Why is "is missing" true if both originalListResourcesHandler and listResourceTemplatesHandler are non-null?

Comment on lines +480 to +481
// https://modelcontextprotocol.io/specification/2024-11-05/server/tools#capabilities
// Look to spec for guidance on ListChanged over collection existance.
Copy link
Contributor

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)
Copy link
Contributor

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
Copy link
Contributor

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 = [];
Copy link
Contributor

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));
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
_disposables.Add(new(() => collection.Changed -= ChangedDelegate));
_disposables.Add(() => collection.Changed -= ChangedDelegate);

/// <summary>
/// Represents a resource that the server supports.
/// </summary>
public class McpServerResource : IMcpServerPrimitive
Copy link
Contributor

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;
}
Copy link
Contributor

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.

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.

None yet

3 participants