Overhaul of Resources (#125)

* Supporting resource templates with read resource callback

Fixes #123

* Aligning the resource data structures with the 2024-11-05 schema

- Introducing separate classes for Text and Blob ResourceContents
- Custom JSON converter to allow those types to be returned, making it easier to follow the type system against the spec
- Updated tests

* Apply suggestions from code review

Co-authored-by: Stephen Toub <stoub@microsoft.com>
Co-authored-by: Peder Holdgaard Pedersen <127606677+PederHP@users.noreply.github.com>

* Putting files back where they belong

* Adding an internal constructor to prevent overloads

* Addressing feedback

* Apply suggestions from code review

Co-authored-by: Stephen Toub <stoub@microsoft.com>

* Update src/ModelContextProtocol/Protocol/Types/ResourceContents.cs

* Addressing feedback

---------

Co-authored-by: Stephen Toub <stoub@microsoft.com>
Co-authored-by: Peder Holdgaard Pedersen <127606677+PederHP@users.noreply.github.com>
This commit is contained in:
Aaron Powell 2025-03-31 13:45:03 +11:00 • committed by GitHub
commit 3c0679e92b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 290 additions and 49 deletions

View file

@ -224,7 +224,9 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIntegratio
Assert.NotNull(result);
Assert.Single(result.Contents);
Assert.NotNull(result.Contents[0].Text);
TextResourceContents textResource = Assert.IsType<TextResourceContents>(result.Contents[0]);
Assert.NotNull(textResource.Text);
}
[Theory]
@ -241,7 +243,9 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIntegratio
Assert.NotNull(result);
Assert.Single(result.Contents);
Assert.NotNull(result.Contents[0].Blob);
BlobResourceContents blobResource = Assert.IsType<BlobResourceContents>(result.Contents[0]);
Assert.NotNull(blobResource.Blob);
}
// Not supported by "everything" server version on npx

View file

@ -0,0 +1,100 @@
using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Server;
namespace ModelContextProtocol.Tests.Server;
public class McpServerResourceTests
{
[Fact]
public void CanCreateServerWithResourceTemplates()
{
var services = new ServiceCollection();
services.AddMcpServer()
.WithStdioServerTransport()
.WithListResourceTemplatesHandler((ctx, ct) =>
{
return Task.FromResult(new ListResourceTemplatesResult
{
ResourceTemplates =
[
new ResourceTemplate { Name = "Static Resource", Description = "A static resource with a numeric ID", UriTemplate = "test://static/resource/{id}" }
]
});
})
.WithReadResourceHandler((ctx, ct) =>
{
return Task.FromResult(new ReadResourceResult
{
Contents = [new TextResourceContents
{
Uri = ctx.Params!.Uri!,
Text = "Static Resource",
MimeType = "text/plain",
}]
});
});
var provider = services.BuildServiceProvider();
provider.GetRequiredService<IMcpServer>();
}
[Fact]
public void CanCreateServerWithResources()
{
var services = new ServiceCollection();
services.AddMcpServer()
.WithStdioServerTransport()
.WithListResourcesHandler((ctx, ct) =>
{
return Task.FromResult(new ListResourcesResult
{
Resources =
[
new Resource { Name = "Static Resource", Description = "A static resource with a numeric ID", Uri = "test://static/resource/foo.txt" }
]
});
})
.WithReadResourceHandler((ctx, ct) =>
{
return Task.FromResult(new ReadResourceResult
{
Contents = [new TextResourceContents
{
Uri = ctx.Params!.Uri!,
Text = "Static Resource",
MimeType = "text/plain",
}]
});
});
var provider = services.BuildServiceProvider();
provider.GetRequiredService<IMcpServer>();
}
[Fact]
public void CreatingReadHandlerWithNoListHandlerFails()
{
var services = new ServiceCollection();
services.AddMcpServer()
.WithStdioServerTransport()
.WithReadResourceHandler((ctx, ct) =>
{
return Task.FromResult(new ReadResourceResult
{
Contents = [new TextResourceContents
{
Uri = ctx.Params!.Uri!,
Text = "Static Resource",
MimeType = "text/plain",
}]
});
});
var sp = services.BuildServiceProvider();
Assert.Throws<McpServerException>(() => sp.GetRequiredService<IMcpServer>());
}
}

View file

@ -349,7 +349,7 @@ public class McpServerTests : LoggedTest
{
return Task.FromResult(new ReadResourceResult
{
Contents = [new() { Text = "test" }]
Contents = [new TextResourceContents() { Text = "test" }]
});
},
ListResourcesHandler = (request, ct) => throw new NotImplementedException(),
@ -364,7 +364,9 @@ public class McpServerTests : LoggedTest
var result = (ReadResourceResult)response;
Assert.NotNull(result.Contents);
Assert.NotEmpty(result.Contents);
Assert.Equal("test", result.Contents[0].Text);
TextResourceContents textResource = Assert.IsType<TextResourceContents>(result.Contents[0]);
Assert.Equal("test", textResource.Text);
});
}

View file

@ -120,7 +120,9 @@ public class SseServerIntegrationTests : LoggedTest, IClassFixture<SseServerInte
Assert.NotNull(result);
Assert.Single(result.Contents);
Assert.NotNull(result.Contents[0].Text);
TextResourceContents textContent = Assert.IsType<TextResourceContents>(result.Contents[0]);
Assert.NotNull(textContent.Text);
}
[Fact]
@ -137,7 +139,9 @@ public class SseServerIntegrationTests : LoggedTest, IClassFixture<SseServerInte
Assert.NotNull(result);
Assert.Single(result.Contents);
Assert.NotNull(result.Contents[0].Blob);
BlobResourceContents blobContent = Assert.IsType<BlobResourceContents>(result.Contents[0]);
Assert.NotNull(blobContent.Blob);
}
[Fact]