> **Building with AI coding agents?** Install the authstack plugin with one command. This equips your agent with accurate Scalekit implementation patterns.
>
> **Recommended**:
> ```bash
> npx @scalekit-inc/cli setup
> ```
>
> Global:
> ```bash
> npm install -g @scalekit-inc/cli
> scalekit setup
> ```
>
> Supports Claude Code, Cursor, GitHub Copilot, Codex + skills for 40+ agents.
> Features: full-stack-auth, agent-auth, mcp-auth, modular-sso, modular-scim.
> [Full setup guide](https://docs.scalekit.com/dev-kit/build-with-ai/)

---

# Authorize mcp-use tools with Scalekit scopes

Sign a user in with Scalekit, grant todo:read and todo:write, and prove allow and deny in the mcp-use Inspector.
> caution: Wait for the npm export
>
> This recipe imports `oauthScalekitProvider` from `mcp-use/oauth/scalekit`. Do not publish this page while that path 404s on npm. The factory is proposed in [mcp-use pull request #2272](https://github.com/mcp-use/mcp-use/pull/2272). Until a release ships it, clone [scalekit-mcpuse-example](https://github.com/scalekit-developers/scalekit-mcpuse-example). That repo still carries a local verifier so clone-and-run works today.

An [mcp-use](https://mcp-use.com) server is one URL that many hosts share. Scalekit login tells you *who* called. It does not decide *which tools* that person may run.

Without a scope check, `list_todos` and `add_todo` are both open to every signed-in user. mcp-use still lists the write tool when the write scope is missing. The handler must refuse.

**By the end of this recipe,** you sign in with `todo:read` only, list todos, and see `add_todo` return `Missing scope todo:write`. That deny path is the test.

This is for an mcp-use v2 resource server over Streamable HTTP. If you use FastMCP instead, see the [FastMCP quickstart](/authenticate/mcp/fastmcp-quickstart/). That guide uses the same `todo:read` / `todo:write` scopes.

## Login is not authorization

A valid token only proves Scalekit issued it for this MCP server. Every tool still runs unless you check a scope.

| Login proves | A scope check proves |
| --- | --- |
| The caller signed in with Scalekit | This caller may run this tool |
| The token `aud` includes this `res_…` | The grant string matches the handler |

Four other facts make the deny path easy to miss:

- **mcp-use still lists the tool.** A missing scope does not hide `add_todo` in Inspector.
- **Scopes live in two places.** You create `todo:read` and `todo:write` on the Scalekit MCP server. You check the same strings in `ctx.auth.scopes`. A mismatch looks like a random 403.
- **Consent is sticky.** If you add a scope after the first login, the existing token does not gain it. The user must sign in again.
- **`resource` is not `aud`.** `resourceId` is the JWT audience (`res_…`). `resource` (`MCP_URL`) is the public MCP URL. mcp-use puts `resource` in RFC 9728 protected-resource metadata.

## Who this recipe is for

Use this recipe when:

- You run mcp-use over Streamable HTTP
- Users must sign in with Scalekit before they call tools
- Some tools are read-only and some tools write

Skip this recipe when you only need a shared machine credential and no user identity. For Scalekit M2M JWT verification on a regular API, see [M2M JWT verification with JWKS and OAuth scopes](/saaskit/cookbooks/m2m-jwks-and-oauth-scopes/).

## Two tools, two scopes

Scalekit issues an OAuth 2.1 access token that lists the scopes the user granted. mcp-use verifies that token with `oauthScalekitProvider`. Each tool then checks `ctx.auth.scopes` before it runs.

Use two tools and two scopes, the same pair as the FastMCP todo app:

| Tool | Required scope | What it does |
| --- | --- | --- |
| `list_todos` | `todo:read` | Lists todos owned by `ctx.auth.user.id` |
| `add_todo` | `todo:write` | Creates a todo for that user |

mcp-use does not enforce those scopes for you. The check belongs next to the tool.

## Four checks in Inspector

The recipe succeeds when all of this is true:

1. You sign in and grant `todo:read` only.
2. `whoami` shows a `usr_…` id, `subjectType: "user"`, and `todo:read`.
3. `list_todos` succeeds.
4. `add_todo` is visible and returns `Missing scope todo:write`.

A green `whoami` is not enough.

## Register the MCP server and add scopes

Scalekit issues the token. Your process only verifies it. There is no Scalekit client id or secret on the resource server.

**Prerequisites:** Node.js 22.22.2 or newer, a [Scalekit account](https://app.scalekit.com), and at least one [auth method](/mcp/auth-methods/social/) enabled.

1. ### Add the MCP server

   Open [Scalekit Dashboard](https://app.scalekit.com) → **MCP servers** → **Add MCP server**. Give it a name. That name appears on the consent screen.

2. ### Enable DCR and CIMD

   Turn on **dynamic client registration** and **Client ID Metadata Document (CIMD)**. Inspector needs at least one of these. Keep both on.

   Reconnect the MCP client after you change DCR or CIMD. Inspector and other clients cache authorization-server metadata. This process does not.

3. ### Set the Server URL with no trailing slash

   Under advanced settings, set **Server URL** to:

   ```text
   http://localhost:3000/mcp
   ```

   Do not add a trailing slash. mcp-use requires the resource path to equal the server base path. A deployed origin uses `https://mcp.example.com/mcp`.

4. ### Create the two scopes

   Create or link these scopes on the same MCP server, then save:

   | Scope | Description |
   | --- | --- |
   | `todo:read` | List todos |
   | `todo:write` | Create todos |

   These strings must match the checks in your tools exactly.

5. ### Copy the three values

   | Value | Where it appears |
   | --- | --- |
   | Environment URL | Dashboard → API credentials → `https://<your-env>.scalekit.cloud` |
   | Resource ID | This MCP server page → `res_…` |
   | Public MCP URL | The same string as **Server URL** (no trailing slash) |

`resourceId` is the audience check. A token minted for a different MCP server in the same environment must fail.

## Authenticate the mcp-use server

```bash
# Never hard-code secrets. Use environment variables.
npm install mcp-use zod
```

```env
SCALEKIT_ENVIRONMENT_URL=https://your-env.scalekit.cloud
SCALEKIT_RESOURCE_ID=res_xxxxxxxx
MCP_URL=http://localhost:3000/mcp
```

Pass those three values into the published factory. Advertise the same scopes Scalekit offers, so Inspector can request them:

```ts

function requiredEnv(name: string): string {
  const value = process.env[name]?.trim()
  if (!value) {
    throw new TypeError(
      `${name} is missing. Copy .env.example to .env and fill in values from your Scalekit dashboard.`,
    )
  }
  return value
}

const server = new MCPServer({
  name: 'todo-mcp',
  version: '1.0.0',
  oauth: oauthScalekitProvider({
    environmentUrl: requiredEnv('SCALEKIT_ENVIRONMENT_URL'),
    resourceId: requiredEnv('SCALEKIT_RESOURCE_ID'),
    resource: requiredEnv('MCP_URL'),
    scopesSupported: ['todo:read', 'todo:write'],
  }),
  publicLandingPage: true,
})
```

`resource` must match the Scalekit **Server URL** exactly. It is not a second `aud` check.

`publicLandingPage: true` keeps the HTML page public. `/mcp` stays bearer-gated. The tool checks below still apply if a client requests fewer scopes than you advertise.

## Guard each tool with a scope

Store todos per user. Check the scope first. A missing scope returns an error. It does not throw an HTTP 401. The user is already authenticated.

```ts
type Todo = { id: string; title: string; ownerId: string }

const todos = new Map<string, Todo[]>()

function requireScope(ctx: { auth: { scopes: string[] } }, scope: string) {
  if (!ctx.auth.scopes.includes(scope)) {
    return {
      isError: true,
      content: [{ type: 'text', text: `Missing scope ${scope}` }],
    }
  }
  return undefined
}

server.tool(
  {
    name: 'list_todos',
    description: 'List todos for the signed-in user. Requires todo:read.',
    outputSchema: z.object({
      todos: z.array(
        z.object({
          id: z.string(),
          title: z.string(),
        }),
      ),
    }),
  },
  async (_args, ctx) => {
    const denied = requireScope(ctx, 'todo:read')
    if (denied) return denied

    const items = todos.get(ctx.auth.user.id) ?? []
    return {
      content: [{ type: 'text', text: JSON.stringify(items) }],
      structuredContent: {
        todos: items.map(({ id, title }) => ({ id, title })),
      },
    }
  },
)

server.tool(
  {
    name: 'add_todo',
    description: 'Create a todo for the signed-in user. Requires todo:write.',
    inputSchema: z.object({
      title: z.string().min(1),
    }),
    outputSchema: z.object({
      id: z.string(),
      title: z.string(),
    }),
  },
  async (args, ctx) => {
    const denied = requireScope(ctx, 'todo:write')
    if (denied) return denied

    const item = {
      id: crypto.randomUUID(),
      title: args.title,
      ownerId: ctx.auth.user.id,
    }
    const items = todos.get(ctx.auth.user.id) ?? []
    items.push(item)
    todos.set(ctx.auth.user.id, items)
    return {
      content: [{ type: 'text', text: JSON.stringify(item) }],
      structuredContent: { id: item.id, title: item.title },
    }
  },
)

export default server
```

`ctx.auth.user.id` is the token `sub`. Use it as the owner key. Do not treat a `subjectType: "machine"` caller as a person if your product is user-only.

Add a `whoami` tool so Inspector can show the signed-in user and the granted scopes before you test writes:

```ts
server.tool(
  {
    name: 'whoami',
    description: 'Return the signed-in Scalekit user and granted scopes',
  },
  async (_args, ctx) => {
    const data = {
      user: ctx.auth.user,
      scopes: ctx.auth.scopes,
    }
    return {
      content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
    }
  },
)
```

## Prove allow and deny in Inspector

```bash
npm run dev
```

| Endpoint | URL |
| --- | --- |
| MCP | `http://localhost:3000/mcp` |
| Inspector | `http://localhost:3000/mcp/inspector` |

Leave Inspector's client-id fields empty. This flow uses DCR or CIMD.

1. ### Sign in with read only

   Open Inspector. Connect to `http://localhost:3000/mcp`. The first call returns **401**. Inspector starts Scalekit login. Complete consent. Grant `todo:read` only on the first pass.

2. ### Confirm identity

   Call `whoami`. You should see a `usr_…` id, `subjectType: "user"`, and `scopes` that include `todo:read`. Do not paste live ids into docs or tickets.

3. ### Prove the allow path

   Call `list_todos`. The call succeeds. The list may be empty.

4. ### Prove the deny path

   Call `add_todo` with `{ "title": "Ship scopes" }`. The tool is visible. The handler returns `Missing scope todo:write`. That is the test.

5. ### Grant write and retry

   Sign out. Sign in again and grant `todo:write` as well. Call `add_todo`. The todo appears. Call `list_todos`. You see only your items.

A public URL change is two matching updates: Scalekit **Server URL** and `MCP_URL`. Restart the process. `resourceId` stays the audience check.

## Troubleshooting

| Symptom | Fix |
| --- | --- |
| Inspector never starts login | If DCR and CIMD are both off, turn at least one on and save. If they are already on, reconnect Inspector. Clients cache authorization-server metadata. |
| `add_todo` is visible but returns `Missing scope todo:write` | mcp-use does not hide tools by scope. This is the deny path. Grant `todo:write` and sign in again. |
| Scope is on the dashboard but not on the token | Consent is sticky. Disconnect Inspector and sign in again. |
| Every call returns `Missing scope todo:read` | Dashboard string does not match the code. Use `todo:read` and `todo:write` in both places. |
| Login works, every tool is 401 | Scalekit **Server URL** does not match `MCP_URL`. Check trailing slash, port, and `http` vs `https`. |
| `Cannot find module 'mcp-use/oauth/scalekit'` | The factory is not in the installed mcp-use release yet. Wait for the release that includes [mcp-use#2272](https://github.com/mcp-use/mcp-use/pull/2272), or run the [example](https://github.com/scalekit-developers/scalekit-mcpuse-example). |
| Need claim details on a 401 | Set `MCP_USE_OAUTH_DEBUG=1`. Logs print `iss`, `aud`, and `sub`. Logs never print the raw token. |

See also [MCP auth troubleshooting](/authenticate/mcp/troubleshooting/).

## What to do next

- Clone [scalekit-mcpuse-example](https://github.com/scalekit-developers/scalekit-mcpuse-example) if the published import is not on npm yet.
- Compare the same scopes on FastMCP in the [FastMCP quickstart](/authenticate/mcp/fastmcp-quickstart/).
- Register more MCP servers in the [MCP Auth quickstart](/authenticate/mcp/quickstart/).
- Replace the in-memory `Map` with a database keyed by `ctx.auth.user.id`.
- Refuse `subjectType: "machine"` on user-only tools.


---

## More Scalekit documentation

| Resource | What it contains | When to use it |
|----------|-----------------|----------------|
| [/llms.txt](/llms.txt) | Structured index with routing hints per product area | Start here — find which documentation set covers your topic before loading full content |
| [/llms-full.txt](/llms-full.txt) | Complete documentation for all Scalekit products in one file | Use when you need exhaustive context across multiple products or when the topic spans several areas |
| [sitemap-0.xml](https://docs.scalekit.com/sitemap-0.xml) | Full URL list of every documentation page | Use to discover specific page URLs you can fetch for targeted, page-level answers |
