93 lines
3.5 KiB
TypeScript
93 lines
3.5 KiB
TypeScript
/**
|
|
* MCP-registry adapter for the chatbot V2 `external_tools` capability.
|
|
*
|
|
* The MCP server (`mcp.server.ts`) registers tools at boot via
|
|
* `registerMcpTools` and via `addToolRegistrar`. There is no public
|
|
* `listTools` accessor on the underlying SDK server, so we mirror the
|
|
* registrations into a process-local registry the chatbot reads.
|
|
*
|
|
* Tenant scope: MCP tools are presently registered globally — every
|
|
* tenant sees the same set. The registry helper still takes a
|
|
* `tenantId` so callers can hard-filter once per-tenant registries
|
|
* arrive (Phase 06 plugin system). For now the tenant arg is recorded
|
|
* in the audit-log via the high-risk wrapper that consumes this
|
|
* registry.
|
|
*/
|
|
|
|
/**
|
|
* Local contract for an external-tools provider. Previously imported from the
|
|
* (now-removed) chatbot module; kept here as a self-contained interface so the
|
|
* MCP registry stays usable by future MCP-server callers without depending on
|
|
* the deleted self-hosted-AI surface.
|
|
*/
|
|
export interface ExternalToolsProvider {
|
|
list: (tenantId: string) => Promise<ReadonlyArray<{ toolName: string; description: string }>>;
|
|
invoke: (input: { tenantId: string; userId: string | null; toolName: string; inputs: Record<string, unknown> }) => Promise<unknown>;
|
|
}
|
|
|
|
export interface RegisteredMcpTool {
|
|
readonly toolName: string;
|
|
readonly description: string;
|
|
/** Optional invoker; when set, the chatbot can call this tool. */
|
|
readonly invoke?: (input: Record<string, unknown>, ctx: { tenantId: string; userId: string | null }) => Promise<unknown>;
|
|
}
|
|
|
|
const registry = new Map<string, RegisteredMcpTool>();
|
|
const READ_ONLY_TOOL_MESSAGE = "Tool is registered but cannot be invoked via the chatbot in this build.";
|
|
|
|
/**
|
|
* Register an MCP tool so the chatbot's `external_tools` capability can
|
|
* list and invoke it. Subsequent registrations of the same `toolName`
|
|
* overwrite — last writer wins.
|
|
*/
|
|
export function registerMcpToolForChatbot(tool: RegisteredMcpTool): void {
|
|
registry.set(tool.toolName, tool);
|
|
}
|
|
|
|
/**
|
|
* Drop a tool from the registry. Intended for tests + hot-reload
|
|
* scenarios.
|
|
*/
|
|
export function unregisterMcpToolForChatbot(toolName: string): void {
|
|
registry.delete(toolName);
|
|
}
|
|
|
|
export function listMcpToolsForChatbot(): ReadonlyArray<RegisteredMcpTool> {
|
|
return Array.from(registry.values()).sort((a, b) => a.toolName.localeCompare(b.toolName));
|
|
}
|
|
|
|
export function __clearMcpRegistryForTests(): void {
|
|
registry.clear();
|
|
}
|
|
|
|
/**
|
|
* Adapter implementing `ExternalToolsProvider` for the chatbot
|
|
* capability. Tenant-aware list filter + audit-friendly invocation
|
|
* (the wrapped tool factory adds AuditEvent rows).
|
|
*/
|
|
export function createMcpExternalToolsProvider(): ExternalToolsProvider {
|
|
return {
|
|
list: async (_tenantId: string) => {
|
|
const tools = listMcpToolsForChatbot();
|
|
return tools.map((t) => ({ toolName: t.toolName, description: t.description }));
|
|
},
|
|
invoke: async ({ tenantId, userId, toolName, inputs }) => {
|
|
const tool = registry.get(toolName);
|
|
if (!tool) {
|
|
throw new Error(`MCP tool not registered: ${toolName}`);
|
|
}
|
|
if (!tool.invoke) {
|
|
// Tool is listed (read-only) but has no chatbot-side invoker —
|
|
// surface a structured payload so the LLM relays the limitation
|
|
// to the user rather than fabricating a result.
|
|
return {
|
|
status: "not-invocable",
|
|
toolName,
|
|
message: READ_ONLY_TOOL_MESSAGE,
|
|
};
|
|
}
|
|
return tool.invoke(inputs, { tenantId, userId });
|
|
},
|
|
};
|
|
}
|