Section 1 · Step 4
Tool-level scopes from workforce roles
Pick a user, pick a tool. The middleware is just a claim check — same code path, different outcomes. Descope's policy engine takes this further: per-tool decisions on the MCP server, gated on any user or agent property the token carries.
How this works
Okta roles map to Descope scopes for MCP tools. Each Okta group (workforce role) is mapped — in Descope — to one or more scopes. At login, Descope reads the user's Okta groups, resolves them to scopes, and embeds those scopes in the JWT. The MCP server then checks the required scope per tool.
Okta group: loan-managersDescope scopes: loans:read, loans:approveMCP tools: lookup_loan, approve_loan
Okta group: cs-teamDescope scopes: loans:readMCP tools: lookup_loan
Sign in as
Try a message
JWT scope claim · maya@northwind.com
roles: ["csm"]
scopes: ["loans:read"]
Derived from cs-team, us-staff at login. No database call at request time.
Outcome matrix · 4 combinations
| User → / Tool ↓ | Maya · CSM | Kevin · Loan mgr |
|---|---|---|
| lookup_loan | ALLOW | ALLOW |
| approve_loan | DENY | ALLOW |
Middleware · the whole access control surface
// loan-ops-mcp · middleware/requireScope.ts
export function requireScope(tool: string, required: string) {
return (req, res, next) => {
const { scopes = [] } = req.user; // already validated JWT
if (!scopes.includes(required)) {
return res.status(403).json({
error: "insufficient_scopes",
required,
had: scopes,
});
}
next();
};
}
// tools/index.ts
router.post(
"/tools/lookup_loan",
requireScope("lookup_loan", "loans:read"),
lookupLoanHandler,
);
router.post(
"/tools/approve_loan",
requireScope("approve_loan", "loans:approve"),
approveLoanHandler,
);