Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/modules/mcp/mcp-registry.server.ts
T

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 });
},
};
}