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]>
60 lines
2.3 KiB
TypeScript
60 lines
2.3 KiB
TypeScript
import userAuth from "@/src/functions/backend/auth/user-auth";
|
|
import grabMcpsOverview from "@/src/functions/backend/db/extensions/grab-mcps-overview";
|
|
import setExtensionAccess from "@/src/functions/backend/db/extensions/set-extension-access";
|
|
import logEvent, { grabRequestIP } from "@/src/functions/backend/logger";
|
|
import { SetExtensionAccessRequestSchema } from "@/src/schemas/extension-schema";
|
|
import type { McpServersOverviewPayload } from "@/src/types";
|
|
import AppError from "@/src/utils/app-error";
|
|
import parseWithSchema from "@/src/utils/parse-with-schema";
|
|
import isReadOnlyUser from "@/src/utils/rbac/is-read-only-user";
|
|
import safeErrorMessage from "@/src/utils/safe-error-message";
|
|
import type {
|
|
APIResponseObject,
|
|
BunextAPIRouteHandler,
|
|
} from "@moduletrace/bunext/types";
|
|
|
|
/**
|
|
* Super admins and admins: attaches an MCP server to exactly these bots and
|
|
* workspaces. Only a super admin sets "all bots" (`setExtensionAccess`).
|
|
*/
|
|
export const handler: BunextAPIRouteHandler<
|
|
APIResponseObject<McpServersOverviewPayload>
|
|
> = async ({ req, body }) => {
|
|
try {
|
|
if (req.method != "POST") {
|
|
throw new AppError(`Method not allowed`);
|
|
}
|
|
|
|
const { user, user_types } = await userAuth({ req });
|
|
|
|
if (!user?.logged_in_status || !user.id) {
|
|
return { success: false, msg: `Unauthorized`, logoutUser: true };
|
|
}
|
|
|
|
if (isReadOnlyUser({ user_types: user_types || [] })) {
|
|
throw new AppError(`This user can only read records.`);
|
|
}
|
|
|
|
const access = parseWithSchema({ schema: SetExtensionAccessRequestSchema, data: body });
|
|
|
|
await setExtensionAccess({ user_id: user.id, kind: "mcp", ...access });
|
|
|
|
await logEvent({
|
|
title: "MCP server access changed",
|
|
category: "system",
|
|
user_id: user.id,
|
|
plain_text: `User #${user.id} changed who gets the MCP server #${access.id}.`,
|
|
metadata: access,
|
|
ip_address: grabRequestIP(req),
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
msg: `Saved who gets it.`,
|
|
singleRes: await grabMcpsOverview({ user_id: user.id }),
|
|
};
|
|
} catch (error: unknown) {
|
|
return { success: false, msg: safeErrorMessage({ error }) };
|
|
}
|
|
};
|