
# Per-User Tools

> One endpoint, a different toolset per caller.

The safest way to keep a caller away from a tool is not to advertise it. Because handler options can be a function of the event, the toolset can be decided per request — after auth has run:

```ts
import { H3 } from "h3";
import { defineMcpHandler } from "h3-mcp";
import { readTools, writeTools, adminTools } from "./tools.ts";

const app = new H3();

app.all(
  "/mcp",
  defineMcpHandler((event) => ({
    name: "my-server",
    version: "1.0.0",

    auth: {
      schemes: ["bearer"],
      validate: async (auth, event) => {
        const user = await verifyToken(auth.token);
        if (!user) return false;
        event.context.user = user; // available below and in every handler
        return true;
      },
    },

    tools: toolsFor(event.context.user?.role),
  })),
);

function toolsFor(role?: string) {
  switch (role) {
    case "admin": {
      return [...readTools, ...writeTools, ...adminTools];
    }
    case "editor": {
      return [...readTools, ...writeTools];
    }
    default: {
      return readTools;
    }
  }
}
```

The options function runs at most once per request and the result is cached for that event, so `toolsFor` is not re-evaluated per method call.

> [!IMPORTANT]
> Hiding a tool is not authorization. `tools/call` is reachable for anything you return, and nothing stops a client from guessing a name it was never shown. Check permissions inside each handler too — the list is UX, the check is the control.

## Expensive Per-User Definitions

If building a definition requires I/O, make the entry lazy so it only happens when a client actually lists or calls it:

```ts
defineMcpHandler((event) => ({
  name: "my-server",
  version: "1.0.0",
  tools: [
    ...readTools,
    async () => {
      const schema = await loadTenantSchema(event.context.user.tenant);
      return defineMcpTool({
        name: "query",
        description: "Query your tenant's data",
        inputSchema: schema,
        handler: async (args) => ({ content: [{ type: "text", text: await runQuery(args) }] }),
      });
    },
  ],
}));
```

:read-more{to="/guide/basics/handler#lazy-definitions" title="Lazy definitions"}

## Per-Tenant Resources

The same pattern applies to resources — and to the [cache scope](/guide/modern/caching), which must stay `private` for anything user-specific:

```ts
defineMcpHandler((event) => ({
  name: "my-server",
  version: "1.0.0",
  resources: resourcesFor(event.context.user),
  cache: {
    // discovery is identical for everyone; reads are not
    discover: { ttlMs: 3_600_000, cacheScope: "public" },
    resourceRead: { ttlMs: 5_000, cacheScope: "private" },
  },
}));
```

## Separate Endpoints Instead

When the split is coarse, two mounts are simpler to reason about than one branching function — and the admin surface is not reachable from the public URL at all:

```ts
app.all("/mcp", defineMcpHandler({ name: "public", version: "1.0.0", tools: readTools }));

app.all(
  "/mcp/admin",
  defineMcpHandler({
    name: "admin",
    version: "1.0.0",
    tools: adminTools,
    auth: { tokens: [process.env.ADMIN_TOKEN!] },
    origin: { allow: ["https://admin.example.com"] },
  }),
);
```
