@Resource

@Resource declares an MCP resource: data at a fixed URI, like policy://sla, that a client can list and read. The client application decides when to read it, usually to attach it to a conversation. For a family of URIs that share a pattern, like ticket://{id}, use @ResourceTemplate.

@Resource(options)
class MyResource extends ResourceContext {
  async execute(uri) { /* ... */ }
}

Reference

@Resource(options)

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

sla.resource.ts
import { Resource, ResourceContext } from "@frontmcp/sdk";

@Resource({
  name: "sla",
  uri: "policy://sla",
  description: "How fast the help desk replies, by ticket priority",
  mimeType: "application/json",
})
class SlaPolicy extends ResourceContext {
  async execute() {
    return { high: "4 hours", normal: "1 business day", low: "3 business days" };
  }
}

See more examples below.

Options

Required:

OptionTypeDescription
namestringIdentifies the resource. Unique within the server.
uristringThe address clients read. It must start with a scheme and ://, like policy://sla or file:///docs/faq.md.

Optional:

OptionTypeDescription
titlestringA display name for people. Clients show it instead of name.
descriptionstringWhat the data is and when it's useful. Clients and models read it in resources/list.
mimeTypestringThe content type, like application/json. Listed in resources/list, and used for the content blocks FrontMCP builds from what execute() returns. Without it, FrontMCP picks one per block. Blocks you return in { contents } are sent as they are.
iconsIcon[]Images clients can show: src, and optionally mimeType, sizes and theme ("light" or "dark").
annotationsResourceAnnotationsHints for clients: audience (["user"], ["assistant"] or both), priority (from 0 to 1) and lastModified (an ISO 8601 date).
_metaRecord<string, unknown>Extra metadata, sent as is in resources/list. Use reverse-DNS keys, like com.example/owner.
availableWhenEntryAvailabilityOnly offer the resource on certain platforms, runtimes or deployments. Elsewhere it isn't listed, and reading it fails with -32003. See Environment awareness.

execute(uri, params)

FrontMCP calls execute() every time a client reads the resource.

  • uri: the URI that was read. For a @Resource, that's always its uri option.
  • params: template parameters. Always {} for a @Resource.
  • Returns the content. FrontMCP turns it into the contents array of the resources/read result:
execute() returnsThe client receives
An object or number, like { high: "4 hours" }One text block with the value as JSON. MIME type application/json.
A stringOne text block. The MIME type comes from the URI's extension (.md is text/markdown) or the text itself, and is text/plain otherwise.
A Uint8Array or BufferOne blob block, base64-encoded. MIME type application/octet-stream, unless you set mimeType.
An object with a text or blob stringThat block, with the resource's URI.
An arrayOne block per item, converted as above. Items without their own uri get #0, #1… appended to the resource's URI.
{ contents: [...] }Exactly those blocks. Use this for full control.

Inside execute(), this is the ResourceContext: this.uri and this.params hold the same values as the arguments, this.get() returns providers, this.fetch() makes outbound HTTP requests, this.context and this.auth describe the request and the caller, and this.notifyUpdated() tells subscribed clients that the content changed. this.notify(), this.progress() and this.elicit() belong to tools, not resources.

resource(options)(handler)

The function form takes the same options. The handler receives (uri, params) and returns a ReadResourceResult, the { contents: [...] } shape.

Caveats

  • The class must extend ResourceContext. Using @Resource on any other class is a compile error.
  • uri must have a scheme followed by ://. Otherwise the server doesn't start, and reports "URI must have a valid scheme".
  • Reads match the URI exactly. policy://sla/ and policy://SLA are different URIs, and reading them fails with "Resource not found".
  • A resource takes no arguments. For data that depends on an id, use @ResourceTemplate. If a resource and a template both match a URI, the resource wins.
  • If execute() throws ResourceNotFoundError(uri), the client gets the same error as for a URI that matches nothing: -32602, Resource not found: …. A PublicMcpError also gives -32602, with your error's message as the whole message.
  • Any other error is an internal one: JSON-RPC error -32603, with Resource "…" read failed: and your error's message in development. With NODE_ENV=production, FrontMCP hides the message and sends Internal FrontMCP error. Please contact support with error ID: … instead. See troubleshooting.
  • this.fail(error) fails the read the same way as throw error for a PublicMcpError. For any other error it's still -32603, but the message is your error's message without the read failed: prefix, and data.code is SERVER_ERROR instead of RESOURCE_READ_ERROR. Production hides it the same way.

Usage

Exposing data at a fixed URI

Give the resource a URI with your own scheme and a description that says what's in it. Return a plain object and FrontMCP sends it as JSON.

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

@Resource({
  name: "sla",
  title: "Reply-time policy",
  uri: "policy://sla",
  description: "How fast the help desk replies, by ticket priority. Read it before promising a customer a reply time.",
})
export class SlaPolicy extends ResourceContext {
  async execute() {
    return { high: "4 hours", normal: "1 business day", low: "3 business days" };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Open the Capabilities tab to see the resource as resources/list describes it. Clients read it with resources/read and the URI.

Choosing what the client receives

What execute() returns decides the content blocks and their MIME types.

Content types

Example 1 of 4

Text

A string becomes one text block. The URI ends in .md, so the MIME type is text/markdown.

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

@Resource({ name: "reply-guide", uri: "guide://replies.md", description: "How to word replies to customers" })
export class ReplyGuide extends ResourceContext {
  async execute() {
    return "# Replying to customers\n\n- Greet them by name.\n- Say what you did, then what happens next.\n- Link the ticket id, like T-1.";
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Reading from a provider

Get shared services with this.get(), and register the provider in the same app's providers array.

Open
import { Resource, ResourceContext } from "@frontmcp/sdk";
import { Rota } from "./rota";

@Resource({ name: "on-duty", uri: "desk://on-duty", description: "Which agents are on duty right now" })
export class OnDuty extends ResourceContext {
  async execute() {
    return { agents: this.get(Rota).onDuty() };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Describing a resource for clients

Everything except availableWhen goes out in resources/list. Clients use title and icons for display, and annotations to decide who the content is for and how prominent it should be. The test below reads the list the way a client would.

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

@Resource({
  name: "sla",
  title: "Reply-time policy",
  uri: "policy://sla",
  description: "How fast the help desk replies, by ticket priority",
  mimeType: "application/json",
  annotations: { audience: ["assistant"], priority: 0.8, lastModified: "2026-09-01T09:00:00Z" },
  _meta: { "com.example/owner": "support-ops" },
})
export class SlaPolicy extends ResourceContext {
  async execute() {
    return { high: "4 hours", normal: "1 business day", low: "3 business days" };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Writing a resource as a function

For a resource that doesn't need context, the function form is shorter. Its handler returns the full { contents } shape, which is sent as it is, so set each block's mimeType there.

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

export const openingHours = resource({
  name: "opening-hours",
  uri: "policy://hours",
  mimeType: "application/json",
  description: "When the help desk answers tickets",
})((uri) => ({
  contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ weekdays: "08:00-18:00", weekends: "closed" }) }],
}));

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.


Troubleshooting

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

The uri option needs a scheme followed by ://. policy:sla and /policy/sla are rejected; policy://sla works.

Reads fail with -32602 "Resource not found"

No resource or template matches the URI exactly, or execute() threw ResourceNotFoundError. Check for a trailing slash, different letter case, or a typo, and compare with resources/list. Under MCP 2026-07-28 this error has code -32602; earlier protocol versions use -32002. A resource whose availableWhen.surface leaves out "mcp" gets the same answer: MCP clients can't read it.

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

@Resource({ name: "sla", uri: "policy://sla" })
export class SlaPolicy extends ResourceContext {
  async execute() {
    return { high: "4 hours" };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Reads fail with -32603 "Resource … read failed"

execute() threw an error that isn't a PublicMcpError, so FrontMCP treats it as a failure of the server. In development, the message after read failed: is your error's message:

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

@Resource({ name: "sla", uri: "policy://sla" })
export class SlaPolicy extends ResourceContext {
  async execute(): Promise<unknown> {
    // 🚩 A server failure, and production hides the message
    throw new Error("The SLA policy hasn't been published yet");
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

With NODE_ENV=production, the client only gets Internal FrontMCP error. Please contact support with error ID: …. If the client should read the message, throw a PublicMcpError. The read then fails with -32602 and your message, in production too:

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

@Resource({ name: "sla", uri: "policy://sla" })
export class SlaPolicy extends ResourceContext {
  async execute(): Promise<unknown> {
    // ✅ The client sees exactly this message
    throw new PublicMcpError("The SLA policy hasn't been published yet");
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

A message ending in read failed: Resource output not found means execute() returned nothing. Return the content, or end with this.respond(), which works in resources since 1.9.3.

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

The resource's availableWhen doesn't match where the server runs. It's left out of resources/list, and a client that reads it by URI gets this error, with what the resource requires and what the server is:

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

@Resource({ name: "deploy-notes", uri: "policy://deno-deploy", mimeType: "text/plain", availableWhen: { runtime: ["deno"] } })
export class DeployNotes extends ResourceContext {
  async execute() {
    return "Deploy with deployctl.";
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

The message lists the server's OS, runtime and deployment, in production too. If the resource should be readable here, fix its availableWhen.

My resource isn't in resources/list

Check, in order:

  1. The class is in its app's resources array, and the app is in @FrontMcp({ apps }).
  2. It's a @Resource. Templates are listed separately, in resources/templates/list.
  3. Its availableWhen matches the environment the server runs in.