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]>
99 lines
4.1 KiB
TypeScript
99 lines
4.1 KiB
TypeScript
import { useState } from "react";
|
|
import Stack from "@moduletrace/twui/layout/Stack";
|
|
import { Wrench } from "lucide-react";
|
|
import IconTile from "@/src/components/general/icon-tile";
|
|
import { CustomToolKinds } from "@/src/dict/custom-tool-dict";
|
|
import postAdminApi from "@/src/layouts/bots/(functions)/post-admin-api";
|
|
import type { CustomToolsOverviewPayload, ExtensionTargets, PublicCustomTool } from "@/src/types";
|
|
import ExtensionAccessModal from "../../(partials)/extension-access-modal";
|
|
import SettingsListRow from "../../(partials)/settings-list-row";
|
|
import SettingsRowAction from "../../(partials)/settings-row-action";
|
|
import extensionAccessLabel from "../../(utils)/extension-access-label";
|
|
import CustomToolModal from "./custom-tool-modal";
|
|
import CustomToolTryModal from "./custom-tool-try-modal";
|
|
|
|
type Props = {
|
|
tool: PublicCustomTool;
|
|
targets: ExtensionTargets;
|
|
onChanged: (overview: CustomToolsOverviewPayload, msg: string) => void;
|
|
onError: (msg: string) => void;
|
|
};
|
|
|
|
/**
|
|
* One custom tool: its name, kind and who gets it. Edit, Access and Try
|
|
* open in a modal.
|
|
*/
|
|
export default function CustomToolRow({ tool, targets, onChanged, onError }: Props) {
|
|
const [open, setOpen] = useState<"edit" | "access" | "try">();
|
|
const [busy, setBusy] = useState(false);
|
|
|
|
const toggle = (panel: "edit" | "access" | "try") => setOpen(open == panel ? undefined : panel);
|
|
|
|
const saved = (overview: CustomToolsOverviewPayload, msg: string) => {
|
|
setOpen(undefined);
|
|
onChanged(overview, msg);
|
|
};
|
|
|
|
const remove = async () => {
|
|
if (busy || !window.confirm(`Delete ${tool.name}? Bots lose it at once.`)) return;
|
|
|
|
setBusy(true);
|
|
const res = await postAdminApi<CustomToolsOverviewPayload>({
|
|
path: "tools/delete-tool",
|
|
body: { custom_tool_id: tool.id },
|
|
});
|
|
setBusy(false);
|
|
|
|
if (res.success && res.singleRes) onChanged(res.singleRes, res.msg || "Deleted.");
|
|
else onError(res.msg || "Couldn't delete the tool.");
|
|
};
|
|
|
|
return (
|
|
<Stack className="w-full gap-0! items-stretch border-0 border-b border-solid border-hairline last:border-b-0">
|
|
<SettingsListRow
|
|
avatar={<IconTile Icon={Wrench} />}
|
|
title={tool.name}
|
|
badge={tool.enabled ? undefined : "Off"}
|
|
subtitle={[
|
|
CustomToolKinds.find((k) => k.value == tool.kind)?.title,
|
|
extensionAccessLabel({ access: tool.access }),
|
|
tool.description,
|
|
]
|
|
.filter(Boolean)
|
|
.join(" · ")}
|
|
>
|
|
{targets.can_edit ? (
|
|
<SettingsRowAction title={`Edit ${tool.name}`} label="Edit" disabled={busy} onClick={() => toggle("edit")} />
|
|
) : null}
|
|
<SettingsRowAction
|
|
title={`Choose who gets ${tool.name}`}
|
|
label="Access"
|
|
disabled={busy}
|
|
onClick={() => toggle("access")}
|
|
/>
|
|
{targets.can_edit ? (
|
|
<SettingsRowAction title={`Try ${tool.name}`} label="Try" disabled={busy} onClick={() => toggle("try")} />
|
|
) : null}
|
|
{targets.can_edit ? (
|
|
<SettingsRowAction title={`Delete ${tool.name}`} label="Delete" disabled={busy} onClick={remove} />
|
|
) : null}
|
|
</SettingsListRow>
|
|
{open == "edit" ? (
|
|
<CustomToolModal tool={tool} onSaved={saved} onCancel={() => setOpen(undefined)} />
|
|
) : null}
|
|
{open == "access" ? (
|
|
<ExtensionAccessModal<CustomToolsOverviewPayload>
|
|
id={tool.id}
|
|
name={tool.name}
|
|
access={tool.access}
|
|
targets={targets}
|
|
path="tools/set-tool-access"
|
|
onSaved={saved}
|
|
onCancel={() => setOpen(undefined)}
|
|
/>
|
|
) : null}
|
|
{open == "try" ? <CustomToolTryModal tool={tool} onClose={() => setOpen(undefined)} /> : null}
|
|
</Stack>
|
|
);
|
|
}
|