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
+73 -31
View File
@@ -20,30 +20,42 @@ const TemplateMap = z
const TimeoutMs = z.coerce.number().int().min(1000).max(AppData.CustomToolMaxTimeoutMs).optional();
export const SaveMcpServerRequestSchema = z
.object({
mcp_server_id: Id.optional(),
name: z.string().trim().min(1, "Enter a name").max(80),
description: z.string().trim().max(1000).default(""),
transport: z.enum(McpTransports.map((t) => t.value) as [McpTransport, ...McpTransport[]]),
url: z.string().trim().max(2000).default(""),
headers: TemplateMap,
command: z.string().trim().max(4000).default(""),
setup_command: z.string().trim().max(8000).default(""),
env: TemplateMap,
excluded_tools: z.array(z.string().max(200)).max(500).default([]),
enabled: z.boolean().default(true),
all_bots: z.boolean().default(false),
})
.superRefine((value, ctx) => {
if (value.transport == "remote" && !/^(https?:\/\/|\{\{\s*env\.)/.test(value.url)) {
ctx.addIssue({ code: "custom", path: ["url"], message: "Enter the server's http(s) URL." });
}
const McpServerFields = z.object({
mcp_server_id: Id.optional(),
name: z.string().trim().min(1, "Enter a name").max(80),
description: z.string().trim().max(1000).default(""),
transport: z.enum(McpTransports.map((t) => t.value) as [McpTransport, ...McpTransport[]]),
url: z.string().trim().max(2000).default(""),
headers: TemplateMap,
command: z.string().trim().max(4000).default(""),
setup_command: z.string().trim().max(8000).default(""),
env: TemplateMap,
excluded_tools: z.array(z.string().max(200)).max(500).default([]),
enabled: z.boolean().default(true),
all_bots: z.boolean().default(false),
});
if (value.transport == "stdio" && !value.command) {
ctx.addIssue({ code: "custom", path: ["command"], message: "Enter the command that starts the server." });
}
});
function checkMcpServerReach(
value: { transport: McpTransport; url: string; command: string },
ctx: z.RefinementCtx,
) {
if (value.transport == "remote" && !/^(https?:\/\/|\{\{\s*env\.)/.test(value.url)) {
ctx.addIssue({ code: "custom", path: ["url"], message: "Enter the server's http(s) URL." });
}
if (value.transport == "stdio" && !value.command) {
ctx.addIssue({ code: "custom", path: ["command"], message: "Enter the command that starts the server." });
}
}
export const SaveMcpServerRequestSchema = McpServerFields.superRefine(checkMcpServerReach);
/**
* A server as the form has it, saved or not: a name isn't needed yet.
*/
export const TestMcpServerRequestSchema = McpServerFields.extend({
name: z.string().trim().max(80).default(""),
}).superRefine(checkMcpServerReach);
export const McpServerIdRequestSchema = z.object({
mcp_server_id: Id,
@@ -63,6 +75,22 @@ export const SetExtensionAccessRequestSchema = z.object({
all_bots: z.boolean().optional(),
});
export const SetBotExtensionRequestSchema = z.object({
bot_id: Id,
kind: z.enum(["mcp", "tool"]),
id: Id,
/**
* The workspace the bot is seen in, for the list that comes back.
*/
workspace_id: Id.nullable().optional(),
});
export const SetWorkspaceExtensionRequestSchema = z.object({
workspace_id: Id,
kind: z.enum(["mcp", "tool"]),
id: Id,
});
/**
* A tool's arguments: a JSON schema of an object.
*/
@@ -121,14 +149,28 @@ export const CustomToolIdRequestSchema = z.object({
custom_tool_id: Id,
});
export const TestCustomToolRequestSchema = z.object({
custom_tool_id: Id,
args: z.record(z.string(), z.unknown()).default({}),
/**
* Where a script tool runs. The main computer when not given.
*/
computer_id: Id.optional(),
});
/**
* A tool as its form has it, saved or not: only what runs it.
*/
const DraftCustomTool = z.object({ parameters: ToolParameters }).and(ToolConfig);
/**
* Tries a saved tool (`custom_tool_id`, the run counts as its last) or the
* one in the form (`draft`, nothing saved).
*/
export const TestCustomToolRequestSchema = z
.object({
custom_tool_id: Id.optional(),
draft: DraftCustomTool.optional(),
args: z.record(z.string(), z.unknown()).default({}),
/**
* Where a script tool runs. The main computer when not given.
*/
computer_id: Id.optional(),
})
.refine((value) => value.custom_tool_id || value.draft, { message: "Pick a tool to try." });
export type SaveMcpServerRequest = z.infer<typeof SaveMcpServerRequestSchema>;
export type SaveCustomToolRequest = z.infer<typeof SaveCustomToolRequestSchema>;
export type TestMcpServerRequest = z.infer<typeof TestMcpServerRequestSchema>;
export type DraftCustomTool = z.infer<typeof DraftCustomTool>;