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:
parent
2ab9db0878
commit
3c0679e92b
13 changed files with 290 additions and 49 deletions
|
|
@ -39,11 +39,7 @@ public static class AIContentExtensions
|
||||||
}
|
}
|
||||||
else if (content is { Type: "resource" } && content.Resource is { } resourceContents)
|
else if (content is { Type: "resource" } && content.Resource is { } resourceContents)
|
||||||
{
|
{
|
||||||
ac = resourceContents.Blob is not null && resourceContents.MimeType is not null ?
|
ac = resourceContents.ToAIContent();
|
||||||
new DataContent(Convert.FromBase64String(resourceContents.Blob), resourceContents.MimeType) :
|
|
||||||
new TextContent(resourceContents.Text);
|
|
||||||
|
|
||||||
(ac.AdditionalProperties ??= [])["uri"] = resourceContents.Uri;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -62,9 +58,12 @@ public static class AIContentExtensions
|
||||||
{
|
{
|
||||||
Throw.IfNull(content);
|
Throw.IfNull(content);
|
||||||
|
|
||||||
AIContent ac = content.Blob is not null && content.MimeType is not null ?
|
AIContent ac = content switch
|
||||||
new DataContent(Convert.FromBase64String(content.Blob), content.MimeType) :
|
{
|
||||||
new TextContent(content.Text);
|
BlobResourceContents blobResource => new DataContent(Convert.FromBase64String(blobResource.Blob), blobResource.MimeType ?? "application/octet-stream"),
|
||||||
|
TextResourceContents textResource => new TextContent(textResource.Text),
|
||||||
|
_ => throw new NotSupportedException($"Resource type '{content.GetType().Name}' is not supported.")
|
||||||
|
};
|
||||||
|
|
||||||
(ac.AdditionalProperties ??= [])["uri"] = content.Uri;
|
(ac.AdditionalProperties ??= [])["uri"] = content.Uri;
|
||||||
ac.RawRepresentation = content;
|
ac.RawRepresentation = content;
|
||||||
|
|
@ -79,7 +78,7 @@ public static class AIContentExtensions
|
||||||
{
|
{
|
||||||
Throw.IfNull(contents);
|
Throw.IfNull(contents);
|
||||||
|
|
||||||
return contents.Select(ToAIContent).ToList();
|
return [.. contents.Select(ToAIContent)];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Creates a list of <see cref="AIContent"/> from a sequence of <see cref="ResourceContents"/>.</summary>
|
/// <summary>Creates a list of <see cref="AIContent"/> from a sequence of <see cref="ResourceContents"/>.</summary>
|
||||||
|
|
@ -89,7 +88,7 @@ public static class AIContentExtensions
|
||||||
{
|
{
|
||||||
Throw.IfNull(contents);
|
Throw.IfNull(contents);
|
||||||
|
|
||||||
return contents.Select(ToAIContent).ToList();
|
return [.. contents.Select(ToAIContent)];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Extracts the data from a <see cref="DataContent"/> as a Base64 string.</summary>
|
/// <summary>Extracts the data from a <see cref="DataContent"/> as a Base64 string.</summary>
|
||||||
|
|
|
||||||
|
|
@ -475,17 +475,7 @@ public static class McpClientExtensions
|
||||||
}
|
}
|
||||||
else if (sm.Content is { Type: "resource", Resource: not null })
|
else if (sm.Content is { Type: "resource", Resource: not null })
|
||||||
{
|
{
|
||||||
ResourceContents resource = sm.Content.Resource;
|
message.Contents.Add(sm.Content.Resource.ToAIContent());
|
||||||
|
|
||||||
if (resource.Text is not null)
|
|
||||||
{
|
|
||||||
message.Contents.Add(new TextContent(resource.Text));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resource.Blob is not null && resource.MimeType is not null)
|
|
||||||
{
|
|
||||||
message.Contents.Add(new DataContent(Convert.FromBase64String(resource.Blob), resource.MimeType));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
messages.Add(message);
|
messages.Add(message);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace ModelContextProtocol.Protocol.Types;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Binary contents of a resource.
|
||||||
|
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/">See the schema for details</see>
|
||||||
|
/// </summary>
|
||||||
|
public class BlobResourceContents : ResourceContents
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The base64-encoded string representing the binary data of the item.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("blob")]
|
||||||
|
public string Blob { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
@ -16,12 +16,16 @@ public record Resource
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A human-readable name for this resource.
|
/// A human-readable name for this resource.
|
||||||
|
///
|
||||||
|
/// This can be used by clients to populate UI elements.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("name")]
|
[JsonPropertyName("name")]
|
||||||
public required string Name { get; init; }
|
public required string Name { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A description of what this resource represents.
|
/// A description of what this resource represents.
|
||||||
|
///
|
||||||
|
/// This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a \"hint\" to the model.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("description")]
|
[JsonPropertyName("description")]
|
||||||
public string? Description { get; init; }
|
public string? Description { get; init; }
|
||||||
|
|
@ -32,6 +36,14 @@ public record Resource
|
||||||
[JsonPropertyName("mimeType")]
|
[JsonPropertyName("mimeType")]
|
||||||
public string? MimeType { get; init; }
|
public string? MimeType { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known.
|
||||||
|
///
|
||||||
|
/// This can be used by Hosts to display file sizes and estimate context window usage.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("size")]
|
||||||
|
public long? Size { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Optional annotations for the resource.
|
/// Optional annotations for the resource.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
using System.Text.Json.Serialization;
|
using System.ComponentModel;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace ModelContextProtocol.Protocol.Types;
|
namespace ModelContextProtocol.Protocol.Types;
|
||||||
|
|
||||||
|
|
@ -6,8 +9,13 @@ namespace ModelContextProtocol.Protocol.Types;
|
||||||
/// Represents the content of a resource.
|
/// Represents the content of a resource.
|
||||||
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/">See the schema for details</see>
|
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/">See the schema for details</see>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ResourceContents
|
[JsonConverter(typeof(Converter))]
|
||||||
|
public abstract class ResourceContents
|
||||||
{
|
{
|
||||||
|
internal ResourceContents()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The URI of the resource.
|
/// The URI of the resource.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -20,16 +28,104 @@ public class ResourceContents
|
||||||
[JsonPropertyName("mimeType")]
|
[JsonPropertyName("mimeType")]
|
||||||
public string? MimeType { get; set; }
|
public string? MimeType { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The text content of the resource.
|
|
||||||
/// </summary>
|
|
||||||
[JsonPropertyName("text")]
|
|
||||||
public string? Text { get; set; }
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The base64-encoded binary content of the resource.
|
/// Converter for <see cref="ResourceContents"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("blob")]
|
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||||
public string? Blob { get; set; }
|
public class Converter : JsonConverter<ResourceContents>
|
||||||
}
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override ResourceContents? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.Null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reader.TokenType != JsonTokenType.StartObject)
|
||||||
|
{
|
||||||
|
throw new JsonException();
|
||||||
|
}
|
||||||
|
|
||||||
|
string? uri = null;
|
||||||
|
string? mimeType = null;
|
||||||
|
string? blob = null;
|
||||||
|
string? text = null;
|
||||||
|
|
||||||
|
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
|
||||||
|
{
|
||||||
|
if (reader.TokenType != JsonTokenType.PropertyName)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
string? propertyName = reader.GetString();
|
||||||
|
|
||||||
|
switch (propertyName)
|
||||||
|
{
|
||||||
|
case "uri":
|
||||||
|
uri = reader.GetString();
|
||||||
|
break;
|
||||||
|
case "mimeType":
|
||||||
|
mimeType = reader.GetString();
|
||||||
|
break;
|
||||||
|
case "blob":
|
||||||
|
blob = reader.GetString();
|
||||||
|
break;
|
||||||
|
case "text":
|
||||||
|
text = reader.GetString();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blob is not null)
|
||||||
|
{
|
||||||
|
return new BlobResourceContents
|
||||||
|
{
|
||||||
|
Uri = uri ?? string.Empty,
|
||||||
|
MimeType = mimeType,
|
||||||
|
Blob = blob
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text is not null)
|
||||||
|
{
|
||||||
|
return new TextResourceContents
|
||||||
|
{
|
||||||
|
Uri = uri ?? string.Empty,
|
||||||
|
MimeType = mimeType,
|
||||||
|
Text = text
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override void Write(Utf8JsonWriter writer, ResourceContents value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
{
|
||||||
|
writer.WriteNullValue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteStartObject();
|
||||||
|
writer.WriteString("uri", value.Uri);
|
||||||
|
writer.WriteString("mimeType", value.MimeType);
|
||||||
|
Debug.Assert(value is BlobResourceContents or TextResourceContents);
|
||||||
|
if (value is BlobResourceContents blobResource)
|
||||||
|
{
|
||||||
|
writer.WriteString("blob", blobResource.Blob);
|
||||||
|
}
|
||||||
|
else if (value is TextResourceContents textResource)
|
||||||
|
{
|
||||||
|
writer.WriteString("text", textResource.Text);
|
||||||
|
}
|
||||||
|
writer.WriteEndObject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace ModelContextProtocol.Protocol.Types;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Text contents of a resource.
|
||||||
|
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/">See the schema for details</see>
|
||||||
|
/// </summary>
|
||||||
|
public class TextResourceContents : ResourceContents
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The text of the item. This must only be set if the item can actually be represented as text (not binary data).
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("text")]
|
||||||
|
public string Text { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
@ -218,19 +218,21 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resourcesCapability.ListResourcesHandler is not { } listResourcesHandler ||
|
var listResourcesHandler = resourcesCapability.ListResourcesHandler;
|
||||||
|
var listResourceTemplatesHandler = resourcesCapability.ListResourceTemplatesHandler;
|
||||||
|
|
||||||
|
if ((listResourcesHandler is not { } && listResourceTemplatesHandler is not { }) ||
|
||||||
resourcesCapability.ReadResourceHandler is not { } readResourceHandler)
|
resourcesCapability.ReadResourceHandler is not { } readResourceHandler)
|
||||||
{
|
{
|
||||||
throw new McpServerException("Resources capability was enabled, but ListResources and/or ReadResource handlers were not specified.");
|
throw new McpServerException("Resources capability was enabled, but ListResources and/or ReadResource handlers were not specified.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
listResourcesHandler ??= (static (_, _) => Task.FromResult(new ListResourcesResult()));
|
||||||
|
|
||||||
SetRequestHandler<ListResourcesRequestParams, ListResourcesResult>("resources/list", (request, ct) => listResourcesHandler(new(this, request), ct));
|
SetRequestHandler<ListResourcesRequestParams, ListResourcesResult>("resources/list", (request, ct) => listResourcesHandler(new(this, request), ct));
|
||||||
SetRequestHandler<ReadResourceRequestParams, ReadResourceResult>("resources/read", (request, ct) => readResourceHandler(new(this, request), ct));
|
SetRequestHandler<ReadResourceRequestParams, ReadResourceResult>("resources/read", (request, ct) => readResourceHandler(new(this, request), ct));
|
||||||
|
|
||||||
// Set the list resource templates handler, or use the default if not specified
|
listResourceTemplatesHandler ??= (static (_, _) => Task.FromResult(new ListResourceTemplatesResult()));
|
||||||
var listResourceTemplatesHandler = resourcesCapability.ListResourceTemplatesHandler
|
|
||||||
?? (static (_, _) => Task.FromResult(new ListResourceTemplatesResult()));
|
|
||||||
|
|
||||||
SetRequestHandler<ListResourceTemplatesRequestParams, ListResourceTemplatesResult>("resources/templates/list", (request, ct) => listResourceTemplatesHandler(new(this, request), ct));
|
SetRequestHandler<ListResourceTemplatesRequestParams, ListResourceTemplatesResult>("resources/templates/list", (request, ct) => listResourceTemplatesHandler(new(this, request), ct));
|
||||||
|
|
||||||
if (resourcesCapability.Subscribe is not true)
|
if (resourcesCapability.Subscribe is not true)
|
||||||
|
|
|
||||||
|
|
@ -327,7 +327,7 @@ internal static class Program
|
||||||
Name = $"Resource {i + 1}",
|
Name = $"Resource {i + 1}",
|
||||||
MimeType = "text/plain"
|
MimeType = "text/plain"
|
||||||
});
|
});
|
||||||
resourceContents.Add(new ResourceContents()
|
resourceContents.Add(new TextResourceContents()
|
||||||
{
|
{
|
||||||
Uri = uri,
|
Uri = uri,
|
||||||
MimeType = "text/plain",
|
MimeType = "text/plain",
|
||||||
|
|
@ -343,7 +343,7 @@ internal static class Program
|
||||||
Name = $"Resource {i + 1}",
|
Name = $"Resource {i + 1}",
|
||||||
MimeType = "application/octet-stream"
|
MimeType = "application/octet-stream"
|
||||||
});
|
});
|
||||||
resourceContents.Add(new ResourceContents()
|
resourceContents.Add(new BlobResourceContents()
|
||||||
{
|
{
|
||||||
Uri = uri,
|
Uri = uri,
|
||||||
MimeType = "application/octet-stream",
|
MimeType = "application/octet-stream",
|
||||||
|
|
@ -417,7 +417,7 @@ internal static class Program
|
||||||
return Task.FromResult(new ReadResourceResult()
|
return Task.FromResult(new ReadResourceResult()
|
||||||
{
|
{
|
||||||
Contents = [
|
Contents = [
|
||||||
new ResourceContents()
|
new TextResourceContents()
|
||||||
{
|
{
|
||||||
Uri = request.Params.Uri,
|
Uri = request.Params.Uri,
|
||||||
MimeType = "text/plain",
|
MimeType = "text/plain",
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ public class Program
|
||||||
Name = $"Resource {i + 1}",
|
Name = $"Resource {i + 1}",
|
||||||
MimeType = "text/plain"
|
MimeType = "text/plain"
|
||||||
});
|
});
|
||||||
resourceContents.Add(new ResourceContents()
|
resourceContents.Add(new TextResourceContents()
|
||||||
{
|
{
|
||||||
Uri = uri,
|
Uri = uri,
|
||||||
MimeType = "text/plain",
|
MimeType = "text/plain",
|
||||||
|
|
@ -97,7 +97,7 @@ public class Program
|
||||||
Name = $"Resource {i + 1}",
|
Name = $"Resource {i + 1}",
|
||||||
MimeType = "application/octet-stream"
|
MimeType = "application/octet-stream"
|
||||||
});
|
});
|
||||||
resourceContents.Add(new ResourceContents()
|
resourceContents.Add(new BlobResourceContents()
|
||||||
{
|
{
|
||||||
Uri = uri,
|
Uri = uri,
|
||||||
MimeType = "application/octet-stream",
|
MimeType = "application/octet-stream",
|
||||||
|
|
@ -262,7 +262,7 @@ public class Program
|
||||||
return Task.FromResult(new ReadResourceResult()
|
return Task.FromResult(new ReadResourceResult()
|
||||||
{
|
{
|
||||||
Contents = [
|
Contents = [
|
||||||
new ResourceContents()
|
new TextResourceContents()
|
||||||
{
|
{
|
||||||
Uri = request.Params.Uri,
|
Uri = request.Params.Uri,
|
||||||
MimeType = "text/plain",
|
MimeType = "text/plain",
|
||||||
|
|
|
||||||
|
|
@ -224,7 +224,9 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIntegratio
|
||||||
|
|
||||||
Assert.NotNull(result);
|
Assert.NotNull(result);
|
||||||
Assert.Single(result.Contents);
|
Assert.Single(result.Contents);
|
||||||
Assert.NotNull(result.Contents[0].Text);
|
|
||||||
|
TextResourceContents textResource = Assert.IsType<TextResourceContents>(result.Contents[0]);
|
||||||
|
Assert.NotNull(textResource.Text);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|
@ -241,7 +243,9 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIntegratio
|
||||||
|
|
||||||
Assert.NotNull(result);
|
Assert.NotNull(result);
|
||||||
Assert.Single(result.Contents);
|
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
|
// Not supported by "everything" server version on npx
|
||||||
|
|
|
||||||
|
|
@ -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>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -349,7 +349,7 @@ public class McpServerTests : LoggedTest
|
||||||
{
|
{
|
||||||
return Task.FromResult(new ReadResourceResult
|
return Task.FromResult(new ReadResourceResult
|
||||||
{
|
{
|
||||||
Contents = [new() { Text = "test" }]
|
Contents = [new TextResourceContents() { Text = "test" }]
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
ListResourcesHandler = (request, ct) => throw new NotImplementedException(),
|
ListResourcesHandler = (request, ct) => throw new NotImplementedException(),
|
||||||
|
|
@ -364,7 +364,9 @@ public class McpServerTests : LoggedTest
|
||||||
var result = (ReadResourceResult)response;
|
var result = (ReadResourceResult)response;
|
||||||
Assert.NotNull(result.Contents);
|
Assert.NotNull(result.Contents);
|
||||||
Assert.NotEmpty(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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,9 @@ public class SseServerIntegrationTests : LoggedTest, IClassFixture<SseServerInte
|
||||||
|
|
||||||
Assert.NotNull(result);
|
Assert.NotNull(result);
|
||||||
Assert.Single(result.Contents);
|
Assert.Single(result.Contents);
|
||||||
Assert.NotNull(result.Contents[0].Text);
|
|
||||||
|
TextResourceContents textContent = Assert.IsType<TextResourceContents>(result.Contents[0]);
|
||||||
|
Assert.NotNull(textContent.Text);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -137,7 +139,9 @@ public class SseServerIntegrationTests : LoggedTest, IClassFixture<SseServerInte
|
||||||
|
|
||||||
Assert.NotNull(result);
|
Assert.NotNull(result);
|
||||||
Assert.Single(result.Contents);
|
Assert.Single(result.Contents);
|
||||||
Assert.NotNull(result.Contents[0].Blob);
|
|
||||||
|
BlobResourceContents blobContent = Assert.IsType<BlobResourceContents>(result.Contents[0]);
|
||||||
|
Assert.NotNull(blobContent.Blob);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue