Skip to main content
Glama
IBM
by IBM
toolDefinitions.ts12.6 kB
/** * @fileoverview Defines the standard structure for a declarative tool definition. * This interface ensures that all tools provide the necessary metadata (name, schemas) * and logic in a consistent, self-contained format, aligned with MCP specifications. * @module src/mcp-server/tools/utils/toolDefinition */ import { CallToolResult, type ContentBlock, } from "@modelcontextprotocol/sdk/types.js"; import { z, type ZodObject, type ZodRawShape } from "zod"; import type { RequestContext } from "@/utils/index.js"; import { ErrorHandler, getRequestContext, measureToolExecution, requestContextService, withRequestContext, } from "@/utils/internal/index.js"; import { JsonRpcErrorCode, McpError } from "@/types-global/errors.js"; // Import shared schemas from centralized location import { StandardSqlToolOutputSchema, ToolAnnotations, StandardSqlToolOutput, } from "@/ibmi-mcp-server/schemas/index.js"; // Import formatting utilities import { markdown, tableFormatter, buildColumnAlignmentMap, formatColumnHeader, type TableStyle, } from "@/utils/formatting/index.js"; /** * Represents the complete, self-contained definition of an MCP tool. */ export interface ToolDefinition< TInputSchema extends ZodObject<ZodRawShape>, TOutputSchema extends ZodObject<ZodRawShape>, > { /** * The programmatic, unique name for the tool (e.g., 'echo_message'). */ name: string; /** * An optional, human-readable title for the tool. This is preferred for display in UIs. * If not provided, the `name` or `annotations.title` may be used as a fallback. */ title?: string; /** * A clear, concise description of what the tool does. * This is sent to the LLM to help it decide when to use the tool. */ description: string; /** * The Zod schema for validating the tool's input parameters. */ inputSchema: TInputSchema; /** * The Zod schema for validating the tool's successful output structure. */ outputSchema: TOutputSchema; /** * Optional metadata providing hints about the tool's behavior. */ annotations?: ToolAnnotations; /** * The core business logic function for the tool. * It receives validated input and returns a structured output or throws an McpError. * @param input The validated tool input. * @param context The request context for logging and tracing. * @returns A promise that resolves with the structured output. */ logic: ( input: z.infer<TInputSchema>, context: RequestContext, ) => Promise<z.infer<TOutputSchema>>; /** * An optional function to format the successful output into an array of ContentBlocks * for the `CallToolResult`. If not provided, a default JSON stringifier is used. * @param result The successful output from the logic function. * @returns An array of ContentBlocks to be sent to the client. */ responseFormatter?: (result: z.infer<TOutputSchema>) => ContentBlock[]; } // Re-export standardized SQL tool output schema from centralized schemas export const standardSqlToolOutputSchema = StandardSqlToolOutputSchema; /** * Configuration options for SQL response formatting. */ export interface SqlFormatterConfig { /** * Table formatting style (default: 'markdown'). */ tableFormat?: TableStyle; /** * Maximum number of rows to display (default: 100). */ maxDisplayRows?: number; } /** * Formats SQL tool output into a professionally formatted markdown response. * Uses MarkdownBuilder for structure and TableFormatter for type-aware table rendering. * Provides column type indicators, NULL tracking, and performance metrics. * * @param result - The SQL tool execution result * @param config - Optional formatting configuration * @returns Array of content blocks for MCP response * * @example * ```typescript * const result = await executeSqlTool(params); * const formatted = sqlResponseFormatter(result, { * tableFormat: 'grid', * maxDisplayRows: 50 * }); * ``` */ export const sqlResponseFormatter = ( result: StandardSqlToolOutput, config?: SqlFormatterConfig, ): ContentBlock[] => { const tableFormat = config?.tableFormat || "markdown"; const maxDisplayRows = config?.maxDisplayRows || 100; // Handle error cases if (!result.success || !result.data) { const errorMessage = result.error || "Unknown error occurred"; const { metadata } = result; const errorBuilder = markdown() .alert("caution", "❌ SQL Query Failed") .blankLine(); if (metadata?.toolName) { errorBuilder.keyValue("Tool", metadata.toolName); } errorBuilder.keyValue("Error", errorMessage); if (result.errorCode) { errorBuilder.keyValue("Error Code", String(result.errorCode)); } if (metadata?.sqlStatement) { const truncatedSql = metadata.sqlStatement.length > 200 ? metadata.sqlStatement.substring(0, 197) + "..." : metadata.sqlStatement; errorBuilder .blankLine() .h3("SQL Statement") .codeBlock(truncatedSql, "sql"); } const errorMarkdown = errorBuilder.build(); return [{ type: "text", text: errorMarkdown }]; } const { data, metadata } = result; const rowCount = data.length; // Start building the markdown response const mdBuilder = markdown(); // Tool header if (metadata?.toolName) { mdBuilder.h2(metadata.toolName); } // Success indicator mdBuilder .alert("tip", "✅ Query completed successfully") .blankLine() .paragraph( `Found **${rowCount} row${rowCount !== 1 ? "s" : ""}** from the database query`, ); // SQL Statement section if (metadata?.sqlStatement) { const truncatedSql = metadata.sqlStatement.length > 500 ? metadata.sqlStatement.substring(0, 497) + "..." : metadata.sqlStatement; mdBuilder.h3("SQL Statement").codeBlock(truncatedSql, "sql"); } // Parameters section if (metadata?.parameters && Object.keys(metadata.parameters).length > 0) { mdBuilder.h3("Parameters"); const paramList = Object.entries(metadata.parameters).map( ([key, value]) => { const displayValue = value === null || value === undefined ? "NULL" : typeof value === "string" && value.length > 100 ? `${String(value).substring(0, 97)}...` : String(value); return `\`${key}\`: ${displayValue}`; }, ); mdBuilder.list(paramList); } // Handle empty results if (rowCount === 0) { mdBuilder .paragraph("No rows returned from the query.") .h3("Execution Summary") .keyValue( "Execution time", metadata?.executionTime ? `${metadata.executionTime}ms` : "N/A", ) .keyValue("Parameters used", String(metadata?.parameterCount || 0)); return [{ type: "text", text: mdBuilder.build() }]; } // Extract column information const columns = metadata?.columns || []; const firstRow = data[0] || {}; const allColumns = columns.length > 0 ? columns : Object.keys(firstRow).map((key) => ({ name: key, type: undefined, label: key, })); // Prepare data for table formatter const displayRows = data.slice(0, maxDisplayRows); const columnCount = allColumns.length; // Format headers with type indicators const headers = allColumns.map((col) => formatColumnHeader(col.label || col.name, col.type), ); // Build column alignment map based on types const alignment = buildColumnAlignmentMap(allColumns); // Convert data rows to string arrays const rows = displayRows.map((row) => allColumns.map((col) => { const value = row[col.name]; if (value === null || value === undefined) return null; return String(value); }), ); // Format table with metadata tracking const tableResult = tableFormatter.formatRawWithMetadata(headers, rows, { style: tableFormat, alignment, nullReplacement: "-", maxWidth: 50, truncate: true, }); // Add truncation notice if applicable if (displayRows.length < rowCount) { mdBuilder.alert( "note", `Showing ${displayRows.length} of ${rowCount} rows. ${rowCount - displayRows.length} rows omitted.`, ); } // Results section mdBuilder.h3("Results").raw(tableResult.table); // NULL value summary (only if there are NULLs) const nullCounts = tableResult.metadata.nullCounts; const totalNulls = Object.values(nullCounts).reduce( (sum, count) => sum + count, 0, ); // Summary section const summaryItems: string[] = [ `**Total rows**: ${rowCount}`, `**Columns**: ${columnCount}`, ]; if (metadata?.executionTime) { summaryItems.push(`**Execution time**: ${metadata.executionTime}ms`); } if (totalNulls > 0) { const nullDetails = Object.entries(nullCounts) .filter(([, count]) => count > 0) .map(([col, count]) => { // Convert column index to name for display const colIndex = parseInt(col); const colName = isNaN(colIndex) ? col : allColumns[colIndex]?.name || col; return `${colName} (${count})`; }) .join(", "); summaryItems.push(`**NULL values**: ${totalNulls} total - ${nullDetails}`); } if (metadata?.affectedRows !== undefined) { summaryItems.push(`**Affected rows**: ${metadata.affectedRows}`); } if (metadata?.parameterCount) { summaryItems.push(`**Parameters processed**: ${metadata.parameterCount}`); } mdBuilder.h3("Summary").list(summaryItems); return [{ type: "text", text: mdBuilder.build() }]; }; /** * Formats successful SQL tool output into a text block that highlights * row counts and execution timing for quick operator feedback. */ // Default formatter for successful responses (fallback) export const defaultResponseFormatter = (result: unknown): ContentBlock[] => [ { type: "text", text: JSON.stringify(result, null, 2) }, ]; /** * Creates an MCP-compatible handler from a declarative tool definition. * Handles input validation, performance measurement, standardized error * formatting, and context propagation. */ export function createHandlerFromDefinition< TInputSchema extends ZodObject<ZodRawShape>, TOutputSchema extends ZodObject<ZodRawShape>, >(definition: ToolDefinition<TInputSchema, TOutputSchema>) { return async ( rawParams: unknown, mcpContext: Record<string, unknown>, ): Promise<CallToolResult> => { const handlerContext = requestContextService.createRequestContext({ parentContext: mcpContext, operation: `tool:${definition.name}`, toolName: definition.name, }); return withRequestContext(handlerContext, async () => { try { const validatedInput = definition.inputSchema.parse( rawParams ?? {}, ) as z.infer<TInputSchema>; const result = await measureToolExecution( definition.name, () => definition.logic(validatedInput, handlerContext), validatedInput, ); const output = definition.outputSchema.parse(result); const contentBlocks = definition.responseFormatter ? definition.responseFormatter(output) : defaultResponseFormatter(output); return { content: contentBlocks, structuredContent: output, } satisfies CallToolResult; } catch (error) { const capturedContext = getRequestContext() ?? handlerContext ?? undefined; const handledError = ErrorHandler.handleError(error, { operation: `tool:${definition.name}`, context: capturedContext, input: rawParams, errorCode: error instanceof McpError ? error.code : undefined, }); const mcpError = handledError instanceof McpError ? handledError : new McpError( JsonRpcErrorCode.InternalError, handledError instanceof Error ? handledError.message : String(handledError), ); return { isError: true, content: [ { type: "text", text: `Error executing '${definition.name}': ${mcpError.message}`, }, ], structuredContent: { success: false, data: [], error: mcpError.message, errorCode: mcpError.code, errorDetails: mcpError.details, }, } satisfies CallToolResult; } }); }; }

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/IBM/ibmi-mcp'

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