Resource content and updates
Declaring a resource says what exists; the reader says what it contains. Every
read arrives at one callback with the URI the client asked for, and that callback
returns a TTMSMCPResourceContent — text or a base64 blob, never both, with a
MIME type that tells the client how to treat it. Content is also not static:
a client can subscribe to a URI and expect to be told when what it read has
changed. This guide covers returning text and binary content, the MIME type, who
owns the content object, how failures are reported, and the two notifications
that keep a client's view current.
The reader callback
Why and when: everything a resource can answer flows through this one callback, so its contract is worth reading once carefully.
A reader is a TTMSMCPResourceMethod — function(const URI: string): TTMSMCPResourceContent. It is assigned through ResourceReader, through the
builder's Reader, or as the last parameter of RegisterDirectResource /
RegisterTemplateResource. Three rules govern it:
| Rule | Why |
|---|---|
| It receives the requested URI | For a template, that is the concrete URI with the placeholders filled in — the only place the key values exist. |
| It must return a fresh content object | The framework serializes the result and frees it. A cached instance is freed out from under you on the first read. |
| It must not free what it returns | Ownership passes to the framework the moment the function returns. |
Returning text content
Why and when: text is the default answer — JSON, Markdown, CSV, plain text, source code, anything a model can read directly.
TTMSMCPResourceContent.FromText takes the URI, the MIME type, and the text:
function TForm1.ReadReportResource(
const URI: string): TTMSMCPResourceContent;
var
FileName: string;
begin
FileName := ReportFileNameFor(URI);
{ Raise on a problem rather than returning nil. The framework converts
the exception into a JSON-RPC error that names the URI; a nil result
becomes a less helpful "reader returned nil" error. }
if not TFile.Exists(FileName) then
raise Exception.CreateFmt('No report stored for %s', [URI]);
{ The URI echoed back in the content should be the one the client asked
for, so a templated resource identifies the concrete instance. }
Result := TTMSMCPResourceContent.FromText(URI, 'text/csv',
TFile.ReadAllText(FileName));
end;
Echo back the URI the reader was given rather than the resource's declared URI. For a template that is what identifies which instance the content belongs to; for a direct resource the two are the same, so echoing costs nothing.
FromText rejects an empty URI but accepts empty text, so a legitimately empty
document is representable.
Returning binary content
Why and when: an image, a PDF, an archive — anything that is not text. MCP carries binary content as base64 in the JSON response, so the bytes are encoded before they reach the content object.
TTMSMCPResourceContent.FromBlob takes the same URI and MIME type plus the
base64 string:
function TForm1.ReadLogoResource(
const URI: string): TTMSMCPResourceContent;
var
Stream: TMemoryStream;
Encoded: string;
begin
{ Blob content travels as base64 text in the JSON response, so encode
the bytes before handing them over. }
Stream := TMemoryStream.Create;
try
Stream.LoadFromFile(LogoFileName);
Stream.Position := 0;
Encoded := TNetEncoding.Base64.EncodeBytesToString(
Stream.Memory, Stream.Size);
finally
Stream.Free;
end;
{ FromBlob rejects empty content, so an empty file is an error, not an
empty resource. }
Result := TTMSMCPResourceContent.FromBlob(URI, 'image/png', Encoded);
end;
Text and Blob are mutually exclusive by construction: assigning Text clears
Blob, and assigning Blob clears Text. Pick the factory that matches the
payload rather than setting both and hoping. When neither is set, the serialized
content carries an empty text field.
Unlike FromText, FromBlob rejects empty content as well as an empty URI — an
empty blob is treated as a mistake, not as an empty file.
MIME types
Why and when: the MIME type is how a client decides whether to render, download, or hand the content to the model, and it appears in two places.
TTMSMCPResource.MimeType is advertised in the listing, so a client knows the
type before reading. The type passed to FromText / FromBlob appears in the
content, so it can vary per URI when one template covers several formats. Set
both; keep them consistent unless the reader genuinely decides the type per
instance, in which case leave the resource-level one empty rather than declaring
a type that is sometimes wrong.
Reporting failures
Why and when: a missing record, a locked file, or a malformed URI is a normal outcome, and how you signal it decides what the user sees.
Raise an exception with a message naming the problem. The framework converts it
into a JSON-RPC error that includes your message, so the client can show
something actionable. Returning nil also produces an error, but only the
generic Resource reader returned nil for URI: … — strictly less useful.
A URI that matches no resource at all never reaches a reader: resources/read
answers with a resource-not-found error after both FindByURI and
FindMatchingTemplate come back empty.
Three standard JSON-RPC codes carry everything the resource layer reports, and knowing which one arrived tells you where the problem is:
| Code | Meaning | Raised when |
|---|---|---|
-32602 |
Invalid parameters | A declaration or lookup argument was empty or nil — an empty name, URI, or URI template, a nil reader, an empty URI passed to FindByURI or FindMatchingTemplate, or an empty URI or blob given to FromText / FromBlob. |
-32603 |
Internal error | The resource has no ResourceReader assigned, so there is nothing to call. |
-32000 |
Operation failed | The reader ran and did not deliver: it returned nil, or it raised — in which case your exception message is wrapped into the error text. |
The practical split is that -32602 and -32603 almost always mean a
registration mistake that shows up on the first read, while -32000 is a
runtime failure inside your own reader.
Announcing changes
Why and when: a client that has already read a resource has no way to know its content changed. Two different notifications cover two different events.
The list changed. Set EnableResourceNotifications to True and the server
broadcasts notifications/resources/list_changed whenever the collection
changes — adding or removing an item raises the collection's OnChanged, which
the server forwards for you. Editing a property of an item already in the
collection does not raise it, so call
Server.SendResourcesChangedNotification yourself after an in-place edit.
One resource's content changed. Set EnableResourceSubscriptions to True
so clients may subscribe, then call
Server.SendResourceUpdatedNotification(URI) when the content behind that URI
changes. SendResourceUpdatedForSession sends the same notification to one
session, and only when that session actually subscribed to the URI.
procedure TForm1.FormCreate(Sender: TObject);
begin
{ Both capabilities are off by default and are advertised during
initialize, so set them before the server starts. }
Server.EnableResourceNotifications := True;
Server.EnableResourceSubscriptions := True;
end;
procedure TForm1.SettingsSaved;
begin
{ A subscribed client is told the content changed and re-reads it.
Sent to every session that subscribed to this exact URI. }
Server.SendResourceUpdatedNotification('config://application/settings');
end;
procedure TForm1.SettingsRenamed(const ANewName: string);
var
Resource: TTMSMCPResource;
begin
Resource := Server.Resources.FindByURI('config://application/settings');
if Resource = nil then
Exit;
{ Editing a property of an existing item does not raise the collection
OnChanged, so announce the changed list explicitly. }
Resource.Title := ANewName;
Server.SendResourcesChangedNotification;
end;
Both switches are capabilities advertised during initialization, so set them
before the server starts. Both send methods are no-ops when their switch is
False, which is the usual reason a notification "does not arrive".
Subscriptions are keyed on the exact URI a client subscribed to. For a
templated resource that is the concrete URI it read — tickets://42/attachment,
never tickets://{id}/attachment — so build the same concrete string when you
notify.
Combining text, binary content, and an update notification
One resource family often carries both kinds of payload. This template answers with text or with a base64 blob depending on what the attachment actually is, sets the MIME type per instance, and pairs the reader with the update notification that tells subscribers to read it again:
procedure TForm1.RegisterAttachmentResource;
begin
Server.EnableResourceNotifications := True;
Server.EnableResourceSubscriptions := True;
Server.Resources.RegisterTemplateResource(
'ticket_attachment',
'tickets://{id}/attachment',
'The attachment of one support ticket, as text or as an image',
'',
function(const URI: string): TTMSMCPResourceContent
var
Parts: TArray<string>;
TicketId, MimeType: string;
Stream: TMemoryStream;
begin
Parts := URI.Split(['/']);
if Length(Parts) < 2 then
raise Exception.CreateFmt('Malformed attachment URI: %s', [URI]);
TicketId := Parts[High(Parts) - 1];
MimeType := AttachmentMimeType(TicketId);
{ Text and Blob are mutually exclusive - setting one clears the
other - so pick the factory that matches the payload. }
if MimeType.StartsWith('text/') then
begin
Result := TTMSMCPResourceContent.FromText(URI, MimeType,
AttachmentAsText(TicketId));
Exit;
end;
Stream := TMemoryStream.Create;
try
LoadAttachment(TicketId, Stream);
Stream.Position := 0;
Result := TTMSMCPResourceContent.FromBlob(URI, MimeType,
TNetEncoding.Base64.EncodeBytesToString(Stream.Memory, Stream.Size));
finally
Stream.Free;
end;
end);
end;
procedure TForm1.AttachmentReplaced(const ATicketId: string);
begin
{ The URI a client subscribed to is the concrete one it read, not the
template, so build the same URI here. }
Server.SendResourceUpdatedNotification(
Format('tickets://%s/attachment', [ATicketId]));
end;
The resource-level MIME type is deliberately left empty here, because it genuinely differs per instance — the content-level type is the honest one.
Common mistakes
- Freeing the content you return. The framework frees it after serialization. Freeing it in the reader leaves a dangling reference.
- Caching a content object between reads. It is freed after each read, so the second read works on freed memory. Build a fresh one every time.
- Setting
TextandBlobon the same object. Each setter clears the other; only the last one assigned survives. - Returning raw bytes as
Blob.Blobis a base64 string. Encode withTNetEncoding.Base64first. - Returning
nilfor an error. Raise instead — the exception message reaches the client, a nil result does not. - Notifying with the template URI. Subscriptions match the concrete URI the client read.
- Expecting notifications without enabling them.
SendResourcesChangedNotificationandSendResourceUpdatedNotificationboth return silently unless their capability is switched on.
See also
- Declaring resources — URIs, templates, and registration
- MCP Server — sessions, capabilities, and notification delivery
- Tools — returning a resource link that points at one of these URIs
TTMSMCPResourceContent,TTMSMCPResource,TTMSMCPResourceMethod