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:
2026-10-19 15:35:13 +01:00
co-authored by Claude Opus 5.5
parent c6ec7d77a2
commit 6294b911e6
69 changed files with 2026 additions and 271 deletions
@@ -0,0 +1,80 @@
import Button from "@moduletrace/twui/layout/Button";
import Row from "@moduletrace/twui/layout/Row";
import { Plug, Wrench } from "lucide-react";
import ExtensionAddSelect from "@/src/components/general/extension-add-select";
import useExtensions from "@/src/hooks/use-extensions";
import type { BotExtensionsPayload } from "@/src/types";
import ExtensionRow from "./extension-row";
import SettingsGroup from "./settings-group";
import SettingsList from "./settings-list";
import SettingsStatus from "./settings-status";
type Props = {
owner: { bot_id: number } | { workspace_id: number };
owner_name: string;
description: string;
initial?: BotExtensionsPayload | null;
};
/**
* The MCP servers and tools a bot or a workspace has, removable, and a
* picker to add more. On the bot and workspace settings pages.
*/
export default function ExtensionsGroup({ owner, owner_name, description, initial }: Props) {
const extensions = useExtensions({ owner, owner_name, initial });
const id = "bot_id" in owner ? `bot-${owner.bot_id}` : `workspace-${owner.workspace_id}`;
return (
<SettingsGroup id="tools" title="Tools and MCP servers" description={description}>
<SettingsList is_empty={!extensions.attached.length} empty="No tools or MCP servers yet.">
{extensions.attached.map((item) => (
<ExtensionRow
key={`${item.kind}:${item.id}`}
item={item}
busy={extensions.busy == `${item.kind}:${item.id}`}
onRemove={
extensions.can_manage
? () => extensions.remove({ kind: item.kind, id: item.id, name: item.name })
: undefined
}
/>
))}
</SettingsList>
{extensions.can_manage ? (
<ExtensionAddSelect
id={`${id}-add-extension`}
addable={extensions.addable}
loading={Boolean(extensions.busy)}
onAdd={extensions.add}
/>
) : null}
<Row className="gap-1!">
<Button
title="Manage MCP servers"
type="button"
variant="ghost"
color="gray"
size="small"
href="/admin/settings/mcps"
beforeIcon={<Plug size={14} aria-hidden />}
className="gap-1!"
>
MCP servers
</Button>
<Button
title="Manage tools"
type="button"
variant="ghost"
color="gray"
size="small"
href="/admin/settings/tools"
beforeIcon={<Wrench size={14} aria-hidden />}
className="gap-1!"
>
Tools
</Button>
</Row>
<SettingsStatus status={extensions.status} />
</SettingsGroup>
);
}