Table of Contents

Declaring resources

A resource is readable content your server publishes at a URI. Where a tool does something and a prompt asks something, a resource simply is something a client can fetch: a configuration file, a customer record, a log, a document currently open in the editor. A client lists what is available and reads a URI when it needs the content, so the declaration has to answer two questions — what exists, and how is it addressed. This guide covers the two kinds of resource (one fixed URI, or a URI template covering a whole family), the convenience registrations and the fluent builder, the presentation metadata a client can show, and how to grow the list at runtime.

Direct resources

Why and when: reach for a direct resource whenever the content lives at one known address — a settings blob, an index, a status document. The URI is fixed, so a client can bookmark it and subscribe to it.

TTMSMCPResources.RegisterDirectResource takes a name, the URI, a description, a MIME type, and the reader that produces the content:

procedure TForm1.RegisterSettingsResource;
begin
  { Server is a TTMSMCPServer dropped on the form. }
  Server.Resources.RegisterDirectResource(
    'app_settings',
    'config://application/settings',
    'Current application settings',
    'application/json',
    function(const URI: string): TTMSMCPResourceContent
    begin
      { Create a fresh content object per read. The framework serializes
        it and frees it - do not free it here and do not cache it. }
      Result := TTMSMCPResourceContent.FromText(URI, 'application/json',
        SettingsAsJSON);
    end);
end;

The URI scheme is yours to choose. MCP does not require http://; a scheme that names your own domain (config://, crm://, doc://) is clearer and avoids implying the content can be fetched by a browser.

RegisterDirectResource rejects an empty name, an empty URI, and a nil reader, each with a JSON-RPC error, so a mis-registration surfaces at start-up rather than on the first read.

Template resources

Why and when: reach for a template when the same shape of content exists for many keys — one record per customer, one article per slug, one log per day. Declaring a thousand direct resources is not the answer; one template is.

A template puts placeholders in curly braces: crm://customers/{id}/profile. It is declared with RegisterTemplateResource, and the reader receives the concrete URI the client asked for, so the placeholder values are read back out of it:

procedure TForm1.RegisterCustomerResource;
begin
  Server.Resources.RegisterTemplateResource(
    'customer_profile',
    'crm://customers/{id}/profile',
    'Profile of one customer, by customer id',
    'application/json',
    function(const URI: string): TTMSMCPResourceContent
    var
      Parts: TArray<string>;
      CustomerId: string;
    begin
      { The reader receives the URI the client actually asked for, not the
        template, so the placeholder value is read back from it.
        crm://customers/42/profile splits into
        ['crm:', '', 'customers', '42', 'profile']. }
      Parts := URI.Split(['/']);
      if Length(Parts) < 2 then
        raise Exception.CreateFmt('Malformed customer URI: %s', [URI]);

      CustomerId := Parts[High(Parts) - 1];

      Result := TTMSMCPResourceContent.FromText(URI, 'application/json',
        LoadCustomerJSON(CustomerId));
    end);
end;

TTMSMCPResource.IsTemplate returns True when URITemplate is set, and that flag decides which list the resource appears in: resources/list returns only direct resources, resources/templates/list returns only templates. A client that shows an empty resource list is often looking at one of the two lists while everything was declared as the other.

How a URI is matched to a template

TTMSMCPResources.FindMatchingTemplate runs three tests, in this order:

  1. Prefix guard. The literal text before the template's first { must be a prefix of the requested URI.
  2. Segment count. Both the template and the URI are split on /, and the two must have the same number of segments.
  3. Pairwise comparison. Segment by segment: a {placeholder} matches anything, a literal segment must match exactly.
Template Matches Does not match
users/{id}/profile users/123/profile, users/abc/profile users/profile, users/123/profile/extra
repo://{owner}/{repo}/{path} repo://tms/aistudio/main.pas repo://tms/aistudio

The consequence of rule 2 is that a placeholder never spans a /. A template cannot match a path of variable depth — repo://{owner}/{repo}/{path} matches a single path segment, not src/units/main.pas. When you need variable depth, encode the path into one segment or declare a template per depth.

A read resolves a direct resource first: resources/read calls FindByURI, and only falls back to FindMatchingTemplate when no exact URI matched. A direct resource therefore always wins over a template that would also match it, which is how you special-case one instance of a templated family.

Declaring with the builder

Why and when: use the builder when a declaration carries more than the five parameters the convenience methods take — a title, icons, or a resource you want to construct in one expression.

TTMSMCPResource.CreateBuilder returns a builder whose every method returns the builder again:

procedure TForm1.RegisterManualResource;
var
  Resource: TTMSMCPResource;
begin
  Resource := TTMSMCPResource.CreateBuilder
    .Name('user_manual')
    .Title('User manual')
    .Description('The application user manual, in Markdown')
    .URI('docs://manual/index.md')
    .MimeType('text/markdown')
    .Reader(
      function(const URI: string): TTMSMCPResourceContent
      begin
        Result := TTMSMCPResourceContent.FromText(URI, 'text/markdown',
          TFile.ReadAllText(ManualFileName));
      end)
    .Build;
  try
    { Build hands the instance to you, and AddResource copies the
      declaration into the collection - so this one is yours to free. }
    Server.Resources.AddResource(Resource);
  finally
    Resource.Free;
  end;
end;

Build validates before it returns: a resource with no name, with neither URI nor URITemplate, or with no reader raises rather than producing a half-declared item. The builder also refuses URI and URITemplate together — a resource is one or the other, and setting the second raises Cannot set both URI and URITemplate.

Ownership here is the mirror image of the prompts collection: Build hands you the instance and clears the builder's own reference, and AddResource copies the declaration into a new item. So the resource you built is yours, and you free it after adding. RegisterDirectResource and RegisterTemplateResource do exactly this internally, which is why they leave nothing for you to clean up.

Names, titles, and icons

Name is the identifier shown in the protocol listing and URI is the address — both are what a client refers to, so keep them stable once a server has shipped. Title is the human-readable label a client may show instead of the name, and AddIcon attaches icon variants (source, optional MIME type, optional size) that a client can show beside it. Each AddIcon call appends to IconsJSON, so several calls declare several sizes of the same icon.

MimeType is advertised in the listing and echoed in the content, so a client knows what it is getting before it reads. Set it whenever you know it; leave it empty only when the reader decides the type per URI.

Listing resources dynamically

Why and when: a resource that only exists while a document is open, a device is connected, or a user is signed in cannot be registered at start-up.

OnListResources on the server fires with the live collection on every resources/list and resources/templates/list call, before the collection is split into the two lists:

procedure TForm1.FormCreate(Sender: TObject);
begin
  Server.EnableResourceNotifications := True;
  Server.OnListResources := HandleListResources;
end;

procedure TForm1.HandleListResources(Sender: TObject;
  const ResourceList: TTMSMCPResources);
var
  Resource: TTMSMCPResource;
  DocumentURI: string;
begin
  { Runs on both resources/list and resources/templates/list, before the
    collection is filtered into direct resources and templates. }
  if not DocumentIsOpen then
    Exit;

  DocumentURI := 'doc://open/' + OpenDocumentName;
  if ResourceList.FindByURI(DocumentURI) <> nil then
    Exit;

  Resource := ResourceList.Add;
  Resource.Name := 'open_document';
  Resource.Title := OpenDocumentName;
  Resource.Description := 'The document currently open in the editor';
  Resource.URI := DocumentURI;
  Resource.MimeType := 'text/plain';
  Resource.ResourceReader :=
    function(const URI: string): TTMSMCPResourceContent
    begin
      Result := TTMSMCPResourceContent.FromText(URI, 'text/plain',
        OpenDocumentText);
    end;
end;

Because it runs on every list request, the handler must be idempotent — check with FindByURI before adding, or the list grows on every refresh. FindByURI raises on an empty URI, so guard any URI you build from user data.

Combining the builder, a template, and presentation metadata

A real declaration usually pairs a direct entry point with a templated family behind it. This one registers an index at a fixed URI so the model has somewhere to start, then declares the articles themselves as a template built with the builder, carrying a title and an icon:

procedure TForm1.RegisterKnowledgeBase;
var
  Article: TTMSMCPResource;
begin
  { A direct resource for the index the model starts from. }
  Server.Resources.RegisterDirectResource(
    'kb_index',
    'kb://articles/index',
    'List of every knowledge base article, with its URI',
    'application/json',
    function(const URI: string): TTMSMCPResourceContent
    begin
      Result := TTMSMCPResourceContent.FromText(URI, 'application/json',
        ArticleIndexAsJSON);
    end);

  { A template resource for the articles themselves. The builder refuses
    URI and URITemplate together, so only URITemplate is set here. }
  Article := TTMSMCPResource.CreateBuilder
    .Name('kb_article')
    .Title('Knowledge base article')
    .Description('One knowledge base article, by section and slug')
    .URITemplate('kb://articles/{section}/{slug}')
    .MimeType('text/markdown')
    .AddIcon('https://cdn.example.com/icons/article-24.png', 'image/png', '24x24')
    .Reader(
      function(const URI: string): TTMSMCPResourceContent
      var
        Parts: TArray<string>;
      begin
        Parts := URI.Split(['/']);
        if Length(Parts) < 2 then
          raise Exception.CreateFmt('Malformed article URI: %s', [URI]);

        Result := TTMSMCPResourceContent.FromText(URI, 'text/markdown',
          LoadArticle(Parts[High(Parts) - 1], Parts[High(Parts)]));
      end)
    .Build;
  try
    Server.Resources.AddResource(Article);
  finally
    Article.Free;
  end;
end;

The index is what makes the template usable: a client cannot enumerate the URIs a template covers, so something has to tell it which ones exist.

Common mistakes

  • Not freeing the resource returned by Build. Build transfers the instance to you and AddResource copies it, so the instance you built leaks unless you free it. This is the opposite of the prompts collection — check which one you are working with.
  • Setting both URI and URITemplate. The builder raises, and a resource that somehow carries both is treated as a template, so its fixed URI never resolves.
  • Expecting a placeholder to span /. Matching is segment-based. A template with three segments never matches a URI with four.
  • Looking for templates in resources/list. Templates are returned by resources/templates/list only, and direct resources by resources/list only.
  • Building the list non-idempotently in OnListResources. The event fires on every list call. Without a FindByURI guard the collection grows without bound.
  • Parsing the template instead of the request. The reader receives the URI the client asked for. Split that, not URITemplate.

See also