Give bots tools and MCP servers per bot and workspace, with status and tests

- Bots and workspaces get tools and MCP servers from their settings
  pages; bots also from chips under the chat composer, removable there
- "Every bot" now gives one to every bot now and every new bot; each can
  still drop it (attach-every-bot-extensions.ts moves old installs over)
- Status lines: MCP servers record whether they connect, tools whether
  their last run worked
- Test connection in the MCP form and a Test panel in the tool form check
  the form as it is, without saving
- Hide Google sign-in on the login form

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-19 15:35:13 +01:00
co-authored by Claude Opus 5.5
parent c6ec7d77a2
commit 6294b911e6
69 changed files with 2026 additions and 271 deletions
@@ -0,0 +1,56 @@
import testMcpServer from "@/src/harness/mcp/test-mcp-server";
import type { TestMcpServerRequest } from "@/src/schemas/extension-schema";
import type { McpToolInfo } from "@/src/types";
import mcpServerSlug from "@/src/utils/mcp-server-slug";
import redactSecrets from "@/src/utils/redact-secrets";
import grabRunEnvs from "./grab-run-envs";
type Params = {
data: TestMcpServerRequest;
};
/**
* Connects to an MCP server as its form has it (saved or not) and lists
* its tools, saving nothing. Never throws for the server's own failure:
* returns why, secrets blanked out.
*/
export default async function testMcpDraft({
data,
}: Params): Promise<{ ok: true; tools: McpToolInfo[] } | { ok: false; error: string }> {
const envs = await grabRunEnvs();
try {
const tools = await testMcpServer({
server: {
id: data.mcp_server_id || 0,
name: data.name || "The MCP server",
slug: mcpServerSlug({ name: data.name }),
description: data.description,
transport: data.transport,
url: data.url,
headers: data.headers,
command: data.command,
setup_command: data.setup_command,
env: data.env,
excluded_tools: data.excluded_tools,
tools: [],
all_bots: data.all_bots,
enabled: data.enabled,
status: "untested",
last_error: "",
last_synced_at: null,
},
envs,
});
return { ok: true, tools };
} catch (error: unknown) {
return {
ok: false,
error: redactSecrets({
text: error instanceof Error ? error.message : String(error),
secrets: envs.filter((env) => env.secret),
}).slice(0, 4000),
};
}
}