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:
- Prefix guard. The literal text before the template's first
{must be a prefix of the requested URI. - Segment count. Both the template and the URI are split on
/, and the two must have the same number of segments. - 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.Buildtransfers the instance to you andAddResourcecopies 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
URIandURITemplate. 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 byresources/templates/listonly, and direct resources byresources/listonly. - Building the list non-idempotently in
OnListResources. The event fires on every list call. Without aFindByURIguard 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
- Resource content and updates — what the reader returns, and how to announce a change
- Tools — a tool result can return a resource link pointing at these URIs
- MCP Server — hosting, sessions, and capabilities
TTMSMCPResource,TTMSMCPResources,TTMSMCPResourceBuilder