> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentfront.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Skills HTTP Authentication

> HTTP authentication for skill endpoints — API key, JWT bearer, and public modes

When skills are exposed over HTTP (via `skillsConfig.enabled`), FrontMCP supports four authentication modes to protect skill endpoints.

## Auth Modes

| Mode | Header | Validation | Use Case |
| - | - | - | - |
| `inherit` (default) | Same as parent server | Same as parent server | Reuse the server's primary auth |
| `public` | None | None | Development, internal networks |
| `api-key` | `X-API-Key` or `Authorization: ApiKey <key>` | Timing-safe equality | Machine-to-machine, server-to-server |
| `bearer` | `Authorization: Bearer <jwt>` | JWT signature via JWKS | User-scoped access, IdP integration |

With `inherit`, a request to `/skills`, `/llm.txt` or `/llm_full.txt` must pass the same check as the MCP endpoint (a static key, a transparent token, a FrontMCP-issued token...); only a `public`-mode server lets everyone in. Skills with `authorities` are listed only for a caller whose verified claims satisfy them. In the other modes no claims are available, so those skills are never served over HTTP.

## Configuration

### Public Mode

Disable authentication on skill endpoints regardless of the parent server's auth.

```typescript theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
@FrontMcp({
  skillsConfig: {
    enabled: true,
    auth: 'public',
  },
})
class Server {}
```

### API Key Mode

```typescript theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
@FrontMcp({
  skillsConfig: {
    enabled: true,
    auth: 'api-key',
    apiKeys: [
      process.env.SKILL_API_KEY_1,
      process.env.SKILL_API_KEY_2,
    ],
  },
})
class Server {}
```

Clients authenticate with either header format:

```
X-API-Key: sk-your-api-key
```

```
Authorization: ApiKey sk-your-api-key
```

<Info>
  API key comparison uses **timing-safe equality** to prevent timing attacks. All configured keys are checked even after a match is found to maintain constant-time behavior.
</Info>

### JWT Bearer Mode

```typescript theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
@FrontMcp({
  skillsConfig: {
    enabled: true,
    auth: 'bearer',
    jwt: {
      issuer: 'https://auth.example.com',
      audience: 'skills-api',           // Optional audience claim validation
    },
  },
})
class Server {}
```

JWT tokens are validated against the issuer's JWKS endpoint (auto-discovered from `{issuer}/.well-known/jwks.json`). A token must carry `exp`: one without it would never expire, so it is refused (401).

## Validation Result

The validator returns a structured result:

```typescript theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
interface SkillHttpAuthResult {
  authorized: boolean;
  error?: string;       // Human-readable error message
  statusCode?: number;  // HTTP status code (401, 403)
}
```

## Authorizing a Request

Use `authorizeSkillHttpRequest()` to apply `skillsConfig.auth` to a request. It covers every mode, including `inherit` (the default), for which it runs the server's `session:verify` flow, and it returns the verified caller that skill `authorities` are evaluated against:

```typescript theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
import { authorizeSkillHttpRequest } from '@frontmcp/sdk';

const access = await authorizeSkillHttpRequest(scope, skillsConfig, request, logger);
if (!access.allowed) {
  return new Response(access.error, { status: access.status, headers: access.headers });
}
// access.authInfo: the verified caller (empty for public, api-key and bearer)
```

## Header-Only Validator (`api-key`, `bearer`)

`createSkillHttpAuthValidator()` builds a validator that checks the request headers for an explicit `auth: 'api-key'` or `auth: 'bearer'`. It returns `null` only for `auth: 'public'`. It can't run the server's auth, so for `inherit` or an unset `auth` it refuses every request (500); use `authorizeSkillHttpRequest()` there.

```typescript theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
import { createSkillHttpAuthValidator } from '@frontmcp/sdk';

// skillsConfig.auth is 'api-key' or 'bearer'
const validator = createSkillHttpAuthValidator(skillsConfig, logger);

if (validator) {
  const result = await validator.validate({ headers: request.headers });
  if (!result.authorized) {
    return new Response(result.error, { status: result.statusCode });
  }
}
```

<Warning>
  **Migrating from the `null` validator.** `createSkillHttpAuthValidator()` used to return `null` for `auth: 'inherit'` and for an unset `auth`, so code that skipped validation on `null` served those requests with no auth at all. For those modes it now refuses every request (500). Switch that code to `authorizeSkillHttpRequest()`, shown above.
</Warning>

***

## Tool Authorization Guard

When a skill session is active, the **Tool Authorization Guard** enforces which tools the skill is allowed to call. This prevents skill sessions from accessing tools outside their declared allowlist.

### Policy Modes

| Mode | Behavior |
| - | - |
| `strict` | Tool must be in the skill's allowlist. Denied tools throw `ToolNotAllowedError`. |
| `approval` | Unlisted tools trigger an approval flow. Throws `ToolApprovalRequiredError` until approved. |
| `permissive` | Unlisted tools are **allowed with a warning** (logging only) — not silently unenforced. This is the default policy mode. |

### Usage

```typescript theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
import { ToolAuthorizationGuard } from '@frontmcp/sdk';

const guard = new ToolAuthorizationGuard(sessionManager, logger, {
  throwOnDenied: true,
  onApprovalRequired: async (toolName, skillId) => {
    // Custom approval logic (e.g., prompt user)
    return true; // or false to deny
  },
});

// Check if tool is allowed
const result = await guard.check('my_tool');

// Simple boolean check (never throws)
const allowed = await guard.isAllowed('my_tool');

// Manual approval/denial
guard.approveTool('my_tool');
guard.denyTool('dangerous_tool');

// Inspect current state
const mode = guard.getPolicyMode();      // 'strict' | 'approval' | 'permissive'
const tools = guard.getAllowlist();       // ['tool_a', 'tool_b']
const hasSkill = guard.hasActiveSkill(); // true/false
```

### Error Types

#### ToolNotAllowedError

Thrown when a tool is not in the skill's allowlist (strict mode).

| Property | Type | Description |
| - | - | - |
| `toolName` | `string` | The denied tool name |
| `skillId` | `string \| undefined` | Active skill session ID |
| `reason` | `string` | The **denial** reason carried by this error: `not_in_allowlist`, `denied`, `rate_limited`, or `no_active_skill`. (The underlying `ToolAuthorizationResult['reason']` enum also includes the *grant* reasons `skill_allowlist` and `dynamically_approved`, which appear on a successful `guard.check()` result rather than on this error.) |
| `allowedTools` | `string[]` | List of allowed tool names |
| `mcpErrorCode` | `number` | MCP error code (`INVALID_REQUEST`) |

#### ToolApprovalRequiredError

Thrown when a tool requires approval but has not been approved yet.

| Property | Type | Description |
| - | - | - |
| `toolName` | `string` | The tool requiring approval |
| `skillId` | `string \| undefined` | Active skill session ID |
| `mcpErrorCode` | `number` | MCP error code (`INVALID_REQUEST`) |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.