Skip to main content
Glama
gatherCollectionInfo.ts9.9 kB
import { promises as fs } from "node:fs"; import path from "node:path"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; import type { Wallet } from "./wallet"; const gatherCollectionInfoArgsSchema = z.object({ folderPath: z .string() .describe("Path to folder containing images to analyze for collection"), }); type GatherCollectionInfoArgs = z.infer<typeof gatherCollectionInfoArgsSchema>; interface ImageInfo { fileName: string; filePath: string; size: number; sizeKB: number; contentType: string; valid: boolean; error?: string; } interface CollectionAnalysis { folderPath: string; totalImages: number; validImages: number; invalidImages: number; totalSizeKB: number; imageTypes: Record<string, number>; images: ImageInfo[]; suggestedMetadata: { collectionName: string; description: string; quantity: number; }; costEstimate: { collectionInscriptionCost: number; perItemCost: number; totalItemsCost: number; totalCost: number; totalCostBSV: number; }; walletInfo: { address: string; balance: number; balanceBSV: number; hasEnoughFunds: boolean; shortfall?: number; }; warnings: string[]; errors: string[]; } async function analyzeImageFile(filePath: string): Promise<ImageInfo> { const fileName = path.basename(filePath); const ext = path.extname(fileName).toLowerCase(); const contentTypeMap: Record<string, string> = { ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".gif": "image/gif", ".webp": "image/webp", ".svg": "image/svg+xml", }; try { const stat = await fs.stat(filePath); const contentType = contentTypeMap[ext]; if (!contentType) { return { fileName, filePath, size: stat.size, sizeKB: Math.round(stat.size / 1024), contentType: "unknown", valid: false, error: `Unsupported file type: ${ext}`, }; } // Check file size (warn if over 50KB) const sizeKB = Math.round(stat.size / 1024); const warnings: string[] = []; if (sizeKB > 50) { warnings.push( `Large file size: ${sizeKB}KB (consider optimizing for lower fees)`, ); } return { fileName, filePath, size: stat.size, sizeKB, contentType, valid: true, error: warnings.length > 0 ? warnings.join("; ") : undefined, }; } catch (error) { return { fileName, filePath, size: 0, sizeKB: 0, contentType: "unknown", valid: false, error: error instanceof Error ? error.message : "Unknown error", }; } } function estimateInscriptionCost(sizeKB: number): number { // Base cost for inscription (1 sat) const baseCost = 1; // Additional cost based on size (rough estimate: 10 sats per KB) const sizeCost = Math.ceil(sizeKB * 10); // Mining fee estimate const miningFee = 500; return baseCost + sizeCost + miningFee; } export function registerGatherCollectionInfoTool( server: McpServer, wallet: Wallet, ) { server.tool( "wallet_gatherCollectionInfo", "Analyzes a folder of images and gathers all necessary information for minting an ordinals collection. This includes validating images, checking wallet balance, estimating costs, and suggesting metadata. Use this before minting to ensure everything is ready.", { args: gatherCollectionInfoArgsSchema }, async ({ args, }: { args: GatherCollectionInfoArgs }): Promise<CallToolResult> => { try { const analysis: CollectionAnalysis = { folderPath: args.folderPath, totalImages: 0, validImages: 0, invalidImages: 0, totalSizeKB: 0, imageTypes: {}, images: [], suggestedMetadata: { collectionName: "", description: "", quantity: 0, }, costEstimate: { collectionInscriptionCost: 0, perItemCost: 0, totalItemsCost: 0, totalCost: 0, totalCostBSV: 0, }, walletInfo: { address: "", balance: 0, balanceBSV: 0, hasEnoughFunds: false, }, warnings: [], errors: [], }; // Check if folder exists try { const folderStat = await fs.stat(args.folderPath); if (!folderStat.isDirectory()) { throw new Error(`Path is not a directory: ${args.folderPath}`); } } catch (error) { analysis.errors.push( `Folder not found or inaccessible: ${args.folderPath}`, ); return { content: [ { type: "text", text: JSON.stringify(analysis, null, 2), }, ], isError: true, }; } // Get wallet info const paymentPk = wallet.getPrivateKey(); if (!paymentPk) { analysis.errors.push("No payment key available in wallet"); return { content: [ { type: "text", text: JSON.stringify(analysis, null, 2), }, ], isError: true, }; } analysis.walletInfo.address = paymentPk.toAddress().toString(); // Get wallet balance try { const { paymentUtxos } = await wallet.getUtxos(); const balance = paymentUtxos.reduce( (sum, utxo) => sum + utxo.satoshis, 0, ); analysis.walletInfo.balance = balance; analysis.walletInfo.balanceBSV = balance / 100000000; } catch (error) { analysis.warnings.push("Could not fetch wallet balance"); } // Scan folder for images const files = await fs.readdir(args.folderPath); const imageExtensions = [ ".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ]; for (const file of files) { const filePath = path.join(args.folderPath, file); const stat = await fs.stat(filePath); if (stat.isFile()) { const ext = path.extname(file).toLowerCase(); if (imageExtensions.includes(ext)) { const imageInfo = await analyzeImageFile(filePath); analysis.images.push(imageInfo); analysis.totalImages++; if (imageInfo.valid) { analysis.validImages++; analysis.totalSizeKB += imageInfo.sizeKB; // Track image types const type = imageInfo.contentType.split("/")[1] || "unknown"; analysis.imageTypes[type] = (analysis.imageTypes[type] || 0) + 1; } else { analysis.invalidImages++; if (imageInfo.error) { analysis.errors.push( `${imageInfo.fileName}: ${imageInfo.error}`, ); } } } } } // Sort images by name for consistent ordering analysis.images.sort((a, b) => a.fileName.localeCompare(b.fileName)); if (analysis.validImages === 0) { analysis.errors.push("No valid images found in the folder"); return { content: [ { type: "text", text: JSON.stringify(analysis, null, 2), }, ], isError: true, }; } // Suggest metadata based on folder name const folderName = path.basename(args.folderPath); analysis.suggestedMetadata = { collectionName: folderName .replace(/[-_]/g, " ") .replace(/\b\w/g, (l) => l.toUpperCase()), description: `A collection of ${analysis.validImages} unique digital artifacts`, quantity: analysis.validImages, }; // Estimate costs const collectionIconSizeKB = 1; // SVG icon is small analysis.costEstimate.collectionInscriptionCost = estimateInscriptionCost(collectionIconSizeKB); // Average cost per item const avgItemSizeKB = analysis.totalSizeKB / analysis.validImages; analysis.costEstimate.perItemCost = estimateInscriptionCost(avgItemSizeKB); analysis.costEstimate.totalItemsCost = analysis.costEstimate.perItemCost * analysis.validImages; analysis.costEstimate.totalCost = analysis.costEstimate.collectionInscriptionCost + analysis.costEstimate.totalItemsCost; analysis.costEstimate.totalCostBSV = analysis.costEstimate.totalCost / 100000000; // Check if wallet has enough funds analysis.walletInfo.hasEnoughFunds = analysis.walletInfo.balance >= analysis.costEstimate.totalCost; if (!analysis.walletInfo.hasEnoughFunds) { analysis.walletInfo.shortfall = analysis.costEstimate.totalCost - analysis.walletInfo.balance; analysis.warnings.push( `Insufficient funds: need ${analysis.costEstimate.totalCost} sats, have ${analysis.walletInfo.balance} sats (shortfall: ${analysis.walletInfo.shortfall} sats)`, ); } // Add warnings for large collections if (analysis.validImages > 100) { analysis.warnings.push( `Large collection (${analysis.validImages} items) will take time and multiple transactions to mint`, ); } if (analysis.totalSizeKB > 1000) { analysis.warnings.push( `Large total size (${Math.round(analysis.totalSizeKB / 1024)}MB) will result in higher fees`, ); } // Generate summary const summary = { ready: analysis.errors.length === 0 && analysis.walletInfo.hasEnoughFunds, summary: `Found ${analysis.validImages} valid images in "${folderName}". Total size: ${Math.round(analysis.totalSizeKB)}KB. Estimated cost: ${analysis.costEstimate.totalCost} sats (${analysis.costEstimate.totalCostBSV.toFixed(8)} BSV). Wallet balance: ${analysis.walletInfo.balance} sats. ${ analysis.walletInfo.hasEnoughFunds ? "Ready to mint!" : "Insufficient funds." }`, }; return { content: [ { type: "text", text: JSON.stringify( { ...analysis, ...summary, }, null, 2, ), }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: `Error analyzing collection: ${errorMessage}`, }, ], isError: true, }; } }, ); }

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/b-open-io/bsv-mcp'

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