Skip to main content
Glama

tbls MCP Server

by yhosok
schema-resource.ts10.7 kB
import { Result, ok, err } from 'neverthrow'; import { promises as fs } from 'fs'; import { join } from 'path'; import * as path from 'path'; import { SchemaListResource } from '../schemas/database'; import { parseTableReferences, parseSchemaOverview, parseSchemaWithFallback, resolveSchemaSource, } from '../parsers/schema-adapter'; import { parseJsonSchemaList } from '../parsers/json-parser'; import { safeExecuteAsync, fromPromise, createError } from '../utils/result'; import { ResourceCache } from '../cache/resource-cache'; /** * Handles the db://schemas MCP resource * Returns a list of all available database schemas with metadata * * @param schemaSource - Path to schema file or directory containing tbls schema files * @param cache - Optional cache instance for performance optimization * @returns Result containing schema list resource or error */ export const handleSchemaListResource = async ( schemaSource: string, cache?: ResourceCache ): Promise<Result<SchemaListResource, Error>> => { // Resolve the schema source to determine if it's a file or directory const resolveResult = resolveSchemaSource(schemaSource); if (resolveResult.isErr()) { // If it's a directory but no JSON files found, treat as empty directory if ( resolveResult.error.message.includes( 'No JSON schema file found in directory' ) ) { return ok({ schemas: [] }); } return err(resolveResult.error); } const { type: sourceType, path: schemaPath } = resolveResult.value; const schemaDir = sourceType === 'file' ? path.dirname(schemaPath) : schemaPath; // Try to get cached schema list first if (cache) { const cachedTableRefs = await cache.getTableReferences(schemaDir); if (cachedTableRefs) { // Build a simple schema list from cached table references const schemas = [ { name: 'default', tableCount: cachedTableRefs.length, description: 'Default schema', }, ]; return ok({ schemas }); } } // For single file sources, check if it contains multiple schemas if (sourceType === 'file') { // Try to parse as multi-schema format first const multiSchemaResult = await parseMultiSchemaInfo(schemaPath, cache); if (multiSchemaResult.isOk()) { return ok({ schemas: multiSchemaResult.value }); } // Fall back to single schema parsing const singleSchemaResult = await parseSingleSchemaInfo( schemaDir, 'default', cache ); if (singleSchemaResult.isOk()) { return ok({ schemas: [singleSchemaResult.value] }); } else { // Fallback for single file return ok({ schemas: [ { name: 'default', tableCount: 0, description: 'Default schema', }, ], }); } } // Read directory contents const readDirResult = await fromPromise( fs.readdir(schemaDir, { withFileTypes: true }), 'Failed to read schema directory' ); if (readDirResult.isErr()) { return err(readDirResult.error); } const dirEntries = readDirResult.value; const schemas: Array<{ name: string; tableCount?: number; description?: string | null; }> = []; // Check for single schema setup (schema.json in root) const hasRootSchemaFile = dirEntries.some( (entry) => entry.isFile() && entry.name === 'schema.json' ); if (hasRootSchemaFile) { const singleSchemaResult = await parseSingleSchemaInfo( schemaDir, 'default', cache ); if (singleSchemaResult.isOk()) { schemas.push(singleSchemaResult.value); } } // Check for multi-schema setup (subdirectories with schema files) const subdirectories = dirEntries.filter((entry) => entry.isDirectory()); for (const subdir of subdirectories) { const subdirPath = join(schemaDir, subdir.name); // Check for schema.json file const hasSchemaFileResult = await safeExecuteAsync(async () => { await fs.access(join(subdirPath, 'schema.json')); return true; }, 'No schema file found in subdirectory'); if (hasSchemaFileResult.isOk()) { const schemaResult = await parseSingleSchemaInfo( subdirPath, subdir.name, cache ); if (schemaResult.isOk()) { schemas.push(schemaResult.value); } } } // Sort schemas by name for consistent ordering schemas.sort((a, b) => a.name.localeCompare(b.name)); const result = { schemas }; // Cache the result if cache is available if (cache && schemas.length > 0) { // For single schema setups, cache the table references for faster subsequent calls if (schemas.length === 1 && schemas[0].name === 'default') { // Try to get table references for the default schema and cache them const tableRefsResult = parseTableReferences(schemaDir); if (tableRefsResult.isOk()) { await cache.setTableReferences(schemaDir, tableRefsResult.value); } } } return ok(result); }; /** * Parses schema information from a schema file (JSON format) * * @param schemaPath - Path to the schema directory * @param schemaName - Name of the schema * @param cache - Optional cache instance for performance optimization * @returns Result containing schema info or error */ const parseSingleSchemaInfo = async ( schemaPath: string, schemaName: string, cache?: ResourceCache ): Promise< Result< { name: string; tableCount?: number; description?: string | null }, Error > > => { // Try to get cached schema info first if (cache) { const cachedSchema = await cache.getSchema(schemaPath); if (cachedSchema?.metadata) { return ok({ name: schemaName, tableCount: cachedSchema.metadata.tableCount ?? cachedSchema.tables?.length ?? 0, description: cachedSchema.metadata.description, }); } } // Try to parse as full schema overview first using the schema adapter const overviewResult = parseSchemaOverview(schemaPath); if (overviewResult.isOk()) { const metadata = overviewResult.value; const result = { name: metadata.name || schemaName, // Use parsed name from schema.json, fall back to provided name tableCount: metadata.tableCount ?? undefined, description: metadata.description, }; // Cache the parsed metadata if cache is available if (cache) { const cacheSchema = { metadata: { name: metadata.name || schemaName, tableCount: metadata.tableCount, generated: metadata.generated ?? null, version: metadata.version ?? null, description: metadata.description, }, tables: [], tableReferences: [], indexes: [], relations: [], }; await cache.setSchema(schemaPath, cacheSchema); } return ok(result); } // Fallback: try to parse table references for table count const tableRefsResult = parseTableReferences(schemaPath); if (tableRefsResult.isOk()) { const tableCount = tableRefsResult.value.length; const result = { name: schemaName, tableCount: tableCount > 0 ? tableCount : 0, description: schemaName === 'default' ? 'Default schema' : null, }; // Cache the table references if cache is available if (cache) { await cache.setTableReferences(schemaPath, tableRefsResult.value); } return ok(result); } // Final fallback: try parseSchemaWithFallback for comprehensive format detection const schemaResult = parseSchemaWithFallback(schemaPath); if (schemaResult.isOk()) { const schema = schemaResult.value; const result = { name: schemaName, tableCount: schema.tables?.length ?? 0, description: schema.metadata?.description ?? (schemaName === 'default' ? 'Default schema' : null), }; // Cache the full schema if cache is available if (cache) { await cache.setSchema(schemaPath, schema); } return ok(result); } // If all parsing fails, still return basic schema info return ok({ name: schemaName, tableCount: 0, description: schemaName === 'default' ? 'Default schema' : null, }); }; /** * Parses multi-schema information from a JSON file with 'schemas' array * * @param schemaPath - Path to the schema file * @param cache - Optional cache instance for performance optimization * @returns Result containing array of schema info or error */ const parseMultiSchemaInfo = async ( schemaPath: string, cache?: ResourceCache ): Promise< Result< Array<{ name: string; tableCount?: number; description?: string | null }>, Error > > => { try { const content = await fs.readFile(schemaPath, 'utf-8'); const metadataListResult = parseJsonSchemaList(content); if (metadataListResult.isOk()) { const metadataList = metadataListResult.value; // If there's only one schema and no explicit name, handle as single schema with "default" if ( metadataList.length === 1 && metadataList[0].name === 'database_schema' ) { return ok([ { name: 'default', tableCount: metadataList[0].tableCount ?? undefined, description: metadataList[0].description || 'Default schema', }, ]); } // Convert metadata to schema info format const schemas = metadataList.map((metadata) => ({ name: metadata.name || 'database_schema', tableCount: metadata.tableCount ?? undefined, description: metadata.description, })); // Cache the results if cache is available if (cache && schemas.length > 0) { // For multi-schema setups, we can cache the schema list for faster subsequent calls // Cache each schema's metadata separately for (const schema of schemas) { const cacheSchema = { metadata: { name: schema.name, tableCount: schema.tableCount ?? null, generated: null, version: null, description: schema.description, }, tables: [], tableReferences: [], indexes: [], relations: [], }; await cache.setSchema(`${schemaPath}:${schema.name}`, cacheSchema); } } return ok(schemas); } return createError('Failed to parse multi-schema format'); } catch (error) { return createError( `Failed to parse multi-schema file: ${error instanceof Error ? error.message : 'Unknown error'}` ); } }; // Export parseSingleSchemaInfo for testing export { parseSingleSchemaInfo, parseMultiSchemaInfo };

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/yhosok/tbls-mcp-server'

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