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:
@@ -11,6 +11,7 @@ import grabBotPrompt from "@/src/functions/backend/db/bots/grab-bot-prompt";
|
||||
import grabBotComputers from "@/src/functions/backend/db/computers/grab-bot-computers";
|
||||
import runTextLoop from "@/src/harness/text/run-text-loop";
|
||||
import grabBotChatTools from "@/src/harness/tools/bot/grab-bot-chat-tools";
|
||||
import openBotExtensions from "@/src/harness/tools/extensions/open-bot-extensions";
|
||||
import botMessagesToHarness from "./bot-messages-to-harness";
|
||||
import composeComputersNote from "./compose-computers-note";
|
||||
import grabSessionCarryOver from "./grab-session-carry-over";
|
||||
@@ -33,7 +34,9 @@ type Params = BotJobContext & {
|
||||
* knowing where it is (its workspace, if the session is in one), what
|
||||
* happened in the session before (`grabSessionCarryOver`) and its
|
||||
* computers (`composeComputersNote`), with its chat tools (memory,
|
||||
* instructions, start_task, and for the coordinator delegate_task).
|
||||
* instructions, start_task, and for the coordinator delegate_task) plus
|
||||
* its MCP servers' and custom tools (`openBotExtensions`, script tools on
|
||||
* its current computer).
|
||||
* Finishes silently when the last message is already the bot's, i.e. an
|
||||
* earlier reply answered everything.
|
||||
*/
|
||||
@@ -82,6 +85,14 @@ export default async function runBotReply({
|
||||
onTakeOver: ({ ref }) => take_overs.push(ref),
|
||||
});
|
||||
|
||||
const extensions = await openBotExtensions({
|
||||
bot_id: bot.id,
|
||||
workspace_id,
|
||||
computer_id: computers[0]?.id || null,
|
||||
taken: tools.map((tool) => tool.name),
|
||||
signal,
|
||||
});
|
||||
|
||||
/**
|
||||
* delegate_task is only there while the coordinator has workers.
|
||||
*/
|
||||
@@ -92,33 +103,38 @@ export default async function runBotReply({
|
||||
? CoordinatorReplyNote
|
||||
: CoordinatorSoloNote;
|
||||
|
||||
const result = await runTextLoop({
|
||||
system: [
|
||||
prompt,
|
||||
carry_over,
|
||||
ChatReplyNote,
|
||||
workspace_id ? WorkspaceLearningNote : "",
|
||||
composeComputersNote({
|
||||
computers,
|
||||
is_coordinator: bot.kind == "coordinator",
|
||||
in_workspace: Boolean(workspace_id),
|
||||
try {
|
||||
const result = await runTextLoop({
|
||||
system: [
|
||||
prompt,
|
||||
carry_over,
|
||||
ChatReplyNote,
|
||||
workspace_id ? WorkspaceLearningNote : "",
|
||||
composeComputersNote({
|
||||
computers,
|
||||
is_coordinator: bot.kind == "coordinator",
|
||||
in_workspace: Boolean(workspace_id),
|
||||
}),
|
||||
crew_note,
|
||||
extensions.note,
|
||||
`Now: ${new Date().toUTCString()}.`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n"),
|
||||
messages: botMessagesToHarness({
|
||||
messages: history,
|
||||
parts: await loadAttachmentParts({ messages: history }),
|
||||
}),
|
||||
crew_note,
|
||||
`Now: ${new Date().toUTCString()}.`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n"),
|
||||
messages: botMessagesToHarness({
|
||||
messages: history,
|
||||
parts: await loadAttachmentParts({ messages: history }),
|
||||
}),
|
||||
tools,
|
||||
max_steps: AppData.BotReplyMaxToolSteps,
|
||||
bot_id: bot.id,
|
||||
workspace_id,
|
||||
signal,
|
||||
onStep: report,
|
||||
});
|
||||
tools: [...tools, ...extensions.tools],
|
||||
max_steps: AppData.BotReplyMaxToolSteps,
|
||||
bot_id: bot.id,
|
||||
workspace_id,
|
||||
signal,
|
||||
onStep: report,
|
||||
});
|
||||
|
||||
return withTakeOverRefs({ text: result.text, refs: take_overs });
|
||||
return withTakeOverRefs({ text: result.text, refs: take_overs });
|
||||
} finally {
|
||||
await extensions.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import grabBotPrompt from "@/src/functions/backend/db/bots/grab-bot-prompt";
|
||||
import grabUserWorkspaces from "@/src/functions/backend/db/workspaces/grab-user-workspaces";
|
||||
import runTextLoop from "@/src/harness/text/run-text-loop";
|
||||
import openComputerTools from "@/src/harness/tools/computer/open-computer-tools";
|
||||
import openBotExtensions, { type OpenBotExtensions } from "@/src/harness/tools/extensions/open-bot-extensions";
|
||||
import grabBotTools from "@/src/harness/tools/bot/grab-bot-tools";
|
||||
import createWorkspaceTool from "@/src/harness/tools/bot/create-workspace-tool";
|
||||
import updateWorkspaceTool from "@/src/harness/tools/bot/update-workspace-tool";
|
||||
@@ -28,8 +29,9 @@ type Params = BotJobContext & {
|
||||
/**
|
||||
* Works through a task in its session's workspace (if any): the bot
|
||||
* follows its brief with its full prompt,
|
||||
* its own tools, search_computers across all its computers and, when the
|
||||
* job has a computer, that computer's tools (over MCP), until it answers. That answer is the report for the user.
|
||||
* its own tools, search_computers across all its computers, its MCP
|
||||
* servers' and custom tools (`openBotExtensions`) and, when the job has a
|
||||
* computer, that computer's tools (over MCP), until it answers. That answer is the report for the user.
|
||||
* When only the user can go on (a login, a captcha), the bot asks them to
|
||||
* take over (`ask_user_to_take_over`) and the report needs them.
|
||||
* Every model turn is reported as a step (`report`).
|
||||
@@ -53,6 +55,8 @@ export default async function runBotTask({
|
||||
})
|
||||
: null;
|
||||
|
||||
let extensions: OpenBotExtensions | undefined;
|
||||
|
||||
try {
|
||||
const session_id = await jobSessionId({ job });
|
||||
const prompt = await grabBotPrompt({ bot_id: bot.id, user_id, workspace_id });
|
||||
@@ -82,6 +86,43 @@ export default async function runBotTask({
|
||||
...computers.filter((c) => c.id != job.computer_id),
|
||||
];
|
||||
|
||||
const tools = [
|
||||
...grabBotTools({ user_id, bot_id: bot.id, workspace_id, computers }),
|
||||
...(bot.kind == "coordinator"
|
||||
? [
|
||||
createWorkspaceTool({ user_id, workspaces: coordinator_workspaces }),
|
||||
...(coordinator_workspaces.length
|
||||
? [
|
||||
updateWorkspaceTool({
|
||||
user_id,
|
||||
workspaces: coordinator_workspaces,
|
||||
workspace_id,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
]
|
||||
: []),
|
||||
...(computers.length
|
||||
? [
|
||||
searchComputersTool({ computers, workspace_id }),
|
||||
askToTakeOverTool({
|
||||
computers: take_over_computers,
|
||||
in_task: true,
|
||||
onAsk: ({ ref }) => take_overs.push(ref),
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
...(computer?.tools || []),
|
||||
];
|
||||
|
||||
extensions = await openBotExtensions({
|
||||
bot_id: bot.id,
|
||||
workspace_id,
|
||||
computer_id: job.computer_id || null,
|
||||
taken: tools.map((tool) => tool.name),
|
||||
signal,
|
||||
});
|
||||
|
||||
const result = await runTextLoop({
|
||||
system: [
|
||||
prompt,
|
||||
@@ -104,6 +145,7 @@ export default async function runBotTask({
|
||||
.map((env) => `- ${env.key}${env.description ? `: ${env.description}` : ""}`)
|
||||
.join("\n")}`
|
||||
: "",
|
||||
extensions.note,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n"),
|
||||
@@ -117,34 +159,7 @@ export default async function runBotTask({
|
||||
}),
|
||||
},
|
||||
],
|
||||
tools: [
|
||||
...grabBotTools({ user_id, bot_id: bot.id, workspace_id, computers }),
|
||||
...(bot.kind == "coordinator"
|
||||
? [
|
||||
createWorkspaceTool({ user_id, workspaces: coordinator_workspaces }),
|
||||
...(coordinator_workspaces.length
|
||||
? [
|
||||
updateWorkspaceTool({
|
||||
user_id,
|
||||
workspaces: coordinator_workspaces,
|
||||
workspace_id,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
]
|
||||
: []),
|
||||
...(computers.length
|
||||
? [
|
||||
searchComputersTool({ computers, workspace_id }),
|
||||
askToTakeOverTool({
|
||||
computers: take_over_computers,
|
||||
in_task: true,
|
||||
onAsk: ({ ref }) => take_overs.push(ref),
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
...(computer?.tools || []),
|
||||
],
|
||||
tools: [...tools, ...extensions.tools],
|
||||
bot_id: bot.id,
|
||||
workspace_id,
|
||||
signal,
|
||||
@@ -157,6 +172,7 @@ export default async function runBotTask({
|
||||
refs: take_overs,
|
||||
});
|
||||
} finally {
|
||||
await extensions?.close();
|
||||
await computer?.close();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user