Add MCP servers and custom tools for bots

Two new settings pages, MCPs and Tools. Super admins define them; super
admins and admins attach them to bots, to workspaces (every bot there),
or to every bot. Bots get them in chat replies and tasks.

- Custom tools: a JSON-schema of arguments plus an HTTP request, a script
  on the bot's computer, or a prompt, templated with {{args.x}} and
  {{env.KEY}}, secrets redacted from results.
- MCP servers: remote (URL and headers) or stdio commands run on the host
  with a setup command and an isolated env, in a per-process pool. Tools
  are listed into a cache and connect on first call.
- Add, edit, access and try open in modals.
- Markdown, JSON and command fields use twui's AceEditor (CodeEditor),
  its scripts loaded on demand.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-19 15:01:39 +01:00
co-authored by Claude Opus 5.5
parent 550aaeabac
commit c6ec7d77a2
95 changed files with 4733 additions and 164 deletions
+39
View File
@@ -0,0 +1,39 @@
import { Database } from "bun:sqlite";
import { beforeAll, describe, expect, test } from "bun:test";
import botExtensionsSql from "./bot-extensions-sql";
const db = new Database(":memory:");
beforeAll(() => {
db.run(`CREATE TABLE custom_tools (id INTEGER PRIMARY KEY, all_bots INTEGER, enabled INTEGER)`);
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.
*/
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 workspace_custom_tools VALUES (9, 3)`);
});
function ids(params: Parameters<typeof botExtensionsSql>[0]) {
const { sql, values } = botExtensionsSql(params);
return db
.query(sql)
.all(...values)
.map((row) => (row as { id: number }).id);
}
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]);
expect(ids({ kind: "tool", bot_id: 6 })).toEqual([1]);
});
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]);
});
});