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:
@@ -9,11 +9,11 @@ beforeAll(() => {
|
||||
db.run(`CREATE TABLE bot_custom_tools (bot_id INTEGER, custom_tool_id INTEGER)`);
|
||||
db.run(`CREATE TABLE workspace_custom_tools (workspace_id INTEGER, custom_tool_id INTEGER)`);
|
||||
/**
|
||||
* 1: every bot. 2: bot 5. 3: workspace 9. 4: every bot, off.
|
||||
* 5: nobody.
|
||||
* 1: every bot (given to 5 and 6). 2: bot 5. 3: workspace 9. 4: off,
|
||||
* given to 5. 5: nobody. 6: every bot, but 6 dropped it.
|
||||
*/
|
||||
db.run(`INSERT INTO custom_tools VALUES (1, 1, 1), (2, 0, 1), (3, 0, 1), (4, 1, 0), (5, 0, 1)`);
|
||||
db.run(`INSERT INTO bot_custom_tools VALUES (5, 2)`);
|
||||
db.run(`INSERT INTO custom_tools VALUES (1, 1, 1), (2, 0, 1), (3, 0, 1), (4, 1, 0), (5, 0, 1), (6, 1, 1)`);
|
||||
db.run(`INSERT INTO bot_custom_tools VALUES (5, 1), (6, 1), (5, 2), (5, 4), (5, 6)`);
|
||||
db.run(`INSERT INTO workspace_custom_tools VALUES (9, 3)`);
|
||||
});
|
||||
|
||||
@@ -26,14 +26,15 @@ function ids(params: Parameters<typeof botExtensionsSql>[0]) {
|
||||
}
|
||||
|
||||
describe("botExtensionsSql", () => {
|
||||
test("a bot gets every-bot ones and its own, never disabled ones", () => {
|
||||
expect(ids({ kind: "tool", bot_id: 5 })).toEqual([1, 2]);
|
||||
test("a bot gets what it's given, never disabled ones, nor an every-bot one it dropped", () => {
|
||||
expect(ids({ kind: "tool", bot_id: 5 })).toEqual([1, 2, 6]);
|
||||
expect(ids({ kind: "tool", bot_id: 6 })).toEqual([1]);
|
||||
expect(ids({ kind: "tool", bot_id: 7 })).toEqual([]);
|
||||
});
|
||||
|
||||
test("in a workspace, the workspace's too", () => {
|
||||
expect(ids({ kind: "tool", bot_id: 6, workspace_id: 9 })).toEqual([1, 3]);
|
||||
expect(ids({ kind: "tool", bot_id: 5, workspace_id: 9 })).toEqual([1, 2, 3]);
|
||||
expect(ids({ kind: "tool", bot_id: 5, workspace_id: 8 })).toEqual([1, 2]);
|
||||
expect(ids({ kind: "tool", bot_id: 5, workspace_id: 9 })).toEqual([1, 2, 3, 6]);
|
||||
expect(ids({ kind: "tool", bot_id: 5, workspace_id: 8 })).toEqual([1, 2, 6]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,16 +27,17 @@ const Tables = {
|
||||
|
||||
/**
|
||||
* The enabled MCP servers or custom tools a bot gets on one run: those
|
||||
* for every bot, those attached to the bot, and in a workspace those
|
||||
* attached to the workspace.
|
||||
* attached to the bot, and in a workspace those attached to the
|
||||
* workspace. "Every bot" only decides what a bot is given (now and when
|
||||
* it's created, `attachToAllBots`, `attachDefaultExtensions`); one it
|
||||
* dropped stays dropped.
|
||||
*/
|
||||
export default function botExtensionsSql({ kind, bot_id, workspace_id }: Params) {
|
||||
const { table, bot_link, workspace_link, column } = Tables[kind];
|
||||
|
||||
return {
|
||||
sql: `SELECT x.* FROM ${table} x WHERE x.enabled = 1 AND (
|
||||
x.all_bots = 1
|
||||
OR x.id IN (SELECT ${column} FROM ${bot_link} WHERE bot_id = ?)
|
||||
x.id IN (SELECT ${column} FROM ${bot_link} WHERE bot_id = ?)
|
||||
${workspace_id ? `OR x.id IN (SELECT ${column} FROM ${workspace_link} WHERE workspace_id = ?)` : ""}
|
||||
) ORDER BY x.id ASC`,
|
||||
values: workspace_id ? [Number(bot_id), Number(workspace_id)] : [Number(bot_id)],
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { extensionOptionValue, parseExtensionOptionValue } from "./extension-option-value";
|
||||
|
||||
describe("extensionOptionValue", () => {
|
||||
test("round-trips", () => {
|
||||
expect(parseExtensionOptionValue({ value: extensionOptionValue({ kind: "mcp", id: 3 }) })).toEqual({
|
||||
kind: "mcp",
|
||||
id: 3,
|
||||
});
|
||||
});
|
||||
|
||||
test("refuses anything else", () => {
|
||||
expect(parseExtensionOptionValue({ value: "" })).toBeNull();
|
||||
expect(parseExtensionOptionValue({ value: "bot:2" })).toBeNull();
|
||||
expect(parseExtensionOptionValue({ value: "tool:x" })).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { BotExtension } from "@/src/types";
|
||||
|
||||
/**
|
||||
* An MCP server or tool as one select value (`mcp:3`), and back.
|
||||
*/
|
||||
export function extensionOptionValue({ kind, id }: { kind: BotExtension["kind"]; id: number }) {
|
||||
return `${kind}:${id}`;
|
||||
}
|
||||
|
||||
export function parseExtensionOptionValue({ value }: { value: string }) {
|
||||
const [kind, id] = value.split(":");
|
||||
|
||||
if ((kind != "mcp" && kind != "tool") || !Number(id)) return null;
|
||||
|
||||
return { kind: kind as BotExtension["kind"], id: Number(id) };
|
||||
}
|
||||
Reference in New Issue
Block a user