import { z } from 'zod' import type { ConnectorManager } from '../../manager/connector/lifecycle.js' import { renderToolSchema } from '../../registry/tool/schema.js' import type { ConnectorMethod } from '../../types/connector/index.js' import type { ConnectorId, ConnectorInstanceId } from '../../types/ids/index.js' import type { ToolContext, ToolDefinition, ToolResult } from '../../types/tool/index.js' import { asConnectorInstanceId } from '../../utils/id.js' import { connectorToolError } from './result.js' export function connectorMethodToTool( connectorId: ConnectorId, instanceId: ConnectorInstanceId, method: ConnectorMethod, manager: ConnectorManager, ): ToolDefinition { const toolName = `${connectorId}_${method.name}` return { name: toolName, description: `[${connectorId}] ${method.description}`, // ConnectorManager owns method parsing. A second Zod parse here would // re-run transforms before the manager sees the value. inputSchema: z.unknown(), modelInputSchema: renderToolSchema(method.inputSchema), ...(method.outputSchema ? { outputSchema: renderToolSchema(method.outputSchema) } : {}), category: 'network', permissions: ['network_access'], isReadOnly: () => false, isDestructive: () => false, isConcurrencySafe: () => true, async execute(input: unknown, context: ToolContext): Promise { const result = await manager.execute({ instanceId, method: method.name, input, signal: context.abortSignal, }) return { success: result.success, output: result.success ? JSON.stringify(result.output, null, 2) : '', data: result.output, error: result.success ? undefined : connectorToolError(result), } }, } } export function connectorInstanceToTools( instanceId: ConnectorInstanceId, manager: ConnectorManager, ): ToolDefinition[] { const instance = manager.getInstance(instanceId) if (!instance) { throw new Error(`Connector instance not found: "${instanceId}"`) } const connectorId = manager.getInstanceConnectorId(instanceId) const definition = manager.getInstanceDefinition(instanceId) return definition.methods.map((method) => connectorMethodToTool(connectorId, instanceId, method, manager), ) } export function allConnectorTools(manager: ConnectorManager): ToolDefinition[] { const tools: ToolDefinition[] = [] for (const instance of manager.listConnectedInstances()) { tools.push(...connectorInstanceToTools(instance.id, manager)) } return tools } const ConnectorRouterInputSchema = z.object({ connectorId: z .string() .describe('The connector definition ID (e.g., "conn_http", "conn_webhook")'), instanceId: z.string().describe('The connector instance ID (e.g., "ci_abc123")'), method: z.string().describe('The method to execute on the connector'), input: z.record(z.unknown()).describe('The input data for the method'), }) export type ConnectorRouterInput = z.infer export function createConnectorRouterTool( manager: ConnectorManager, ): ToolDefinition { return { name: 'connector_execute', description: buildRouterDescription(manager), inputSchema: ConnectorRouterInputSchema, category: 'network', permissions: ['network_access'], isReadOnly: () => false, isDestructive: () => false, isConcurrencySafe: () => true, async execute(input: ConnectorRouterInput, context: ToolContext): Promise { const instanceId = asConnectorInstanceId(String(input.instanceId)) const instance = manager.getInstance(instanceId) if (!instance) { return { success: false, output: '', error: `Connector instance not found: "${input.instanceId}"`, } } const connectorId = manager.getInstanceConnectorId(instanceId) if (connectorId !== input.connectorId) { return { success: false, output: '', error: `Instance "${input.instanceId}" belongs to connector "${connectorId}", not "${input.connectorId}"`, } } const result = await manager.execute({ instanceId, method: input.method, input: input.input, signal: context.abortSignal, }) return { success: result.success, output: result.success ? JSON.stringify(result.output, null, 2) : '', data: result.output, error: result.success ? undefined : connectorToolError(result), } }, } } function buildRouterDescription(manager: ConnectorManager): string { const instances = manager.listConnectedInstances() if (instances.length === 0) { return 'Execute a method on a connected connector instance. No connectors are currently connected.' } const parts = ['Execute a method on a connected connector instance. Available:'] for (const inst of instances) { const connectorId = manager.getInstanceConnectorId(inst.id) const definition = manager.getInstanceDefinition(inst.id) const methods = definition.methods.map((method) => method.name).join(', ') parts.push(`- ${connectorId} (${inst.id}): ${methods}`) } return parts.join('\n') }