Clarify client handling of embedded binary tool resources (#1762)
Co-authored-by: Luis Rodriguez <25299418+luisangelrod@users.noreply.github.com>
This commit is contained in:
parent
aa07c6fcca
commit
24b122afa4
3 changed files with 120 additions and 0 deletions
|
|
@ -121,6 +121,8 @@ public static EmbeddedResourceBlock GetBinaryData(string id)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The SDK sends binary embedded resources using the MCP resource shape: a URI, MIME type, and base64-encoded `blob`. It does not convert formats such as `application/pdf` into images. How an embedded resource is rendered or made available to a model depends on the client. When targeting clients that do not support a binary format, consider also returning a text representation or another client-supported content block.
|
||||||
|
|
||||||
#### Mixed content
|
#### Mixed content
|
||||||
|
|
||||||
Tools can return multiple content blocks by returning `IEnumerable<ContentBlock>`:
|
Tools can return multiple content blocks by returning `IEnumerable<ContentBlock>`:
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using ModelContextProtocol.Protocol;
|
using ModelContextProtocol.Protocol;
|
||||||
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
|
|
||||||
|
|
@ -45,4 +46,42 @@ public static class CallToolResultTests
|
||||||
Assert.Null(deserialized.IsError);
|
Assert.Null(deserialized.IsError);
|
||||||
Assert.Null(deserialized.Meta);
|
Assert.Null(deserialized.Meta);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public static void CallToolResult_SerializationRoundTrip_PreservesEmbeddedPdfResource()
|
||||||
|
{
|
||||||
|
byte[] pdfBytes = Encoding.ASCII.GetBytes("%PDF-1.7\n");
|
||||||
|
var original = new CallToolResult
|
||||||
|
{
|
||||||
|
Content =
|
||||||
|
[
|
||||||
|
new EmbeddedResourceBlock
|
||||||
|
{
|
||||||
|
Resource = BlobResourceContents.FromBytes(
|
||||||
|
pdfBytes,
|
||||||
|
"file:///mypdf.pdf",
|
||||||
|
"application/pdf")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
string json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions);
|
||||||
|
|
||||||
|
using JsonDocument document = JsonDocument.Parse(json);
|
||||||
|
JsonElement resourceBlock = document.RootElement.GetProperty("content")[0];
|
||||||
|
Assert.Equal("resource", resourceBlock.GetProperty("type").GetString());
|
||||||
|
|
||||||
|
JsonElement resource = resourceBlock.GetProperty("resource");
|
||||||
|
Assert.Equal("file:///mypdf.pdf", resource.GetProperty("uri").GetString());
|
||||||
|
Assert.Equal("application/pdf", resource.GetProperty("mimeType").GetString());
|
||||||
|
Assert.Equal(Convert.ToBase64String(pdfBytes), resource.GetProperty("blob").GetString());
|
||||||
|
|
||||||
|
var deserialized = JsonSerializer.Deserialize<CallToolResult>(json, McpJsonUtilities.DefaultOptions);
|
||||||
|
Assert.NotNull(deserialized);
|
||||||
|
var embeddedResource = Assert.IsType<EmbeddedResourceBlock>(Assert.Single(deserialized.Content));
|
||||||
|
var pdfResource = Assert.IsType<BlobResourceContents>(embeddedResource.Resource);
|
||||||
|
Assert.Equal("file:///mypdf.pdf", pdfResource.Uri);
|
||||||
|
Assert.Equal("application/pdf", pdfResource.MimeType);
|
||||||
|
Assert.Equal(pdfBytes, pdfResource.DecodedData.ToArray());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ using ModelContextProtocol.Server;
|
||||||
using ModelContextProtocol.Tests.Utils;
|
using ModelContextProtocol.Tests.Utils;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
|
|
||||||
|
|
@ -977,6 +978,84 @@ public class McpServerTests : LoggedTest
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Can_Handle_Call_Tool_Requests_With_Embedded_Pdf_Resource_On_Wire()
|
||||||
|
{
|
||||||
|
byte[] pdfBytes = Encoding.ASCII.GetBytes("%PDF-1.7\n");
|
||||||
|
await using var transport = new TestServerTransport();
|
||||||
|
var options = CreateOptions(new ServerCapabilities { Tools = new() });
|
||||||
|
options.Handlers.CallToolHandler = async (request, ct) =>
|
||||||
|
{
|
||||||
|
return new CallToolResult
|
||||||
|
{
|
||||||
|
Content =
|
||||||
|
[
|
||||||
|
new EmbeddedResourceBlock
|
||||||
|
{
|
||||||
|
Resource = BlobResourceContents.FromBytes(
|
||||||
|
pdfBytes,
|
||||||
|
"file:///mypdf.pdf",
|
||||||
|
"application/pdf")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
options.Handlers.ListToolsHandler = (request, ct) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
await using var server = McpServer.Create(transport, options, LoggerFactory);
|
||||||
|
var runTask = server.RunAsync(TestContext.Current.CancellationToken);
|
||||||
|
var receivedMessage = new TaskCompletionSource<JsonRpcResponse>();
|
||||||
|
|
||||||
|
transport.OnMessageSent = message =>
|
||||||
|
{
|
||||||
|
if (message is JsonRpcResponse response && response.Id.ToString() == "55")
|
||||||
|
{
|
||||||
|
receivedMessage.SetResult(response);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await transport.SendMessageAsync(
|
||||||
|
new JsonRpcRequest
|
||||||
|
{
|
||||||
|
Method = RequestMethods.ToolsCall,
|
||||||
|
Id = new RequestId(55)
|
||||||
|
},
|
||||||
|
TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
var response = await receivedMessage.Task.WaitAsync(
|
||||||
|
TestConstants.DefaultTimeout,
|
||||||
|
TestContext.Current.CancellationToken);
|
||||||
|
string wireJson = JsonSerializer.Serialize<JsonRpcMessage>(
|
||||||
|
response,
|
||||||
|
McpJsonUtilities.DefaultOptions);
|
||||||
|
|
||||||
|
using JsonDocument document = JsonDocument.Parse(wireJson);
|
||||||
|
JsonElement root = document.RootElement;
|
||||||
|
Assert.Equal("2.0", root.GetProperty("jsonrpc").GetString());
|
||||||
|
Assert.Equal(55, root.GetProperty("id").GetInt32());
|
||||||
|
|
||||||
|
JsonElement resourceBlock = root.GetProperty("result").GetProperty("content")[0];
|
||||||
|
Assert.Equal("resource", resourceBlock.GetProperty("type").GetString());
|
||||||
|
JsonElement resource = resourceBlock.GetProperty("resource");
|
||||||
|
Assert.Equal("file:///mypdf.pdf", resource.GetProperty("uri").GetString());
|
||||||
|
Assert.Equal("application/pdf", resource.GetProperty("mimeType").GetString());
|
||||||
|
Assert.Equal(Convert.ToBase64String(pdfBytes), resource.GetProperty("blob").GetString());
|
||||||
|
|
||||||
|
var roundTrippedMessage = JsonSerializer.Deserialize<JsonRpcMessage>(
|
||||||
|
wireJson,
|
||||||
|
McpJsonUtilities.DefaultOptions);
|
||||||
|
var roundTrippedResponse = Assert.IsType<JsonRpcResponse>(roundTrippedMessage);
|
||||||
|
var result = roundTrippedResponse.Result.Deserialize<CallToolResult>(
|
||||||
|
McpJsonUtilities.DefaultOptions);
|
||||||
|
Assert.NotNull(result);
|
||||||
|
var embeddedResource = Assert.IsType<EmbeddedResourceBlock>(Assert.Single(result.Content));
|
||||||
|
var pdfResource = Assert.IsType<BlobResourceContents>(embeddedResource.Resource);
|
||||||
|
Assert.Equal(pdfBytes, pdfResource.DecodedData.ToArray());
|
||||||
|
|
||||||
|
await transport.DisposeAsync();
|
||||||
|
await runTask;
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Can_Handle_Call_Tool_Requests_Throws_Exception_If_No_Handler_Assigned()
|
public async Task Can_Handle_Call_Tool_Requests_Throws_Exception_If_No_Handler_Assigned()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue