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.

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.

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

MemberTypeWhat you can do with it
toolsToolRegistryList and look up tools. See tools.
resourcesResourceRegistryList resources and templates, find the one for a URI, and add resources at runtime. See resources.
promptsPromptRegistrygetPrompts(), findByName(name), findAllByName(name), getExported(name), subscribe().
agentsAgentRegistrygetAgents(), findByName(name), findById(id), subscribe().
skillsSkillRegistrygetSkills(), findByName(name), listSkills(), search(query), loadSkill(id), and registerSkillContent(content) and unregisterSkill(id) to add and remove skills at runtime. See skills and @Skill.
appsAppRegistrygetApps(): each app's id, metadata, and its own tools, resources, prompts, skills, providers and plugins registries.
providersProviderRegistryThe 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.
hooksHookRegistrygetHooks(), getFlowHooks(flow): every registered hook, FrontMCP's own included, for inspection.
jobs, workflowsJobRegistry, WorkflowRegistry, or undefinedgetJobs() / getWorkflows(), findByName(name), findById(id), search(query?). undefined until an app has jobs or workflows; then both exist.
channelsChannelRegistry, or undefinedgetChannels(), findByName(name). undefined unless channels.enabled, as are channelEventBus and channelNotifications, which send events. See @Channel.
pluginsPluginRegistryInterface, or undefinedgetPlugins(), 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.
authProvidersAuthRegistrygetPrimary(), getAuthProviders(): how the server authenticates callers. For the current caller, use this.auth.

Other members

MemberWhat it is
id"root".
metadataThe server's @FrontMcp options after parsing, defaults filled in: info, apps, providers, transport and the rest. Read it; don't change it.
entryPath, routeBase, fullPathThe MCP endpoint's path: http.entryPath, "" by default.
loggerThe server's logger.
rateLimitManagerThe 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

MethodReturns
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

MethodReturns
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

MethodReturns
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:

FieldTypeDescription
idstringRequired. 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.
namestringRequired. Unlike @Skill, FrontMCP doesn't check it: "Billing Escalation" is accepted. Use kebab-case, like an app's skills.
descriptionstringWhat the procedure is for.
instructionsstringThe 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, ratingAs 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/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.
  • 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.

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:

FieldTypeDescription
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.
versionnumberGoes up with every change.
snapshotentriesEverything 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):

Open
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:

Open
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:

Open
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:

Open
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:

Open
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.

Open
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:

Open
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:

Open
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:

Open
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():

Open
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:

Open
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:

Open
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.