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 { customToolRegistry } from "../tools/custom-tool-registry.js";
import type { ParameterConfig } 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 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 getToolMetadataForSource(sourceId: string): ToolMetadata {
const sourceIds = ConnectorManager.getAvailableSourceIds();
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
const executeOptions = ConnectorManager.getCurrentExecuteOptions(sourceId);
const dbType = sourceConfig?.type || "database";
// Determine tool name based on single vs multi-source configuration
const toolName = sourceId === "default" ? "execute_sql" : `execute_sql_${normalizeSourceId(sourceId)}`;
// Determine title (human-readable display name)
const isDefault = sourceIds[0] === sourceId;
const title = isDefault
? `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 = `Execute SQL queries on the '${sourceId}' ${dbType} database${isDefault ? " (default)" : ""}${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,
// In readonly mode, it's safer to operate on arbitrary tables (just reading)
// In write mode, operating on arbitrary tables is more dangerous
openWorldHint: isReadonly,
};
return {
name: toolName,
description,
schema: executeSqlSchema,
annotations,
};
}
/**
* 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,
}));
}
/**
* Get tools for a specific source (API response format)
* Includes both built-in tools (execute_sql, search_objects) and custom tools
* @param sourceId - The source ID to get tools for
* @returns Array of tools with simplified parameters
*/
export function getToolsForSource(sourceId: string): Tool[] {
const tools: Tool[] = [];
// 1. Add built-in execute_sql tool
const executeSqlMetadata = getToolMetadataForSource(sourceId);
const executeSqlParameters = zodToParameters(executeSqlMetadata.schema);
tools.push({
name: executeSqlMetadata.name,
description: executeSqlMetadata.description,
parameters: executeSqlParameters,
});
// 2. Add built-in search_objects tool
const searchToolName = sourceId === "default" ? "search_objects" : `search_objects_${normalizeSourceId(sourceId)}`;
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
const dbType = sourceConfig?.type || "database";
const sourceIds = ConnectorManager.getAvailableSourceIds();
const isDefault = sourceIds[0] === sourceId;
tools.push({
name: searchToolName,
description: `Search and list database objects (schemas, tables, columns, procedures, indexes) on the '${sourceId}' ${dbType} database${isDefault ? " (default)" : ""}`,
parameters: [
{
name: "object_type",
type: "string",
required: true,
description: "Type of database object to search for (schema, table, column, procedure, index)",
},
{
name: "pattern",
type: "string",
required: false,
description: "Search pattern (SQL LIKE syntax: % for wildcard, _ for single char). Case-insensitive. Defaults to '%' (match all).",
},
{
name: "schema",
type: "string",
required: false,
description: "Filter results to a specific schema/database",
},
{
name: "detail_level",
type: "string",
required: false,
description: "Level of detail to return: names (minimal), summary (with metadata), full (complete structure). Defaults to 'names'.",
},
{
name: "limit",
type: "integer",
required: false,
description: "Maximum number of results to return (default: 100, max: 1000)",
},
],
});
// 3. Add custom tools for this source
if (customToolRegistry.isInitialized()) {
const customTools = customToolRegistry.getTools();
const sourceCustomTools = customTools.filter((tool) => tool.source === sourceId);
for (const customTool of sourceCustomTools) {
tools.push({
name: customTool.name,
description: customTool.description,
parameters: customParamsToToolParams(customTool.parameters),
});
}
}
return tools;
}