Skip to main content
Glama
tool-metadata.ts7.4 kB
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; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/bytebase/dbhub'

If you have feedback or need assistance with the MCP directory API, please join our Discord server