Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/concepts/tools/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Tools can return multiple content blocks by returning `IEnumerable<ContentBlock>`:
Expand Down
39 changes: 39 additions & 0 deletions tests/ModelContextProtocol.Tests/Protocol/CallToolResultTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using ModelContextProtocol.Protocol;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;

Expand Down Expand Up @@ -45,4 +46,42 @@ public static void CallToolResult_SerializationRoundTrip_WithMinimalProperties()
Assert.Null(deserialized.IsError);
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());
}
}
79 changes: 79 additions & 0 deletions tests/ModelContextProtocol.Tests/Server/McpServerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using ModelContextProtocol.Tests.Utils;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;

Expand Down Expand Up @@ -977,6 +978,84 @@ await Can_Handle_Requests(
});
}

[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]
public async Task Can_Handle_Call_Tool_Requests_Throws_Exception_If_No_Handler_Assigned()
{
Expand Down