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
+316
View File
@@ -20,6 +20,8 @@ import { BotJobKinds, BotJobUpdateKinds } from "@/src/dict/bot-queue-dict";
import { AppData } from "@/src/data/app-data";
import type { BUN_SQLITE_DatabaseSchemaType } from "@moduletrace/bun-sqlite/types";
import { EnvParadigms } from "@/src/dict/env-dict";
import { McpServerStatuses, McpTransports } from "@/src/dict/mcp-dict";
import { CustomToolKinds } from "@/src/dict/custom-tool-dict";
const schema: BUN_SQLITE_DatabaseSchemaType = {
tables: [
@@ -1682,6 +1684,320 @@ const schema: BUN_SQLITE_DatabaseSchemaType = {
},
],
},
{
tableName: "mcp_servers",
tableDescription: `MCP servers bots can use (Settings, under MCPs). Per bot (bot_mcp_servers), per workspace (workspace_mcp_servers) or every bot (all_bots). Private: headers and env may hold literal secrets`,
fields: [
{
fieldName: "name",
dataType: "TEXT",
},
{
fieldName: "slug",
fieldDescription: `Prefixes its tools' names (<slug>__<tool>). Unique`,
dataType: "TEXT",
unique: true,
},
{
fieldName: "description",
dataType: "TEXT",
},
{
fieldName: "transport",
dataType: "TEXT",
options: McpTransports.map((t) => t.value),
defaultValue: "remote",
},
{
fieldName: "url",
fieldDescription: `remote: the server's MCP endpoint`,
dataType: "TEXT",
},
{
fieldName: "headers",
fieldDescription: `remote: request headers, {"Authorization":"Bearer {{env.KEY}}"}. Values are templates`,
dataType: "TEXT",
json: true,
},
{
fieldName: "command",
fieldDescription: `stdio: the command that starts the server, run with the host's shell in DATA_DIR/mcp/<id>`,
dataType: "TEXT",
},
{
fieldName: "setup_command",
fieldDescription: `stdio: installs what the command needs. Runs before the first start and again whenever it changes`,
dataType: "TEXT",
},
{
fieldName: "env",
fieldDescription: `stdio: the only environment the server gets (besides PATH and HOME), {"GITHUB_TOKEN":"{{env.GITHUB_TOKEN}}"}. Values are templates`,
dataType: "TEXT",
json: true,
},
{
fieldName: "excluded_tools",
fieldDescription: `Tool names (the server's own) bots don't get`,
dataType: "TEXT",
json: true,
},
{
fieldName: "tools_cache",
fieldDescription: `The server's tools as last listed ([{name, description, input_schema}]). Runs build their tools from it and connect on the first call`,
dataType: "TEXT",
json: true,
},
{
fieldName: "all_bots",
fieldDescription: `1 gives it to every bot, the coordinator included`,
dataType: "INTEGER",
options: [0, 1],
defaultValue: 0,
},
{
fieldName: "enabled",
dataType: "INTEGER",
options: [0, 1],
defaultValue: 1,
},
{
fieldName: "status",
dataType: "TEXT",
options: McpServerStatuses.map((s) => s.value),
defaultValue: "untested",
},
{
fieldName: "last_error",
dataType: "TEXT",
},
{
fieldName: "last_synced_at",
fieldDescription: `Unix ms of the last tool listing`,
dataType: "INTEGER",
},
{
fieldName: "user_id",
fieldDescription: `Who created it. Not a foreign key: it belongs to the install`,
dataType: "INTEGER",
},
],
},
{
tableName: "custom_tools",
tableDescription: `Tools bots can call (Settings, under Tools): an HTTP request, a script on a computer, or a prompt. Per bot (bot_custom_tools), per workspace (workspace_custom_tools) or every bot (all_bots)`,
fields: [
{
fieldName: "name",
fieldDescription: `What the model calls it: snake_case, unique, never a built-in tool's name`,
dataType: "TEXT",
unique: true,
},
{
fieldName: "description",
fieldDescription: `What the model reads to decide when to call it`,
dataType: "TEXT",
},
{
fieldName: "kind",
dataType: "TEXT",
options: CustomToolKinds.map((k) => k.value),
},
{
fieldName: "parameters",
fieldDescription: `JSON schema (an object) of its arguments`,
dataType: "TEXT",
json: true,
},
{
fieldName: "config",
fieldDescription: `Per kind. http: {method, url, headers, body, timeout_ms}; script: {language, code, timeout_ms}; prompt: {prompt}. Text fields are templates ({{args.x}}, {{env.KEY}})`,
dataType: "TEXT",
json: true,
},
{
fieldName: "all_bots",
fieldDescription: `1 gives it to every bot, the coordinator included`,
dataType: "INTEGER",
options: [0, 1],
defaultValue: 0,
},
{
fieldName: "enabled",
dataType: "INTEGER",
options: [0, 1],
defaultValue: 1,
},
{
fieldName: "user_id",
fieldDescription: `Who created it. Not a foreign key: it belongs to the install`,
dataType: "INTEGER",
},
],
},
{
tableName: "bot_mcp_servers",
tableDescription: `A bot that gets an MCP server's tools everywhere it works`,
fields: [
{
fieldName: "bot_id",
dataType: "INTEGER",
foreignKey: {
cascadeDelete: true,
destinationTableName: "bots",
destinationTableColumnName: "id",
destinationTableColumnType: "INTEGER",
foreignKeyName: "bot_mcp_servers_bots_fk",
},
},
{
fieldName: "mcp_server_id",
dataType: "INTEGER",
foreignKey: {
cascadeDelete: true,
destinationTableName: "mcp_servers",
destinationTableColumnName: "id",
destinationTableColumnType: "INTEGER",
foreignKeyName: "bot_mcp_servers_mcp_servers_fk",
},
},
],
uniqueConstraints: [
{
alias: "one_mcp_server_per_bot",
constraintName: "A bot gets an MCP server once",
constraintTableFields: [{ value: "bot_id" }, { value: "mcp_server_id" }],
},
],
indexes: [
{
indexName: "idx_bot_mcp_servers_mcp_server_id",
indexTableFields: ["mcp_server_id"],
},
],
},
{
tableName: "workspace_mcp_servers",
tableDescription: `Every bot in the workspace gets the MCP server's tools there`,
fields: [
{
fieldName: "workspace_id",
dataType: "INTEGER",
foreignKey: {
cascadeDelete: true,
destinationTableName: "workspaces",
destinationTableColumnName: "id",
destinationTableColumnType: "INTEGER",
foreignKeyName: "workspace_mcp_servers_workspaces_fk",
},
},
{
fieldName: "mcp_server_id",
dataType: "INTEGER",
foreignKey: {
cascadeDelete: true,
destinationTableName: "mcp_servers",
destinationTableColumnName: "id",
destinationTableColumnType: "INTEGER",
foreignKeyName: "workspace_mcp_servers_mcp_servers_fk",
},
},
],
uniqueConstraints: [
{
alias: "one_mcp_server_per_workspace",
constraintName: "A workspace gets an MCP server once",
constraintTableFields: [{ value: "workspace_id" }, { value: "mcp_server_id" }],
},
],
indexes: [
{
indexName: "idx_workspace_mcp_servers_mcp_server_id",
indexTableFields: ["mcp_server_id"],
},
],
},
{
tableName: "bot_custom_tools",
tableDescription: `A bot that gets a custom tool everywhere it works`,
fields: [
{
fieldName: "bot_id",
dataType: "INTEGER",
foreignKey: {
cascadeDelete: true,
destinationTableName: "bots",
destinationTableColumnName: "id",
destinationTableColumnType: "INTEGER",
foreignKeyName: "bot_custom_tools_bots_fk",
},
},
{
fieldName: "custom_tool_id",
dataType: "INTEGER",
foreignKey: {
cascadeDelete: true,
destinationTableName: "custom_tools",
destinationTableColumnName: "id",
destinationTableColumnType: "INTEGER",
foreignKeyName: "bot_custom_tools_custom_tools_fk",
},
},
],
uniqueConstraints: [
{
alias: "one_custom_tool_per_bot",
constraintName: "A bot gets a custom tool once",
constraintTableFields: [{ value: "bot_id" }, { value: "custom_tool_id" }],
},
],
indexes: [
{
indexName: "idx_bot_custom_tools_custom_tool_id",
indexTableFields: ["custom_tool_id"],
},
],
},
{
tableName: "workspace_custom_tools",
tableDescription: `Every bot in the workspace gets the custom tool there`,
fields: [
{
fieldName: "workspace_id",
dataType: "INTEGER",
foreignKey: {
cascadeDelete: true,
destinationTableName: "workspaces",
destinationTableColumnName: "id",
destinationTableColumnType: "INTEGER",
foreignKeyName: "workspace_custom_tools_workspaces_fk",
},
},
{
fieldName: "custom_tool_id",
dataType: "INTEGER",
foreignKey: {
cascadeDelete: true,
destinationTableName: "custom_tools",
destinationTableColumnName: "id",
destinationTableColumnType: "INTEGER",
foreignKeyName: "workspace_custom_tools_custom_tools_fk",
},
},
],
uniqueConstraints: [
{
alias: "one_custom_tool_per_workspace",
constraintName: "A workspace gets a custom tool once",
constraintTableFields: [{ value: "workspace_id" }, { value: "custom_tool_id" }],
},
],
indexes: [
{
indexName: "idx_workspace_custom_tools_custom_tool_id",
indexTableFields: ["custom_tool_id"],
},
],
},
{
tableName: "logs",
fields: [
+181 -1
View File
@@ -29,6 +29,12 @@ export const BunSQLiteTables = [
"harness_settings",
"bot_harness_settings",
"workspace_harness_settings",
"mcp_servers",
"custom_tools",
"bot_mcp_servers",
"workspace_mcp_servers",
"bot_custom_tools",
"workspace_custom_tools",
"logs",
"envs",
] as const
@@ -1026,6 +1032,180 @@ export type BUN_SQLITE_COBBLE_WORKSPACE_HARNESS_SETTINGS = {
options?: string;
}
export type BUN_SQLITE_COBBLE_MCP_SERVERS = {
/**
* The unique identifier of the record.
*/
id?: number | "";
/**
* The time when the record was created. (Unix Timestamp)
*/
created_at?: number | "";
/**
* The time when the record was updated. (Unix Timestamp)
*/
updated_at?: number | "";
name?: string;
/**
* Prefixes its tools' names (<slug>__<tool>). Unique
*/
slug?: string;
description?: string;
transport?: "remote" | "stdio" | "";
/**
* remote: the server's MCP endpoint
*/
url?: string;
/**
* remote: request headers, {"Authorization":"Bearer {{env.KEY}}"}. Values are templates
*/
headers?: string;
/**
* stdio: the command that starts the server, run with the host's shell in DATA_DIR/mcp/<id>
*/
command?: string;
/**
* stdio: installs what the command needs. Runs before the first start and again whenever it changes
*/
setup_command?: string;
/**
* stdio: the only environment the server gets (besides PATH and HOME), {"GITHUB_TOKEN":"{{env.GITHUB_TOKEN}}"}. Values are templates
*/
env?: string;
/**
* Tool names (the server's own) bots don't get
*/
excluded_tools?: string;
/**
* The server's tools as last listed ([{name, description, input_schema}]). Runs build their tools from it and connect on the first call
*/
tools_cache?: string;
/**
* 1 gives it to every bot, the coordinator included
*/
all_bots?: 0 | 1 | "";
enabled?: 0 | 1 | "";
status?: "untested" | "ok" | "error" | "";
last_error?: string;
/**
* Unix ms of the last tool listing
*/
last_synced_at?: number | "";
/**
* Who created it. Not a foreign key: it belongs to the install
*/
user_id?: number | "";
}
export type BUN_SQLITE_COBBLE_CUSTOM_TOOLS = {
/**
* The unique identifier of the record.
*/
id?: number | "";
/**
* The time when the record was created. (Unix Timestamp)
*/
created_at?: number | "";
/**
* The time when the record was updated. (Unix Timestamp)
*/
updated_at?: number | "";
/**
* What the model calls it: snake_case, unique, never a built-in tool's name
*/
name?: string;
/**
* What the model reads to decide when to call it
*/
description?: string;
kind?: "http" | "script" | "prompt" | "";
/**
* JSON schema (an object) of its arguments
*/
parameters?: string;
/**
* Per kind. http: {method, url, headers, body, timeout_ms}; script: {language, code, timeout_ms}; prompt: {prompt}. Text fields are templates ({{args.x}}, {{env.KEY}})
*/
config?: string;
/**
* 1 gives it to every bot, the coordinator included
*/
all_bots?: 0 | 1 | "";
enabled?: 0 | 1 | "";
/**
* Who created it. Not a foreign key: it belongs to the install
*/
user_id?: number | "";
}
export type BUN_SQLITE_COBBLE_BOT_MCP_SERVERS = {
/**
* The unique identifier of the record.
*/
id?: number | "";
/**
* The time when the record was created. (Unix Timestamp)
*/
created_at?: number | "";
/**
* The time when the record was updated. (Unix Timestamp)
*/
updated_at?: number | "";
bot_id?: number | "";
mcp_server_id?: number | "";
}
export type BUN_SQLITE_COBBLE_WORKSPACE_MCP_SERVERS = {
/**
* The unique identifier of the record.
*/
id?: number | "";
/**
* The time when the record was created. (Unix Timestamp)
*/
created_at?: number | "";
/**
* The time when the record was updated. (Unix Timestamp)
*/
updated_at?: number | "";
workspace_id?: number | "";
mcp_server_id?: number | "";
}
export type BUN_SQLITE_COBBLE_BOT_CUSTOM_TOOLS = {
/**
* The unique identifier of the record.
*/
id?: number | "";
/**
* The time when the record was created. (Unix Timestamp)
*/
created_at?: number | "";
/**
* The time when the record was updated. (Unix Timestamp)
*/
updated_at?: number | "";
bot_id?: number | "";
custom_tool_id?: number | "";
}
export type BUN_SQLITE_COBBLE_WORKSPACE_CUSTOM_TOOLS = {
/**
* The unique identifier of the record.
*/
id?: number | "";
/**
* The time when the record was created. (Unix Timestamp)
*/
created_at?: number | "";
/**
* The time when the record was updated. (Unix Timestamp)
*/
updated_at?: number | "";
workspace_id?: number | "";
custom_tool_id?: number | "";
}
export type BUN_SQLITE_COBBLE_LOGS = {
/**
* The unique identifier of the record.
@@ -1120,4 +1300,4 @@ export type BUN_SQLITE_COBBLE_ENVS = {
expose_to_computers?: 0 | 1 | "";
}
export type BUN_SQLITE_COBBLE_ALL_TYPEDEFS = BUN_SQLITE_COBBLE_USERS & BUN_SQLITE_COBBLE_USER_TYPES & BUN_SQLITE_COBBLE_SSO_LOGIN_CODES & BUN_SQLITE_COBBLE_USER_PASSWORDS & BUN_SQLITE_COBBLE_MEDIA & BUN_SQLITE_COBBLE_MEDIA_PARADIGMS & BUN_SQLITE_COBBLE_BOTS & BUN_SQLITE_COBBLE_BOT_SESSIONS & BUN_SQLITE_COBBLE_BOT_MESSAGES & BUN_SQLITE_COBBLE_BOT_TASKS & BUN_SQLITE_COBBLE_BOT_TASK_STEPS & BUN_SQLITE_COBBLE_BOTS_QUEUE & BUN_SQLITE_COBBLE_BOTS_QUEUE_UPDATES & BUN_SQLITE_COBBLE_CRON_JOBS & BUN_SQLITE_COBBLE_WORKSPACES & BUN_SQLITE_COBBLE_WORKSPACE_BOTS & BUN_SQLITE_COBBLE_WORKSPACE_USERS & BUN_SQLITE_COBBLE_BOT_USERS & BUN_SQLITE_COBBLE_BOT_USER_MEMORIES & BUN_SQLITE_COBBLE_BOT_USER_FACES & BUN_SQLITE_COBBLE_BOT_MESSAGE_MEDIA & BUN_SQLITE_COBBLE_CONTAINERS & BUN_SQLITE_COBBLE_COMPUTERS & BUN_SQLITE_COBBLE_BOT_COMPUTERS & BUN_SQLITE_COBBLE_WORKSPACE_COMPUTERS & BUN_SQLITE_COBBLE_HARNESS_PROVIDERS & BUN_SQLITE_COBBLE_HARNESS_MODELS & BUN_SQLITE_COBBLE_HARNESS_SETTINGS & BUN_SQLITE_COBBLE_BOT_HARNESS_SETTINGS & BUN_SQLITE_COBBLE_WORKSPACE_HARNESS_SETTINGS & BUN_SQLITE_COBBLE_LOGS & BUN_SQLITE_COBBLE_ENVS
export type BUN_SQLITE_COBBLE_ALL_TYPEDEFS = BUN_SQLITE_COBBLE_USERS & BUN_SQLITE_COBBLE_USER_TYPES & BUN_SQLITE_COBBLE_SSO_LOGIN_CODES & BUN_SQLITE_COBBLE_USER_PASSWORDS & BUN_SQLITE_COBBLE_MEDIA & BUN_SQLITE_COBBLE_MEDIA_PARADIGMS & BUN_SQLITE_COBBLE_BOTS & BUN_SQLITE_COBBLE_BOT_SESSIONS & BUN_SQLITE_COBBLE_BOT_MESSAGES & BUN_SQLITE_COBBLE_BOT_TASKS & BUN_SQLITE_COBBLE_BOT_TASK_STEPS & BUN_SQLITE_COBBLE_BOTS_QUEUE & BUN_SQLITE_COBBLE_BOTS_QUEUE_UPDATES & BUN_SQLITE_COBBLE_CRON_JOBS & BUN_SQLITE_COBBLE_WORKSPACES & BUN_SQLITE_COBBLE_WORKSPACE_BOTS & BUN_SQLITE_COBBLE_WORKSPACE_USERS & BUN_SQLITE_COBBLE_BOT_USERS & BUN_SQLITE_COBBLE_BOT_USER_MEMORIES & BUN_SQLITE_COBBLE_BOT_USER_FACES & BUN_SQLITE_COBBLE_BOT_MESSAGE_MEDIA & BUN_SQLITE_COBBLE_CONTAINERS & BUN_SQLITE_COBBLE_COMPUTERS & BUN_SQLITE_COBBLE_BOT_COMPUTERS & BUN_SQLITE_COBBLE_WORKSPACE_COMPUTERS & BUN_SQLITE_COBBLE_HARNESS_PROVIDERS & BUN_SQLITE_COBBLE_HARNESS_MODELS & BUN_SQLITE_COBBLE_HARNESS_SETTINGS & BUN_SQLITE_COBBLE_BOT_HARNESS_SETTINGS & BUN_SQLITE_COBBLE_WORKSPACE_HARNESS_SETTINGS & BUN_SQLITE_COBBLE_MCP_SERVERS & BUN_SQLITE_COBBLE_CUSTOM_TOOLS & BUN_SQLITE_COBBLE_BOT_MCP_SERVERS & BUN_SQLITE_COBBLE_WORKSPACE_MCP_SERVERS & BUN_SQLITE_COBBLE_BOT_CUSTOM_TOOLS & BUN_SQLITE_COBBLE_WORKSPACE_CUSTOM_TOOLS & BUN_SQLITE_COBBLE_LOGS & BUN_SQLITE_COBBLE_ENVS