# Scope and registries

> What this.scope gives a FrontMCP tool, resource or prompt, which registries you can list and search, what you can add at runtime, and which parts are FrontMCP's own machinery.

Source: https://frontmcp.dev/reference/sdk/scope

`this.scope` is the server a call runs in. It holds a registry for each kind of thing the server offers (tools, resources, prompts, agents, skills, jobs, providers and more) with every app's entries in it, and the services FrontMCP runs on. A tool can use it to list what the server offers, find the resource behind a URI, read the server's configuration, or add a resource or a skill at runtime. Much of the rest is FrontMCP's own machinery, and this page says which parts those are.

```ts
this.scope.tools.getTools()
this.scope.resources.findResourceForUri(uri)
this.scope.metadata.info
```

---

## Reference

### `this.scope`

Tools, resources, prompts, agents, jobs and channels all have `this.scope`.

```ts list-capabilities.tool.ts
import { Tool, ToolContext } from "@frontmcp/sdk";

@Tool({ name: "list_capabilities", description: "List every tool and resource this server offers", inputSchema: {} })
export class ListCapabilities extends ToolContext {
  async execute() {
    return {
      tools: this.scope.tools.getTools().map((tool) => tool.name),
      resources: this.scope.resources.getResources().map((resource) => resource.uri),
    };
  }
}
```

[See more examples below.](#usage)

A server has one scope, whose `id` is `"root"` (with [`splitByApp`](https://frontmcp.dev/reference/sdk/frontmcp#options), one per app), and its registries hold the entries of every app, plus the ones FrontMCP adds itself, like `execute_job` on a server with jobs. What you change in a registry changes the server for every caller at once.

The type of `this.scope` is `ScopeEntry`, which declares every registry below. (Changed in 1.9.1: before, `jobs`, `workflows`, `channels`, `plugins` and a few services existed at run time but not on the type, so code had to cast `this.scope`: see [Troubleshooting](#property-jobs-does-not-exist-on-type-scopeentry).)

#### Registries

| Member | Type | What you can do with it |
| --- | --- | --- |
| `tools` | `ToolRegistry` | List and look up tools. See [`tools`](#tools). |
| `resources` | `ResourceRegistry` | List resources and templates, find the one for a URI, and add resources at runtime. See [`resources`](#resources). |
| `prompts` | `PromptRegistry` | `getPrompts()`, `findByName(name)`, `findAllByName(name)`, `getExported(name)`, `subscribe()`. |
| `agents` | `AgentRegistry` | `getAgents()`, `findByName(name)`, `findById(id)`, `subscribe()`. |
| `skills` | `SkillRegistry` | `getSkills()`, `findByName(name)`, `listSkills()`, `search(query)`, `loadSkill(id)`, and `registerSkillContent(content)` and `unregisterSkill(id)` to add and remove skills at runtime. See [`skills`](#skills) and [`@Skill`](https://frontmcp.dev/reference/sdk/skill). |
| `apps` | `AppRegistry` | `getApps()`: each app's `id`, `metadata`, and its own `tools`, `resources`, `prompts`, `skills`, `providers` and `plugins` registries. |
| `providers` | `ProviderRegistry` | The server's own providers, from `@FrontMcp({ providers })` and those [plugins](https://frontmcp.dev/reference/sdk/plugin) add to the server: `get(token)`, and `addDynamicProviders([...])` to add one at runtime. **Not** an app's providers: use [`this.get()`](https://frontmcp.dev/reference/sdk/get) for those. |
| `hooks` | `HookRegistry` | `getHooks()`, `getFlowHooks(flow)`: every registered hook, FrontMCP's own included, for inspection. |
| `jobs`, `workflows` | `JobRegistry`, `WorkflowRegistry`, or `undefined` | `getJobs()` / `getWorkflows()`, `findByName(name)`, `findById(id)`, `search(query?)`. `undefined` until an app has jobs or workflows; then both exist. |
| `channels` | `ChannelRegistry`, or `undefined` | `getChannels()`, `findByName(name)`. `undefined` unless `channels.enabled`, as are `channelEventBus` and `channelNotifications`, which send events. See [`@Channel`](https://frontmcp.dev/reference/sdk/channel#emitting-events-from-your-code). |
| `plugins` | `PluginRegistryInterface`, or `undefined` | `getPlugins()`, `getPluginNames()`: the plugins in `@FrontMcp({ plugins })`, and `undefined` without any. An app's plugins are in its own registry, under `apps`. `getPlugins()` returns each plugin's instance, typed `PluginInstance` (`{ get(token) }`) since 1.9.1. |
| `authProviders` | `AuthRegistry` | `getPrimary()`, `getAuthProviders()`: how the server authenticates callers. For the current caller, use [`this.auth`](https://frontmcp.dev/reference/sdk/auth). |

#### Other members

| Member | What it is |
| --- | --- |
| `id` | `"root"`. |
| `metadata` | The server's `@FrontMcp` options after parsing, defaults filled in: `info`, `apps`, `providers`, `transport` and the rest. Read it; don't change it. |
| `entryPath`, `routeBase`, `fullPath` | The MCP endpoint's path: `http.entryPath`, `""` by default. |
| `logger` | The server's logger. |
| `rateLimitManager` | The [guard's](https://frontmcp.dev/reference/sdk/guard) `GuardManager`, or `undefined` when no limits are set. |
| `getAllSupportedScopes()` | The OAuth scopes the server supports, like `["email", "openid", "profile"]`. |
| `onServerStarted(callback)` | Runs `callback` once the HTTP server listens. It never runs for a server that doesn't listen, like one from `create()`. |
| `onDispose(callback)` | Runs `callback` once, when the scope is disposed, as `dispose()` on a `create()` server does. Returns a function that removes it. New in 1.9: see [`registerTool()`](https://frontmcp.dev/reference/sdk/register-tool). |

The rest is FrontMCP's own machinery, and changing it can break the server: `auth`, `notifications`, `elicitationStore`, `tasks`, `taskStore`, `eventStore`, `toolUI`, `authUi`, `transportService`, `healthService`, `skillSession`, `authorities*`, `runFlow()`, `runFlowForOutput()`, `registryFlows()`, `shutdown()` and `dispose()`. To call another tool, use [`this.callTool()`](https://frontmcp.dev/reference/sdk/call-tool), not `runFlow()`.

### `tools`

| Method | Returns |
| --- | --- |
| `getTools(includeHidden?)` | Every tool clients can see. With `true`, also the ones with `visibility: "hidden"` or `"internal"`. |
| `getExported(name)` | The tool clients call by `name`, or `undefined`. |
| `exportResolvedNames()` | `{ name, instance }` for every tool, hidden ones included, under the name clients use for it. |
| `listByOwner(ownerPath)`, `lineageOf(entry)` | Tools by where they come from, like an app or a plugin. |
| `subscribe(opts, callback)` | See [change events](#change-events). |
| `hasAny()` | Whether there's at least one tool. |

A `ToolEntry` has `name`, `fullName` (the owner's id and the name, like `"help-desk:search_tickets"`), `owner` (`{ kind, id }`, where `kind` is `"app"`, `"scope"`, `"plugin"`, `"adapter"` or `"agent"`), `metadata` (the tool's options, parsed), and `getInputJsonSchema()` / `getOutputJsonSchema()` for the JSON Schemas clients see.

**`this.scope.tools` has no way to add a tool.** `registerToolInstance()` takes a `ToolInstance` built from an internal record, and `replaceAll(list, owner)` replaces every tool the registry itself owns. On `this.scope.tools` that's FrontMCP's own tools: `replaceAll()` does add yours, but `execute_job` and the other job and workflow tools disappear with it. To add tools while the server runs, use `registerTool()` on the server `create()` returns (new in 1.9): see [`registerTool()`](https://frontmcp.dev/reference/sdk/register-tool). Otherwise, register every tool at startup, and hide what isn't ready with [`visibility`](https://frontmcp.dev/reference/sdk/tool), [`availableWhen`](https://frontmcp.dev/reference/sdk/tool) or [authorities](https://frontmcp.dev/learn/authorizing-calls).

### `resources`

| Method | Returns |
| --- | --- |
| `getResources(includeHidden?)` | Every fixed-URI resource. |
| `getResourceTemplates()` | Every resource template. |
| `findByUri(uri)` | The fixed-URI resource with exactly that URI, or `undefined`. |
| `findResourceForUri(uri)` | `{ instance, params }`: the resource or template that serves `uri`, as `resources/read` would pick it, with the template's variables. `undefined` if none does. |
| `getExported(name)` | A resource by name. |
| `registerDynamicResource(resource)` | Adds a `@Resource` or `@ResourceTemplate` class, or a `resource()`, at runtime. Clients see it in their next `resources/list`, and can read it. Registering the same class twice does nothing. |
| `subscribe(opts, callback)` | See [change events](#change-events). |

A `ResourceEntry` has `name`, `fullName`, `owner`, `metadata`, `isTemplate`, and `uri` or `uriTemplate`.

`replaceAll()`, `registerResourceInstance()` and `notifyContentChanged()` are FrontMCP's own.

### `skills`

| Method | Returns |
| --- | --- |
| `registerSkillContent(content, options?)` | Adds a skill while the server runs, or replaces the one added earlier with the same `id`. Returns a promise of `{ id, unregister, removed }`: `unregister()` removes the skill again. See below. |
| `unregisterSkill(id)` | A promise of `true` when it removed a skill added with `registerSkillContent()`, and `false` for any other id, including a skill an app declares, which stays. |
| `setExternalProvider(provider)`, `syncToExternal()` | Keep skills in storage outside the server. See [External skill storage](https://frontmcp.dev/reference/sdk/skill#external-skill-storage). |
| `subscribe(opts, callback)` | See [change events](#change-events). |

This is what a plugin with [`dynamicSkills: true`](https://frontmcp.dev/reference/sdk/plugin#options) calls, like [Skilled OpenAPI](https://frontmcp.dev/reference/plugins/skilled-openapi) when its bundle loads. `content` is a skill written out in full, with the instructions as text:

| Field | Type | Description |
| --- | --- | --- |
| `id` | `string` | **Required.** The id `skills/load` takes, and the path of its `SKILL.md`: `skill://<id>/SKILL.md`. Without one, the call throws `registerSkillContent: SkillContent.id is required`. |
| `name` | `string` | **Required.** Unlike `@Skill`, FrontMCP doesn't check it: `"Billing Escalation"` is accepted. Use kebab-case, like an app's skills. |
| `description` | `string` | What the procedure is for. |
| `instructions` | `string` | The procedure. |
| `tools` | `{ name, purpose?, required? }[]` | **Required**, `[]` for none: without it the call throws `Cannot read properties of undefined (reading 'map')`. Marked available or missing like the tools of an app's skill. |
| `parameters`, `examples`, `license`, `compatibility`, `specMetadata`, `allowedTools`, `resources`, `category`, `rating` | | As in [`@Skill`'s options](https://frontmcp.dev/reference/sdk/skill#options). |

`options.source` names where the skill came from, in FrontMCP's debug log. `options.supersedes`, for swapping one set of runtime skills for another, isn't covered here.

What a skill added this way does:

- **It's served like an app's skill.** `skills/list`, `skills/search` and `skills/load` have it, `skill://index.json` lists it, and its `SKILL.md` can be read. It isn't in `resources/list`, which lists only the skills the apps declare.
- **Clients hear about it.** Each add, replace and removal sends `notifications/skills/list_changed` to every client with a session. A `false` from `unregisterSkill()` sends nothing.
- **The server needs to know skills may come.** On a server with no skills of its own, `skills/list`, `skills/search` and `skills/load` answer `Method not found` unless a plugin sets `dynamicSkills: true`, and in-process clients, like `connect()`'s, keep getting it after a skill is added. See [Troubleshooting](#skillslist-answers-method-not-found-after-a-skill-was-added).
- **The same `id` as an app's skill takes that skill's place** in `skills/list`, `skills/search`, `skills/load` and its `SKILL.md`, until it's removed.
- **It lasts until the server restarts**, like a [resource added at runtime](#adding-a-resource-at-runtime).

### Change events

`subscribe({ immediate?, filter? }, callback)`, on `tools`, `resources`, `prompts`, `agents`, `skills`, `jobs`, `workflows` and `channels`, calls `callback` whenever the registry changes, and returns a function that unsubscribes. With `immediate: true`, it's also called once right away. The event:

| Field | Type | Description |
| --- | --- | --- |
| `kind` | `"added" \| "updated" \| "removed" \| "reset"` | What happened. Registering a resource at runtime, and `immediate`, give `"reset"`. |
| `changeScope` | `"global" \| "session"` | Whether the change is for everyone. |
| `version` | `number` | Goes up with every change. |
| `snapshot` | entries | Everything in the registry after the change. |

#### Caveats

- **`this.scope.providers` holds only the server's providers.** `this.scope.providers.get(X)` for a provider registered in an app throws `Provider "X" is not available`. `this.get(X)` finds it.
- **`getTools()` leaves out hidden tools.** Pass `true` to include them. Tools that FrontMCP adds hidden, like `list_jobs`, only show up that way.
- **The registries hold every app's entries**, and FrontMCP's own. Filter by `owner` if you only want one app's.
- **Changes apply to every caller.** A resource added at runtime is there for everyone, until the server restarts. Nothing in a registry is per user or per request.

---

## Usage

### Listing what the server offers

A tool that describes the server, for a model that wants an overview before it starts. The server has two apps, and `this.scope` sees the tools of both. `admin_reset` is hidden, so it's only listed with `getTools(true)`:

```ts describe-server.tool.ts active
import { Tool, ToolContext } from "@frontmcp/sdk";

@Tool({ name: "describe_server", description: "List this server's tools, resources and prompts, with what each is for", inputSchema: {} })
export class DescribeServer extends ToolContext {
  async execute() {
    const { tools, resources, prompts, metadata } = this.scope;
    return {
      server: metadata.info.name,
      tools: tools.getTools().map((tool) => ({ name: tool.name, app: tool.owner.id, description: tool.metadata.description })),
      resources: resources.getResources().map((resource) => resource.uri),
      templates: resources.getResourceTemplates().map((template) => template.uriTemplate),
      prompts: prompts.getPrompts().map((prompt) => prompt.name),
      hiddenTools: tools.getTools(true).filter((tool) => tool.metadata.visibility === "hidden").map((tool) => tool.name),
    };
  }
}
```

```ts main.ts
import { App, FrontMcp } from "@frontmcp/sdk";
import { DescribeServer } from "./describe-server.tool";
import { AdminReset, SearchTickets, SummarizeTicket, Ticket, TicketStats } from "./help-desk";
import { GetInvoice } from "./billing";

@App({ id: "help-desk", name: "Help Desk", tools: [SearchTickets, AdminReset, DescribeServer], resources: [TicketStats, Ticket], prompts: [SummarizeTicket] })
class HelpDeskApp {}

@App({ id: "billing", name: "Billing", tools: [GetInvoice] })
class BillingApp {}

@FrontMcp({ info: { name: "help-desk", version: "1.0.0" }, apps: [HelpDeskApp, BillingApp] })
export default class Server {}
```

```ts help-desk.ts
import { Prompt, PromptContext, Resource, ResourceContext, ResourceTemplate, Tool, ToolContext, z } from "@frontmcp/sdk";

@Tool({ name: "search_tickets", description: "Search tickets by title", inputSchema: { query: z.string() } })
export class SearchTickets extends ToolContext {
  async execute({ query }: { query: string }) {
    return { query, tickets: [] };
  }
}

@Tool({ name: "admin_reset", description: "Reset the ticket store", inputSchema: {}, visibility: "hidden" })
export class AdminReset extends ToolContext {
  async execute() {
    return { reset: true };
  }
}

@Resource({ name: "ticket-stats", uri: "tickets://stats", mimeType: "application/json" })
export class TicketStats extends ResourceContext {
  async execute(uri: string) {
    return { contents: [{ uri, text: JSON.stringify({ open: 12 }) }] };
  }
}

@ResourceTemplate({ name: "ticket", uriTemplate: "tickets://{id}", mimeType: "application/json" })
export class Ticket extends ResourceContext<{ id: string }> {
  async execute(uri: string, { id }: { id: string }) {
    return { contents: [{ uri, text: JSON.stringify({ id }) }] };
  }
}

@Prompt({ name: "summarize_ticket", description: "Summarize a ticket", arguments: [{ name: "id", required: true }] })
export class SummarizeTicket extends PromptContext {
  async execute({ id }: Record<string, string>) {
    return { messages: [{ role: "user" as const, content: { type: "text" as const, text: `Summarize ticket ${id}.` } }] };
  }
}
```

```ts billing.ts
import { Tool, ToolContext, z } from "@frontmcp/sdk";

@Tool({ name: "get_invoice", description: "Get an invoice by number", inputSchema: { number: z.string() } })
export class GetInvoice extends ToolContext {
  async execute({ number }: { number: string }) {
    return { number, total: 120 };
  }
}
```

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

test("the scope sees every app's tools, resources and prompts", async ({ mcp }) => {
  const result = (await mcp.tools.call("describe_server", {})).json();
  expect(result.server).toBe("help-desk");
  expect(result.tools).toEqual(
    expect.arrayContaining([
      { name: "search_tickets", app: "help-desk", description: "Search tickets by title" },
      { name: "get_invoice", app: "billing", description: "Get an invoice by number" },
    ]),
  );
  expect(result.resources).toEqual(["tickets://stats"]);
  expect(result.templates).toEqual(["tickets://{id}"]);
  expect(result.prompts).toEqual(["summarize_ticket"]);
});

test("hidden tools are only listed with getTools(true)", async ({ mcp }) => {
  const result = (await mcp.tools.call("describe_server", {})).json();
  expect(result.tools.map((t: { name: string }) => t.name)).not.toContain("admin_reset");
  expect(result.hiddenTools).toEqual(["admin_reset"]);
});
```

### Finding what serves a URI

`findResourceForUri()` picks the resource or template the way `resources/read` would, and returns the template's variables. Here a tool checks a link before handing it to the model:

```ts check-link.tool.ts active
import { Tool, ToolContext, z } from "@frontmcp/sdk";

@Tool({ name: "check_link", description: "Check that this server can read a resource URI", inputSchema: { uri: z.string() } })
export class CheckLink extends ToolContext {
  async execute({ uri }: { uri: string }) {
    const match = this.scope.resources.findResourceForUri(uri);
    if (!match) return { readable: false };
    return { readable: true, resource: match.instance.name, template: match.instance.isTemplate, params: match.params };
  }
}
```

```ts tickets.resources.ts
import { Resource, ResourceContext, ResourceTemplate } from "@frontmcp/sdk";

@Resource({ name: "ticket-stats", uri: "tickets://stats", mimeType: "application/json" })
export class TicketStats extends ResourceContext {
  async execute(uri: string) {
    return { contents: [{ uri, text: JSON.stringify({ open: 12 }) }] };
  }
}

@ResourceTemplate({ name: "ticket", uriTemplate: "tickets://{id}", mimeType: "application/json" })
export class Ticket extends ResourceContext<{ id: string }> {
  async execute(uri: string, { id }: { id: string }) {
    return { contents: [{ uri, text: JSON.stringify({ id }) }] };
  }
}
```

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

test("a template match comes with its variables", async ({ mcp }) => {
  const result = await mcp.tools.call("check_link", { uri: "tickets://T-4" });
  expect(result.json()).toEqual({ readable: true, resource: "ticket", template: true, params: { id: "T-4" } });
});

test("a fixed URI wins over a template that also matches", async ({ mcp }) => {
  const result = await mcp.tools.call("check_link", { uri: "tickets://stats" });
  expect(result.json()).toEqual({ readable: true, resource: "ticket-stats", template: false, params: {} });
});

test("a URI nothing serves", async ({ mcp }) => {
  expect((await mcp.tools.call("check_link", { uri: "invoices://1" })).json()).toEqual({ readable: false });
});
```

### Adding a resource at runtime

`registerDynamicResource()` adds a resource class to the server while it runs. Clients see it in their next `resources/list`. Here the weekly report only exists once someone has generated it:

```ts publish-weekly-report.tool.ts active
import { Tool, ToolContext } from "@frontmcp/sdk";
import { WeeklyReport } from "./weekly-report.resource";

export const changes: string[] = [];

@Tool({ name: "publish_weekly_report", description: "Generate this week's report and publish it as reports://weekly", inputSchema: {} })
export class PublishWeeklyReport extends ToolContext {
  async execute() {
    const stop = this.scope.resources.subscribe({}, (event) => changes.push(`${event.kind}: ${event.snapshot.length} resources`));
    this.scope.resources.registerDynamicResource(WeeklyReport);
    stop();
    return { published: "reports://weekly" };
  }
}
```

```ts main.ts
import { App, FrontMcp } from "@frontmcp/sdk";
import { PublishWeeklyReport } from "./publish-weekly-report.tool";

@App({ id: "reports", name: "Reports", tools: [PublishWeeklyReport] })
class ReportsApp {}

@FrontMcp({ info: { name: "help-desk", version: "1.0.0" }, apps: [ReportsApp] })
export default class Server {}
```

```ts weekly-report.resource.ts
import { Resource, ResourceContext } from "@frontmcp/sdk";

// Not in the app's `resources`: publish_weekly_report adds it.
@Resource({ name: "weekly-report", uri: "reports://weekly", mimeType: "text/plain" })
export class WeeklyReport extends ResourceContext {
  async execute(uri: string) {
    return { contents: [{ uri, text: "41 tickets opened, 38 closed." }] };
  }
}
```

```ts publish.test.ts
import { test, expect } from "@frontmcp/testing";
import { changes } from "./publish-weekly-report.tool";

test("the report isn't there before it's published", async ({ mcp }) => {
  expect(await mcp.resources.list()).toEqual([]);
  expect(await mcp.resources.read("reports://weekly")).toBeError();
});

test("once published, clients can list and read it", async ({ mcp }) => {
  await mcp.tools.call("publish_weekly_report", {});
  await mcp.tools.call("publish_weekly_report", {}); // the same class again: no change
  expect((await mcp.resources.list()).map((r: { uri: string }) => r.uri)).toEqual(["reports://weekly"]);
  expect((await mcp.resources.read("reports://weekly")).text()).toBe("41 tickets opened, 38 closed.");
});

test("subscribers hear about it", () => {
  expect(changes).toEqual(["reset: 1 resources"]);
});
```

A resource added this way belongs to the scope, not to an app, and lasts until the server restarts. On a server with several instances, each instance has only what was added to it.

### Adding a skill at runtime

`registerSkillContent()` adds a [skill](https://frontmcp.dev/reference/sdk/skill) while the server runs. Here each team writes its escalation runbook in the help desk, and `publish_runbook` turns it into a skill the client's model can load. The plugin's `dynamicSkills: true` tells the server skills may arrive later, so it answers the skills requests from the start:

```ts runbooks.ts active
import { Plugin, Tool, ToolContext, z } from "@frontmcp/sdk";

// The server has no skills of its own: without this, it wouldn't answer skills/list
@Plugin({ name: "runbooks", dynamicSkills: true })
export class RunbooksPlugin {}

@Tool({ name: "publish_runbook", description: "Publish a team's escalation runbook as a skill", inputSchema: { team: z.string() } })
export class PublishRunbook extends ToolContext {
  async execute({ team }: { team: string }) {
    const { id } = await this.scope.skills.registerSkillContent(
      {
        id: `escalate-to-${team}`,
        name: `escalate-to-${team}`,
        description: `Escalate a ticket to the ${team} team.`,
        instructions: `1. Read the ticket with get_ticket.\n2. Page the ${team} on-call engineer with page_on_call.`,
        tools: [{ name: "get_ticket" }, { name: "page_on_call", purpose: "Page the on-call engineer" }],
      },
      { source: "publish_runbook" },
    );
    return { published: id };
  }
}

@Tool({ name: "retire_runbook", description: "Remove a team's escalation runbook", inputSchema: { team: z.string() } })
export class RetireRunbook extends ToolContext {
  async execute({ team }: { team: string }) {
    return { retired: await this.scope.skills.unregisterSkill(`escalate-to-${team}`) };
  }
}
```

```ts help-desk.app.ts
import { App, Tool, ToolContext, z } from "@frontmcp/sdk";
import { PublishRunbook, RetireRunbook, RunbooksPlugin } from "./runbooks";

@Tool({ name: "get_ticket", description: "Get a support ticket by id.", inputSchema: { id: z.string() } })
export class GetTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    return { id, title: "Charged twice for invoice 1042" };
  }
}

@App({ id: "help-desk", name: "Help Desk", tools: [GetTicket, PublishRunbook, RetireRunbook], plugins: [RunbooksPlugin] })
export class HelpDeskApp {}
```

```ts runbooks.test.ts
import { test, expect } from "@frontmcp/testing";
import { create } from "@frontmcp/sdk";
import { GetTicket } from "./help-desk.app";
import { PublishRunbook, RetireRunbook, RunbooksPlugin } from "./runbooks";

const request = (method: string, params: object) => ({ jsonrpc: "2.0" as const, id: 1, method, params });
const listed = async (mcp: any) =>
  (await mcp.raw.request(request("skills/list", {}))).result.skills.map((s: { id: string }) => s.id);

test("the skill is there once published", async ({ mcp }) => {
  expect(await listed(mcp)).toEqual([]);
  expect((await mcp.tools.call("publish_runbook", { team: "billing" })).json()).toEqual({ published: "escalate-to-billing" });
  expect(await listed(mcp)).toEqual(["escalate-to-billing"]);

  const { result } = await mcp.raw.request(request("skills/load", { skillIds: ["escalate-to-billing"] }));
  expect(result.skills[0]).toMatchObject({
    instructions: "1. Read the ticket with get_ticket.\n2. Page the billing on-call engineer with page_on_call.",
    availableTools: ["get_ticket"],
    missingTools: ["page_on_call"],
  });
});

test("clients read its SKILL.md and find it in the index, not in resources/list", async ({ mcp }) => {
  const text = (await mcp.resources.read("skill://escalate-to-billing/SKILL.md")).text();
  expect(text).toMatch(/^---\nname: escalate-to-billing\ndescription: Escalate a ticket to the billing team\.\n/);
  const index = (await mcp.resources.read("skill://index.json")).json();
  expect(index.skills.map((s: { url: string }) => s.url)).toContain("skill://escalate-to-billing/SKILL.md");
  expect((await mcp.resources.list()).map((r: { uri: string }) => r.uri)).toEqual(["skill://index.json"]);
});

test("retiring removes it; a second time there's nothing to remove", async ({ mcp }) => {
  expect((await mcp.tools.call("retire_runbook", { team: "billing" })).json()).toEqual({ retired: true });
  expect((await mcp.tools.call("retire_runbook", { team: "billing" })).json()).toEqual({ retired: false });
  expect(await listed(mcp)).toEqual([]);
});

test("connected clients get notifications/skills/list_changed", async () => {
  const server = await create({
    info: { name: "help-desk", version: "1.0.0" },
    tools: [GetTicket, PublishRunbook, RetireRunbook],
    plugins: [RunbooksPlugin],
  });
  const client = await server.connect();
  const heard: string[] = [];
  client.onNotification((notification: { method: string }) => {
    heard.push(notification.method);
  });
  try {
    await client.callTool("publish_runbook", { team: "billing" });
    await client.callTool("retire_runbook", { team: "billing" });
    await new Promise((resolve) => setTimeout(resolve, 20));
    expect(heard).toEqual(["notifications/skills/list_changed", "notifications/skills/list_changed"]);
  } finally {
    await client.close();
    await server.dispose();
  }
});
```

`page_on_call` isn't a tool of this server, so `skills/load` lists it in `missingTools`, as for an app's skill. Publishing the same team again replaces its skill, and clients hear about that too. A skill added this way is for every caller, and a server with several instances has it only on the instance that added it.

### Reading the server's configuration

`this.scope.metadata` is the server's `@FrontMcp` options, with defaults filled in, and `this.scope.apps` has each app's. Here a tool reports the server's version, and a server-level provider is read through `this.scope.providers`:

```ts main.ts active
import { App, FrontMcp, Provider, ProviderScope, Tool, ToolContext } from "@frontmcp/sdk";

@Provider({ name: "Deployment", scope: ProviderScope.GLOBAL })
export class Deployment {
  region = "eu-west-1";
}

@Tool({ name: "server_version", description: "Which version of the server is running, and where", inputSchema: {} })
export class ServerVersion extends ToolContext {
  async execute() {
    return {
      scope: this.scope.id,
      name: this.scope.metadata.info.name,
      version: this.scope.metadata.info.version,
      apps: this.scope.apps.getApps().map((app) => app.id),
      region: this.scope.providers.get(Deployment).region,
    };
  }
}

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

@FrontMcp({ info: { name: "help-desk", version: "2.3.0" }, apps: [HelpDeskApp], providers: [Deployment] })
export default class Server {}
```

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

test("the tool reads the server's info, apps and providers", async ({ mcp }) => {
  const result = await mcp.tools.call("server_version", {});
  expect(result.json()).toEqual({ scope: "root", name: "help-desk", version: "2.3.0", apps: ["help-desk"], region: "eu-west-1" });
});
```

`this.scope.providers` has the server's providers, not an app's. For a provider registered on an app, `this.get()` is the way ([see Troubleshooting](#provider-x-is-not-available-from-thisscopeprovidersget)).

### Using the scope outside a call

`FrontMcpInstance.createForGraph()` builds a server without serving it, and `getScopes()` returns its scope, for scripts and tests that inspect a server. See [`FrontMcpInstance`](https://frontmcp.dev/reference/sdk/frontmcp-instance).

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

@Tool({ name: "search_tickets", description: "Search tickets by title", inputSchema: { query: z.string() } })
export class SearchTickets extends ToolContext {
  async execute({ query }: { query: string }) {
    return { query, tickets: [] };
  }
}

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

```ts inventory.test.ts
import { test, expect } from "@frontmcp/testing";
import { FrontMcpInstance } from "@frontmcp/sdk";
import { HelpDeskApp } from "./help-desk.app";

test("a script can list a server's tools without serving it", async () => {
  const server = await FrontMcpInstance.createForGraph({ info: { name: "help-desk", version: "1.0.0" }, apps: [HelpDeskApp] });
  const [scope] = server.getScopes();
  const tools = scope.tools.getTools().map((tool) => ({ name: tool.name, input: tool.getInputJsonSchema() }));
  expect(tools).toEqual([
    { name: "search_tickets", input: expect.objectContaining({ type: "object", properties: { query: { type: "string" } } }) },
  ]);
});
```

---

## Troubleshooting

### `Provider "X" is not available` from `this.scope.providers.get()`

`this.scope.providers` holds the server's providers, the ones registered on `@FrontMcp`. A provider registered on an app lives in that app's registry:

```ts count-tickets.tool.ts active
import { App, Provider, Tool, ToolContext } from "@frontmcp/sdk";

@Provider({ name: "TicketStore" })
export class TicketStore {
  count() {
    return 12;
  }
}

@Tool({ name: "count_tickets", description: "How many tickets there are", inputSchema: {} })
export class CountTickets extends ToolContext {
  async execute() {
    return { count: this.scope.providers.get(TicketStore).count() }; // 🚩 an app's provider
  }
}

@Tool({ name: "count_tickets_fixed", description: "How many tickets there are", inputSchema: {} })
export class CountTicketsFixed extends ToolContext {
  async execute() {
    return { count: this.get(TicketStore).count() }; // ✅
  }
}

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

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

test("the scope doesn't have the app's provider", async ({ mcp }) => {
  const result = await mcp.tools.call("count_tickets", {});
  expect(result.text()).toContain('Provider "TicketStore" is not available: not found in local or parent registries');
});

test("this.get() finds it", async ({ mcp }) => {
  expect((await mcp.tools.call("count_tickets_fixed", {})).json()).toEqual({ count: 12 });
});
```

Use `this.get(X)` from the tool, which looks in the tool's app first, then the server. See [where `this.get()` looks](https://frontmcp.dev/reference/sdk/get#where-frontmcp-looks).

### `Property 'jobs' does not exist on type 'ScopeEntry'`

The project has a FrontMCP older than 1.9.1. There, `jobs`, `workflows`, `channels` and `plugins` existed on the scope at run time but not on its type, `ScopeEntry`, and code cast `this.scope` to reach them. Since 1.9.1, `ScopeEntry` declares them, so upgrade and read them directly:

```ts list-job-names.tool.ts active
import { Tool, ToolContext } from "@frontmcp/sdk";

@Tool({ name: "list_job_names", description: "List the jobs this server can run", inputSchema: {} })
export class ListJobNames extends ToolContext {
  async execute() {
    return { jobs: this.scope.jobs?.getJobs().map((job) => job.name) ?? [] };
  }
}
```

```ts nightly-cleanup.job.ts
import { Job, JobContext } from "@frontmcp/sdk";

@Job({ name: "nightly_cleanup", description: "Close tickets nobody has touched in 30 days", inputSchema: {}, outputSchema: {} })
export class NightlyCleanup extends JobContext {
  async execute() {
    return {};
  }
}
```

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

test("the tool lists the server's jobs", async ({ mcp }) => {
  expect((await mcp.tools.call("list_job_names", {})).json()).toEqual({ jobs: ["nightly_cleanup"] });
});
```

`jobs` is `undefined` on a server without jobs, so keep the `?.`.

### `Tool "execute_job" not found` after adding a tool with `replaceAll()`

`this.scope.tools.replaceAll()` replaced every tool the scope owns, and those are FrontMCP's own, like the job and workflow tools:

```ts enable-exports.tool.ts active
import { Tool, ToolContext } from "@frontmcp/sdk";

@Tool({ name: "export_tickets", description: "Export every ticket as CSV", inputSchema: {} })
export class ExportTickets extends ToolContext {
  async execute() {
    return { csv: "id,title\nT-1,Cannot log in" };
  }
}

@Tool({ name: "enable_exports", description: "Turn on the export tool", inputSchema: {} })
export class EnableExports extends ToolContext {
  async execute() {
    this.scope.tools.replaceAll([ExportTickets], this.scope.tools.owner); // 🚩 replaces the scope's own tools
    return { enabled: true };
  }
}
```

```ts main.ts
import { App, FrontMcp, Job, JobContext } from "@frontmcp/sdk";
import { EnableExports } from "./enable-exports.tool";

@Job({ name: "nightly_cleanup", description: "Close stale tickets", inputSchema: {}, outputSchema: {} })
class NightlyCleanup extends JobContext {
  async execute() {
    return {};
  }
}

@App({ id: "help-desk", name: "Help Desk", tools: [EnableExports], jobs: [NightlyCleanup] })
class HelpDeskApp {}

@FrontMcp({ info: { name: "help-desk", version: "1.0.0" }, apps: [HelpDeskApp] })
export default class Server {}
```

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

test("the new tool arrives, and the job tools are gone", async ({ mcp }) => {
  const before = (await mcp.tools.list()).map((t: { name: string }) => t.name);
  expect(before).toContain("execute_job");

  await mcp.tools.call("enable_exports", {});
  const after = (await mcp.tools.list()).map((t: { name: string }) => t.name);
  expect(after).toContain("export_tickets");
  expect(after).not.toContain("execute_job");
  expect((await mcp.tools.call("execute_job", { name: "nightly_cleanup", input: {} })).text()).toBe('Tool "execute_job" not found');
});
```

From inside a call, there's no supported way to add a tool: register every tool at startup, and hide the ones that aren't ready with `visibility`, `availableWhen` or authorities. A `create()` server can add tools with [`registerTool()`](https://frontmcp.dev/reference/sdk/register-tool), since 1.9. See [`tools`](#tools).

### `Invalid resource '…'. Expected a class or a resource function.`

`registerDynamicResource()` was given a plain object, like `{ name, uri, read }`. It takes what an app's `resources` takes: a `@Resource` or `@ResourceTemplate` class, or a `resource()`:

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

@Tool({ name: "search_tickets", description: "Search tickets by title", inputSchema: { query: z.string() } })
export class SearchTickets extends ToolContext {
  async execute({ query }: { query: string }) {
    return { query, tickets: [] };
  }
}

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

```ts register.test.ts
import { test, expect } from "@frontmcp/testing";
import { FrontMcpInstance, resource } from "@frontmcp/sdk";
import { HelpDeskApp } from "./help-desk.app";

const scopeOf = async () =>
  (await FrontMcpInstance.createForGraph({ info: { name: "help-desk", version: "1.0.0" }, apps: [HelpDeskApp] })).getScopes()[0];

test("a plain object is refused", async () => {
  const scope = await scopeOf();
  const config = { name: "runtime-config", uri: "config://runtime", read: async () => ({ text: "{}" }) };
  expect(() => scope.resources.registerDynamicResource(config as never)).toThrow(
    "Invalid resource 'runtime-config'. Expected a class or a resource function.",
  );
});

test("a resource() works", async () => {
  const scope = await scopeOf();
  const runtimeConfig = resource({ name: "runtime-config", uri: "config://runtime", mimeType: "application/json" })(
    async (uri: string) => ({ contents: [{ uri, text: "{}" }] }),
  );
  scope.resources.registerDynamicResource(runtimeConfig);
  expect(scope.resources.getResources().map((r) => r.uri)).toEqual(["config://runtime"]);
});
```

### `skills/list` answers `Method not found` after a skill was added

The server had no skills when it started, and nothing told it skills would be added, so `skills/list`, `skills/search` and `skills/load` answer `-32601` `Method not found`. Adding a skill with `registerSkillContent()` doesn't fix it for in-process clients, from `connect()` or `server.connect()`: the skill is in the registry, but they can't reach it. Register a plugin with `dynamicSkills: true` on the app or the server, as in [Adding a skill at runtime](#adding-a-skill-at-runtime):

```ts refund-policy.ts active
import { Tool, ToolContext } from "@frontmcp/sdk";

@Tool({ name: "publish_refund_policy", description: "Publish the refund policy as a skill", inputSchema: {} })
export class PublishRefundPolicy extends ToolContext {
  async execute() {
    await this.scope.skills.registerSkillContent({
      id: "refund-policy",
      name: "refund-policy",
      description: "How to answer a customer who asks for a refund.",
      instructions: "Refunds within 30 days of the charge are automatic. After 30 days, assign the ticket to billing.",
      tools: [],
    });
    return { published: true };
  }
}
```

```ts method.test.ts
import { test, expect } from "@frontmcp/testing";
import { Plugin, create } from "@frontmcp/sdk";
import { PublishRefundPolicy } from "./refund-policy";

const info = { name: "help-desk", version: "1.0.0" };

test("🚩 without dynamicSkills, skills/list isn't found, even after the skill is added", async () => {
  const server = await create({ info, tools: [PublishRefundPolicy] });
  const client = await server.connect();
  try {
    await expect(client.listSkills()).rejects.toThrow("Method not found");
    await client.callTool("publish_refund_policy", {});
    await expect(client.listSkills()).rejects.toThrow("Method not found");
  } finally {
    await client.close();
    await server.dispose();
  }
});

test("✅ with a plugin that sets dynamicSkills, it lists the skill", async () => {
  @Plugin({ name: "policies", dynamicSkills: true })
  class PoliciesPlugin {}
  const server = await create({ info, tools: [PublishRefundPolicy], plugins: [PoliciesPlugin] });
  const client = await server.connect();
  try {
    expect((await client.listSkills()).skills).toEqual([]);
    await client.callTool("publish_refund_policy", {});
    expect((await client.listSkills()).skills.map((s: { id: string }) => s.id)).toEqual(["refund-policy"]);
  } finally {
    await client.close();
    await server.dispose();
  }
});
```

A server whose apps declare at least one skill has the skills requests anyway.

### `Cannot read properties of undefined (reading 'registerSkillContent')`

A provider's factory called `scope.skills.registerSkillContent()` while the server was starting. FrontMCP builds the apps' providers before it creates the skill registry, so `scope.skills` is still `undefined`, and the server doesn't start:

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

export class RefundPolicy {}

export const refundPolicy = {
  name: "RefundPolicy",
  provide: RefundPolicy,
  inject: () => [ScopeEntry] as const,
  useFactory: async (scope: ScopeEntry) => {
    // 🚩 Runs before the skill registry exists
    await scope.skills.registerSkillContent({
      id: "refund-policy",
      name: "refund-policy",
      description: "How to answer a customer who asks for a refund.",
      instructions: "Refunds within 30 days of the charge are automatic.",
      tools: [],
    });
    return new RefundPolicy();
  },
};

@App({ id: "help-desk", name: "Help Desk" })
export class HelpDeskApp {}
```

```ts factory.test.ts
import { test, expect } from "@frontmcp/testing";
import { App, FrontMcpInstance } from "@frontmcp/sdk";
import { refundPolicy } from "./help-desk.app";

test("the server doesn't start", async () => {
  @App({ id: "help-desk", name: "Help Desk", providers: [refundPolicy] })
  class Desk {}
  await expect(FrontMcpInstance.createDirect({ info: { name: "help-desk", version: "1.0.0" }, apps: [Desk] })).rejects.toThrow(
    `Failed to construct provider "RefundPolicy": Cannot read properties of undefined (reading 'registerSkillContent')`,
  );
});
```

Add skills from code that runs once the server is up, like a tool, or declare them in the app's `skills`.

### `registerSkillContent: id "…" of skill "…" is the skill:// path of skill "…"`

The new skill's `id` is the `skill://` path of another skill, so `skill://<id>/SKILL.md` would serve one for the other: for example, `id: "triage-ticket"` with `name: "escalate"`, on a server that has a `triage-ticket` skill. Nothing is added. Give the new skill another `id`, or the same `id` as its `name`.

### A tool I registered doesn't show up in `getTools()`

It has `visibility: "hidden"` or `"internal"`. `getTools(true)` includes them.
