Scope and registries
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.
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.
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),
};
}
}A server has one scope, whose id is "root" (with splitByApp, 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.)
Registries
| Member | Type | What you can do with it |
|---|---|---|
tools | ToolRegistry | List and look up tools. See tools. |
resources | ResourceRegistry | List resources and templates, find the one for a URI, and add resources at runtime. See 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 and @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 add to the server: get(token), and addDynamicProviders([...]) to add one at runtime. Not an app's providers: use this.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. |
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. |
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 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(). |
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(), 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. |
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(). Otherwise, register every tool at startup, and hide what isn't ready with visibility, availableWhen or authorities.
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. |
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. |
subscribe(opts, callback) | See change events. |
This is what a plugin with dynamicSkills: true calls, like 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. |
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/searchandskills/loadhave it,skill://index.jsonlists it, and itsSKILL.mdcan be read. It isn't inresources/list, which lists only the skills the apps declare. - Clients hear about it. Each add, replace and removal sends
notifications/skills/list_changedto every client with a session. AfalsefromunregisterSkill()sends nothing. - The server needs to know skills may come. On a server with no skills of its own,
skills/list,skills/searchandskills/loadanswerMethod not foundunless a plugin setsdynamicSkills: true, and in-process clients, likeconnect()'s, keep getting it after a skill is added. See Troubleshooting. - The same
idas an app's skill takes that skill's place inskills/list,skills/search,skills/loadand itsSKILL.md, until it's removed. - It lasts until the server restarts, like a resource added 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.providersholds only the server's providers.this.scope.providers.get(X)for a provider registered in an app throwsProvider "X" is not available.this.get(X)finds it.getTools()leaves out hidden tools. Passtrueto include them. Tools that FrontMCP adds hidden, likelist_jobs, only show up that way.- The registries hold every app's entries, and FrontMCP's own. Filter by
ownerif 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):
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),
};
}
}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
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:
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 };
}
}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
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:
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" };
}
}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
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 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:
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}`) };
}
}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
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:
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 {}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
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).
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.
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 {}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
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:
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 {}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
Use this.get(X) from the tool, which looks in the tool's app first, then the server. See where this.get() 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:
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) ?? [] };
}
}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
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:
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 };
}
}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
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(), since 1.9. See 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():
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 {}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
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:
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 };
}
}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
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:
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 {}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
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.