# @ResourceTemplate

> Declare a family of resources whose URIs follow a pattern, like ticket://{id}, and read the parameters from the URI.

Source: https://frontmcp.dev/reference/sdk/resource-template

`@ResourceTemplate` declares a family of MCP [resources](https://frontmcp.dev/learn/exposing-data-with-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`](https://frontmcp.dev/reference/sdk/resource).

```ts
@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.

```ts 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.](#usage)

#### Options

Required:

| Option | Type | Description |
| --- | --- | --- |
| `name` | `string` | Identifies the template. Unique within the server. |
| `uriTemplate` | `string` | The URI pattern, with parameters in braces, like `ticket://{id}`. It must start with a scheme and `://`. See [URI template syntax](#uri-template-syntax). |

Optional:

| Option | Type | Description |
| --- | --- | --- |
| `title` | `string` | A display name for people. Clients show it instead of `name`. |
| `description` | `string` | What each resource is, and what the parameters mean. Clients and models read it in `resources/templates/list`. |
| `mimeType` | `string` | The content type of every matching resource. Listed in `resources/templates/list`, and used for content blocks FrontMCP builds from what `execute()` returns. |
| `icons` | `Icon[]` | Images clients can show: `src`, and optionally `mimeType`, `sizes` and `theme`. |
| `annotations` | `ResourceAnnotations` | `audience`, `priority` and `lastModified`, as for [`@Resource`](https://frontmcp.dev/reference/sdk/resource#options). FrontMCP 1.8 accepts them but leaves them out of `resources/templates/list`. |
| `_meta` | `Record<string, unknown>` | Extra metadata. Like `annotations`, FrontMCP 1.8 doesn't send it in `resources/templates/list`. |
| `availableWhen` | `EntryAvailability` | Only 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`](#reads-fail-with--32003-resourcetemplate--is-not-available-in-the-current-environment). See [Environment awareness](https://frontmcp.dev/reference/server/environment). |

#### URI template syntax

| Pattern | Matches | Example |
| --- | --- | --- |
| `{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 included | `kb://{+path}` matches `kb://billing/refunds.md` with `path` = `"billing/refunds.md"` |
| Other text | Itself, exactly | `customer://{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`](https://frontmcp.dev/reference/sdk/resource#executeuri-params). 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.

```ts
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`](https://frontmcp.dev/reference/sdk/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`](https://frontmcp.dev/reference/sdk/resource#caveats).

---

## 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.

```ts ticket.resource.ts active
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);
  }
}
```

```ts tickets.ts
export const tickets = [
  { id: "T-1", customer: "acme", title: "Cannot log in", status: "open" },
  { id: "T-2", customer: "acme", title: "Invoice total is wrong", status: "closed" },
  { id: "T-3", customer: "globex", title: "Login link expired", status: "open" },
];
```

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.

```ts ticket.resource.ts active
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;
  }
}
```

```ts tickets.ts
export const tickets = [
  { id: "T-1", title: "Cannot log in", status: "open" },
  { id: "T-2", title: "Invoice total is wrong", status: "closed" },
];
```

```ts not-found.test.ts
import { test, expect } from "@frontmcp/testing";

test("an unknown id is not found", async ({ mcp }) => {
  const result = await mcp.resources.read("ticket://T-9");
  expect(result).toBeError(-32602);
  expect(result.error?.message).toBe("Resource not found: ticket://T-9");
});

test("a known id still reads", async ({ mcp }) => {
  expect((await mcp.resources.read("ticket://T-1")).json()).toMatchObject({ id: "T-1", status: "open" });
});
```

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`](https://frontmcp.dev/reference/sdk/resource#reads-fail-with--32603-resource--read-failed).

### Using several parameters

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

```ts customer-tickets.resource.ts active
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 }) }] };
  }
}
```

```ts tickets.ts
export const tickets = [
  { id: "T-1", customer: "acme", title: "Cannot log in", status: "open" },
  { id: "T-2", customer: "acme", title: "Invoice total is wrong", status: "closed" },
  { id: "T-3", customer: "acme", title: "Login link expired", status: "open" },
];
```

### Matching paths with slashes

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

<Examples title="Path parameters">

#### Example: {+path} matches the whole path
```ts kb.resource.ts
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.` }] };
  }
}
```

#### Example: {path} doesn't match a slash
The same read fails with "Resource not found", because `{path}` only matches one segment.

```ts kb.resource.ts
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}` }] };
  }
}
```

#### Example: An encoded slash matches {path}
A client can percent-encode the slash instead. FrontMCP decodes it, so `path` is `"billing/refunds.md"` again.

```ts kb.resource.ts
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}` }] };
  }
}
```

### 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.

```ts ticket.resource.ts active
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 };
  }
}
```

```ts tickets.ts
export const tickets = [
  { id: "T-1", title: "Cannot log in" },
  { id: "T-2", title: "Invoice total is wrong" },
  { id: "T-12", title: "Export times out" },
];
```

```ts complete.test.ts
import { test, expect } from "@frontmcp/testing";

test("typing T-1 suggests T-1 and T-12", async ({ mcp }) => {
  const response = await mcp.raw.request({
    jsonrpc: "2.0",
    id: 1,
    method: "completion/complete",
    params: { ref: { type: "ref/resource", uri: "ticket://{id}" }, argument: { name: "id", value: "T-1" } },
  });
  expect(response).toHaveResult();
  expect(response.result.completion).toEqual({ values: ["T-1", "T-12"], total: 2 });
});

test("the template is listed", async ({ mcp }) => {
  expect(await mcp.resources.listTemplates()).toContainResourceTemplate("ticket://{id}");
});
```

### 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.

```ts queue.resource.ts
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"] }) }],
}));
```

---

## 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:

```ts deploy-notes.template.ts active
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}`;
  }
}
```

```ts deploy.test.ts
import { test, expect } from "@frontmcp/testing";

test("it isn't listed, and reading a matching URI fails", async ({ mcp }) => {
  expect(await mcp.resources.listTemplates()).toEqual([]);
  const result = await mcp.resources.read("deploy://T-1/notes");
  expect(result).toBeError(-32003);
  expect(result.error?.message).toMatch(/^ResourceTemplate "deploy:\/\/T-1\/notes" is not available in the current environment \(requires: \{"runtime":\["deno"\]\}\)/);
});
```

### 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.
