Problem
The Claude Code MCP client connects to `mcp.stackbilt.dev/mcp` via OAuth successfully. The session is authenticated and appears healthy. But every `generate`-gated tool call fails with:
```
MCP error -32600: Tool "scaffold_create" requires one of these scopes: generate. Your token has: (none).
```
Same error for `scaffold_classify` and `scaffold_status` (`read` or `generate`).
Root cause
`src/oauth-handler.ts` line 609-622 (and again at line 709-721) threads `oauthReqInfo.scope` directly into `props.scopes`:
```ts
// C-1a remediation: thread the actual OAuth scope grant through
// to the gateway's apiHandler so resolveAuth can enforce it at
// call time (instead of hardcoding ['generate','read']).
scopes: oauthReqInfo.scope,
```
Claude Code's MCP client doesn't include `scope` in its OAuth initiate, so `oauthReqInfo.scope` is empty, `props.scopes` is empty, and every scope-gated tool is unreachable.
The C-1a remediation correctly removed the hardcoded scope grant as a security fix — but it has the unintended consequence of locking out the assistant that is supposed to drive the engine for dogfooding.
Impact
This makes the scaffold engine effectively unreachable from inside Claude Code — which is exactly the environment where we want to build, iterate, and test the engine. Every run requires shelling out to the tarotscript repo locally, defeating the point of having the MCP in the first place.
Proposed fix (pick one or combine)
Option A — Client allow-list default scopes
Add a hardcoded allow-list for known trusted clients:
```ts
const DEFAULT_SCOPES_BY_CLIENT: Record<string, string[]> = {
'claude-code': ['read', 'generate'],
'claude-desktop': ['read', 'generate'],
// etc.
};
const scopes = oauthReqInfo.scope.length > 0
? oauthReqInfo.scope
: (DEFAULT_SCOPES_BY_CLIENT[oauthReqInfo.clientId] ?? []);
```
Pros: no security regression for unknown clients; dogfooding works.
Cons: hardcoded list needs maintenance; `clientId` is client-controlled.
Option B — Static-bearer bypass via API key
Accept `ea_`-prefixed keys from edge-auth in `auth.ts` `isApiKey()` (currently only matches `sb_live_`/`sb_test_*`), mint a generate-scoped key via edge-auth, and configure it as a static bearer in `.mcp.json`. Bypasses OAuth entirely.
Pros: explicit, auditable, scoped to our control; no security regression.
Cons: requires `isApiKey()` patch AND a matching edge-auth issue for minting. Requires us to not lose the key.
Option C — Opt-in via wider consent screen
On the OAuth initiate, if no scope is requested, default to showing the consent screen with `['read', 'generate']` pre-checked. User approves explicitly.
Pros: user is in the loop; no hardcoded grants.
Cons: Claude Code's MCP flow may not route through the consent screen if it's auto-approved elsewhere.
Recommendation
Start with Option B (static bearer) for immediate unblock. File a companion issue in edge-auth to mint the key. Follow up with Option A (client allow-list) for a cleaner long-term OAuth path.
Repro
```
Any scaffold_create call via the stackbilt MCP
mcp__stackbilt__scaffold_create(intention="test", project_type="api")
→ MCP error -32600: Tool "scaffold_create" requires one of these scopes: generate. Your token has: (none).
```
Related
Problem
The Claude Code MCP client connects to `mcp.stackbilt.dev/mcp` via OAuth successfully. The session is authenticated and appears healthy. But every `generate`-gated tool call fails with:
```
MCP error -32600: Tool "scaffold_create" requires one of these scopes: generate. Your token has: (none).
```
Same error for `scaffold_classify` and `scaffold_status` (`read` or `generate`).
Root cause
`src/oauth-handler.ts` line 609-622 (and again at line 709-721) threads `oauthReqInfo.scope` directly into `props.scopes`:
```ts
// C-1a remediation: thread the actual OAuth scope grant through
// to the gateway's apiHandler so resolveAuth can enforce it at
// call time (instead of hardcoding ['generate','read']).
scopes: oauthReqInfo.scope,
```
Claude Code's MCP client doesn't include `scope` in its OAuth initiate, so `oauthReqInfo.scope` is empty, `props.scopes` is empty, and every scope-gated tool is unreachable.
The C-1a remediation correctly removed the hardcoded scope grant as a security fix — but it has the unintended consequence of locking out the assistant that is supposed to drive the engine for dogfooding.
Impact
This makes the scaffold engine effectively unreachable from inside Claude Code — which is exactly the environment where we want to build, iterate, and test the engine. Every run requires shelling out to the tarotscript repo locally, defeating the point of having the MCP in the first place.
Proposed fix (pick one or combine)
Option A — Client allow-list default scopes
Add a hardcoded allow-list for known trusted clients:
```ts
const DEFAULT_SCOPES_BY_CLIENT: Record<string, string[]> = {
'claude-code': ['read', 'generate'],
'claude-desktop': ['read', 'generate'],
// etc.
};
const scopes = oauthReqInfo.scope.length > 0
? oauthReqInfo.scope
: (DEFAULT_SCOPES_BY_CLIENT[oauthReqInfo.clientId] ?? []);
```
Pros: no security regression for unknown clients; dogfooding works.
Cons: hardcoded list needs maintenance; `clientId` is client-controlled.
Option B — Static-bearer bypass via API key
Accept `ea_`-prefixed keys from edge-auth in `auth.ts` `isApiKey()` (currently only matches `sb_live_`/`sb_test_*`), mint a generate-scoped key via edge-auth, and configure it as a static bearer in `.mcp.json`. Bypasses OAuth entirely.
Pros: explicit, auditable, scoped to our control; no security regression.
Cons: requires `isApiKey()` patch AND a matching edge-auth issue for minting. Requires us to not lose the key.
Option C — Opt-in via wider consent screen
On the OAuth initiate, if no scope is requested, default to showing the consent screen with `['read', 'generate']` pre-checked. User approves explicitly.
Pros: user is in the loop; no hardcoded grants.
Cons: Claude Code's MCP flow may not route through the consent screen if it's auto-approved elsewhere.
Recommendation
Start with Option B (static bearer) for immediate unblock. File a companion issue in edge-auth to mint the key. Follow up with Option A (client allow-list) for a cleaner long-term OAuth path.
Repro
```
Any scaffold_create call via the stackbilt MCP
mcp__stackbilt__scaffold_create(intention="test", project_type="api")
→ MCP error -32600: Tool "scaffold_create" requires one of these scopes: generate. Your token has: (none).
```
Related