csharp-sdk/tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs
Stephen Toub d86b3277d4
Add McpServer/ClientResource{Template} and friends (#391)
* Add McpServer/ClientResource{Template} and friends

This PR copies the same design used by tools and prompts to add McpServerResource, McpServerResourceAttribute, McpServerResourceTypeAttribute, McpClientResource, and then all of those for ResourceTemplate as well.

Unlike the others, [McpServerResource] is permitted not only on methods but also on properties. This convenience is afforded because such resources aren't parameterized, and thus can be easily modeled as properties, likely auto-props, e.g.
```C#
[McpServerResource]
public static BlobResourceContents MyAmazingData { get; } = Create();
```

Details about resources can be encoded into the attributes or provided via McpServerResource{Template}.Create methods. If not URI is explicitly defined, one is derived from the member name and arguments. For templates, this means manufacturing a URI template that includes variables for all non-DI parameters.

This also fixes a few issues discovered along the way:
- McpClientExtensions are updated to use `ValueTask<T>` instead of `Task<T>` where relevant.
- McpServerPrompt was missing handling for progress notifications; that's been rectified. It was also missing configuring the description on the ProtocolPrompt.
- Renamed IMcpServerPrimitive.Name to Id. For resources, names aren't unique.
- After going back and forth, updated McpServer to not fail for missing handlers and to instead register default handlers. It simplifies the implementation and makes it more consistent.
- Added a bunch of primitive types into the McpJsonUtilities JSON source generator context so that such primitives are usable with tools/prompts/resources without folks being required to add them into their own context for Native AOT. I think we should consider pushing this down to AIJsonUtilities, instead, and that will then flow up to McpJsonUtilities automatically.

* Address feedback
2025-05-09 18:21:11 -04:00

579 lines
28 KiB
C#

using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Server;
using Moq;
using System.Reflection;
using System.Text.Json.Serialization;
namespace ModelContextProtocol.Tests.Server;
public partial class McpServerResourceTests
{
[Fact]
public void CanCreateServerWithResource()
{
var services = new ServiceCollection();
services.AddMcpServer()
.WithStdioServerTransport()
.WithListResourcesHandler(async (ctx, ct) =>
{
return new ListResourcesResult
{
Resources =
[
new Resource { Name = "Static Resource", Description = "A static resource with a numeric ID", Uri = "test://static/resource" }
]
};
})
.WithReadResourceHandler(async (ctx, ct) =>
{
return 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 CanCreateServerWithResourceTemplates()
{
var services = new ServiceCollection();
services.AddMcpServer()
.WithStdioServerTransport()
.WithListResourceTemplatesHandler(async (ctx, ct) =>
{
return new ListResourceTemplatesResult
{
ResourceTemplates =
[
new ResourceTemplate { Name = "Static Resource", Description = "A static resource with a numeric ID", UriTemplate = "test://static/resource/{id}" }
]
};
})
.WithReadResourceHandler(async (ctx, ct) =>
{
return 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 CreatingReadHandlerWithNoListHandlerSucceeds()
{
var services = new ServiceCollection();
services.AddMcpServer()
.WithStdioServerTransport()
.WithReadResourceHandler(async (ctx, ct) =>
{
return new ReadResourceResult
{
Contents = [new TextResourceContents
{
Uri = ctx.Params!.Uri!,
Text = "Static Resource",
MimeType = "text/plain",
}]
};
});
var sp = services.BuildServiceProvider();
sp.GetRequiredService<IMcpServer>();
}
[Fact]
public void Create_InvalidArgs_Throws()
{
Assert.Throws<ArgumentNullException>("function", () => McpServerResource.Create((AIFunction)null!, new() { UriTemplate = "test://hello" }));
Assert.Throws<ArgumentNullException>("method", () => McpServerResource.Create((MethodInfo)null!));
Assert.Throws<ArgumentNullException>("method", () => McpServerResource.Create((MethodInfo)null!, typeof(object)));
Assert.Throws<ArgumentNullException>("targetType", () => McpServerResource.Create(typeof(McpServerResourceTests).GetMethod(nameof(Create_InvalidArgs_Throws))!, (Type)null!));
Assert.Throws<ArgumentNullException>("method", () => McpServerResource.Create((Delegate)null!));
Assert.NotNull(McpServerResource.Create(typeof(DisposableResourceType).GetMethod(nameof(DisposableResourceType.InstanceMethod))!, new DisposableResourceType()));
Assert.NotNull(McpServerResource.Create(typeof(DisposableResourceType).GetMethod(nameof(DisposableResourceType.StaticMethod))!));
Assert.Throws<ArgumentNullException>("target", () => McpServerResource.Create(typeof(DisposableResourceType).GetMethod(nameof(DisposableResourceType.InstanceMethod))!, target: null!));
}
[Fact]
public async Task UriTemplate_CreatedFromParameters_LotsOfTypesSupported()
{
const string Name = "Hello";
McpServerResource t;
ReadResourceResult? result;
IMcpServer server = new Mock<IMcpServer>().Object;
t = McpServerResource.Create(() => "42", new() { Name = Name });
Assert.Equal($"resource://{Name}", t.ProtocolResourceTemplate.UriTemplate);
result = await t.ReadAsync(
new RequestContext<ReadResourceRequestParams>(server) { Params = new() { Uri = $"resource://{Name}" } },
TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.Equal("42", ((TextResourceContents)result.Contents[0]).Text);
t = McpServerResource.Create((IMcpServer server) => "42", new() { Name = Name });
Assert.Equal($"resource://{Name}", t.ProtocolResourceTemplate.UriTemplate);
result = await t.ReadAsync(
new RequestContext<ReadResourceRequestParams>(server) { Params = new() { Uri = $"resource://{Name}" } },
TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.Equal("42", ((TextResourceContents)result.Contents[0]).Text);
t = McpServerResource.Create((string arg1) => arg1, new() { Name = Name });
Assert.Equal($"resource://{Name}{{?arg1}}", t.ProtocolResourceTemplate.UriTemplate);
result = await t.ReadAsync(
new RequestContext<ReadResourceRequestParams>(server) { Params = new() { Uri = $"resource://{Name}?arg1=wOrLd" } },
TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.Equal("wOrLd", ((TextResourceContents)result.Contents[0]).Text);
t = McpServerResource.Create((string arg1, string? arg2 = null) => arg1 + arg2, new() { Name = Name });
Assert.Equal($"resource://{Name}{{?arg1,arg2}}", t.ProtocolResourceTemplate.UriTemplate);
result = await t.ReadAsync(
new RequestContext<ReadResourceRequestParams>(server) { Params = new() { Uri = $"resource://{Name}?arg1=wo&arg2=rld" } },
TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.Equal("world", ((TextResourceContents)result.Contents[0]).Text);
t = McpServerResource.Create((object a1, bool a2, char a3, byte a4, sbyte a5) => a1.ToString() + a2 + a3 + a4 + a5, new() { Name = Name });
Assert.Equal($"resource://{Name}{{?a1,a2,a3,a4,a5}}", t.ProtocolResourceTemplate.UriTemplate);
result = await t.ReadAsync(
new RequestContext<ReadResourceRequestParams>(server) { Params = new() { Uri = $"resource://{Name}?a1=hi&a2=true&a3=s&a4=12&a5=34" } },
TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.Equal("hiTrues1234", ((TextResourceContents)result.Contents[0]).Text);
t = McpServerResource.Create((ushort a1, short a2, uint a3, int a4, ulong a5) => (a1 + a2 + a3 + a4 + (long)a5).ToString(), new() { Name = Name });
Assert.Equal($"resource://{Name}{{?a1,a2,a3,a4,a5}}", t.ProtocolResourceTemplate.UriTemplate);
result = await t.ReadAsync(
new RequestContext<ReadResourceRequestParams>(server) { Params = new() { Uri = $"resource://{Name}?a1=10&a2=20&a3=30&a4=40&a5=50" } },
TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.Equal("150", ((TextResourceContents)result.Contents[0]).Text);
t = McpServerResource.Create((long a1, float a2, double a3, decimal a4, TimeSpan a5) => a5.ToString(), new() { Name = Name });
Assert.Equal($"resource://{Name}{{?a1,a2,a3,a4,a5}}", t.ProtocolResourceTemplate.UriTemplate);
result = await t.ReadAsync(
new RequestContext<ReadResourceRequestParams>(server) { Params = new() { Uri = $"resource://{Name}?a1=1&a2=2&a3=3&a4=4&a5=5" } },
TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.Equal("5.00:00:00", ((TextResourceContents)result.Contents[0]).Text);
t = McpServerResource.Create((DateTime a1, DateTimeOffset a2, Uri a3, Guid a4, Version a5) => a4.ToString("N") + a5, new() { Name = Name });
Assert.Equal($"resource://{Name}{{?a1,a2,a3,a4,a5}}", t.ProtocolResourceTemplate.UriTemplate);
result = await t.ReadAsync(
new RequestContext<ReadResourceRequestParams>(server) { Params = new() { Uri = $"resource://{Name}?a1={DateTime.UtcNow:r}&a2={DateTimeOffset.UtcNow:r}&a3=http%3A%2F%2Ftest&a4=14e5f43d-0d41-47d6-8207-8249cf669e41&a5=1.2.3.4" } },
TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.Equal("14e5f43d0d4147d682078249cf669e411.2.3.4", ((TextResourceContents)result.Contents[0]).Text);
t = McpServerResource.Create((Half a2, Int128 a3, UInt128 a4, IntPtr a5) => (a3 + (Int128)a4 + a5).ToString(), new() { Name = Name });
Assert.Equal($"resource://{Name}{{?a2,a3,a4,a5}}", t.ProtocolResourceTemplate.UriTemplate);
result = await t.ReadAsync(
new RequestContext<ReadResourceRequestParams>(server) { Params = new() { Uri = $"resource://{Name}?a2=1.0&a3=3&a4=4&a5=5" } },
TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.Equal("12", ((TextResourceContents)result.Contents[0]).Text);
t = McpServerResource.Create((UIntPtr a1, DateOnly a2, TimeOnly a3) => a1.ToString(), new() { Name = Name });
Assert.Equal($"resource://{Name}{{?a1,a2,a3}}", t.ProtocolResourceTemplate.UriTemplate);
result = await t.ReadAsync(
new RequestContext<ReadResourceRequestParams>(server) { Params = new() { Uri = $"resource://{Name}?a1=123&a2=0001-02-03&a3=01%3A02%3A03" } },
TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.Equal("123", ((TextResourceContents)result.Contents[0]).Text);
t = McpServerResource.Create((bool? a2, char? a3, byte? a4, sbyte? a5) => a2?.ToString() + a3 + a4 + a5, new() { Name = Name });
Assert.Equal($"resource://{Name}{{?a2,a3,a4,a5}}", t.ProtocolResourceTemplate.UriTemplate);
result = await t.ReadAsync(
new RequestContext<ReadResourceRequestParams>(server) { Params = new() { Uri = $"resource://{Name}?a2=true&a3=s&a4=12&a5=34" } },
TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.Equal("Trues1234", ((TextResourceContents)result.Contents[0]).Text);
t = McpServerResource.Create((ushort? a1, short? a2, uint? a3, int? a4, ulong? a5) => (a1 + a2 + a3 + a4 + (long?)a5).ToString(), new() { Name = Name });
Assert.Equal($"resource://{Name}{{?a1,a2,a3,a4,a5}}", t.ProtocolResourceTemplate.UriTemplate);
result = await t.ReadAsync(
new RequestContext<ReadResourceRequestParams>(server) { Params = new() { Uri = $"resource://{Name}?a1=10&a2=20&a3=30&a4=40&a5=50" } },
TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.Equal("150", ((TextResourceContents)result.Contents[0]).Text);
t = McpServerResource.Create((long? a1, float? a2, double? a3, decimal? a4, TimeSpan? a5) => a5?.ToString(), new() { Name = Name });
Assert.Equal($"resource://{Name}{{?a1,a2,a3,a4,a5}}", t.ProtocolResourceTemplate.UriTemplate);
result = await t.ReadAsync(
new RequestContext<ReadResourceRequestParams>(server) { Params = new() { Uri = $"resource://{Name}?a1=1&a2=2&a3=3&a4=4&a5=5" } },
TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.Equal("5.00:00:00", ((TextResourceContents)result.Contents[0]).Text);
t = McpServerResource.Create((DateTime? a1, DateTimeOffset? a2, Guid? a4) => a4?.ToString("N"), new() { Name = Name });
Assert.Equal($"resource://{Name}{{?a1,a2,a4}}", t.ProtocolResourceTemplate.UriTemplate);
result = await t.ReadAsync(
new RequestContext<ReadResourceRequestParams>(server) { Params = new() { Uri = $"resource://{Name}?a1={DateTime.UtcNow:r}&a2={DateTimeOffset.UtcNow:r}&a4=14e5f43d-0d41-47d6-8207-8249cf669e41" } },
TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.Equal("14e5f43d0d4147d682078249cf669e41", ((TextResourceContents)result.Contents[0]).Text);
t = McpServerResource.Create((Half? a2, Int128? a3, UInt128? a4, IntPtr? a5) => (a3 + (Int128?)a4 + a5).ToString(), new() { Name = Name });
Assert.Equal($"resource://{Name}{{?a2,a3,a4,a5}}", t.ProtocolResourceTemplate.UriTemplate);
result = await t.ReadAsync(
new RequestContext<ReadResourceRequestParams>(server) { Params = new() { Uri = $"resource://{Name}?a2=1.0&a3=3&a4=4&a5=5" } },
TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.Equal("12", ((TextResourceContents)result.Contents[0]).Text);
t = McpServerResource.Create((UIntPtr? a1, DateOnly? a2, TimeOnly? a3) => a1?.ToString(), new() { Name = Name });
Assert.Equal($"resource://{Name}{{?a1,a2,a3}}", t.ProtocolResourceTemplate.UriTemplate);
result = await t.ReadAsync(
new RequestContext<ReadResourceRequestParams>(server) { Params = new() { Uri = $"resource://{Name}?a1=123&a2=0001-02-03&a3=01%3A02%3A03" } },
TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.Equal("123", ((TextResourceContents)result.Contents[0]).Text);
}
[Theory]
[InlineData("resource://Hello?arg1=42&arg2=84")]
[InlineData("resource://Hello?arg1=42&arg2=84&arg3=123")]
[InlineData("resource://Hello#fragment")]
public async Task UriTemplate_NonMatchingUri_ReturnsNull(string uri)
{
McpServerResource t = McpServerResource.Create((string arg1) => arg1, new() { Name = "Hello" });
Assert.Equal("resource://Hello{?arg1}", t.ProtocolResourceTemplate.UriTemplate);
Assert.Null(await t.ReadAsync(
new RequestContext<ReadResourceRequestParams>(new Mock<IMcpServer>().Object) { Params = new() { Uri = uri } },
TestContext.Current.CancellationToken));
}
[Theory]
[InlineData("resource://Hello?arg1=test")]
[InlineData("resource://Hello?arg2=test")]
public async Task UriTemplate_MissingParameter_Throws(string uri)
{
McpServerResource t = McpServerResource.Create((string arg1, int arg2) => arg1, new() { Name = "Hello" });
Assert.Equal("resource://Hello{?arg1,arg2}", t.ProtocolResourceTemplate.UriTemplate);
await Assert.ThrowsAsync<ArgumentException>(async () => await t.ReadAsync(
new RequestContext<ReadResourceRequestParams>(new Mock<IMcpServer>().Object) { Params = new() { Uri = uri } },
TestContext.Current.CancellationToken));
}
[Fact]
public async Task UriTemplate_MissingOptionalParameter_Succeeds()
{
McpServerResource t = McpServerResource.Create((string? arg1 = null, int? arg2 = null) => arg1 + arg2, new() { Name = "Hello" });
Assert.Equal("resource://Hello{?arg1,arg2}", t.ProtocolResourceTemplate.UriTemplate);
ReadResourceResult? result;
result = await t.ReadAsync(
new RequestContext<ReadResourceRequestParams>(new Mock<IMcpServer>().Object) { Params = new() { Uri = "resource://Hello" } },
TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.Equal("", ((TextResourceContents)result.Contents[0]).Text);
result = await t.ReadAsync(
new RequestContext<ReadResourceRequestParams>(new Mock<IMcpServer>().Object) { Params = new() { Uri = "resource://Hello?arg1=first" } },
TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.Equal("first", ((TextResourceContents)result.Contents[0]).Text);
result = await t.ReadAsync(
new RequestContext<ReadResourceRequestParams>(new Mock<IMcpServer>().Object) { Params = new() { Uri = "resource://Hello?arg2=42" } },
TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.Equal("42", ((TextResourceContents)result.Contents[0]).Text);
result = await t.ReadAsync(
new RequestContext<ReadResourceRequestParams>(new Mock<IMcpServer>().Object) { Params = new() { Uri = "resource://Hello?arg1=first&arg2=42" } },
TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.Equal("first42", ((TextResourceContents)result.Contents[0]).Text);
}
[Fact]
public async Task SupportsIMcpServer()
{
Mock<IMcpServer> mockServer = new();
McpServerResource resource = McpServerResource.Create((IMcpServer server) =>
{
Assert.Same(mockServer.Object, server);
return "42";
}, new() { Name = "Test" });
var result = await resource.ReadAsync(
new RequestContext<ReadResourceRequestParams>(mockServer.Object) { Params = new() { Uri = "resource://Test" } },
TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.Equal("42", ((TextResourceContents)result.Contents[0]).Text);
}
[Theory]
[InlineData(ServiceLifetime.Singleton)]
[InlineData(ServiceLifetime.Scoped)]
[InlineData(ServiceLifetime.Transient)]
public async Task SupportsServiceFromDI(ServiceLifetime injectedArgumentLifetime)
{
MyService singletonService = new();
ServiceCollection sc = new();
switch (injectedArgumentLifetime)
{
case ServiceLifetime.Singleton:
sc.AddSingleton(singletonService);
break;
case ServiceLifetime.Scoped:
sc.AddScoped(_ => new MyService());
break;
case ServiceLifetime.Transient:
sc.AddTransient(_ => new MyService());
break;
}
sc.AddSingleton(services =>
{
return McpServerResource.Create((MyService actualMyService) =>
{
Assert.NotNull(actualMyService);
if (injectedArgumentLifetime == ServiceLifetime.Singleton)
{
Assert.Same(singletonService, actualMyService);
}
return "42";
}, new() { Services = services, Name = "Test" });
});
IServiceProvider services = sc.BuildServiceProvider();
McpServerResource resource = services.GetRequiredService<McpServerResource>();
Mock<IMcpServer> mockServer = new();
await Assert.ThrowsAsync<ArgumentNullException>(async () => await resource.ReadAsync(
new RequestContext<ReadResourceRequestParams>(mockServer.Object) { Params = new() { Uri = "resource://Test" } },
TestContext.Current.CancellationToken));
var result = await resource.ReadAsync(
new RequestContext<ReadResourceRequestParams>(mockServer.Object) { Services = services, Params = new() { Uri = "resource://Test" } },
TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.Equal("42", ((TextResourceContents)result.Contents[0]).Text);
}
[Fact]
public async Task SupportsOptionalServiceFromDI()
{
MyService expectedMyService = new();
ServiceCollection sc = new();
sc.AddSingleton(expectedMyService);
IServiceProvider services = sc.BuildServiceProvider();
McpServerResource resource = McpServerResource.Create((MyService? actualMyService = null) =>
{
Assert.Null(actualMyService);
return "42";
}, new() { Services = services, Name = "Test" });
var result = await resource.ReadAsync(
new RequestContext<ReadResourceRequestParams>(new Mock<IMcpServer>().Object) { Params = new() { Uri = "resource://Test" } },
TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.Equal("42", ((TextResourceContents)result.Contents[0]).Text);
}
[Fact]
public async Task SupportsDisposingInstantiatedDisposableTargets()
{
int before = DisposableResourceType.Disposals;
McpServerResource resource1 = McpServerResource.Create(
typeof(DisposableResourceType).GetMethod(nameof(DisposableResourceType.InstanceMethod))!,
typeof(DisposableResourceType));
var result = await resource1.ReadAsync(
new RequestContext<ReadResourceRequestParams>(new Mock<IMcpServer>().Object) { Params = new() { Uri = "test://static/resource/instanceMethod" } },
TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.Equal("0", ((TextResourceContents)result.Contents[0]).Text);
Assert.Equal(1, DisposableResourceType.Disposals);
}
[Fact]
public async Task CanReturnReadResult()
{
Mock<IMcpServer> mockServer = new();
McpServerResource resource = McpServerResource.Create((IMcpServer server) =>
{
Assert.Same(mockServer.Object, server);
return new ReadResourceResult() { Contents = new List<ResourceContents>() { new TextResourceContents() { Text = "hello" } } };
}, new() { Name = "Test" });
var result = await resource.ReadAsync(
new RequestContext<ReadResourceRequestParams>(mockServer.Object) { Params = new() { Uri = "resource://Test" } },
TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.Single(result.Contents);
Assert.Equal("hello", ((TextResourceContents)result.Contents[0]).Text);
}
[Fact]
public async Task CanReturnResourceContents()
{
Mock<IMcpServer> mockServer = new();
McpServerResource resource = McpServerResource.Create((IMcpServer server) =>
{
Assert.Same(mockServer.Object, server);
return new TextResourceContents() { Text = "hello" };
}, new() { Name = "Test" });
var result = await resource.ReadAsync(
new RequestContext<ReadResourceRequestParams>(mockServer.Object) { Params = new() { Uri = "resource://Test" } },
TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.Single(result.Contents);
Assert.Equal("hello", ((TextResourceContents)result.Contents[0]).Text);
}
[Fact]
public async Task CanReturnCollectionOfResourceContents()
{
Mock<IMcpServer> mockServer = new();
McpServerResource resource = McpServerResource.Create((IMcpServer server) =>
{
Assert.Same(mockServer.Object, server);
return new List<ResourceContents>()
{
new TextResourceContents() { Text = "hello" },
new BlobResourceContents() { Blob = Convert.ToBase64String(new byte[] { 1, 2, 3 }) },
};
}, new() { Name = "Test" });
var result = await resource.ReadAsync(
new RequestContext<ReadResourceRequestParams>(mockServer.Object) { Params = new() { Uri = "resource://Test" } },
TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.Equal(2, result.Contents.Count);
Assert.Equal("hello", ((TextResourceContents)result.Contents[0]).Text);
Assert.Equal(Convert.ToBase64String(new byte[] { 1, 2, 3 }), ((BlobResourceContents)result.Contents[1]).Blob);
}
[Fact]
public async Task CanReturnString()
{
Mock<IMcpServer> mockServer = new();
McpServerResource resource = McpServerResource.Create((IMcpServer server) =>
{
Assert.Same(mockServer.Object, server);
return "42";
}, new() { Name = "Test" });
var result = await resource.ReadAsync(
new RequestContext<ReadResourceRequestParams>(mockServer.Object) { Params = new() { Uri = "resource://Test" } },
TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.Single(result.Contents);
Assert.Equal("42", ((TextResourceContents)result.Contents[0]).Text);
}
[Fact]
public async Task CanReturnCollectionOfStrings()
{
Mock<IMcpServer> mockServer = new();
McpServerResource resource = McpServerResource.Create((IMcpServer server) =>
{
Assert.Same(mockServer.Object, server);
return new List<string>() { "42", "43" };
}, new() { Name = "Test" });
var result = await resource.ReadAsync(
new RequestContext<ReadResourceRequestParams>(mockServer.Object) { Params = new() { Uri = "resource://Test" } },
TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.Equal(2, result.Contents.Count);
Assert.Equal("42", ((TextResourceContents)result.Contents[0]).Text);
Assert.Equal("43", ((TextResourceContents)result.Contents[1]).Text);
}
[Fact]
public async Task CanReturnDataContent()
{
Mock<IMcpServer> mockServer = new();
McpServerResource resource = McpServerResource.Create((IMcpServer server) =>
{
Assert.Same(mockServer.Object, server);
return new DataContent(new byte[] { 0, 1, 2 }, "application/octet-stream");
}, new() { Name = "Test" });
var result = await resource.ReadAsync(
new RequestContext<ReadResourceRequestParams>(mockServer.Object) { Params = new() { Uri = "resource://Test" } },
TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.Single(result.Contents);
Assert.Equal(Convert.ToBase64String(new byte[] { 0, 1, 2 }), ((BlobResourceContents)result.Contents[0]).Blob);
Assert.Equal("application/octet-stream", ((BlobResourceContents)result.Contents[0]).MimeType);
}
[Fact]
public async Task CanReturnCollectionOfAIContent()
{
Mock<IMcpServer> mockServer = new();
McpServerResource resource = McpServerResource.Create((IMcpServer server) =>
{
Assert.Same(mockServer.Object, server);
return new List<AIContent>()
{
new TextContent("hello!"),
new DataContent(new byte[] { 4, 5, 6 }, "application/json"),
};
}, new() { Name = "Test" });
var result = await resource.ReadAsync(
new RequestContext<ReadResourceRequestParams>(mockServer.Object) { Params = new() { Uri = "resource://Test" } },
TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.Equal(2, result.Contents.Count);
Assert.Equal("hello!", ((TextResourceContents)result.Contents[0]).Text);
Assert.Equal(Convert.ToBase64String(new byte[] { 4, 5, 6 }), ((BlobResourceContents)result.Contents[1]).Blob);
Assert.Equal("application/json", ((BlobResourceContents)result.Contents[1]).MimeType);
}
private sealed class MyService;
private class DisposableResourceType : IDisposable
{
public static int Disposals { get; private set; }
public void Dispose() => Disposals++;
[McpServerResource(UriTemplate = "test://static/resource/instanceMethod")]
public object InstanceMethod() => Disposals.ToString();
[McpServerResource(UriTemplate = "test://static/resource/staticMethod")]
public static object StaticMethod() => "42";
}
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(DisposableResourceType))]
partial class JsonContext6 : JsonSerializerContext;
}