import { z } from "zod";
import { ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
import { ConnectorManager } from "../connectors/manager.js";
import { normalizeSourceId } from "./normalize-id.js";
import { executeSqlSchema } from "../tools/execute-sql.js";
import { getToolRegistry } from "../tools/registry.js";
import type { ParameterConfig, ToolConfig } from "../types/config.js";
/**
* Tool parameter definition for API responses
*/
export interface ToolParameter {
name: string;
type: string;
required: boolean;
description: string;
}
/**
* Tool metadata for API responses
*/
export interface Tool {
name: string;
description: string;
parameters: ToolParameter[];
}
/**
* Tool metadata with Zod schema (used internally for registration)
*/
export interface ToolMetadata {
name: string;
description: string;
schema: Record<string, z.ZodType<any>>;
annotations: ToolAnnotations;
}
/**
* Convert a Zod schema object to simplified parameter list
* @param schema - Zod schema object (e.g., { sql: z.string().describe("...") })
* @returns Array of tool parameters
*/
export function zodToParameters(schema: Record<string, z.ZodType<any>>): ToolParameter[] {
const parameters: ToolParameter[] = [];
for (const [key, zodType] of Object.entries(schema)) {
// Extract description from Zod schema
const description = zodType.description || "";
// Determine if required (Zod types are required by default unless optional)
const required = !(zodType instanceof z.ZodOptional);
// Determine type from Zod type
let type = "string"; // default
if (zodType instanceof z.ZodString) {
type = "string";
} else if (zodType instanceof z.ZodNumber) {
type = "number";
} else if (zodType instanceof z.ZodBoolean) {
type = "boolean";
} else if (zodType instanceof z.ZodArray) {
type = "array";
} else if (zodType instanceof z.ZodObject) {
type = "object";
}
parameters.push({
name: key,
type,
required,
description,
});
}
return parameters;
}
/**
* Get execute_sql tool metadata for a specific source
* @param sourceId - The source ID to get tool metadata for
* @returns Tool metadata with name, description, and Zod schema
*/
export function getExecuteSqlMetadata(sourceId: string): ToolMetadata {
const sourceIds = ConnectorManager.getAvailableSourceIds();
const sourceConfig = ConnectorManager.getSourceConfig(sourceId)!;
const executeOptions = ConnectorManager.getCurrentExecuteOptions(sourceId);
const dbType = sourceConfig.type;
const isSingleSource = sourceIds.length === 1;
// Determine tool name based on single vs multi-source configuration
const toolName = isSingleSource ? "execute_sql" : `execute_sql_${normalizeSourceId(sourceId)}`;
// Determine title (human-readable display name)
const title = isSingleSource
? `Execute SQL (${dbType})`
: `Execute SQL on ${sourceId} (${dbType})`;
// Determine description with more context
const readonlyNote = executeOptions.readonly ? " [READ-ONLY MODE]" : "";
const maxRowsNote = executeOptions.maxRows ? ` (limited to ${executeOptions.maxRows} rows)` : "";
const description = isSingleSource
? `Execute SQL queries on the ${dbType} database${readonlyNote}${maxRowsNote}`
: `Execute SQL queries on the '${sourceId}' ${dbType} database${readonlyNote}${maxRowsNote}`;
// Build annotations object with all standard MCP hints
const isReadonly = executeOptions.readonly === true;
const annotations = {
title,
readOnlyHint: isReadonly,
destructiveHint: !isReadonly, // Can be destructive if not readonly
// In readonly mode, queries are more predictable (though still not strictly idempotent due to data changes)
// In write mode, queries are definitely not idempotent
idempotentHint: false,
// Database operations are always against internal/closed systems, not open-world
openWorldHint: false,
};
return {
name: toolName,
description,
schema: executeSqlSchema,
annotations,
};
}
/**
* Get search_objects tool metadata for a specific source
* @param sourceId - The source ID to get tool metadata for
* @returns Tool name, description, and annotations
*/
export function getSearchObjectsMetadata(sourceId: string): { name: string; description: string; title: string } {
const sourceIds = ConnectorManager.getAvailableSourceIds();
const sourceConfig = ConnectorManager.getSourceConfig(sourceId)!;
const dbType = sourceConfig.type;
const isSingleSource = sourceIds.length === 1;
const toolName = isSingleSource ? "search_objects" : `search_objects_${normalizeSourceId(sourceId)}`;
const title = isSingleSource
? `Search Database Objects (${dbType})`
: `Search Database Objects on ${sourceId} (${dbType})`;
const description = isSingleSource
? `Search and list database objects (schemas, tables, columns, procedures, indexes) on the ${dbType} database`
: `Search and list database objects (schemas, tables, columns, procedures, indexes) on the '${sourceId}' ${dbType} database`;
return {
name: toolName,
description,
title,
};
}
/**
* Convert custom tool parameter configs to Tool parameter format
* @param params - Parameter configurations from custom tool
* @returns Array of tool parameters
*/
function customParamsToToolParams(params: ParameterConfig[] | undefined): ToolParameter[] {
if (!params || params.length === 0) {
return [];
}
return params.map((param) => ({
name: param.name,
type: param.type,
required: param.required !== false && param.default === undefined,
description: param.description,
}));
}
/**
* Build execute_sql tool metadata for API response
*/
function buildExecuteSqlTool(sourceId: string): Tool {
const executeSqlMetadata = getExecuteSqlMetadata(sourceId);
const executeSqlParameters = zodToParameters(executeSqlMetadata.schema);
return {
name: executeSqlMetadata.name,
description: executeSqlMetadata.description,
parameters: executeSqlParameters,
};
}
/**
* Build search_objects tool metadata for API response
*/
function buildSearchObjectsTool(sourceId: string): Tool {
const searchMetadata = getSearchObjectsMetadata(sourceId);
return {
name: searchMetadata.name,
description: searchMetadata.description,
parameters: [
{
name: "object_type",
type: "string",
required: true,
description: "Object type to search",
},
{
name: "pattern",
type: "string",
required: false,
description: "LIKE pattern (% = any chars, _ = one char). Default: %",
},
{
name: "schema",
type: "string",
required: false,
description: "Filter to schema",
},
{
name: "table",
type: "string",
required: false,
description: "Filter to table (requires schema; column/index only)",
},
{
name: "detail_level",
type: "string",
required: false,
description: "Detail: names (minimal), summary (metadata), full (all)",
},
{
name: "limit",
type: "integer",
required: false,
description: "Max results (default: 100, max: 1000)",
},
],
};
}
/**
* Build custom tool metadata for API response
*/
function buildCustomTool(toolConfig: ToolConfig): Tool {
return {
name: toolConfig.name,
description: toolConfig.description!,
parameters: customParamsToToolParams(toolConfig.parameters),
};
}
/**
* Get tools for a specific source (API response format)
* Only includes tools that are actually enabled in the ToolRegistry
* @param sourceId - The source ID to get tools for
* @returns Array of enabled tools with simplified parameters
*/
export function getToolsForSource(sourceId: string): Tool[] {
// Get enabled tools from registry
const registry = getToolRegistry();
const enabledToolConfigs = registry.getEnabledToolConfigs(sourceId);
// Uniform iteration: map each enabled tool config to its API representation
return enabledToolConfigs.map((toolConfig) => {
// Dispatch based on tool name
if (toolConfig.name === "execute_sql") {
return buildExecuteSqlTool(sourceId);
} else if (toolConfig.name === "search_objects") {
return buildSearchObjectsTool(sourceId);
} else {
// Custom tool
return buildCustomTool(toolConfig);
}
});
}