@ResourceTemplate

@ResourceTemplate declares a family of MCP resources that share a URI pattern, like ticket://{id}. Clients fill in the pattern to read one member, such as ticket://T-1, and FrontMCP passes the parts it matched to your code. For a single resource at a fixed URI, use @Resource.

@ResourceTemplate(options)
class MyTemplate extends ResourceContext<Params> {
  async execute(uri, params) { /* ... */ }
}

Reference

@ResourceTemplate(options)

Apply @ResourceTemplate to a class that extends ResourceContext, and list the class in an app's resources array, next to static resources.

ticket.resource.ts
import { ResourceContext, ResourceNotFoundError, ResourceTemplate } from "@frontmcp/sdk";

@ResourceTemplate({
  name: "ticket",
  uriTemplate: "ticket://{id}",
  description: "One support ticket by id, like ticket://T-1",
  mimeType: "application/json",
})
class TicketResource extends ResourceContext<{ id: string }> {
  async execute(uri: string, params: { id: string }) {
    const ticket = await findTicket(params.id);
    if (!ticket) throw new ResourceNotFoundError(uri);
    return ticket;
  }
}

See more examples below.

Options

Required:

OptionTypeDescription
namestringIdentifies the template. Unique within the server.
uriTemplatestringThe URI pattern, with parameters in braces, like ticket://{id}. It must start with a scheme and ://. See URI template syntax.

Optional:

OptionTypeDescription
titlestringA display name for people. Clients show it instead of name.
descriptionstringWhat each resource is, and what the parameters mean. Clients and models read it in resources/templates/list.
mimeTypestringThe content type of every matching resource. Listed in resources/templates/list, and used for content blocks FrontMCP builds from what execute() returns.
iconsIcon[]Images clients can show: src, and optionally mimeType, sizes and theme.
annotationsResourceAnnotationsaudience, priority and lastModified, as for @Resource. FrontMCP 1.8 accepts them but leaves them out of resources/templates/list.
_metaRecord<string, unknown>Extra metadata. Like annotations, FrontMCP 1.8 doesn't send it in resources/templates/list.
availableWhenEntryAvailabilityOnly offer the template on certain platforms, runtimes or deployments. Elsewhere it isn't in resources/templates/list, and reading a URI it matches fails with -32003. See Environment awareness.

URI template syntax

PatternMatchesExample
{name}One path segment: anything up to the next /ticket://{id} matches ticket://T-1 with id = "T-1"
{+name}One or more segments, slashes includedkb://{+path} matches kb://billing/refunds.md with path = "billing/refunds.md"
Other textItself, exactlycustomer://{customer}/tickets needs the literal /tickets

Parameter values are percent-decoded, so reading note://a%2Fb from note://{id} gives id = "a/b".

execute(uri, params)

FrontMCP calls execute() for each read whose URI matches the template.

  • uri: the URI that was read, like ticket://T-1.
  • params: the matched parameters, like { id: "T-1" }. Values are always strings. Type them with ResourceContext<{ id: string }>.
  • Returns the content, converted as for @Resource. Content FrontMCP builds for you is labelled with the URI that was read, like ticket://T-1, and a string's MIME type can come from that URI's extension. Blocks you return in { contents } are sent as they are.

this.uri and this.params hold the same values as the arguments. The rest of this is the same ResourceContext as for @Resource.

Completing parameters

Add a method named after a parameter with Completer appended, like idCompleter for {id}. When a client sends completion/complete for the template, FrontMCP calls it with what the user has typed so far.

async idCompleter(partial: string) {
  return { values: ["T-1", "T-12"], total: 2, hasMore: false };
}

It returns values, and optionally total and hasMore. Completers can use this.get(). To choose a completer at run time instead, override getArgumentCompleter(name) and return a function, or null.

resourceTemplate(options)(handler)

The function form takes the same options. The handler receives (uri, params) and returns the { contents: [...] } shape. Function-form templates can't have completers.

Caveats

  • The class must extend ResourceContext. Using @ResourceTemplate on any other class is a compile error.
  • Parameters are strings, and they aren't validated. ticket://anything matches ticket://{id}. Check the value in execute().
  • A {name} parameter doesn't match /. Use {+name} for paths.
  • If a @Resource has exactly the URI that was read, it wins over any template.
  • Templates are listed in resources/templates/list, not resources/list.
  • To answer "not found" for parameters that don't point at anything, throw ResourceNotFoundError(uri). The client gets -32602, the same error as for a URI no template matches. Other errors fail the read as they do for @Resource.

Usage

Reading one item by id

Put the id in the URI, and return the item. FrontMCP sends it as JSON, labelled with the URI that was read.

Open
import { ResourceContext, ResourceTemplate } from "@frontmcp/sdk";
import { tickets } from "./tickets";

@ResourceTemplate({
  name: "ticket",
  uriTemplate: "ticket://{id}",
  description: "One support ticket by id, like ticket://T-1",
  mimeType: "application/json",
})
export class TicketResource extends ResourceContext<{ id: string }> {
  async execute(uri: string, { id }: { id: string }) {
    return tickets.find((t) => t.id === id);
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

In the Call tab, pick ticket://{id} from the Request menu and read any id. The content's uri is the one you read.

Handling an id that doesn't exist

Throw ResourceNotFoundError with the URI. The client gets a -32602 error that names it, the same as for a URI that matches no template at all.

Open
import { ResourceContext, ResourceNotFoundError, ResourceTemplate } from "@frontmcp/sdk";
import { tickets } from "./tickets";

@ResourceTemplate({ name: "ticket", uriTemplate: "ticket://{id}", mimeType: "application/json" })
export class TicketResource extends ResourceContext<{ id: string }> {
  async execute(uri: string, { id }: { id: string }) {
    const ticket = tickets.find((t) => t.id === id);
    if (!ticket) throw new ResourceNotFoundError(uri);
    return ticket;
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

To send a message of your own instead, like There's no ticket T-9. Ticket ids look like T-1., throw a PublicMcpError: the read fails with -32602 and exactly that message. A plain Error is a server failure, -32603, and production hides its message. See @Resource.

Using several parameters

Each {name} becomes a key in params. Literal text between them has to match exactly.

Open
import { ResourceContext, ResourceTemplate } from "@frontmcp/sdk";
import { tickets } from "./tickets";

type Params = { customer: string; status: string };

@ResourceTemplate({
  name: "customer-tickets",
  uriTemplate: "customer://{customer}/tickets/{status}",
  description: "A customer's tickets with one status, open or closed",
  mimeType: "application/json",
})
export class CustomerTickets extends ResourceContext<Params> {
  async execute(uri: string, { customer, status }: Params) {
    const found = tickets.filter((t) => t.customer === customer && t.status === status);
    return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ customer, status, tickets: found }) }] };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Matching paths with slashes

{name} stops at the first /. Use {+name} when a parameter is a path.

Path parameters

Example 1 of 3

{+path} matches the whole path

Open
import { ResourceContext, ResourceTemplate } from "@frontmcp/sdk";

@ResourceTemplate({ name: "kb-article", uriTemplate: "kb://{+path}", description: "A knowledge-base article by path" })
export class KbArticle extends ResourceContext<{ path: string }> {
  async execute(uri: string, { path }: { path: string }) {
    return { contents: [{ uri, mimeType: "text/markdown", text: `# ${path}\n\nRefunds take 5 business days.` }] };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Suggesting parameter values

Clients can offer suggestions as the user types an id. Add an idCompleter() method, and FrontMCP answers completion/complete requests for the {id} parameter with it. The test sends the request a client would.

Open
import { ResourceContext, ResourceTemplate } from "@frontmcp/sdk";
import { tickets } from "./tickets";

@ResourceTemplate({ name: "ticket", uriTemplate: "ticket://{id}", mimeType: "application/json" })
export class TicketResource extends ResourceContext<{ id: string }> {
  async execute(uri: string, { id }: { id: string }) {
    return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify(tickets.find((t) => t.id === id)) }] };
  }

  async idCompleter(partial: string) {
    const values = tickets.map((t) => t.id).filter((id) => id.startsWith(partial));
    return { values, total: values.length };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Writing a template as a function

For a template that doesn't need context, the function form is shorter. Its handler gets the same (uri, params) and returns the full { contents } shape.

Open
import { resourceTemplate } from "@frontmcp/sdk";

export const agentQueue = resourceTemplate({
  name: "agent-queue",
  uriTemplate: "agent://{agent}/queue",
  description: "The tickets assigned to one support agent",
  mimeType: "application/json",
})((uri, { agent }) => ({
  contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ agent, tickets: ["T-1", "T-3"] }) }],
}));

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.


Troubleshooting

The server doesn't start: "URI template must have a valid scheme"

uriTemplate needs a scheme followed by ://, like ticket://{id}. {id} or ticket:{id} are rejected.

Reads fail with -32602 "Resource not found"

No template matched the URI. Check, in order:

  1. The literal parts match exactly, including case and slashes.
  2. No {name} value contains a /. Use {+name}, or have the client percent-encode the value with encodeURIComponent().
  3. The template is in its app's resources array.
  4. execute() threw ResourceNotFoundError, which gives the same error. Check that the item exists.
  5. The template's availableWhen.surface, if it has one, includes "mcp". MCP clients can't read a template that leaves it out.

Reads fail with -32003 "ResourceTemplate "…" is not available in the current environment"

The template's availableWhen doesn't match where the server runs. It's left out of resources/templates/list, and reading a URI it matches fails with this error, which names the URI, what the template requires and what the server is:

Open
import { ResourceContext, ResourceTemplate } from "@frontmcp/sdk";

@ResourceTemplate({ name: "deploy-notes", uriTemplate: "deploy://{id}/notes", mimeType: "text/plain", availableWhen: { runtime: ["deno"] } })
export class DeployNotes extends ResourceContext<{ id: string }> {
  async execute(uri: string, { id }: { id: string }) {
    return `Deploy notes for ${id}`;
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

My template isn't in resources/list

That's expected. Clients find templates with resources/templates/list, and mcp.resources.listTemplates() in tests.

Completions come back empty

Check that the method is named after the parameter, idCompleter for {id}, and that the template is a class. If the completer throws, FrontMCP logs a warning and answers with no values.