Skip to content

Self-hosted polish: BYOK + expose locally-registered MCP tools + Copilot Keys local fallback#4420

Closed
NextLevelManagementAdvisors wants to merge 7 commits into
simstudioai:mainfrom
NextLevelManagementAdvisors:feature/expose-direct-tools-via-mcp
Closed

Self-hosted polish: BYOK + expose locally-registered MCP tools + Copilot Keys local fallback#4420
NextLevelManagementAdvisors wants to merge 7 commits into
simstudioai:mainfrom
NextLevelManagementAdvisors:feature/expose-direct-tools-via-mcp

Conversation

@NextLevelManagementAdvisors
Copy link
Copy Markdown

What

Seven small commits that make self-hosted sim usable end-to-end without a sim.ai dependency:

# Commit What
1 feat(byok) Make getApiKeyWithBYOK consult workspace_byok_keys on !isHosted (was gated entirely behind isHosted)
2 feat(mcp) Add 33 entries to DIRECT_TOOL_DEFS exposing locally-registered tool handlers (workflow CRUD/inspect, deployment, MCP server publishing, custom tools/skills/credentials, OAuth, jobs, VFS) — handlers already exist in register-handlers.ts, this just makes them visible to external MCP clients
3 feat(self-hosted) authenticateCopilotApiKey does a local keyHash lookup before remote validation; agent block apiKey field is required: isHosted
4 feat(self-hosted) Surface BYOK + Copilot Keys nav items in settings sidebar (drop requiresHosted)
5 fix(self-hosted) Hash the incoming key before lookup (mirror authenticateApiKeyByHash in lib/api-key/service.ts)
6 fix(mcp) Drop the four run_* tools from DIRECT_TOOL_DEFS — they're route: 'client' in tool-catalog-v1.ts so MCP dispatch falls through to executeAppTool and fails with 'Built-in tool not found'
7 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 isHosted gates 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 practice Mothership (copilot.sim.ai) only trusts requests from sim.ai's production INTERNAL_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-validation passes (no new boundary contracts touched)
  • BYOK Anthropic key triggers real Claude Sonnet 4.6 calls from agent blocks
  • tools/list on /api/mcp/copilot returns 52 (was 23) — the 33 new direct tools are callable via OAuth and X-API-Key
  • list_workspace_mcp_servers, check_deployment_status, deploy_mcp invocations from claude.ai web work end-to-end
  • Workspace API key (sk-sim-..., type='workspace') and locally-generated Copilot keys (type='personal') both authenticate via the X-API-Key header on /api/mcp/copilot
  • Copilot Keys settings page creates, lists, and deletes keys against the local api_key table

Notes for review

  • The 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 use POST /api/workflows/{id}/execute (the same endpoint simstudio-ts-sdk wraps).
  • Commit 3 changes the agent block's apiKey field from required: true to required: 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.
  • The authenticateCopilotApiKey local lookup in commit 3 (refined in 5) has the same expires_at semantics as authenticateApiKeyByHash. It does not enforce key-type (accepts both personal and workspace) 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.

ForrestOfFidum and others added 7 commits May 3, 2026 00:46
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>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 3, 2026

@ForrestOfFidum is attempting to deploy a commit to the Sim Team on Vercel.

A member of the Team first needs to authorize it.

@cursor
Copy link
Copy Markdown

cursor Bot commented May 3, 2026

PR Summary

Medium Risk
Moderate risk because it changes authentication/key-handling flows and exposes many additional MCP tool operations (including destructive ones) to external clients, albeit largely gated to !isHosted. Hosted behavior is intended to remain unchanged but regressions could impact key validation or tool availability.

Overview
Improves self-hosted operation by adding local fallbacks where the hosted “mothership” proxy flow fails: Copilot key generation/list/delete now read/write the local api_key table, and the MCP Copilot endpoint accepts locally-stored API keys via keyHash lookup before attempting remote validation.

Expands the MCP tool surface by adding many more entries to DIRECT_TOOL_DEFS so externally connected MCP clients can see and invoke existing local handlers for workflow inspection/mutation, deployment actions, MCP server publishing, workspace assets, OAuth helpers, jobs, and workspace file operations (while explicitly not exposing the run_* tools).

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 apiKey to be required only when isHosted, and surfacing BYOK and Copilot Keys in workspace settings navigation for self-hosted instances.

Reviewed by Cursor Bugbot for commit 04be306. Bugbot is set up for automated code reviews on this repo. Configure here.

@NextLevelManagementAdvisors
Copy link
Copy Markdown
Author

Closing — withdrawing while user reviews.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 3, 2026

Greptile Summary

This 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 requiresHosted. All isHosted gates are preserved for the hosted path. The run_* tools are correctly excluded from DIRECT_TOOL_DEFS per the route: 'client' catalog constraint.

Confidence Score: 4/5

Safe 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

Filename Overview
apps/sim/app/api/copilot/api-keys/generate/route.ts Adds local DB key generation on !isHosted — correctly hashes the key, stores it as type='personal', and returns the plaintext key once. Logic is sound; no issues found.
apps/sim/app/api/copilot/api-keys/route.ts Adds local GET (list personal keys) and DELETE (scoped to userId) on !isHosted. Both paths are correctly scoped to the authenticated user and use parameterized queries.
apps/sim/app/api/mcp/copilot/route.ts Adds local hash-based API key lookup before Mothership on !isHosted. Two P2 issues: lastUsed is never updated in the local path (always shows null in UI), and expired keys fall through to remote validation producing a misleading error message.
apps/sim/app/workspace/[workspaceId]/settings/navigation.ts Removes requiresHosted from BYOK and Copilot Keys nav items. Contains a stale comment claiming key generation still proxies to sim.ai, when it now works locally.
apps/sim/blocks/utils.ts Changes apiKey field from required:true to required:isHosted, making the per-block API key optional on self-hosted where BYOK supplies credentials at execution time. Change is intentional and correctly gated.
apps/sim/lib/api-key/byok.ts Adds a self-hosted BYOK path that returns workspace BYOK keys before any hosted-model gate. Supported providers (openai, anthropic, google, mistral) are correctly matched; falls through on !byokResult so per-block key still works when no BYOK key is configured.
apps/sim/lib/copilot/tools/mcp/definitions.ts Adds 33 DIRECT_TOOL_DEFS entries exposing workflow CRUD, deployment, MCP server publishing, assets, OAuth, jobs, and VFS. All toolIds verified against registered handlers in register-handlers.ts. run_* tools correctly excluded per catalog route constraints.

Sequence Diagram

sequenceDiagram
    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
Loading

Reviews (1): Last reviewed commit: "feat(self-hosted): handle Copilot API ke..." | Re-trigger Greptile

Comment on lines +153 to +154
// Copilot keys page works locally for managing keys (proxies to sim.ai
// for new key generation but listing/storing existing keys works).
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Suggested change
// 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).

Comment on lines +66 to +82
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,
})
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Comment on lines +74 to +82
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,
})
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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 })
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 04be306. Configure here.

@NextLevelManagementAdvisors NextLevelManagementAdvisors deleted the feature/expose-direct-tools-via-mcp branch May 3, 2026 08:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants