this.auth

this.auth describes the caller that your server's authentication established for this request: who they are (user.sub), whether they signed in (isAnonymous), what they were granted (scopes, roles, permissions), and what their token said (claims), with checks like hasScope(). The model writes a tool's arguments, but it can't touch any of this, so this.auth is what access decisions should be based on. It's there in tools, resources, prompts, agents and jobs.

const { user, isAnonymous, scopes, roles, permissions, claims } = this.auth
this.auth.hasScope(scope)

Reference

this.auth

Read this.auth inside execute().

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

@Tool({ name: "close_ticket", description: "Close a support ticket. Support agents only.", inputSchema: { id: z.string() } })
export class CloseTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    if (!this.auth.hasScope("tickets:write")) {
      this.fail(new PublicMcpError("Only signed-in support agents can close tickets. Ask the user to sign in.", "FORBIDDEN"));
    }
    return { id, status: "closed", closedBy: this.auth.user.sub };
  }
}

See more examples below.

Properties

PropertyTypeDescription
user{ sub, name?, email?, picture? }The caller. sub is their id: the token's sub claim (or, in a token without one, its client_id or azp; a token with none of them is refused), or the id FrontMCP gives callers without a token. name, email and picture are the claims of the same name, when there are any.
isAnonymousbooleantrue when user.sub is empty or starts with anon:, that is, when the caller didn't sign in. isAnonymousSubject(sub), exported by @frontmcp/sdk, applies the same rule to a sub you hold.
scopesreadonly string[]The scopes the caller was granted: a token's scope and scp claims, or the scopes your auth mode gives.
rolesreadonly string[]The token's roles claim, or the claim claimsMapping.roles points to. [] when there's none.
permissionsreadonly string[]The token's permissions claim, or the claim claimsMapping.permissions points to. A string is split on spaces.
claimsReadonly<Record<string, unknown>>Every claim of the caller's token, like tenant or realm_access. Callers without a token get a few made-up claims: see what each mode fills in.
sessionIdstringThe same as this.context.sessionId: under MCP 2026-07-28, a new anon: id on every request, even for a caller with a static key, and direct: and a UUID in-process. Don't use it to identify anyone. (Changed in 1.8.5: before, the auth layer could record an id of its own here.)
modestringMeant to name the auth mode. In FrontMCP 1.9.3 it's "public" for a caller holding an anonymous token, and "authenticated" for every other caller, in every mode, public included, so don't rely on it.

Methods

MethodReturns true when
hasScope(scope)scopes includes scope.
hasAllScopes(scopes), hasAnyScope(scopes)scopes includes every one, or at least one, of them.
hasRole(role)roles includes role.
hasAllRoles(roles), hasAnyRole(roles)roles includes every one, or at least one, of them.
hasPermission(permission)permissions includes permission.
hasAllPermissions(permissions), hasAnyPermission(permissions)permissions includes every one, or at least one, of them.

Each is an exact, case-sensitive match: hasScope("tickets") is false for a caller with tickets:read, and nothing expands wildcards.

What each auth mode fills in

public (the default)statictransparent, with a tokentransparent with allowAnonymous, no token
user.subanon: and a new id per requeststatic: and 12 hex charactersThe token's subanon: and a new id per request
user.name, user.email"Anonymous", none"Static token", noneThe token's name and email"Anonymous", none
isAnonymoustruefalsefalsetrue
scopesanonymousScopes, ["anonymous"] by defaultThe mode's scopes, ["static"] by defaultThe token's scope and scpanonymousScopes
roles, permissions[][]From the token's claims[]
claims{ iss: "public", sub, name, scope }{ iss: "static", sub, name, scope }Every claim in the token{ iss: "transparent-anon", sub, name, scope }

Seeing what each mode gives shows all four, and an anonymous token. local and remote, the modes where FrontMCP runs the sign-in itself, need a browser sign-in, so they aren't shown on this page: Local auth and Remote and proxied auth show what their tools see.

Anonymous tokens

In public mode, and in local mode with allowDefaultPublic, a client can also ask /oauth/token for a token with grant_type=anonymous and its client_id. Whoever holds that token is still anonymous: user.sub is anon: and an id that stays the same for as long as the token lasts, isAnonymous is true, and mode is "public". Its scopes are the mode's anonymousScopes, as are those of a caller without a token.

What each entry point fills in

The table of modes above is for requests over HTTP: the Node server, createFetchHandler(), and the Playground. In-process entry points have no token to check, so the calling code says who the user is:

Entry pointthis.auth
create() or createDirect(), without authContextuser.sub is "direct", and isAnonymous is false: a signed-in user with no scopes and no roles.
create() or createDirect(), with authContext: { user, scopes }user.sub, name and email, roles and permissions come from user, and claims holds all of it. scopes is authContext.scopes, or [] without it: a scope claim in user isn't read.
connect(), without session.useruser.sub is "", and isAnonymous is true. authToken isn't checked or read.
connect(), with session: { user, scopes }As for authContext.user. scopes is session.scopes, or [] without it, with or without a user.

Nothing checks the user you pass in-process: your code is the authentication, so pass only a user your code has verified.

Clients on MCP versions before 2026-07-28, which keep a session over HTTP, get the token's scopes in this.auth.scopes like any other caller. createFetchHandler() and the Playground keep no sessions, so this page can't show it: it was checked against a server listening over HTTP, with a client that opened a session with initialize.

Changed in 1.8.7: before, this.auth.scopes was empty for those clients, although this.context.authInfo.scopes had the scopes, so a tool that checked hasScope() refused them. this.auth now merges what the request's context knows about the caller, so the two agree.

Where it's available

Inthis.auth
Tools, resources, agentsThe caller of the request.
JobsThe caller who started the run.
ChannelsAn empty caller: user.sub is "" and isAnonymous is true. See Context classes.
PromptsThe caller of the request. See Reading the caller in a prompt.

Options that change this.auth

@FrontMcp({ authorities }) holds the server's authorization rules. Its claimsMapping also tells this.auth where the claims are:

OptionWhat it changes
claimsMapping.rolesA dot path to the roles claim, like "realm_access.roles". this.auth.roles reads it instead of roles.
claimsMapping.permissionsA dot path to the permissions claim. "scope" turns the token's scopes into permissions.
claimsMapping.userIdA dot path to the claim to use as user.sub, instead of sub. The token must still have a sub, client_id or azp: one with none of them is refused before the mapping applies.
pipesFunctions that add fields of your own to this.auth, from the caller's claims. See Adding fields with pipes. (Changed in 1.9: before, FrontMCP never ran them.)

To work roles out in code instead, see claimsResolver. With one, claimsMapping isn't read: this.auth.roles, permissions and claims are what the resolver returns, the same as rules see, and user.sub is the token's sub. (Changed in 1.9.2: before, the resolver changed only what rules saw.)

Caveats

  • this.auth is built from the raw result of authentication, which is also this.context.authInfo. Use authInfo for what this.auth leaves out, like token, the caller's token itself.
  • FrontMCP checked the token's signature, issuer, expiry and audience. What a claim means is up to your identity provider: a roles claim holds whatever it put there.
  • Everything else about a request, like its arguments, clientInfo and headers, is written by the client. Reading the Request explains why only this.auth should decide access.
  • How the caller is established depends on the server's mode, in Auth modes, and on how its token is checked, in Tokens and sessions.

Usage

Checking a scope before acting

This server accepts tokens from an identity provider in transparent mode, and lets callers without a token in with the scope tickets:read. close_ticket needs tickets:write. The Playground has no token, so its call is refused; the tests call as two signed-in users, with tokens a test key signed ahead of time.

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

@Tool({ name: "close_ticket", description: "Close a support ticket. Support agents only.", inputSchema: { id: z.string() } })
export class CloseTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    if (!this.auth.hasScope("tickets:write")) {
      this.fail(new PublicMcpError("Only signed-in support agents can close tickets. Ask the user to sign in.", "FORBIDDEN"));
    }
    return { id, status: "closed", closedBy: this.auth.user.sub };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Check before you change anything, so a refused call has no effect, and say in the tool's description who may use it. To refuse the call before execute() runs, and hide the tool from callers who can't use it, declare authorities instead.

Using who the caller is

user and claims carry what the token says about the caller. Here a note records its author, and is filed under the caller's tenant, a claim this provider adds. The tests use the same callAs() and tokens as above.

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

@Tool({ name: "add_note", description: "Add a note to a support ticket", inputSchema: { ticket: z.string(), text: z.string() } })
export class AddNote extends ToolContext {
  async execute({ ticket, text }: { ticket: string; text: string }) {
    const { user, isAnonymous, claims } = this.auth;
    return {
      ticket,
      text,
      author: isAnonymous ? "a guest" : `${user.name} <${user.email}>`,
      tenant: typeof claims.tenant === "string" ? claims.tenant : null,
    };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

claims is typed unknown per claim, because a token can hold anything. Check a claim's type before you use it.

Reading roles from another claim

Identity providers put roles in different places. Keycloak, for one, puts them in realm_access.roles. Point claimsMapping.roles at them, and this.auth.roles and hasRole() read them from there. permissions: "scope" turns the token's scopes into permissions too, and userId picks the claim that becomes user.sub:

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

@Tool({ name: "whoami", description: "Say who the server thinks is calling", inputSchema: {} })
export class WhoAmI extends ToolContext {
  async execute() {
    const { user, roles, permissions } = this.auth;
    return { sub: user.sub, roles, permissions, isLead: this.auth.hasRole("lead") };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Without claimsMapping, the same token gives roles: ["agent"], from its roles claim, and permissions: [], as the second test shows. The same mapping is used by authorities rules, so a rule and a check in execute() agree on what a role is.

Seeing what each mode gives

whoami returns the whole of this.auth except the session id. The Playground's server is in public mode; the tests start one server in each of the other modes and call it over HTTP.

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

@Tool({ name: "whoami", description: "Say who the server thinks is calling", inputSchema: {} })
export class WhoAmI extends ToolContext {
  async execute() {
    const { user, isAnonymous, scopes, roles, permissions, claims, mode } = this.auth;
    const matches = { exact: this.auth.hasScope("tickets:read"), prefix: this.auth.hasScope("tickets"), upperCase: this.auth.hasScope("TICKETS:READ") };
    return { user, isAnonymous, scopes, roles, permissions, claims, mode, matches };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Calling as a user from tests and scripts

In-process, the code that calls the server says who the user is. create() and createDirect() take an authContext per call, and connect() a session per client:

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

@Tool({ name: "whoami", description: "Say who the server thinks is calling", inputSchema: {} })
export class WhoAmI extends ToolContext {
  async execute() {
    const { user, isAnonymous, scopes, roles, permissions } = this.auth;
    return { sub: user.sub, isAnonymous, scopes, roles, permissions };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

create() and createDirect() give a caller the scopes in authContext.scopes, and connect() the scopes in session.scopes, so a test can check a tool's hasScope() in-process. A scope claim in user isn't read by either. (Changed in 1.9.2: before, in-process callers never had scopes. Changed in 1.9.3: connect() takes session.scopes; before, its callers always had [].) Running FrontMCP Anywhere covers create(), createDirect() and connect().

The caller in resources, agents and jobs

ResourceContext, AgentContext and JobContext have the same this.auth. In a job, it's the caller who started the run.

Other entries

Example 1 of 3

Resource

Open
import { Resource, ResourceContext } from "@frontmcp/sdk";

const assigned: Record<string, string[]> = { nour: ["T-1", "T-3"] };

@Resource({ name: "my_tickets", uri: "me://tickets", mimeType: "application/json", description: "Tickets assigned to the caller" })
export class MyTickets extends ResourceContext {
  async execute(uri: string) {
    const { user, isAnonymous } = this.auth;
    const tickets = isAnonymous ? [] : (assigned[user.sub] ?? []);
    return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ caller: user.sub, tickets }) }] };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Adding fields with pipes

authorities.pipes turns claims into fields of your own on this.auth. Each pipe gets the caller's claims and returns an object, at once or as a promise, and FrontMCP adds its fields before any hook or execute() reads this.auth, in tools, resources, prompts, agents and jobs. Declare the fields on ExtendFrontMcpAuthContext to give them types:

Open
import { App, FrontMcp, Tool, ToolContext } from "@frontmcp/sdk";
import { planOf } from "./plans";

declare global {
  interface ExtendFrontMcpAuthContext {
    tenantId: string;
    plan: "enterprise" | "free";
  }
}

@Tool({ name: "whoami", description: "Say which tenant the caller belongs to, and its plan", inputSchema: {} })
export class WhoAmI extends ToolContext {
  async execute() {
    return { sub: this.auth.user.sub, tenantId: this.auth.tenantId, plan: this.auth.plan };
  }
}

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

export const config = {
  info: { name: "help-desk", version: "1.0.0" },
  apps: [HelpDesk],
  authorities: {
    profiles: {},
    pipes: [
      (claims: Readonly<Record<string, unknown>>) => ({ tenantId: String(claims.tenant ?? "none") }),
      async (claims: Readonly<Record<string, unknown>>) => ({ plan: await planOf(String(claims.tenant ?? "")) }),
    ],
  },
};

@FrontMcp(config)
export default class Server {}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

The Playground's caller has no token, so its claims have no tenant, and the pipes fall back. In-process, the claims are authContext.user, as the second test shows. A pipe that does a lookup runs on every request, so keep it fast, or cache what it finds. Changed in 1.9: before, FrontMCP accepted pipes and never ran them, so the fields were always undefined.

Reading the caller in a prompt

A prompt has the same this.auth as a tool, so it can shape its messages for the caller:

Open
import { Prompt, PromptContext, type GetPromptResult } from "@frontmcp/sdk";

@Prompt({
  name: "handover_note",
  description: "Write a handover note for a ticket",
  arguments: [{ name: "id", description: "Ticket id, like T-1", required: true }],
})
export class HandoverNote extends PromptContext {
  async execute({ id }: Record<string, string>): Promise<GetPromptResult> {
    const { user, isAnonymous, scopes } = this.auth;
    const from = isAnonymous ? "a colleague" : (user.name ?? user.sub);
    const text = `Write a handover note for ticket ${id}, from ${from}. Scopes: ${scopes.join(", ")}.`;
    return { messages: [{ role: "user", content: { type: "text", text } }] };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

PromptContext also still has this.authInfo, the raw result of authentication, which is deprecated: its shape differs between entry points, and claimsMapping doesn't apply to it. To apply the rule of isAnonymous to a sub you hold, use isAnonymousSubject(sub), exported by @frontmcp/sdk: true for a missing or empty sub, and for one that starts with anon:. Changed in 1.9.2: before, prompts had no this.auth, and this.authInfo was the way to read the caller.


Troubleshooting

hasScope() is false though the caller has the scope

Check, in order:

  1. Is the call in-process? create() and createDirect() give a caller only the scopes in authContext.scopes, and connect() none. See Calling as a user.
  2. Is it the exact string? hasScope() doesn't match prefixes or wildcards: tickets isn't tickets:read.

this.auth.roles is empty, though the token has roles

Without a mapping, this.auth.roles reads only a top-level roles claim. Point authorities.claimsMapping.roles at the claim your provider uses, as in Reading roles from another claim. In-process, put roles in authContext.user.

this.auth.user.sub is "direct"

The tool was called with create() or createDirect() without an authContext. FrontMCP treats that as a signed-in user called direct, with isAnonymous: false. Pass authContext: { user: { sub } } on each call, or check the scopes or roles you need rather than isAnonymous.

this.auth.user.sub is ""

The tool was called through connect() without a session.user, or the code runs in a channel's onEvent(), which has no caller. Both count as anonymous.

this.auth.user.sub is different on every call

The caller is anonymous. Under MCP 2026-07-28 each request from a caller without a token gets a new anon: id, so it can't be used to recognize a returning caller, or to keep anything per user. Require a sign-in for tools that need to.

this.auth.mode is "authenticated" in public mode

FrontMCP 1.9.3 sets mode to "authenticated" for callers without a token, in every auth mode, and for every signed-in caller. Only a caller holding an anonymous token gets "public". Use isAnonymous to tell signed-in callers from others.

A field from authorities.pipes is undefined

Check, in order:

  1. Did the pipe throw? FrontMCP logs [FrontMcpAuth] pipe failed: and the error's message, leaves that pipe's fields out, and goes on with the request. The other pipes' fields are there.
  2. Is the claim there? A pipe gets the caller's claims: a token's, authContext.user in-process, and for callers without a token only iss, sub, name and scope. With a claimsResolver, it gets the claims the resolver returns instead. Give a field a default for a claim that's missing, as tenantId does in Adding fields with pipes.