Table of Contents

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 TTMSMCPResourceMethodfunction(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 Text and Blob on the same object. Each setter clears the other; only the last one assigned survives.
  • Returning raw bytes as Blob. Blob is a base64 string. Encode with TNetEncoding.Base64 first.
  • Returning nil for 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. SendResourcesChangedNotification and SendResourceUpdatedNotification both return silently unless their capability is switched on.

See also