@Provider

@Provider declares a service that tools, resources and prompts share: a database client, an API wrapper, a cache, your configuration. FrontMCP creates it, keeps it for as long as its scope says, and hands it to any code that asks with this.get(). This is dependency injection: your tools say what they need, not how to build it.

@Provider(options)
class MyService { /* ... */ }

Reference

@Provider(options)

Apply @Provider to a class, list it in the providers of an app or of the server, and get it inside execute() with this.get(MyService).

ticket-store.ts
import { Provider, ProviderScope } from "@frontmcp/sdk";

@Provider({ name: "TicketStore", scope: ProviderScope.GLOBAL })
export class TicketStore {
  private tickets = new Map([["T-1", { id: "T-1", title: "Cannot log in", status: "open" }]]);

  find(id: string) {
    return this.tickets.get(id);
  }
}
help-desk.app.ts
import { App } from "@frontmcp/sdk";
import { GetTicket } from "./get-ticket.tool";
import { TicketStore } from "./ticket-store";

@App({ id: "help-desk", name: "Help Desk", providers: [TicketStore], tools: [GetTicket] })
export class HelpDeskApp {}

See more examples below.

Options

OptionTypeDescription
namestringRequired. Names the provider in logs and error messages.
scopeProviderScopeHow long one instance lives. Defaults to ProviderScope.GLOBAL. See scopes.
descriptionstringWhat the provider is for.
idstringA stable identifier, if name isn't one.

Scopes

ScopeValueOne instance per
ProviderScope.GLOBAL"global"Server. Created when the server starts, and kept until it stops. The default.
ProviderScope.CONTEXT"context"Session, for clients that open one (MCP before 2026-07-28). Under 2026-07-28, which has no sessions, one per request, for callers with a token too. Created when the request starts.
ProviderScope.SESSION, ProviderScope.REQUEST"session", "request"Deprecated. Both mean CONTEXT.

The Playground speaks 2026-07-28, so a CONTEXT provider in an example is new on every call.

Registering providers

A provider is available where it's registered, and nowhere else:

WhereVisible to
@App({ providers })That app's tools, resources and prompts, and its other providers.
@FrontMcp({ providers })Every app on the server.
@Plugin({ providers })The plugin only. Also list a provider in the plugin's exports to make it visible to the apps that use the plugin. Each app that installs the plugin gets its own instance.

Each entry in providers is one of:

FormExampleNotes
A classTicketStoreMust be decorated with @Provider. The class is also the token you pass to this.get().
A factory{ provide: DeskConfig, name: "DeskConfig", inject: () => [], useFactory: () => ({ slaHours: 4 }) }name is required. inject is a function that returns the tokens to pass to useFactory. useFactory can be async. Add scope to change the default GLOBAL.
An instance{ provide: TicketStore, useValue: new TicketStore() }useValue must be an instance of a @Provider class.

provide is the token: a class, an abstract class, a symbol or a string. An abstract class makes a good token for configuration, because this.get() then knows its type.

this.get(token) and this.tryGet(token)

Available in ToolContext, ResourceContext and PromptContext.

  • this.get(token) returns the instance. If no provider for token is registered where the caller can see it, it throws, and a tool call fails with Provider "…" is not available.
  • this.tryGet(token) returns undefined instead of throwing.

For a symbol or string token, pass the type yourself: this.get<number>(SLA_HOURS).

Constructor injection

In a project compiled by tsc with emitDecoratorMetadata, a provider's constructor parameters are injected by type:

ticket-repo.ts
import { Provider } from "@frontmcp/sdk";
import { TicketStore } from "./ticket-store";

@Provider({ name: "TicketRepo" })
export class TicketRepo {
  constructor(private store: TicketStore) {}

  titleOf(id: string) {
    return this.store.find(id)?.title;
  }
}

The Playground can't run this: it compiles with esbuild, which doesn't emit decorator metadata, so store would be undefined. The examples below use factories with inject instead, which work everywhere.

Caveats

  • A provider must be registered in providers. Decorating the class isn't enough.
  • In FrontMCP 1.8, providers rejects { provide, useValue } with a plain value and { provide, useClass }, even though the TypeScript types allow them. Use a factory.
  • A GLOBAL provider can't depend on a CONTEXT provider. The server fails to start.
  • GLOBAL means one instance per server process. Several processes or serverless instances each have their own, so keep shared data in a database or Redis.
  • A provider on an @App can declare hooks, and they run for that app's tools. Hooks on a @FrontMcp provider or a CONTEXT provider never run. See where hooks can be declared.

Usage

Sharing state between tools

A GLOBAL provider is created once, so every tool that gets it sees the same data. Call assign_ticket, then my_queue, and the ticket is there.

Open
import { Tool, ToolContext, z } from "@frontmcp/sdk";
import { Assignments } from "./assignments";

@Tool({
  name: "assign_ticket",
  description: "Assign a ticket to a support agent",
  inputSchema: { id: z.string(), agent: z.string() },
})
export class AssignTicket extends ToolContext {
  async execute({ id, agent }: { id: string; agent: string }) {
    this.get(Assignments).assign(id, agent);
    return { id, agent };
  }
}

@Tool({
  name: "my_queue",
  description: "List the tickets assigned to an agent",
  inputSchema: { agent: z.string() },
})
export class MyQueue extends ToolContext {
  async execute({ agent }: { agent: string }) {
    return { tickets: this.get(Assignments).queue(agent) };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Keeping state for one request

A CONTEXT provider is new for each request (each session, for clients that use sessions). Within a request, every this.get() returns the same instance, so it can collect what happens during one call without leaking into the next.

Open
import { Tool, ToolContext, z } from "@frontmcp/sdk";
import { AuditTrail } from "./audit-trail";

@Tool({ name: "close_ticket", description: "Close a ticket", inputSchema: { id: z.string() } })
export class CloseTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    this.get(AuditTrail).record(`checked that ${id} exists`);
    this.get(AuditTrail).record(`closed ${id}`);
    return { id, status: "closed", trail: this.get(AuditTrail).steps };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

With ProviderScope.GLOBAL, the second call would return all four steps.

Providing a value or configuration

To register something that isn't a @Provider class, like a settings object or a client from a library, use a factory. The object needs a name, and inject returns the tokens whose instances useFactory receives, in order.

Factories

Example 1 of 3

A value

An abstract class works as a typed token: this.get(DeskConfig) knows it has a slaHours.

Open
import { App, Tool, ToolContext, z } from "@frontmcp/sdk";

export abstract class DeskConfig {
  abstract slaHours: { high: number; normal: number };
}

@Tool({
  name: "reply_due",
  description: "How many hours we have to reply to a ticket of this priority",
  inputSchema: { priority: z.enum(["high", "normal"]) },
})
export class ReplyDue extends ToolContext {
  async execute({ priority }: { priority: "high" | "normal" }) {
    return { hours: this.get(DeskConfig).slaHours[priority] };
  }
}

@App({
  id: "help-desk",
  name: "Help Desk",
  tools: [ReplyDue],
  providers: [
    { provide: DeskConfig, name: "DeskConfig", inject: () => [], useFactory: () => ({ slaHours: { high: 4, normal: 24 } }) },
  ],
})
export class HelpDeskApp {}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Reading the request in a provider

Inject FRONTMCP_CONTEXT into a CONTEXT factory to build something for the current request: its id, its trace, who is calling. Here each ticket is stamped with the request that created it.

Open
import { App, FRONTMCP_CONTEXT, ProviderScope, Tool, ToolContext, z } from "@frontmcp/sdk";

export abstract class RequestInfo {
  abstract requestId: string;
  abstract traceId: string;
}

@Tool({ name: "create_ticket", description: "Open a new support ticket", inputSchema: { title: z.string() } })
export class CreateTicket extends ToolContext {
  async execute({ title }: { title: string }) {
    const { requestId } = this.get(RequestInfo);
    return { id: "T-4", title, createdBy: requestId };
  }
}

@App({
  id: "help-desk",
  name: "Help Desk",
  tools: [CreateTicket],
  providers: [
    {
      provide: RequestInfo,
      name: "RequestInfo",
      scope: ProviderScope.CONTEXT,
      inject: () => [FRONTMCP_CONTEXT] as const,
      useFactory: (ctx) => ({ requestId: ctx.requestId, traceId: ctx.traceContext.traceId }),
    },
  ],
})
export class HelpDeskApp {}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

FRONTMCP_CONTEXT only makes sense per request, so inject it into CONTEXT providers only.

A CONTEXT provider can also hold something built for the caller, like a client for your own API that acts as them. Under 2026-07-28, every request gets its own instance, whoever is calling, with a token or without, and even when two requests send the same mcp-session-id header. So one caller's client is never handed to another. This server uses transparent auth, and the test calls it as two users, one after the other:

Open
import { App, FRONTMCP_CONTEXT, ProviderScope, Tool, ToolContext } from "@frontmcp/sdk";

/** A client for the ticket API that acts as the caller. */
export abstract class TicketApi {
  abstract actingAs: string | undefined;
}

@Tool({ name: "my_tickets", description: "List the tickets assigned to the caller", inputSchema: {} })
export class MyTickets extends ToolContext {
  async execute() {
    return { actingAs: this.get(TicketApi).actingAs };
  }
}

@App({
  id: "help-desk",
  name: "Help Desk",
  tools: [MyTickets],
  providers: [
    {
      provide: TicketApi,
      name: "TicketApi",
      scope: ProviderScope.CONTEXT,
      inject: () => [FRONTMCP_CONTEXT] as const,
      // In transparent mode, clientId is the token's sub.
      useFactory: (ctx) => ({ actingAs: ctx.authInfo.clientId }),
    },
  ],
})
export class HelpDeskApp {}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Clients on protocol versions before 2026-07-28 keep a CONTEXT provider for their whole session, and that includes this one: FrontMCP 1.8 builds it on the session's first request, so later requests in the session see that request's requestId. When a value has to be right for every request, read it from this.context in the tool instead.

Using a provider only if it's there

this.tryGet() returns undefined for a provider that isn't registered, so a tool can work with or without it. Here notifications are optional. Add Notifier to the app's providers, and notified becomes true.

Open
import { Tool, ToolContext, z } from "@frontmcp/sdk";
import { Notifier } from "./notifier";

@Tool({ name: "close_ticket", description: "Close a ticket, and tell the customer if we can", inputSchema: { id: z.string() } })
export class CloseTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    const notifier = this.tryGet(Notifier);
    notifier?.send(`Ticket ${id} was closed.`);
    return { id, status: "closed", notified: notifier !== undefined };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.


Troubleshooting

Provider "TicketStore" is not available: not found in local or parent registries

A tool asked for a provider that isn't registered anywhere it can see, so the call failed:

Open
import { App, Provider, Tool, ToolContext, z } from "@frontmcp/sdk";

@Provider({ name: "TicketStore" })
export class TicketStore {
  find(id: string) {
    return { id, title: "Cannot log in" };
  }
}

@Tool({ name: "get_ticket", description: "Get one ticket by id", inputSchema: { id: z.string() } })
export class GetTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    return this.get(TicketStore).find(id);
  }
}

// 🚩 TicketStore is decorated, but not in `providers`
@App({ id: "help-desk", name: "Help Desk", tools: [GetTicket] })
export class HelpDeskApp {}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Add it to the providers of the tool's app, or of @FrontMcp to share it with every app. A provider registered in another app isn't visible: apps don't see each other's providers.

providers items must be annotated with @Provider() | @FrontMcpProvider().

The server fails to start with @App invalid metadata for "providers" and the index of the bad entry. FrontMCP 1.8 accepts three forms, and rejects everything else:

providers: [
  TicketStore, // ✅ a @Provider class
  { provide: DeskConfig, name: "DeskConfig", inject: () => [], useFactory: () => config }, // ✅ a named factory
  { provide: TicketStore, useValue: new TicketStore() }, // ✅ an instance of a @Provider class

  PlainClass, // 🚩 no @Provider decorator
  { provide: DeskConfig, useValue: config }, // 🚩 a plain value: use a factory
  { provide: DeskConfig, useClass: EnvConfig }, // 🚩 useClass: use a factory that returns new EnvConfig()
  { provide: DeskConfig, useFactory: () => config }, // 🚩 a factory without `name`
];

Invalid dependency: DEFAULT-scoped provider Audit cannot depend on scoped provider RequestLog.

A GLOBAL provider (here Audit) injects a CONTEXT provider (RequestLog). The global one would outlive the request it was built for, so the server refuses to start. Make the dependent provider CONTEXT too, or pass the request-scoped value in as a method argument.

Provider TicketRepo depends on TicketStore, which is not registered (local or parent).

A constructor parameter or an inject token names a provider that isn't registered. Add it to providers. If the message says depends on Function or depends on Object, the parameter's type was imported with import type, which leaves no class at run time for emitDecoratorMetadata to record. Use a normal import.

A constructor parameter is undefined

The provider was built without its dependencies. Check that:

  1. tsconfig.json sets experimentalDecorators and emitDecoratorMetadata to true, and your build uses tsc (or a compiler that emits decorator metadata).
  2. import "reflect-metadata" is the first line of your entry file.

In the Playground this always happens, because esbuild doesn't emit decorator metadata. Use a factory with inject there.

Failed to construct provider "DeskConfig": Cannot read properties of undefined

The factory was called without the providers it needs. Most often, inject is an array. It must be a function that returns one; FrontMCP ignores an array and calls the factory with no arguments:

// 🚩 An array: `settings` is undefined
{ provide: DeskConfig, name: "DeskConfig", inject: [Settings], useFactory: (settings) => ({ slaHours: settings.slaHours }) }

// ✅ A function that returns the array
{ provide: DeskConfig, name: "DeskConfig", inject: () => [Settings] as const, useFactory: (settings) => ({ slaHours: settings.slaHours }) }

TypeScript reports the array form as an error, unless it's been cast away. A GLOBAL factory runs when the server starts, so the server fails to start. A CONTEXT factory runs at the start of every request, so every call to that app's tools fails, even calls to tools that don't use it.

State I stored in a provider is gone on the next call

Check the provider's scope. A CONTEXT provider is rebuilt for every request under MCP 2026-07-28, and for every session on older clients. Use GLOBAL for state that should last. If it already is GLOBAL, your server probably runs as several processes or serverless instances, each with its own copy; keep the state in a database or Redis.