Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Workbench is a client of [Interchange](https://github.com/faremeter/interchange).
The hub mounts only stock `@intx/hub-api` routes plus a set of Corbits
libraries, each mounted once (`@corbits/mailbox`, `@corbits/memory`,
`@corbits/artifacts`, `@corbits/webhooks`, `@corbits/cron`, `@corbits/mcp`, an
`@corbits/artifacts`, `@corbits/webhooks`, `@corbits/cron`, `@corbits/mcp`, `@corbits/agent-token`, an
OAuth library on `@corbits/oauth-core`); any other hub mount is cutover debt with a Linear
issue, never a pattern to extend. A workbench is a plain
Interchange tenant, nothing more. The web client sets itself up over stock
Expand Down
2 changes: 1 addition & 1 deletion VENDORED.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Pinned to [faremeter/interchange](https://github.com/faremeter/interchange)
| `vendor/intx/db` | `@intx/db`, with one local serving-time credential-refresh delta | maintainers | 2026-11-03 |
| `vendor/intx/harness` | `@intx/harness` at a newer commit than npm has published | maintainers | 2026-11-03 |
| `vendor/intx/hub-agent` | `@intx/hub-agent`, with a local OAuth-login-over-websocket delta | maintainers | 2026-11-03 |
| `vendor/intx/hub-api` | `@intx/hub-api`, with local approval and workflow-deploy-bearer routes | maintainers | 2026-11-03 |
| `vendor/intx/hub-api` | `@intx/hub-api`, with the local workflow-deploy-bearer route | maintainers | 2026-11-03 |
| `vendor/intx/hub-sessions` | `@intx/hub-sessions`, with several local run/pack-acceptance fixes | maintainers | 2026-11-03 |
| `vendor/intx/inference` | `@intx/inference`, with one local Google file-upload compile fix | maintainers | 2026-11-03 |
| `vendor/intx/mail-memory` | `@intx/mail-memory` at a newer commit than npm has published | maintainers | 2026-11-03 |
Expand Down
2 changes: 0 additions & 2 deletions apps/hub/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
"dependencies": {
"@chat-adapter/state-memory": "^4.35.0",
"@corbits/agent-token": "github:corbitsdev/corbits-agent-token#ed286f4592190f911de480ef5330ce03d8605f00",
"@corbits/approvals": "workspace:*",
"@corbits/artifacts": "github:corbitsdev/corbits-artifacts#4a2a3c22187991a6ce598faf794908fedd252e98",
"@corbits/codex-provider": "github:corbitsdev/corbits-codex-provider#a1f475f341261c05fa8dade0c82f1246c175b5b3",
"@corbits/cron": "0.2.1",
Expand All @@ -23,7 +22,6 @@
"@corbits/mcp": "github:corbitsdev/corbits-mcp#9df1ab81d2f50d798bca2d2f44eb4759c92fac0e",
"@corbits/memory": "github:corbitsdev/corbits-memory#8be608becc749fa0795a899826277823dca6db3e",
"@corbits/oauth-core": "github:corbitsdev/corbits-oauth-core#3028f8604155eee63f5a1711f0d4022d7984e3f7",
"@corbits/url-path": "workspace:*",
"@corbits/webhooks": "github:corbitsdev/webhooks#3f4f83147fd826d3aec5bcac7ef4b0ae7f75c00d",
"@corbits/workflows": "workspace:*",
"@corbits/xai-provider": "github:corbitsdev/corbits-xai-provider#9f2d4bac40ea075df092fef807404a13c638cf14",
Expand Down
59 changes: 2 additions & 57 deletions apps/web/src/mcp-servers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
// workbench Worker binds it by walking up, so two workbenches share one Exa
// row and a second workspace sees none of it.
import { MCP_NO_TOKEN_SENTINEL, MCP_STREAMABLE_HTTP_PROVIDER_KEY } from "@corbits/credential-mcp";
import { reportError } from "@corbits/error-sink";
import {
EXA_MCP_SERVER,
MCP_SERVER_CATALOG,
Expand Down Expand Up @@ -344,52 +343,6 @@ export async function removeMcpServer(
});
}

/** Legacy workbench-scoped MCP rows predate the workspace catalog: before it,
* every workbench stored its own Exa. Moves the keyless (`auth === "none"`)
* rows up to the workspace — their sentinel secret is reconstructible, so
* re-adding rediscovers without touching a real credential — deduped by
* handle, then drops the workbench rows they came from. A token row carries
* a secret the client may never read, so it stays where it is: orphaned but
* untouched (accepted — the next redeploy binds the workspace catalog and a
* leftover row simply stops taking effect). */
export async function migrateWorkbenchMcpCatalogToWorkspace(
workbenchTenantId: string,
workspaceTenantId: string,
fetchImpl: typeof fetch = fetch,
): Promise<void> {
if (workbenchTenantId === workspaceTenantId) return;
const [workbenchServers, workspaceServers] = await Promise.all([
listMcpServers(workbenchTenantId, fetchImpl),
listMcpServers(workspaceTenantId, fetchImpl),
]);
const workspaceHandles = new Set(workspaceServers.map((server) => server.handle));
for (const server of workbenchServers) {
// Only a keyless row can move without its secret; a token row stays
// orphaned on the workbench (see above).
if (server.auth !== "none") continue;
if (!workspaceHandles.has(server.handle)) {
const moved = await addMcpServer(
{
tenantId: workspaceTenantId,
handle: server.handle,
name: server.name,
url: server.url,
},
fetchImpl,
);
workspaceHandles.add(moved.handle);
}
await removeMcpServer(
{
tenantId: workbenchTenantId,
credentialId: server.credentialId,
providerId: server.providerId,
},
fetchImpl,
);
}
}

/** Ensures that land on one workspace (two workbench Workers deploying, a
* StrictMode double-invoke) run one after another, so a later one sees the
* Exa an earlier one added instead of adding its own. */
Expand All @@ -399,15 +352,13 @@ const ensureQueue = new Map<string, Promise<readonly McpServer[]>>();
* the workspace (top-level) tenant, then left alone so a later removal is
* not undone by the next start. Callers pass any tenant id — a workbench id
* resolves up to its workspace — and each workbench Worker binds the shared
* row by walking up. Also moves that caller's legacy workbench rows up,
* best-effort: a failed cleanup must not block deploying Worker with the
* workspace catalog that is already correct. */
* row by walking up. */
export async function ensureBuiltInMcpServers(
tenantId: string,
fetchImpl: typeof fetch = fetch,
): Promise<readonly McpServer[]> {
const workspaceTenantId = await resolveWorkspaceTenantId(tenantId, fetchImpl);
const run = () => runEnsureBuiltInMcpServers(tenantId, workspaceTenantId, fetchImpl);
const run = () => runEnsureBuiltInMcpServers(workspaceTenantId, fetchImpl);
const previous = ensureQueue.get(workspaceTenantId);
// A failed earlier ensure already rejected to its own caller; this one runs regardless.
const flight = previous === undefined ? run() : previous.then(run, run);
Expand All @@ -420,15 +371,9 @@ export async function ensureBuiltInMcpServers(
}

async function runEnsureBuiltInMcpServers(
tenantId: string,
workspaceTenantId: string,
fetchImpl: typeof fetch,
): Promise<readonly McpServer[]> {
try {
await migrateWorkbenchMcpCatalogToWorkspace(tenantId, workspaceTenantId, fetchImpl);
} catch (cause) {
reportError(cause, { operation: "mcp_servers_catalog_migration" });
}
const existing = await listMcpServers(workspaceTenantId, fetchImpl);
if (existing.some((server) => server.handle === EXA_MCP_SERVER.handle)) return existing;
const added = await addMcpServer(
Expand Down
38 changes: 21 additions & 17 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading