Self-hosted polish: BYOK + expose locally-registered MCP tools + Copilot Keys local fallback#4420
Conversation
Previously, BYOK key lookup was gated by isHosted, meaning self-hosted deployments could add provider keys via the BYOK UI but they would never be consulted at execution time. Self-hosted users had to fall back to per-block apiKey fields or env-var template references. Add a self-hosted-only branch that consults BYOK first for any supported provider (openai, anthropic, google, mistral) when no hosted-model gate applies. Hosted behavior is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DIRECT_TOOL_DEFS previously surfaced only 10 of the ~40+ direct tool handlers registered in apps/sim/lib/copilot/tool-executor/register-handlers.ts. Internal callers (sim's UI Copilot panel, the sim_* SUBAGENT tools) had access to the full surface, but external MCP clients (claude.ai, etc.) saw only the small subset. This commit extends DIRECT_TOOL_DEFS to expose the rest of the local handlers — workflow CRUD/inspect, execution, deployment, MCP server publishing, custom tools/skills/credentials, OAuth, jobs, and workspace VFS. No new handler implementations. Each entry maps to an existing toolId in tool-catalog-v1.ts; runtime dispatch in apps/sim/app/api/mcp/copilot/route.ts already finds the handler via executeTool(). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…quired field Two related self-hosted polish changes: 1. authenticateCopilotApiKey() now does a local lookup against the api_key table first when !isHosted. Self-hosted Mothership rejects all calls (it only trusts sim.ai's production INTERNAL_API_SECRET), so the workspace API key (sk-sim-...) was useless for MCP X-API-Key auth even though it worked everywhere else. Local lookup gives external MCP clients a single credential surface. 2. Agent block apiKey field is no longer required on self-hosted. BYOK (workspace_byok_keys) is the canonical credential source there — making the field required forced workflows to either embed a per-block key, reference an env var template, or use the placeholder workaround. Together these eliminate the last self-hosted credential papercuts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both pages render correctly on self-hosted (BYOK is fully functional via the byok.ts patch; Copilot Keys page works for storing keys locally even though new-key generation still proxies to sim.ai). Removing the requiresHosted gate makes them discoverable instead of requiring users to type the URL by hand. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The local-first auth path I added used eq(apiKeyTable.key, apiKey) but sim stores api keys as a sha256 hash in keyHash, not as plaintext in key. Mirror authenticateApiKeyByHash() in lib/api-key/service.ts: hash the incoming key with hashApiKey() and look up by keyHash. Also honor expiresAt to match the existing service behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
run_workflow, run_workflow_until_block, run_from_block, and run_block
are catalogued as route: 'client' in tool-catalog-v1.ts — sim's
tool-executor.routeToolCall returns 'client' for them and the dispatch
in executor.ts calls executeAppTool() instead of the registered handler.
That falls through to the workflow-block tool registry, which doesn't
have these IDs, producing 'Built-in tool not found: run_workflow'.
External MCP callers should use POST /api/workflows/{id}/execute for
synchronous execution — the same endpoint simstudio-ts-sdk wraps and
the existing MCP-published-as-tool path uses.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Copilot Keys settings page (POST/GET/DELETE /api/copilot/api-keys) forwards every operation to the Mothership service at copilot.sim.ai. Self-hosted callers get rejected because Mothership only trusts sim.ai's production INTERNAL_API_SECRET, so the UI shows 'Failed to create API key. Please check your connection and try again.' on every action. When isHosted is false, fall back to local DB operations against the api_key table — same pattern the personal Sim Keys page already uses. Generated keys land as type='personal' and are valid for the authenticateCopilotApiKey local-first lookup in apps/sim/app/api/mcp/copilot/route.ts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@ForrestOfFidum is attempting to deploy a commit to the Sim Team on Vercel. A member of the Team first needs to authorize it. |
PR SummaryMedium Risk Overview Expands the MCP tool surface by adding many more entries to Adjusts self-hosted UX/validation around credentials by making BYOK keys take precedence on self-hosted (not gated by hosted-model checks), relaxing per-block Reviewed by Cursor Bugbot for commit 04be306. Bugbot is set up for automated code reviews on this repo. Configure here. |
|
Closing — withdrawing while user reviews. |
Greptile SummaryThis PR makes seven self-hosted improvements: BYOK key resolution for non-hosted instances, 33 new MCP tool definitions matching registered handlers, local Copilot API key generate/list/delete without Mothership, a local hash-based authentication fallback in the MCP copilot route, and two nav items ungated from Confidence Score: 4/5Safe to merge — all findings are P2 quality-of-life gaps, hosted behavior is unchanged, and the self-hosted auth/key paths are correctly scoped. All seven changes are correctly gated on !isHosted. Registered handler IDs were verified against tool-catalog-v1. Two P2 issues exist in the MCP copilot route: lastUsed is never updated in the local lookup path, and expired keys silently fall through to remote validation producing a misleading error. A stale comment in navigation.ts describes key generation as still proxying to sim.ai when it now works locally. No P0/P1 issues found. apps/sim/app/api/mcp/copilot/route.ts — lastUsed not updated and expired-key fall-through messaging. Important Files Changed
Sequence DiagramsequenceDiagram
participant Client as MCP Client
participant Route as /api/mcp/copilot
participant LocalDB as Local api_key table
participant Mothership as sim.ai Mothership
Client->>Route: X-API-Key header
alt Self-hosted (!isHosted)
Route->>LocalDB: SELECT userId, expiresAt WHERE keyHash = hash(key)
LocalDB-->>Route: row found / not found
alt Key found and not expired
Route-->>Client: authenticated (userId)
else Key not found or expired
Route->>Mothership: POST /api/validate-key
Mothership-->>Route: 401 rejects self-hosted
Route-->>Client: auth error
end
else Hosted
Route->>Mothership: POST /api/validate-key
Mothership-->>Route: userId or error
Route-->>Client: result
end
Client->>Route: tools/call (direct tool)
Route->>Route: ensureHandlersRegistered()
Route->>Route: executeTool(toolDef.toolId, args, ctx)
Route-->>Client: CallToolResult
Reviews (1): Last reviewed commit: "feat(self-hosted): handle Copilot API ke..." | Re-trigger Greptile |
| // Copilot keys page works locally for managing keys (proxies to sim.ai | ||
| // for new key generation but listing/storing existing keys works). |
There was a problem hiding this comment.
The comment is stale — commit 7 (
generate/route.ts) adds a local-DB key-generation path on !isHosted, so new keys are no longer proxied to sim.ai on self-hosted instances. The comment will mislead future readers into thinking generation still depends on Mothership.
| // Copilot keys page works locally for managing keys (proxies to sim.ai | |
| // for new key generation but listing/storing existing keys works). | |
| // Copilot keys page works fully locally on self-hosted (generate, | |
| // list, and delete all operate against the local api_key table). |
| if (!isHosted) { | ||
| try { | ||
| const keyHash = hashApiKey(apiKey) | ||
| const [row] = await db | ||
| .select({ userId: apiKeyTable.userId, expiresAt: apiKeyTable.expiresAt }) | ||
| .from(apiKeyTable) | ||
| .where(eq(apiKeyTable.keyHash, keyHash)) | ||
| .limit(1) | ||
| if (row?.userId && (!row.expiresAt || row.expiresAt > new Date())) { | ||
| return { success: true, userId: row.userId } | ||
| } | ||
| } catch (error) { | ||
| logger.warn('Local Copilot API key lookup failed, falling through to remote validation', { | ||
| error: toError(error).message, | ||
| }) | ||
| } | ||
| } |
There was a problem hiding this comment.
lastUsed is never updated in the self-hosted local-lookup path. On hosted instances the Go service records key usage; on self-hosted this field will remain null for every key, so the "Last used" column in the Copilot Keys settings page will always show as empty. Consider adding a fire-and-forget db.update(apiKeyTable).set({ lastUsed: new Date() }).where(eq(apiKeyTable.keyHash, keyHash)) after returning success.
| if (row?.userId && (!row.expiresAt || row.expiresAt > new Date())) { | ||
| return { success: true, userId: row.userId } | ||
| } | ||
| } catch (error) { | ||
| logger.warn('Local Copilot API key lookup failed, falling through to remote validation', { | ||
| error: toError(error).message, | ||
| }) | ||
| } | ||
| } |
There was a problem hiding this comment.
When a locally-found key is expired (
row.expiresAt <= new Date()), the function falls through to the remote Mothership validation. On self-hosted, Mothership rejects the call and the caller receives "Invalid Copilot API key — generate a new key" rather than a more accurate "key has expired" message. Adding an explicit early-return for the expired case would give a clearer error and avoid the unnecessary remote round-trip.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 04be306. Configure here.
| return NextResponse.json({ error: 'Key not found' }, { status: 404 }) | ||
| } | ||
| return NextResponse.json({ success: true }, { status: 200 }) | ||
| } |
There was a problem hiding this comment.
Self-hosted DELETE endpoint missing key type filter
Medium Severity
The self-hosted GET handler correctly scopes its query to eq(apiKeyTable.type, 'personal'), but the self-hosted DELETE handler omits this filter — it deletes any key matching (id, userId) regardless of type. Since workspace keys also carry a userId, a caller who knows (or guesses) a workspace key ID can delete it through the Copilot Keys endpoint, even though Copilot Keys are only meant to manage personal-type keys.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 04be306. Configure here.


What
Seven small commits that make self-hosted sim usable end-to-end without a sim.ai dependency:
feat(byok)getApiKeyWithBYOKconsultworkspace_byok_keyson!isHosted(was gated entirely behindisHosted)feat(mcp)DIRECT_TOOL_DEFSexposing locally-registered tool handlers (workflow CRUD/inspect, deployment, MCP server publishing, custom tools/skills/credentials, OAuth, jobs, VFS) — handlers already exist inregister-handlers.ts, this just makes them visible to external MCP clientsfeat(self-hosted)authenticateCopilotApiKeydoes a localkeyHashlookup before remote validation; agent blockapiKeyfield isrequired: isHostedfeat(self-hosted)requiresHosted)fix(self-hosted)authenticateApiKeyByHashinlib/api-key/service.ts)fix(mcp)run_*tools from DIRECT_TOOL_DEFS — they'reroute: 'client'intool-catalog-v1.tsso MCP dispatch falls through toexecuteAppTooland fails with 'Built-in tool not found'feat(self-hosted)/api/copilot/api-keys/{generate,GET,DELETE}fall back to local DB ops on!isHosted(was always forwarding to Mothership which rejects self-hosted callers)Why
Stock OSS sim has multiple
isHostedgates that effectively disable BYOK, the MCP surface, the Copilot Keys page, and various nav items on self-hosted instances. The docs imply these features work for self-hosters, but in practiceMothership(copilot.sim.ai) only trusts requests from sim.ai's productionINTERNAL_API_SECRET, so any code path that proxies to Mothership returns 401/404 to self-hosted callers.These changes are all 'where appropriate, do the same thing locally instead of forwarding'. Hosted behavior is unchanged — every change is gated on
!isHosted.How tested
Deployed end-to-end on a Hostinger VPS at
sim.nlma.io:bun run check:api-validationpasses (no new boundary contracts touched)tools/liston/api/mcp/copilotreturns 52 (was 23) — the 33 new direct tools are callable via OAuth and X-API-Keylist_workspace_mcp_servers,check_deployment_status,deploy_mcpinvocations from claude.ai web work end-to-endsk-sim-..., type='workspace') and locally-generated Copilot keys (type='personal') both authenticate via the X-API-Key header on/api/mcp/copilotapi_keytableNotes for review
run_*removal (commit 6) is a behaviour change for any external MCP client that was somehow hitting these. They never worked from the MCP endpoint (would always 'Built-in tool not found') so this is removing a misleading entry, not breaking an existing flow. External callers wanting synchronous workflow execution should usePOST /api/workflows/{id}/execute(the same endpointsimstudio-ts-sdkwraps).apiKeyfield fromrequired: truetorequired: isHosted. Agent blocks authored on hosted sim continue to require the field. On self-hosted, BYOK supplies the credential at execution time via the patch in commit 1.authenticateCopilotApiKeylocal lookup in commit 3 (refined in 5) has the same expires_at semantics asauthenticateApiKeyByHash. It does not enforce key-type (accepts bothpersonalandworkspace) intentionally, since both already work for/api/v1/workflows/{id}/execute.Happy to split into smaller PRs if preferred — the BYOK fix (1) and the MCP tool exposure (2) are the most contained and could ship independently of the others.