# Sharing State with Providers

> Why tools shouldn't keep shared state in module variables, and how @Provider shares services instead. Scopes per request, factories built from configuration, and swapping a provider in tests.

Source: https://frontmcp.dev/learn/sharing-state-with-providers

Tools rarely work alone. They read the same ticket store, call the same API client and follow the same settings. The quickest way to share something between tools is a variable at the top of a file, and that works right up until two calls run at the same time, or a test needs a different one. A **provider** is FrontMCP's place for shared things. You declare it with `@Provider`, register it where it belongs, and any tool that needs it asks for it with `this.get()`. FrontMCP decides when to create it and how long it lives.

**You will learn**
- Why a module-level variable is the wrong place for shared state
- How `@Provider` and `this.get()` share one service between tools
- What `GLOBAL` and `CONTEXT` scopes mean for each request
- How to build a provider from configuration with a factory
- How to swap a provider for a fake in tests, and why that decides where you register it

## State in a module variable

`close_ticket` returns a receipt of what it did, so the model can tell the user. It collects the steps in an array that lives at the top of `receipt.ts`, and empties it at the start of each call:

```ts close-ticket.tool.ts active
import { Tool, ToolContext, z } from "@frontmcp/sdk";
import { receipt, sendEmail } from "./receipt";

@Tool({
  name: "close_ticket",
  description: "Close a support ticket, and optionally email the customer. Returns a receipt of the steps taken.",
  inputSchema: {
    id: z.string().describe("Ticket id, like T-1"),
    notify: z.boolean().optional().describe("Email the customer that the ticket is closed"),
  },
})
export class CloseTicket extends ToolContext {
  async execute({ id, notify }: { id: string; notify?: boolean }) {
    receipt.length = 0; // start this call's receipt
    receipt.push(`closed ${id}`);
    if (notify) {
      await sendEmail(id);
      receipt.push(`emailed the customer about ${id}`);
    }
    return { id, receipt: [...receipt] };
  }
}
```

```ts receipt.ts
// 🚩 One array for the whole server, shared by every call
export const receipt: string[] = [];

export async function sendEmail(ticketId: string) {
  await new Promise((resolve) => setTimeout(resolve, 200)); // stands in for a mail server
}
```

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

test("one call on its own gets the right receipt", async ({ mcp }) => {
  const result = await mcp.tools.call("close_ticket", { id: "T-1", notify: true });
  expect(result.json().receipt).toEqual(["closed T-1", "emailed the customer about T-1"]);
});

test("two calls at once share one receipt", async ({ mcp }) => {
  const [first, second] = await Promise.all([
    mcp.tools.call("close_ticket", { id: "T-1", notify: true }),
    mcp.tools.call("close_ticket", { id: "T-2" }),
  ]);
  expect(second.json().receipt).toEqual(["closed T-2"]);
  // 🚩 T-1's receipt lost its first step and gained one from T-2
  expect(first.json().receipt).toEqual(["closed T-2", "emailed the customer about T-1"]);
});
```

One call at a time, the receipt is right. Open the **Tests** tab to see two calls at once. While the first call waits for the mail server, the second one empties the array and writes its own step, and the first call's receipt comes back with a step from a ticket it never touched. A server handles many requests at the same time, from many clients, so this isn't rare in production.

A module variable belongs to the file, not to the call, and that causes more than this one bug:

1. **Every request shares it.** Anything that should belong to one call, like this receipt, leaks between calls.
2. **Nothing can replace it.** A test can't give the tool a different store or a fake mail server, because the tool imports the real one directly.
3. **It's built when the file is imported**, before your configuration is read or a database is connected.
4. **It's invisible.** Nothing in the app's declaration says `close_ticket` depends on it.

Providers fix all four, and the rest of this lesson shows how. For a value one caller should get back on their next call, like a preference, [Remembering Across Calls](https://frontmcp.dev/learn/remembering-across-calls) uses the Remember plugin.

## Sharing a service with `@Provider`

A provider is a class that FrontMCP creates for you. Using one takes three steps: decorate the class with `@Provider`, list it in the app's `providers`, and get it inside `execute()` with `this.get()`:

```ts tools.ts active
import { Tool, ToolContext, z } from "@frontmcp/sdk";
import { TicketStore } from "./ticket-store";

@Tool({
  name: "close_ticket",
  description: "Close a support ticket by its id",
  inputSchema: { id: z.string().describe("Ticket id, like T-1") },
})
export class CloseTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    return this.get(TicketStore).close(id);
  }
}

@Tool({
  name: "list_open_tickets",
  description: "List the support tickets that are still open",
  inputSchema: {},
  annotations: { readOnlyHint: true },
})
export class ListOpenTickets extends ToolContext {
  async execute() {
    return { tickets: this.get(TicketStore).open().map((t) => t.id) };
  }
}
```

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

type Ticket = { id: string; title: string; status: "open" | "closed" };

@Provider({ name: "TicketStore", scope: ProviderScope.GLOBAL })
export class TicketStore {
  private tickets: Ticket[] = [
    { id: "T-1", title: "Cannot log in", status: "open" },
    { id: "T-2", title: "Invoice total is wrong", status: "closed" },
    { id: "T-3", title: "Login link expired", status: "open" },
  ];

  open() {
    return this.tickets.filter((t) => t.status === "open");
  }

  close(id: string) {
    const ticket = this.tickets.find((t) => t.id === id);
    if (ticket) ticket.status = "closed";
    return { id, status: ticket?.status ?? "not found" };
  }
}
```

```ts help-desk.app.ts
import { App } from "@frontmcp/sdk";
import { CloseTicket, ListOpenTickets } from "./tools";
import { TicketStore } from "./ticket-store";

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

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

test("a ticket closed by one tool is gone from the other's list", async ({ mcp }) => {
  expect((await mcp.tools.call("list_open_tickets", {})).json()).toEqual({ tickets: ["T-1", "T-3"] });
  await mcp.tools.call("close_ticket", { id: "T-1" });
  expect((await mcp.tools.call("list_open_tickets", {})).json()).toEqual({ tickets: ["T-3"] });
});
```

- **`@Provider({ name })`** marks the class. `name` is required and shows up in logs and error messages.
- **`providers: [TicketStore]`** registers it. Decorating the class isn't enough: if neither the tool's app nor the server lists it, the call fails with `Provider "TicketStore" is not available`.
- **`this.get(TicketStore)`** returns the instance. The class itself is the key you ask with, so `this.get()` knows the type it returns.

Both tools get the same `TicketStore`, so a ticket closed by one is gone from the other's list. The tools also say what they depend on now: the app's `providers` lists it, and a test can put something else in its place.

> **Note**
`ProviderScope.GLOBAL` is the default, and means one instance for each place the provider is registered. List the same class in two apps and each app gets its own. To share one instance between apps, register it once on the server, as [Grouping Capabilities into Apps](https://frontmcp.dev/learn/grouping-capabilities-into-apps#sharing-a-provider-between-apps) shows.

## State for one request: `CONTEXT`

The ticket store should outlive every call. The receipt should live for exactly one. That's what a provider's **scope** decides:

| Scope | One instance per | Created |
| --- | --- | --- |
| `ProviderScope.GLOBAL` (the default) | place it's registered: an app, or the server | When the server starts |
| `ProviderScope.CONTEXT` | request, under MCP 2026-07-28 | At the start of each tool call |

Here is the receipt as a `CONTEXT` provider. Every `this.get(Receipt)` during one call returns the same receipt, and the next call gets a new one:

```ts close-ticket.tool.ts active
import { Tool, ToolContext, z } from "@frontmcp/sdk";
import { Receipt, sendEmail } from "./receipt";

@Tool({
  name: "close_ticket",
  description: "Close a support ticket, and optionally email the customer. Returns a receipt of the steps taken.",
  inputSchema: {
    id: z.string().describe("Ticket id, like T-1"),
    notify: z.boolean().optional().describe("Email the customer that the ticket is closed"),
  },
})
export class CloseTicket extends ToolContext {
  async execute({ id, notify }: { id: string; notify?: boolean }) {
    const receipt = this.get(Receipt);
    receipt.add(`closed ${id}`);
    if (notify) {
      await sendEmail(id);
      receipt.add(`emailed the customer about ${id}`);
    }
    return { id, receipt: receipt.steps };
  }
}
```

```ts receipt.ts
import { Provider, ProviderScope } from "@frontmcp/sdk";

// ✅ A new receipt for every request
@Provider({ name: "Receipt", scope: ProviderScope.CONTEXT })
export class Receipt {
  steps: string[] = [];

  add(step: string) {
    this.steps.push(step);
  }
}

export async function sendEmail(ticketId: string) {
  await new Promise((resolve) => setTimeout(resolve, 200)); // stands in for a mail server
}
```

```ts help-desk.app.ts
import { App } from "@frontmcp/sdk";
import { CloseTicket } from "./close-ticket.tool";
import { Receipt } from "./receipt";

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

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

// In Node the two calls run at once. Your browser has no AsyncLocalStorage, so there two calls
// that overlap can see each other's providers: the Playground sends them one after the other.
const inNode = typeof (globalThis as { WorkerGlobalScope?: unknown }).WorkerGlobalScope === "undefined";

test("two calls keep separate receipts", async ({ mcp }) => {
  const calls = [
    () => mcp.tools.call("close_ticket", { id: "T-1", notify: true }),
    () => mcp.tools.call("close_ticket", { id: "T-2" }),
  ];
  const [first, second] = inNode ? await Promise.all(calls.map((call) => call())) : [await calls[0](), await calls[1]()];
  expect(first.json().receipt).toEqual(["closed T-1", "emailed the customer about T-1"]);
  expect(second.json().receipt).toEqual(["closed T-2"]);
});
```

The code no longer empties anything, because there's nothing left over to empty. The **Tests** tab runs the same two calls as before, and each receipt has only its own steps. (In Node they run at once. Your browser can't keep two overlapping calls apart, so the Playground runs them one after the other there.)

"Per request" is how MCP 2026-07-28 works: every request stands alone, and there are no sessions to keep a provider alive between them. Clients on older protocol versions open a session, and for them a `CONTEXT` provider lasts for the whole session instead. If something must survive between calls, don't count on either: make it `GLOBAL`, or better, keep it in a database.

> **Pitfall: A GLOBAL provider can't use a CONTEXT one**
A `GLOBAL` provider lives longer than any request, so FrontMCP won't build one that depends on a `CONTEXT` provider: the server refuses to start with `Invalid dependency: DEFAULT-scoped provider … cannot depend on scoped provider …`. Pass the per-request value into the global provider's methods as an argument instead.

**Deep dive: When exactly is a provider built?**
A `GLOBAL` provider is built once, before the server answers its first request. A `CONTEXT` provider is built at the start of **every tool call in its app**, whether or not the tool asks for it. This example counts constructions. Call `ping` a few times:

```ts help-desk.app.ts active
import { App, Provider, ProviderScope, Tool, ToolContext } from "@frontmcp/sdk";

const built: string[] = [];

@Provider({ name: "Shared", scope: ProviderScope.GLOBAL })
export class Shared {
  constructor() {
    built.push("Shared");
  }
}

@Provider({ name: "PerRequest", scope: ProviderScope.CONTEXT })
export class PerRequest {
  constructor() {
    built.push("PerRequest");
  }
}

@Tool({ name: "ping", description: "List the providers built so far", inputSchema: {} })
export class Ping extends ToolContext {
  async execute() {
    return { built: [...built] }; // never calls this.get()
  }
}

@App({ id: "help-desk", name: "Help Desk", tools: [Ping], providers: [Shared, PerRequest] })
export class HelpDeskApp {}
```

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

test("CONTEXT providers are built for every call, even unused", async ({ mcp }) => {
  await mcp.tools.call("ping", {});
  await mcp.tools.call("ping", {});
  const result = await mcp.tools.call("ping", {});
  expect(result.json().built).toEqual(["Shared", "PerRequest", "PerRequest", "PerRequest"]);
});
```

So keep `CONTEXT` providers cheap to construct: hold per-request data in them, and leave connections and clients to `GLOBAL` ones.

## Building a provider from configuration

Some things you want to share aren't classes you wrote: a settings object, a client from a library, something that has to be loaded first. For those, register a **factory**: an object with the key to provide, a `name`, the providers it needs, and a function that builds the value.

The SLA hours below come from `Settings`, which stands in for environment variables, so they arrive as strings. The factory turns them into the numbers the tool needs:

```ts help-desk.app.ts active
import { App } from "@frontmcp/sdk";
import { DeskConfig, Settings } from "./config";
import { ReplyDue } from "./reply-due.tool";

@App({
  id: "help-desk",
  name: "Help Desk",
  tools: [ReplyDue],
  providers: [
    Settings,
    {
      provide: DeskConfig,
      name: "DeskConfig",
      inject: () => [Settings] as const,
      useFactory: (settings) => ({
        slaHours: {
          high: Number(settings.get("SLA_HIGH_HOURS")),
          normal: Number(settings.get("SLA_NORMAL_HOURS")),
        },
      }),
    },
  ],
})
export class HelpDeskApp {}
```

```ts config.ts
import { Provider } from "@frontmcp/sdk";

// The key tools ask for. An abstract class gives this.get() a type.
export abstract class DeskConfig {
  abstract slaHours: { high: number; normal: number };
}

@Provider({ name: "Settings" })
export class Settings {
  private values: Record<string, string> = { SLA_HIGH_HOURS: "4", SLA_NORMAL_HOURS: "24" };

  get(key: string) {
    return this.values[key];
  }
}
```

```ts reply-due.tool.ts
import { Tool, ToolContext, z } from "@frontmcp/sdk";
import { DeskConfig } from "./config";

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

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

test("hours come from the settings, as numbers", async ({ mcp }) => {
  expect((await mcp.tools.call("reply_due", { priority: "high" })).json()).toEqual({ priority: "high", hours: 4 });
  expect((await mcp.tools.call("reply_due", { priority: "normal" })).json()).toEqual({ priority: "normal", hours: 24 });
});
```

A factory has four parts:

- **`provide`** is the key tools pass to `this.get()`. An abstract class works well: it exists at run time, so it can be a key, and `this.get(DeskConfig)` knows the value's type. A `Symbol` works too, with the type passed yourself: `this.get<number>(SLA_HOURS)`.
- **`name`** is required for factories.
- **`inject`** is a function that returns the providers to pass in, in order. With `as const`, TypeScript infers `settings` in `useFactory`.
- **`useFactory`** builds the value. It can be `async`, for something that has to be loaded first ([example](https://frontmcp.dev/reference/sdk/provider#providing-a-value-or-configuration)).

A factory can take a `scope` too. It's `GLOBAL` unless you say otherwise, so this one runs once, when the server starts.

> **Pitfall: Not every provider shape works in 1.8**
FrontMCP 1.8 accepts three shapes in `providers`: a `@Provider` class, a factory with a `name`, and `{ provide, name, useValue }` where the value is an **instance of a `@Provider` class**, like the clocks below. It refuses to start for a plain object in `useValue` and for `useClass`, even though the TypeScript types allow both. It also expects `inject` to be a function: an array is ignored, and the factory is called with no arguments. The [`@Provider` troubleshooting](https://frontmcp.dev/reference/sdk/provider#troubleshooting) has the exact error messages.

## Swapping a provider in tests

`snooze_ticket` hides a ticket for some hours and says when it comes back. It reads the time from a `Clock` provider rather than calling `new Date()` itself, and that's what makes it testable. The real server gives it the system clock:

```ts src/main.ts
import { FrontMcp } from "@frontmcp/sdk";
import { TicketsApp } from "./tickets.app";
import { Clock, SystemClock } from "./clock";

@FrontMcp({
  info: { name: "Help Desk", version: "1.0.0" },
  apps: [TicketsApp],
  providers: [{ provide: Clock, name: "Clock", useValue: new SystemClock() }],
})
export default class HelpDeskServer {}
```

A test can't check the answer against a clock that keeps moving. So the tests start a second entry file, which lists the same app with a clock stopped at 09:00. This Playground runs that test server:

```ts e2e/test-server.ts active
import { FrontMcp } from "@frontmcp/sdk";
import { TicketsApp } from "../src/tickets.app";
import { Clock } from "../src/clock";
import { FixedClock } from "./fixed-clock";

@FrontMcp({
  info: { name: "Help Desk (tests)", version: "1.0.0" },
  apps: [TicketsApp],
  providers: [{ provide: Clock, name: "Clock", useValue: new FixedClock("2026-03-02T09:00:00.000Z") }],
})
export default class TestServer {}
```

```ts e2e/fixed-clock.ts
import { Provider } from "@frontmcp/sdk";
import { Clock } from "../src/clock";

@Provider({ name: "FixedClock" })
export class FixedClock extends Clock {
  constructor(private readonly time: string) {
    super();
  }

  now() {
    return new Date(this.time);
  }
}
```

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

test("a 2-hour snooze from 09:00 ends at 11:00", async ({ mcp }) => {
  const result = await mcp.tools.call("snooze_ticket", { id: "T-1", hours: 2 });
  expect(result.json()).toEqual({ id: "T-1", until: "2026-03-02T11:00:00.000Z" });
});
```

```ts src/clock.ts
import { Provider } from "@frontmcp/sdk";

export abstract class Clock {
  abstract now(): Date;
}

@Provider({ name: "SystemClock" })
export class SystemClock extends Clock {
  now() {
    return new Date();
  }
}
```

```ts src/snooze.tool.ts
import { Tool, ToolContext, z } from "@frontmcp/sdk";
import { Clock } from "./clock";

@Tool({
  name: "snooze_ticket",
  description: "Hide a ticket from the queue for some hours. Returns when it comes back.",
  inputSchema: {
    id: z.string().describe("Ticket id, like T-1"),
    hours: z.number().int().min(1).max(72),
  },
})
export class SnoozeTicket extends ToolContext {
  async execute({ id, hours }: { id: string; hours: number }) {
    const until = new Date(this.get(Clock).now().getTime() + hours * 60 * 60 * 1000);
    return { id, until: until.toISOString() };
  }
}
```

```ts src/tickets.app.ts
import { App } from "@frontmcp/sdk";
import { SnoozeTicket } from "./snooze.tool";

// No Clock here: each entry file (main.ts, test-server.ts) provides its own.
@App({ id: "tickets", name: "Tickets", tools: [SnoozeTicket] })
export class TicketsApp {}
```

The tool and the app are the same code in both servers. Only the entry file differs, and it chooses the clock. In a project, point `@frontmcp/testing` at the test entry file:

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

test.use({ server: "./e2e/test-server.ts" });

test("a 2-hour snooze from 09:00 ends at 11:00", async ({ mcp }) => {
  const result = await mcp.tools.call("snooze_ticket", { id: "T-1", hours: 2 });
  expect(result.json()).toEqual({ id: "T-1", until: "2026-03-02T11:00:00.000Z" });
});
```

The same approach works for anything a test shouldn't touch for real: a mail sender that records messages instead of sending them, or an API client that returns canned answers.

### An app's own provider comes first

Notice that `TicketsApp` doesn't register `Clock`. When a tool calls `this.get()`, FrontMCP looks in the tool's own app first and only then in the server. So an app that registers a provider hides the server's provider with the same key. That's handy for overrides: here the whole server replies within 24 hours, and the VIP app within 2.

```ts main.ts active
import { FrontMcp } from "@frontmcp/sdk";
import { TicketsApp, VipApp } from "./apps";
import { deskConfig } from "./config";

@FrontMcp({
  info: { name: "Help Desk", version: "1.0.0" },
  apps: [TicketsApp, VipApp],
  providers: [deskConfig(24)],
})
export default class HelpDeskServer {}
```

```ts apps.ts
import { App, Tool, ToolContext } from "@frontmcp/sdk";
import { DeskConfig, deskConfig } from "./config";

@Tool({ name: "reply_due", description: "Hours support has to reply to a ticket", inputSchema: {} })
export class ReplyDue extends ToolContext {
  async execute() {
    return { hours: this.get(DeskConfig).replyHours };
  }
}

@Tool({ name: "vip_reply_due", description: "Hours support has to reply to a VIP ticket", inputSchema: {} })
export class VipReplyDue extends ToolContext {
  async execute() {
    return { hours: this.get(DeskConfig).replyHours };
  }
}

@App({ id: "tickets", name: "Tickets", tools: [ReplyDue] })
export class TicketsApp {}

@App({ id: "vip", name: "VIP", tools: [VipReplyDue], providers: [deskConfig(2)] })
export class VipApp {}
```

```ts config.ts
export abstract class DeskConfig {
  abstract replyHours: number;
}

export function deskConfig(replyHours: number) {
  return { provide: DeskConfig, name: "DeskConfig", inject: () => [] as const, useFactory: () => ({ replyHours }) };
}
```

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

test("the VIP app's own DeskConfig wins over the server's", async ({ mcp }) => {
  expect((await mcp.tools.call("vip_reply_due", {})).json()).toEqual({ hours: 2 });
});

test("other apps still get the server's", async ({ mcp }) => {
  expect((await mcp.tools.call("reply_due", {})).json()).toEqual({ hours: 24 });
});
```

The same rule works against you in tests. If `TicketsApp` registered `SystemClock` itself, the test server's `FixedClock` would never be seen, and the snooze test would fail. Register a provider that differs between environments on the **server**, in each entry file, and keep it out of the app. The last challenge below starts from exactly that mistake.

## Recap

- A module-level variable is shared by every request and every test, can't be replaced, and doesn't show up as a dependency. Use a provider.
- Decorate the class with `@Provider({ name })`, list it in `providers`, and get it with `this.get()`. The class is the key.
- `GLOBAL` (the default) is one instance per place it's registered. `CONTEXT` is a new instance for each request under MCP 2026-07-28, built at the start of every tool call.
- A factory (`provide`, `name`, `inject: () => [...]`, `useFactory`) builds a provider from other providers or configuration, and can be `async`.
- `this.get()` looks in the tool's app first, then in the server. Register providers that differ between environments on the server, and swap them in a test entry file.

## Try some challenges

Each challenge runs hidden checks against your code. Edit the code, then press **Check**.

### Challenge: Give each call its own receipt
The receipt is a provider now, but the second call's receipt still starts with the first call's steps. Make every request get a fresh receipt, without emptying it by hand.

```ts receipt.ts
import { Provider, ProviderScope } from "@frontmcp/sdk";

@Provider({ name: "Receipt", scope: ProviderScope.GLOBAL })
export class Receipt {
  steps: string[] = [];

  add(step: string) {
    this.steps.push(step);
  }
}
```

```ts receipt.ts solution
import { Provider, ProviderScope } from "@frontmcp/sdk";

@Provider({ name: "Receipt", scope: ProviderScope.CONTEXT })
export class Receipt {
  steps: string[] = [];

  add(step: string) {
    this.steps.push(step);
  }
}
```

```ts close-ticket.tool.ts
import { Tool, ToolContext, z } from "@frontmcp/sdk";
import { Receipt } from "./receipt";

@Tool({
  name: "close_ticket",
  description: "Close a support ticket. Returns a receipt of the steps taken.",
  inputSchema: { id: z.string().describe("Ticket id, like T-1") },
})
export class CloseTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    const receipt = this.get(Receipt);
    receipt.add(`checked ${id} exists`);
    receipt.add(`closed ${id}`);
    return { id, receipt: [...receipt.steps] };
  }
}
```

```ts help-desk.app.ts
import { App } from "@frontmcp/sdk";
import { CloseTicket } from "./close-ticket.tool";
import { Receipt } from "./receipt";

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

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

test("the first call's receipt has its two steps", async ({ mcp }) => {
  const result = await mcp.tools.call("close_ticket", { id: "T-1" });
  expect(result.json().receipt).toEqual(["checked T-1 exists", "closed T-1"]);
});

test("the second call's receipt has only its own steps", async ({ mcp }) => {
  await mcp.tools.call("close_ticket", { id: "T-1" });
  const result = await mcp.tools.call("close_ticket", { id: "T-2" });
  expect(result.json().receipt).toEqual(["checked T-2 exists", "closed T-2"]);
});
```

**Hint:**
The class is fine. What decides how long one instance lives?

**Solution:**
`ProviderScope.CONTEXT` makes FrontMCP build a new `Receipt` at the start of every request, so each call starts with an empty `steps` array. With `GLOBAL`, there was one receipt for the whole app, and it kept growing.

### Challenge: Build the SLA config with a factory
`reply_due` asks for `DeskConfig`, but nothing provides it, so every call fails. Register a factory that builds `DeskConfig` from `Settings`. The settings are strings, and `hours` should be a number.

```ts help-desk.app.ts
import { App } from "@frontmcp/sdk";
import { Settings } from "./config";
import { ReplyDue } from "./reply-due.tool";

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

```ts help-desk.app.ts solution
import { App } from "@frontmcp/sdk";
import { DeskConfig, Settings } from "./config";
import { ReplyDue } from "./reply-due.tool";

@App({
  id: "help-desk",
  name: "Help Desk",
  tools: [ReplyDue],
  providers: [
    Settings,
    {
      provide: DeskConfig,
      name: "DeskConfig",
      inject: () => [Settings] as const,
      useFactory: (settings) => ({
        slaHours: {
          high: Number(settings.get("SLA_HIGH_HOURS")),
          normal: Number(settings.get("SLA_NORMAL_HOURS")),
        },
      }),
    },
  ],
})
export class HelpDeskApp {}
```

```ts config.ts
import { Provider } from "@frontmcp/sdk";

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

@Provider({ name: "Settings" })
export class Settings {
  private values: Record<string, string> = { SLA_HIGH_HOURS: "2", SLA_NORMAL_HOURS: "12" };

  get(key: string) {
    return this.values[key];
  }
}
```

```ts reply-due.tool.ts
import { Tool, ToolContext, z } from "@frontmcp/sdk";
import { DeskConfig } from "./config";

@Tool({
  name: "reply_due",
  description: "How many hours support has 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 { priority, hours: this.get(DeskConfig).slaHours[priority] };
  }
}
```

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

test("`reply_due` works", async ({ mcp }) => {
  expect(await mcp.tools.call("reply_due", { priority: "high" })).toBeSuccessful();
});

test("high priority comes from SLA_HIGH_HOURS, as a number", async ({ mcp }) => {
  expect((await mcp.tools.call("reply_due", { priority: "high" })).json()).toEqual({ priority: "high", hours: 2 });
});

test("normal priority comes from SLA_NORMAL_HOURS, as a number", async ({ mcp }) => {
  expect((await mcp.tools.call("reply_due", { priority: "normal" })).json()).toEqual({ priority: "normal", hours: 12 });
});
```

**Hint:**
A factory needs `provide`, `name`, `inject` and `useFactory`. `inject` is a function that returns an array, not an array.

**Solution:**
The factory is registered under `DeskConfig`, the key the tool asks for. `inject: () => [Settings] as const` hands it the `Settings` instance, and `useFactory` converts the strings with `Number()` so the result matches the `DeskConfig` type.

### Challenge: Let the test clock through
The test server provides a clock stopped at 09:00, but the snooze test still gets the real time. Find out why, and fix the app so the test server's clock is the one the tool sees.

```ts src/tickets.app.ts
import { App } from "@frontmcp/sdk";
import { SnoozeTicket } from "./snooze.tool";
import { Clock, SystemClock } from "./clock";

@App({
  id: "tickets",
  name: "Tickets",
  tools: [SnoozeTicket],
  providers: [{ provide: Clock, name: "Clock", useValue: new SystemClock() }],
})
export class TicketsApp {}
```

```ts src/tickets.app.ts solution
import { App } from "@frontmcp/sdk";
import { SnoozeTicket } from "./snooze.tool";

@App({ id: "tickets", name: "Tickets", tools: [SnoozeTicket] })
export class TicketsApp {}
```

```ts e2e/test-server.ts
import { FrontMcp } from "@frontmcp/sdk";
import { TicketsApp } from "../src/tickets.app";
import { Clock } from "../src/clock";
import { FixedClock } from "./fixed-clock";

@FrontMcp({
  info: { name: "Help Desk (tests)", version: "1.0.0" },
  apps: [TicketsApp],
  providers: [{ provide: Clock, name: "Clock", useValue: new FixedClock("2026-03-02T09:00:00.000Z") }],
})
export default class TestServer {}
```

```ts e2e/fixed-clock.ts
import { Provider } from "@frontmcp/sdk";
import { Clock } from "../src/clock";

@Provider({ name: "FixedClock" })
export class FixedClock extends Clock {
  constructor(private readonly time: string) {
    super();
  }

  now() {
    return new Date(this.time);
  }
}
```

```ts src/clock.ts
import { Provider } from "@frontmcp/sdk";

export abstract class Clock {
  abstract now(): Date;
}

@Provider({ name: "SystemClock" })
export class SystemClock extends Clock {
  now() {
    return new Date();
  }
}
```

```ts src/snooze.tool.ts
import { Tool, ToolContext, z } from "@frontmcp/sdk";
import { Clock } from "./clock";

@Tool({
  name: "snooze_ticket",
  description: "Hide a ticket from the queue for some hours. Returns when it comes back.",
  inputSchema: {
    id: z.string().describe("Ticket id, like T-1"),
    hours: z.number().int().min(1).max(72),
  },
})
export class SnoozeTicket extends ToolContext {
  async execute({ id, hours }: { id: string; hours: number }) {
    const until = new Date(this.get(Clock).now().getTime() + hours * 60 * 60 * 1000);
    return { id, until: until.toISOString() };
  }
}
```

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

test("a 2-hour snooze from the test clock's 09:00 ends at 11:00", async ({ mcp }) => {
  const result = await mcp.tools.call("snooze_ticket", { id: "T-1", hours: 2 });
  expect(result.json()).toEqual({ id: "T-1", until: "2026-03-02T11:00:00.000Z" });
});

test("a 24-hour snooze ends the next morning", async ({ mcp }) => {
  const result = await mcp.tools.call("snooze_ticket", { id: "T-1", hours: 24 });
  expect(result.json().until).toBe("2026-03-03T09:00:00.000Z");
});
```

**Hint:**
Where does `this.get(Clock)` look first: in the tool's app, or in the server?

**Solution:**
`this.get()` checks the tool's own app before the server, so the app's `SystemClock` hid the test server's `FixedClock`. With `Clock` out of the app, each entry file decides: `src/main.ts` registers `SystemClock`, and `e2e/test-server.ts` registers the fixed one.
