Resources are the read surface of an MCP server.

Use a resource when the caller is reading stable or URI-addressable content:

  • configuration
  • generated files
  • reports
  • object snapshots
  • mounted provider content

Use a tool when the caller is asking the server to perform work. Use a prompt when the output is message content intended for a model.

Resources vs Resource Templates

FastestMCP supports:

  • resources: fixed URIs such as config://release
  • resource templates: URI patterns such as users://{id}

Both execute through the same runtime pipeline. The difference is whether the target is a fixed URI or a parameterized URI shape.

Fixed Resources

Define a resource with FastestMCP.add_resource/4:

server =
  FastestMCP.server("resources")
  |> FastestMCP.add_resource("config://app", fn _arguments, _ctx ->
    %{
      app_name: "FastestMCP",
      version: "0.1.0",
      environment: "production"
    }
  end)

Read it in process:

FastestMCP.read_resource("resources", "config://app")
# => %{app_name: "FastestMCP", version: "0.1.0", environment: "production"}

Resource Templates

Define a dynamic URI shape with FastestMCP.add_resource_template/4:

server =
  FastestMCP.server("resources")
  |> FastestMCP.add_resource_template(
    "weather://{city}/current",
    fn %{"city" => city}, _ctx ->
      %{
        city: String.capitalize(city),
        temperature: 22,
        condition: "Sunny",
        unit: "celsius"
      }
    end
  )
FastestMCP.read_resource("resources", "weather://london/current")
# => %{city: "London", temperature: 22, condition: "Sunny", unit: "celsius"}

FastestMCP currently supports:

  • path placeholders such as {id}
  • hyphenated path placeholders such as {user-id}
  • wildcard path placeholders such as {path*}
  • optional query variables such as {?format,limit}
  • reserved expansions such as {+path}
  • path-segment expansions such as {/path*}
  • label expansions such as {.format}
  • path-style parameter expansions such as {;version}
  • query continuation expansions such as {&page}

Example:

server =
  FastestMCP.server("template-query")
  |> FastestMCP.add_resource_template(
    "repos://{owner}/{repo}/info{?format}",
    fn %{"owner" => owner, "repo" => repo, "format" => format}, _ctx ->
      %{
        owner: owner,
        repo: repo,
        format: format || "summary",
        stars: 120,
        forks: 48
      }
    end
  )
FastestMCP.read_resource("template-query", "repos://phoenixframework/phoenix/info?format=json")
# => %{owner: "phoenixframework", repo: "phoenix", format: "json", stars: 120, forks: 48}

Wildcard captures keep path separators and are URI-decoded before they reach the handler:

server =
  FastestMCP.server("template-wildcards")
  |> FastestMCP.add_resource_template(
    "repo://{owner}/{path*}",
    fn arguments, _ctx -> arguments end
  )
FastestMCP.read_resource("template-wildcards", "repo://prefecthq/src/templates/release.md")
# => %{"owner" => "prefecthq", "path" => "src/templates/release.md"}

Hyphenated template names are normalized to underscore handler keys:

server =
  FastestMCP.server("template-hyphen")
  |> FastestMCP.add_resource_template(
    "users://{user-id}{?include-empty}",
    fn %{"user_id" => user_id, "include_empty" => include_empty}, _ctx ->
      %{user_id: user_id, include_empty: include_empty}
    end
  )

Blank query values are preserved. Path captures take precedence over query captures, and templates that would create a hyphen/underscore collision are rejected. Literal and expanded fragments are matched consistently for exact and templated lookup.

Template Parameter Validation and Completion

Resource templates can validate captures and query parameters with parameters:

schema = %{
  "type" => "object",
  "properties" => %{
    "owner" => %{"type" => "string"},
    "repo" => %{"type" => "string"},
    "page" => %{"type" => "string", "pattern" => "^[0-9]+$"}
  },
  "required" => ["owner", "repo"]
}

server =
  FastestMCP.server("resource-parameters")
  |> FastestMCP.add_resource_template(
    "repos://{owner}/{repo}/issues{?page}",
    fn %{"owner" => owner, "repo" => repo, "page" => page}, _ctx ->
      %{owner: owner, repo: repo, page: page}
    end,
    parameters: schema
  )
FastestMCP.read_resource("resource-parameters", "repos://phoenixframework/phoenix/issues?page=2")
# => %{owner: "phoenixframework", repo: "phoenix", page: "2"}

URI captures remain strings. Parameter validation never parses or coerces them.

Templates can also expose completion sources for URI variables:

server =
  FastestMCP.server("resource-completion")
  |> FastestMCP.add_resource_template(
    "repos://{owner}/{repo}",
    fn arguments, _ctx -> arguments end,
    completions: [
      owner: fn partial, _ctx ->
        ["prefecthq", "phoenixframework", "elixir-lang"]
        |> Enum.filter(&String.starts_with?(&1, partial))
      end,
      repo: fn partial, _ctx ->
        ["fastestmcp", "phoenix", "elixir"]
        |> Enum.filter(&String.starts_with?(&1, partial))
      end
    ]
  )
FastestMCP.complete(
  "resource-completion",
  %{"type" => "ref/resource", "uri" => "repos://{owner}/{repo}"},
  %{"name" => "owner", "value" => "pre"}
)
# => %{values: ["prefecthq"], total: 1}

Completion providers stay server-side. They are not leaked into public list metadata or exposed parameter schemas. MCP 2025-11-25 uses only the standard ref/resource shape for resource-template completion; the older ref/resourceTemplate spelling remains an Elixir-native compatibility input and is rejected on the wire.

Explicit Resource Result Helpers

Simple resources can return normal Elixir values and let the runtime normalize them automatically. When you need more control, use the helper structs:

Plain maps, lists, tuples, dates, URIs, MapSet, and safe finite enumerable values such as ranges are encoded as JSON resource bodies:

FastestMCP.server("resources")
|> FastestMCP.add_resource("memo://numbers", fn _arguments, _ctx ->
  %{values: 1..3}
end)

Return lists for lazy streams or other enumerables whose size is not known. FastestMCP avoids automatically materializing arbitrary Stream values because they may be infinite.

Example:

alias FastestMCP.Resources.Binary
alias FastestMCP.Resources.Result
alias FastestMCP.Resources.Text

server =
  FastestMCP.server("resource-results")
  |> FastestMCP.add_resource("reports://daily", fn _arguments, _ctx ->
    Result.new(
      [
        Text.new("Daily report is ready",
          uri: "reports://daily/summary",
          meta: %{slot: "summary"}
        ),
        Binary.new(<<0, 1, 2>>,
          uri: "reports://daily/attachment",
          meta: %{slot: "attachment"}
        )
      ],
      meta: %{source: "reporting"}
    )
  end)
FastestMCP.read_resource("resource-results", "reports://daily")
# => %{
#      contents: [
#        %{
#          uri: "reports://daily/summary",
#          content: "Daily report is ready",
#          mime_type: "text/plain",
#          meta: %{slot: "summary"}
#        },
#        %{
#          uri: "reports://daily/attachment",
#          content: <<0, 1, 2>>,
#          mime_type: "application/octet-stream",
#          meta: %{slot: "attachment"}
#        }
#      ],
#      meta: %{source: "reporting"}
#    }

These helpers are useful when you need:

  • multiple content items
  • distinct subresource URIs, which are preserved on each transport content item
  • per-item MIME types
  • per-item metadata
  • result-level metadata

File, HTTP, and Directory Resource Helpers

FastestMCP also exposes higher-level helpers for common resource sources:

File-backed resources:

file = FastestMCP.Resources.File.new("/tmp/meeting_notes.md")

server =
  FastestMCP.server("file-resources")
  |> FastestMCP.add_resource("file:///tmp/meeting_notes.md", fn _arguments, _ctx ->
    FastestMCP.Resources.File.read(file)
  end)

FastestMCP.Resources.File handles:

  • absolute-path validation
  • UTF-8 text reads by default
  • explicit binary mode
  • encoding overrides
  • normalized read errors

HTTP-backed resources:

resource =
  FastestMCP.Resources.HTTP.new("https://api.github.com/repos/phoenixframework/phoenix",
    headers: %{"accept" => "application/json"}
  )

server =
  FastestMCP.server("http-resources")
  |> FastestMCP.add_resource("https://github/phoenixframework/phoenix", fn _arguments, _ctx ->
    FastestMCP.Resources.HTTP.read(resource)
  end)

Directory-backed resources:

directory =
  FastestMCP.Resources.Directory.new("/tmp/reports",
    recursive: true
  )

server =
  FastestMCP.server("directory-resources")
  |> FastestMCP.add_resource("dir:///tmp/reports", fn _arguments, _ctx ->
    FastestMCP.Resources.Directory.read(directory)
  end)

FastestMCP.Resources.Directory handles:

  • absolute-path validation
  • file listing for one directory or a recursive tree
  • exclusion of external and cyclic symlink traversal during recursive reads
  • optional hidden-file inclusion
  • normalized read errors
  • JSON resource payload generation

Metadata and Annotations

Resources and templates support the same shaping metadata as other component types:

  • title
  • description
  • icons
  • annotations
  • mime_type
  • tags
  • visibility
  • version
  • authorization
  • meta
  • timeout
  • task

Annotations are preserved through direct list APIs and transport serialization:

server =
  FastestMCP.server("resource-metadata")
  |> FastestMCP.add_resource(
    "weather://forecast",
    fn _arguments, _ctx -> "Sunny all week" end,
    annotations: %{readOnlyHint: true, idempotentHint: true}
  )
  |> FastestMCP.add_resource_template(
    "repos://{owner}/{repo}/info",
    fn arguments, _ctx -> arguments end,
    annotations: %{readOnlyHint: true, openWorldHint: true}
  )

Transport metadata uses the same source options, but exposes them in the MCP shape clients expect:

  • direct Elixir list APIs keep meta, tags, version, and local task configuration on the resource or template struct
  • transport list APIs merge tags and version into _meta.fastestmcp
  • resource and resource-template task configuration is not advertised as a remote request capability; MCP 2025-11-25 task augmentation is tool-only
  • underscore-prefixed keys inside meta[:fastestmcp] are stripped from the public transport payload, while your own keys are preserved

meta.fastestmcp is the FastestMCP wire namespace for transport-facing metadata.

Example:

server =
  FastestMCP.server("resource-contract")
  |> FastestMCP.add_resource(
    "memo://report",
    fn _arguments, _ctx -> %{ok: true} end,
    tags: ["utility", "docs"],
    version: "2.0.0",
    task: true,
    meta: %{
      vendor: %{surface: "resource"},
      fastestmcp: %{hint: "cached", _internal: "hidden"}
    }
  )
  |> FastestMCP.add_resource_template(
    "memo://users/{id}",
    fn %{"id" => id}, _ctx -> %{id: id} end,
    tags: ["utility", "docs"],
    version: "2.0.0",
    task: true,
    meta: %{
      vendor: %{surface: "template"},
      fastestmcp: %{hint: "cached", _internal: "hidden"}
    }
  )

In process:

[resource] = FastestMCP.list_resources("resource-contract")
[template] = FastestMCP.list_resource_templates("resource-contract")

resource.meta
# => %{vendor: %{surface: "resource"}, fastestmcp: %{hint: "cached", _internal: "hidden"}}

template.version
# => "2.0.0"

Over transport:

client =
  FastestMCP.Client.connect!("http://127.0.0.1:4100/mcp",
    client_info: %{"name" => "docs-client", "version" => "1.0.0"}
  )

%{items: [%{
  "_meta" => %{
    "vendor" => %{"surface" => "resource"},
    "fastestmcp" => %{
      "hint" => "cached",
      "tags" => ["docs", "utility"],
      "version" => "2.0.0"
    }
  },
  "uri" => "memo://report"
}]} = FastestMCP.Client.list_resources(client)

The same _meta.fastestmcp metadata contract is used for resource templates in FastestMCP.Client.list_resource_templates/2.

Context-Aware Resources

Resources can inspect request state, auth state, or session state through the explicit context:

alias FastestMCP.Context

server =
  FastestMCP.server("resource-context")
  |> FastestMCP.add_resource("request://snapshot", fn _arguments, ctx ->
    request = Context.request_context(ctx)

    %{
      request_id: request.request_id,
      path: request.path,
      meta: request.meta
    }
  end)

Background Tasks

Resources can opt into local, in-process task execution:

server =
  FastestMCP.server("resource-tasks")
  |> FastestMCP.add_resource(
    "file://report.txt",
    fn _arguments, _ctx ->
      "ready"
    end,
    task: true
  )

This is useful when generating the resource itself is slow, even if the final shape is still a read result.

The same resource can then be read synchronously or as a local task:

task =
  FastestMCP.read_resource("resource-tasks", "file://report.txt",
    task: true
  )

FastestMCP.await_task(task, 5_000)

Use this when the read itself may block inside an Elixir-owned workflow. Remote MCP resources/read requests are synchronous; task metadata on that wire method is rejected. See Background Tasks for the local/remote boundary.

Runtime Changes

Resources and resource templates can be added, disabled, or removed after startup through the component manager:

manager = FastestMCP.component_manager("dynamic-resources")

{:ok, _resource} =
  FastestMCP.ComponentManager.add_resource(
    manager,
    "config://runtime",
    fn _arguments, _ctx -> %{status: "ok"} end,
    on_duplicate: :replace
  )

{:ok, _template} =
  FastestMCP.ComponentManager.add_resource_template(
    manager,
    "users://{id}",
    fn %{"id" => id}, _ctx -> %{id: id} end
  )

Dynamic entries take precedence over static startup entries for the same URI or URI template until the dynamic version is disabled or removed. That keeps runtime patches explicit without mutating the original server definition.

Errors and Duplicate Behavior

Resource registration uses the same unified duplicate policy as tools and prompts. The Elixir default is on_duplicate: :error.

Use :replace, :ignore, or :warn when you need a different registration policy:

server =
  FastestMCP.server("resource-duplicates", on_duplicate: :replace)
  |> FastestMCP.add_resource("config://release", fn _arguments, _ctx ->
    %{source: "first"}
  end)
  |> FastestMCP.add_resource("config://release", fn _arguments, _ctx ->
    %{source: "second"}
  end)

Runtime additions follow the same policy through the component manager:

{:ok, _resource} =
  FastestMCP.ComponentManager.add_resource(
    manager,
    "config://runtime",
    fn _arguments, _ctx -> %{status: "ok"} end,
    on_duplicate: :replace
  )

Read failures still surface as normal FastestMCP.Error values:

  • nonexistent URI or unmatched template -> :not_found
  • disabled resource or template version -> :disabled
  • parameter validation failure -> :invalid_params
  • handler-raised application errors -> the raised FastestMCP.Error

Subscriptions and Update Notifications

FastestMCP supports session-scoped resource subscriptions through the MCP transport surface. A subscription targets one concrete resource URI:

client = FastestMCP.Client.connect!("http://127.0.0.1:4100/mcp", session_stream: true)

%{} = FastestMCP.Client.subscribe_resource(client, "config://release")

Resource templates remain available for discovery and reads, but their template strings are not subscription targets. Subscribe separately to each expanded concrete URI whose updates the client needs.

When the server knows that resource changed, emit an update:

:ok = FastestMCP.notify_resource_updated("resources", "config://release")

Or from inside a handler:

FastestMCP.add_tool(server, "refresh_config", fn _arguments, ctx ->
  :ok = FastestMCP.Context.notify_resource_updated(ctx, "config://release")
  %{ok: true}
end)

Current behavior:

  • subscriptions are exact concrete resource URIs
  • notifications/resources/updated is delivered only to subscribed, initialized sessions with an attached HTTP or stdio output sink
  • notifications/resources/list_changed is emitted when the visible set of resources or resource templates changes for a session
  • bidirectional stdio receives the same negotiated resource notifications as Streamable HTTP

Helper Types in Practice

alias FastestMCP.Resources.File
alias FastestMCP.Resources.Directory
alias FastestMCP.Resources.Result
alias FastestMCP.Resources.Text

file = File.new("/tmp/release.md")
directory = Directory.new("/tmp/releases", recursive: true)

server =
  FastestMCP.server("resource-helper-example")
  |> FastestMCP.add_resource("file:///tmp/release.md", fn _arguments, _ctx ->
    File.read(file)
  end)
  |> FastestMCP.add_resource("dir:///tmp/releases", fn _arguments, _ctx ->
    Directory.read(directory)
  end)
  |> FastestMCP.add_resource("memo://inline", fn _arguments, _ctx ->
    Result.new([Text.new("hello from Elixir")])
  end)

This keeps the resource builder, helper type, and handler return shape explicit.

Resource Design Shape

FastestMCP keeps the public resource contract transport-safe while using explicit Elixir APIs and OTP-owned runtime state internally.

  • resource registration uses explicit FastestMCP.add_resource/4 and FastestMCP.add_resource_template/4 calls
  • request data is carried by an explicit %FastestMCP.Context{} passed to every handler
  • storage and orchestration use OTP processes plus ETS-backed runtime state unless you intentionally swap in a backend seam
  • mounted providers, versioning, and visibility are first-class Elixir runtime concerns

That is why examples in this guide stay explicit about handler arguments, context access, and runtime APIs.

RFC 6570 URI Templates

Resource templates are parsed and expanded through the direct Texture dependency and cover RFC 6570 levels 1 through 4: every operator, fragments, prefix modifiers, explode, scalar/list/map values, percent encoding, and empty or undefined variables. Malformed templates are rejected during registration. Reverse routing keeps the existing handler map interface and decodes captures deterministically as strings, ordered lists, or maps.

The repository runs the official uri-templates/uritemplate-test positive and negative fixtures, pinned at commit 4171dac22aa67fc710b3f6df308a50bd08552986, together with reverse-routing fixtures for fragment, prefix, exploded, and ambiguous templates.

Resource update notifications require an active session event stream.

Why This Shape

FastestMCP keeps resources URI-first and transport-safe.

The runtime treats resources as reads with explicit MIME typing and explicit URI matching. That keeps mounted providers, templates, and local resources aligned without turning the resource layer into a hidden file-server framework.