| title | author | description | uid |
|---|---|---|---|
Resources |
jeffhandley |
How to implement and consume MCP resources for exposing data to clients. |
resources |
MCP resources allow servers to expose data and content to clients. Resources represent any kind of data that a server wants to make available—files, database records, API responses, live system data, and more.
This document covers implementing resources on the server, consuming them from the client, resource templates, subscriptions, and change notifications.
Resources can be defined in several ways:
- Using the xref:ModelContextProtocol.Server.McpServerResourceAttribute attribute on methods within a class marked with xref:ModelContextProtocol.Server.McpServerResourceTypeAttribute
- Using xref:ModelContextProtocol.Server.McpServerResource.Create* factory methods from a delegate,
MethodInfo, orAIFunction - Deriving from xref:ModelContextProtocol.Server.McpServerResource or xref:ModelContextProtocol.Server.DelegatingMcpServerResource
- Implementing a custom xref:ModelContextProtocol.Server.McpRequestHandler`2 via xref:ModelContextProtocol.Server.McpServerHandlers
- Implementing a low-level xref:ModelContextProtocol.Server.McpRequestFilter`2
The attribute-based approach is the most common and is shown throughout this document.
Direct resources have a fixed URI and are returned in the resource list:
[McpServerResourceType]
public class MyResources
{
[McpServerResource(UriTemplate = "config://app/settings", Name = "App Settings", MimeType = "application/json")]
[Description("Returns application configuration settings")]
public static string GetSettings() => JsonSerializer.Serialize(new { theme = "dark", language = "en" });
}Template resources use URI templates (RFC 6570) with parameters. They are returned separately in the resource templates list and can match a range of URIs:
[McpServerResourceType]
public class DocumentResources
{
[McpServerResource(UriTemplate = "docs://articles/{id}", Name = "Article")]
[Description("Returns an article by its ID")]
public static ResourceContents GetArticle(string id)
{
string? content = LoadArticle(id); // application logic to load by ID
if (content is null)
{
throw new McpException($"Article not found: {id}");
}
return new TextResourceContents
{
Uri = $"docs://articles/{id}",
MimeType = "text/plain",
Text = content
};
}
}Register resource types when building the server:
builder.Services.AddMcpServer()
.WithHttpTransport()
.WithResources<MyResources>()
.WithResources<DocumentResources>();Text resources return their content as xref:ModelContextProtocol.Protocol.TextResourceContents with a Text property:
[McpServerResource(UriTemplate = "notes://daily/{date}", Name = "Daily Notes")]
[Description("Returns notes for a given date")]
public static TextResourceContents GetDailyNotes(string date)
{
return new TextResourceContents
{
Uri = $"notes://daily/{date}",
MimeType = "text/markdown",
Text = $"# Notes for {date}\n\n- Meeting at 10am\n- Review PRs"
};
}Binary resources return their content as xref:ModelContextProtocol.Protocol.BlobResourceContents with a Blob property containing the raw bytes. Use the xref:ModelContextProtocol.Protocol.BlobResourceContents.FromBytes* factory method to construct instances:
[McpServerResource(UriTemplate = "images://photos/{id}", Name = "Photo")]
[Description("Returns a photo by ID")]
public static BlobResourceContents GetPhoto(int id)
{
byte[] imageData = LoadPhoto(id);
return BlobResourceContents.FromBytes(imageData, $"images://photos/{id}", "image/png");
}Clients can discover and read resources using xref:ModelContextProtocol.Client.McpClient:
// List direct resources
IList<McpClientResource> resources = await client.ListResourcesAsync();
foreach (var resource in resources)
{
Console.WriteLine($"{resource.Name} ({resource.Uri})");
Console.WriteLine($" MIME: {resource.MimeType}");
Console.WriteLine($" Description: {resource.Description}");
}// List resource templates (parameterized URIs)
IList<McpClientResourceTemplate> templates = await client.ListResourceTemplatesAsync();
foreach (var template in templates)
{
Console.WriteLine($"{template.Name}: {template.UriTemplate}");
}// Read a direct resource by URI
ReadResourceResult result = await client.ReadResourceAsync("config://app/settings");
foreach (var content in result.Contents)
{
if (content is TextResourceContents text)
{
Console.WriteLine($"[{text.MimeType}] {text.Text}");
}
else if (content is BlobResourceContents blob)
{
Console.WriteLine($"[{blob.MimeType}] {blob.Blob.Length} bytes");
}
}// Read a resource using a URI template with parameter values
ReadResourceResult result = await client.ReadResourceAsync(
"file:///{path}",
new Dictionary<string, object?> { ["path"] = "docs/readme.md" });Clients can subscribe to resource updates to be notified when a resource's content changes. The server must declare subscription support in its capabilities.
// Subscribe with an inline handler
IAsyncDisposable subscription = await client.SubscribeToResourceAsync(
"config://app/settings",
async (notification, cancellationToken) =>
{
Console.WriteLine($"Resource updated: {notification.Uri}");
// Re-read the resource to get updated content
var updated = await client.ReadResourceAsync(notification.Uri, cancellationToken: cancellationToken);
// Process updated content...
});
// Later, unsubscribe by disposing
await subscription.DisposeAsync();Clients can also subscribe and unsubscribe separately:
// Subscribe without a handler (use a global notification handler instead)
await client.SubscribeToResourceAsync("config://app/settings");
// Unsubscribe when no longer interested
await client.UnsubscribeFromResourceAsync("config://app/settings");Register subscription handlers when building the server:
builder.Services.AddMcpServer()
.WithHttpTransport()
.WithResources<MyResources>()
.WithSubscribeToResourcesHandler(async (ctx, ct) =>
{
if (ctx.Params?.Uri is { } uri)
{
// Track the subscription (e.g., in a concurrent dictionary)
subscriptions[ctx.Server.SessionId].TryAdd(uri, 0);
}
return new EmptyResult();
})
.WithUnsubscribeFromResourcesHandler(async (ctx, ct) =>
{
if (ctx.Params?.Uri is { } uri)
{
subscriptions[ctx.Server.SessionId].TryRemove(uri, out _);
}
return new EmptyResult();
});When a resource's content changes, the server notifies subscribed clients:
// Notify that a specific resource was updated
await server.SendNotificationAsync(
NotificationMethods.ResourceUpdatedNotification,
new ResourceUpdatedNotificationParams { Uri = "config://app/settings" });When the set of available resources changes (resources added or removed), the server notifies clients:
// After adding or removing resources dynamically
await server.SendNotificationAsync(
NotificationMethods.ResourceListChangedNotification,
new ResourceListChangedNotificationParams());mcpClient.RegisterNotificationHandler(
NotificationMethods.ResourceListChangedNotification,
async (notification, cancellationToken) =>
{
// Refresh the resource list
var updatedResources = await mcpClient.ListResourcesAsync(cancellationToken: cancellationToken);
Console.WriteLine($"Resource list updated. {updatedResources.Count} resources available.");
});