Skip to main content
Glama

Stampchain MCP Server

Official
stamps.ts25.5 kB
/** * Stamp-related MCP tools implementation * These tools provide access to Bitcoin stamp information */ import type { z } from 'zod'; import type { Tool as MCPTool } from '@modelcontextprotocol/sdk/types.js'; import type { ToolResponse, ToolContext } from '../interfaces/tool.js'; import { textResponse, multiResponse, BaseTool } from '../interfaces/tool.js'; import { ToolExecutionError, ValidationError } from '../utils/errors.js'; import { StampchainClient } from '../api/stampchain-client.js'; import type { Stamp, RecentSalesResponse, StampMarketData } from '../api/types.js'; import { GetStampParamsSchema, SearchStampsParamsSchema, GetRecentStampsParamsSchema, GetRecentSalesParamsSchema, GetMarketDataParamsSchema, GetStampMarketDataParamsSchema, type GetStampParams, type SearchStampsParams, type GetRecentStampsParams, type GetRecentSalesParams, type GetMarketDataParams, type GetStampMarketDataParams, } from '../schemas/stamps.js'; import { formatStamp, formatStampList, stampToJSON } from '../utils/formatters.js'; import { parseStampId } from '../utils/validators.js'; /** * Tool for retrieving detailed information about a specific Bitcoin stamp */ export class GetStampTool extends BaseTool<z.input<typeof GetStampParamsSchema>, GetStampParams> { public readonly name = 'get_stamp'; public readonly description = 'Retrieve detailed information about a specific Bitcoin stamp by its ID'; public readonly inputSchema: MCPTool['inputSchema'] = { type: 'object', properties: { stamp_id: { type: ['number', 'string'], description: 'The ID of the stamp to retrieve', }, include_base64: { type: 'boolean', description: 'Whether to include base64 image data', default: false, }, }, required: ['stamp_id'], }; public readonly schema = GetStampParamsSchema; public readonly metadata = { version: '1.0.0', tags: ['stamps', 'query'], requiresNetwork: true, apiDependencies: ['stampchain'], }; private apiClient: StampchainClient; constructor(apiClient?: StampchainClient) { super(); this.apiClient = apiClient || new StampchainClient(); } public async execute(params: GetStampParams, context?: ToolContext): Promise<ToolResponse> { try { context?.logger?.info('Executing get_stamp tool', { params }); // Validate parameters const validatedParams = this.validateParams(params); const stampId = parseStampId(validatedParams.stamp_id); // Use API client from context if available, otherwise use instance client const client = context?.apiClient || this.apiClient; // Fetch stamp data const stamp: Stamp = await client.getStamp(stampId); if (!stamp) { throw new ToolExecutionError(`Stamp with ID ${stampId} not found`, this.name, { stampId }); } // Format the response const formattedStamp = formatStamp(stamp, { includeBase64: validatedParams.include_base64, }); // Return both formatted text and JSON data return multiResponse({ type: 'text', text: formattedStamp }, stampToJSON(stamp)); } catch (error) { context?.logger?.error('Error executing get_stamp tool', { error }); if (error instanceof ValidationError) { throw error; } if (error instanceof ToolExecutionError) { throw error; } // Pass through the original error message for API errors if (error instanceof Error) { throw new ToolExecutionError(error.message, this.name, error); } throw new ToolExecutionError('Failed to retrieve stamp information', this.name, error); } } } /** * Tool for searching stamps with various filtering criteria */ export class SearchStampsTool extends BaseTool< z.input<typeof SearchStampsParamsSchema>, SearchStampsParams > { public readonly name = 'search_stamps'; public readonly description = 'Search for Bitcoin stamps with various filtering criteria including creator, collection, and stamp type'; public readonly inputSchema: MCPTool['inputSchema'] = { type: 'object', properties: { query: { type: 'string', description: 'Search query string', }, creator: { type: 'string', description: 'Filter by creator address', }, collection_id: { type: 'string', description: 'Filter by collection ID', }, cpid: { type: 'string', description: 'Filter by CPID', }, is_btc_stamp: { type: 'boolean', description: 'Filter for BTC stamps only', }, is_cursed: { type: 'boolean', description: 'Filter for cursed stamps only', }, sort_order: { type: 'string', enum: ['ASC', 'DESC'], description: 'Sort order by stamp ID', default: 'DESC', }, page: { type: 'number', description: 'Page number', minimum: 1, default: 1, }, page_size: { type: 'number', description: 'Items per page', minimum: 1, maximum: 100, default: 20, }, }, required: [], }; public readonly schema = SearchStampsParamsSchema; public readonly metadata = { version: '1.0.0', tags: ['stamps', 'search', 'query'], requiresNetwork: true, apiDependencies: ['stampchain'], }; private apiClient: StampchainClient; constructor(apiClient?: StampchainClient) { super(); this.apiClient = apiClient || new StampchainClient(); } public async execute(params: SearchStampsParams, context?: ToolContext): Promise<ToolResponse> { try { context?.logger?.info('Executing search_stamps tool', { params }); // Validate parameters const validatedParams = this.validateParams(params); // Build query parameters const queryParams = { query: validatedParams.query, creator: validatedParams.creator, collection_id: validatedParams.collection_id, cpid: validatedParams.cpid, is_btc_stamp: validatedParams.is_btc_stamp, is_cursed: validatedParams.is_cursed, sort_order: validatedParams.sort_order, page: validatedParams.page, page_size: validatedParams.page_size, }; // Remove undefined values Object.keys(queryParams).forEach((key) => { if (queryParams[key as keyof typeof queryParams] === undefined) { delete queryParams[key as keyof typeof queryParams]; } }); // Use API client from context if available, otherwise use instance client const client = context?.apiClient || this.apiClient; // Search stamps const stamps: Stamp[] = await client.searchStamps(queryParams); if (!stamps || stamps.length === 0) { return textResponse('No stamps found matching the search criteria'); } // Since the API returns an array, we need to handle pagination info differently const total = stamps.length; const page = validatedParams.page || 1; const limit = validatedParams.page_size || 20; // Format the response const formattedList = formatStampList(stamps, total, page, limit); // Include metadata about the search const metadata = { total_results: total, current_page: page, page_size: limit, total_pages: Math.ceil(total / limit), query_params: queryParams, }; return multiResponse( { type: 'text', text: formattedList }, { type: 'text', text: `\nSearch Metadata:\n${JSON.stringify(metadata, null, 2)}` } ); } catch (error) { context?.logger?.error('Error executing search_stamps tool', { error }); if (error instanceof ValidationError) { throw error; } if (error instanceof ToolExecutionError) { throw error; } // Pass through the original error message for API errors if (error instanceof Error) { throw new ToolExecutionError(error.message, this.name, error); } throw new ToolExecutionError('Failed to search stamps', this.name, error); } } } /** * Tool for retrieving recently created stamps */ export class GetRecentStampsTool extends BaseTool< z.input<typeof GetRecentStampsParamsSchema>, GetRecentStampsParams > { public readonly name = 'get_recent_stamps'; public readonly description = 'Retrieve the most recently created Bitcoin stamps'; public readonly inputSchema: MCPTool['inputSchema'] = { type: 'object', properties: { limit: { type: 'number', description: 'Number of recent stamps to retrieve', minimum: 1, maximum: 100, default: 20, }, }, required: [], }; public readonly schema = GetRecentStampsParamsSchema; public readonly metadata = { version: '1.0.0', tags: ['stamps', 'recent'], requiresNetwork: true, apiDependencies: ['stampchain'], }; private apiClient: StampchainClient; constructor(apiClient?: StampchainClient) { super(); this.apiClient = apiClient || new StampchainClient(); } public async execute( params: GetRecentStampsParams, context?: ToolContext ): Promise<ToolResponse> { try { context?.logger?.info('Executing get_recent_stamps tool', { params }); // Validate parameters const validatedParams = this.validateParams(params); // Get recent stamps by searching with sort order DESC const queryParams = { sort_order: 'DESC' as const, page: 1, page_size: validatedParams.limit, }; // Use API client from context if available, otherwise use instance client const client = context?.apiClient || this.apiClient; const stamps: Stamp[] = await client.searchStamps(queryParams); if (!stamps || stamps.length === 0) { return textResponse('No recent stamps found'); } // Create a summary of recent stamps const lines = [`${stamps.length} Most Recent Stamps:`]; lines.push('---'); stamps.forEach((stamp, index) => { lines.push(`${index + 1}. Stamp #${stamp.stamp}`); lines.push(` Block: ${stamp.block_index}`); lines.push(` Creator: ${stamp.creator}`); lines.push(` Type: ${stamp.ident}`); lines.push(` CPID: ${stamp.cpid}`); if (stamp.floorPrice) { lines.push(` Floor Price: ${stamp.floorPrice} BTC`); } lines.push(''); }); return textResponse(lines.join('\n')); } catch (error) { context?.logger?.error('Error executing get_recent_stamps tool', { error }); if (error instanceof ValidationError) { throw error; } if (error instanceof ToolExecutionError) { throw error; } // Pass through the original error message for API errors if (error instanceof Error) { throw new ToolExecutionError(error.message, this.name, error); } throw new ToolExecutionError('Failed to retrieve recent stamps', this.name, error); } } } /** * Tool for retrieving recent sales data (v2.3 feature) */ export class GetRecentSalesTool extends BaseTool< z.input<typeof GetRecentSalesParamsSchema>, GetRecentSalesParams > { public readonly name = 'get_recent_sales'; public readonly description = 'Retrieve recent stamp sales with enhanced transaction details (v2.3 feature)'; public readonly inputSchema: MCPTool['inputSchema'] = { type: 'object', properties: { stamp_id: { type: 'number', description: 'Filter by specific stamp ID', }, dayRange: { type: 'number', description: 'Number of days to look back for sales', minimum: 1, maximum: 365, default: 30, }, fullDetails: { type: 'boolean', description: 'Enable enhanced transaction information', default: false, }, page: { type: 'number', description: 'Page number', minimum: 1, default: 1, }, page_size: { type: 'number', description: 'Items per page', minimum: 1, maximum: 100, default: 20, }, sort_order: { type: 'string', enum: ['ASC', 'DESC'], description: 'Sort order by timestamp', default: 'DESC', }, }, required: [], }; public readonly schema = GetRecentSalesParamsSchema; public readonly metadata = { version: '1.0.0', tags: ['stamps', 'sales', 'market', 'v2.3'], requiresNetwork: true, apiDependencies: ['stampchain'], }; private apiClient: StampchainClient; constructor(apiClient?: StampchainClient) { super(); this.apiClient = apiClient || new StampchainClient(); } public async execute(params: GetRecentSalesParams, context?: ToolContext): Promise<ToolResponse> { try { context?.logger?.info('Executing get_recent_sales tool', { params }); // Validate parameters const validatedParams = this.validateParams(params); // Use API client from context if available, otherwise use instance client const client = context?.apiClient || this.apiClient; // Check if v2.3 features are available const features = client.getFeatureAvailability(); if (!features.recentSales) { return textResponse( 'Recent sales data is not available in the current API version. Please upgrade to v2.3 or later.' ); } // Get recent sales data const salesData: RecentSalesResponse = await client.getRecentSales(validatedParams); if (!salesData.data || salesData.data.length === 0) { return textResponse('No recent sales found for the specified criteria'); } // Format the response const lines = [ `Recent Sales (${salesData.data.length} results, ${validatedParams.dayRange} days):`, ]; lines.push('---'); salesData.data.forEach((sale, index) => { lines.push(`${index + 1}. Stamp #${sale.stamp_id}`); lines.push(` Transaction: ${sale.tx_hash}`); lines.push(` Block: ${sale.block_index}`); lines.push(` Price: ${sale.price_btc} BTC`); if (sale.price_usd) { lines.push(` Price USD: $${sale.price_usd.toFixed(2)}`); } if (sale.buyer_address) { lines.push(` Buyer: ${sale.buyer_address}`); } if (sale.time_ago) { lines.push(` Time: ${sale.time_ago}`); } if (sale.dispenser_address) { lines.push(` Dispenser: ${sale.dispenser_address}`); } lines.push(''); }); // Add metadata lines.push('Metadata:'); lines.push(`- Day Range: ${salesData.metadata.dayRange} days`); lines.push(`- Last Updated: ${new Date(salesData.metadata.lastUpdated).toISOString()}`); lines.push(`- Total Results: ${salesData.metadata.total}`); lines.push(`- Last Block: ${salesData.last_block}`); return textResponse(lines.join('\n')); } catch (error) { context?.logger?.error('Error executing get_recent_sales tool', { error }); if (error instanceof ValidationError) { throw error; } if (error instanceof ToolExecutionError) { throw error; } // Pass through the original error message for API errors if (error instanceof Error) { throw new ToolExecutionError(error.message, this.name, error); } throw new ToolExecutionError('Failed to retrieve recent sales data', this.name, error); } } } /** * Tool for retrieving market data for stamps (v2.3 feature) */ export class GetMarketDataTool extends BaseTool< z.input<typeof GetMarketDataParamsSchema>, GetMarketDataParams > { public readonly name = 'get_market_data'; public readonly description = 'Retrieve market data for stamps with trading activity indicators (v2.3 feature)'; public readonly inputSchema: MCPTool['inputSchema'] = { type: 'object', properties: { stamp_id: { type: 'number', description: 'Filter by specific stamp ID', }, activity_level: { type: 'string', enum: ['HOT', 'WARM', 'COOL', 'DORMANT', 'COLD'], description: 'Filter by trading activity level', }, min_floor_price: { type: 'number', description: 'Minimum floor price in BTC', minimum: 0, }, max_floor_price: { type: 'number', description: 'Maximum floor price in BTC', minimum: 0, }, include_volume_data: { type: 'boolean', description: 'Include volume data in response', default: true, }, page: { type: 'number', description: 'Page number', minimum: 1, default: 1, }, page_size: { type: 'number', description: 'Items per page', minimum: 1, maximum: 100, default: 20, }, }, required: [], }; public readonly schema = GetMarketDataParamsSchema; public readonly metadata = { version: '1.0.0', tags: ['stamps', 'market', 'trading', 'v2.3'], requiresNetwork: true, apiDependencies: ['stampchain'], }; private apiClient: StampchainClient; constructor(apiClient?: StampchainClient) { super(); this.apiClient = apiClient || new StampchainClient(); } public async execute(params: GetMarketDataParams, context?: ToolContext): Promise<ToolResponse> { try { context?.logger?.info('Executing get_market_data tool', { params }); // Validate parameters const validatedParams = this.validateParams(params); // Use API client from context if available, otherwise use instance client const client = context?.apiClient || this.apiClient; // Check if v2.3 features are available const features = client.getFeatureAvailability(); if (!features.marketData) { return textResponse( 'Market data is not available in the current API version. Please upgrade to v2.3 or later.' ); } // Get market data const marketData = await client.getMarketData(validatedParams); if (!marketData.data || marketData.data.length === 0) { return textResponse('No market data found for the specified criteria'); } // Format the response const lines = [`Market Data (${marketData.data.length} results):`]; lines.push('---'); marketData.data.forEach((data: StampMarketData, index: number) => { lines.push(`${index + 1}. Market Data Entry`); lines.push(` Floor Price: ${data.floorPriceBTC || 'N/A'} BTC`); if (data.floorPriceUSD) { lines.push(` Floor Price USD: $${data.floorPriceUSD.toFixed(2)}`); } // Note: marketCapUSD not available in v2.3 marketData object lines.push(` Activity Level: ${data.activityLevel}`); if (data.lastActivityTime) { lines.push(` Last Activity: ${new Date(data.lastActivityTime * 1000).toISOString()}`); } if (data.volume24hBTC) { lines.push(` 24h Volume: ${data.volume24hBTC} BTC`); } if (data.lastSaleTxHash) { lines.push(` Last Sale TX: ${data.lastSaleTxHash}`); } if (data.lastSaleBuyerAddress) { lines.push(` Last Buyer: ${data.lastSaleBuyerAddress}`); } lines.push(''); }); // Add metadata lines.push('Metadata:'); lines.push(`- Total Results: ${marketData.total}`); lines.push(`- Page: ${marketData.page}`); lines.push(`- Page Size: ${marketData.limit}`); lines.push(`- Last Block: ${marketData.last_block}`); return textResponse(lines.join('\n')); } catch (error) { context?.logger?.error('Error executing get_market_data tool', { error }); if (error instanceof ValidationError) { throw error; } if (error instanceof ToolExecutionError) { throw error; } // Pass through the original error message for API errors if (error instanceof Error) { throw new ToolExecutionError(error.message, this.name, error); } throw new ToolExecutionError('Failed to retrieve market data', this.name, error); } } } /** * Tool for retrieving market data for a specific stamp (v2.3 feature) */ export class GetStampMarketDataTool extends BaseTool< z.input<typeof GetStampMarketDataParamsSchema>, GetStampMarketDataParams > { public readonly name = 'get_stamp_market_data'; public readonly description = 'Retrieve detailed market data for a specific stamp (v2.3 feature)'; public readonly inputSchema: MCPTool['inputSchema'] = { type: 'object', properties: { stamp_id: { type: ['number', 'string'], description: 'The ID of the stamp to get market data for', }, }, required: ['stamp_id'], }; public readonly schema = GetStampMarketDataParamsSchema; public readonly metadata = { version: '1.0.0', tags: ['stamps', 'market', 'trading', 'v2.3'], requiresNetwork: true, apiDependencies: ['stampchain'], }; private apiClient: StampchainClient; constructor(apiClient?: StampchainClient) { super(); this.apiClient = apiClient || new StampchainClient(); } public async execute( params: GetStampMarketDataParams, context?: ToolContext ): Promise<ToolResponse> { try { context?.logger?.info('Executing get_stamp_market_data tool', { params }); // Validate parameters const validatedParams = this.validateParams(params); // Use API client from context if available, otherwise use instance client const client = context?.apiClient || this.apiClient; // Check if v2.3 features are available const features = client.getFeatureAvailability(); if (!features.marketData) { return textResponse( 'Market data is not available in the current API version. Please upgrade to v2.3 or later.' ); } // Get stamp market data const marketData: StampMarketData = await client.getStampMarketData(validatedParams.stamp_id); if (!marketData) { return textResponse(`No market data found for stamp ${validatedParams.stamp_id}`); } // Format the response const lines = [`Market Data for Stamp #${validatedParams.stamp_id}:`]; lines.push('---'); lines.push(`Floor Price: ${marketData.floorPriceBTC || 'N/A'} BTC`); if (marketData.floorPriceUSD) { lines.push(`Floor Price USD: $${marketData.floorPriceUSD.toFixed(2)}`); } // Note: marketCapUSD not available in v2.3 marketData object lines.push(`Activity Level: ${marketData.activityLevel}`); if (marketData.lastActivityTime) { lines.push(`Last Activity: ${new Date(marketData.lastActivityTime * 1000).toISOString()}`); } if (marketData.volume24hBTC) { lines.push(`24h Volume: ${marketData.volume24hBTC} BTC`); } if (marketData.volume7dBTC) { lines.push(`7d Volume: ${marketData.volume7dBTC} BTC`); } if (marketData.volume30dBTC) { lines.push(`30d Volume: ${marketData.volume30dBTC} BTC`); } if (marketData.lastSaleTxHash) { lines.push(''); lines.push('Last Sale Details:'); lines.push(`- Transaction: ${marketData.lastSaleTxHash}`); if (marketData.lastSaleBuyerAddress) { lines.push(`- Buyer: ${marketData.lastSaleBuyerAddress}`); } if (marketData.lastSaleDispenserAddress) { lines.push(`- Dispenser: ${marketData.lastSaleDispenserAddress}`); } if (marketData.lastSaleBtcAmount) { lines.push(`- Amount: ${marketData.lastSaleBtcAmount} BTC`); } if (marketData.lastSaleBlockIndex) { lines.push(`- Block: ${marketData.lastSaleBlockIndex}`); } } return textResponse(lines.join('\n')); } catch (error) { context?.logger?.error('Error executing get_stamp_market_data tool', { error }); if (error instanceof ValidationError) { throw error; } if (error instanceof ToolExecutionError) { throw error; } // Pass through the original error message for API errors if (error instanceof Error) { throw new ToolExecutionError(error.message, this.name, error); } throw new ToolExecutionError('Failed to retrieve stamp market data', this.name, error); } } } /** * Export all stamp tools */ export const stampTools = { get_stamp: GetStampTool, search_stamps: SearchStampsTool, get_recent_stamps: GetRecentStampsTool, get_recent_sales: GetRecentSalesTool, get_market_data: GetMarketDataTool, get_stamp_market_data: GetStampMarketDataTool, }; /** * Factory function to create all stamp tool instances */ export function createStampTools(apiClient?: StampchainClient) { return { get_stamp: new GetStampTool(apiClient), search_stamps: new SearchStampsTool(apiClient), get_recent_stamps: new GetRecentStampsTool(apiClient), get_recent_sales: new GetRecentSalesTool(apiClient), get_market_data: new GetMarketDataTool(apiClient), get_stamp_market_data: new GetStampMarketDataTool(apiClient), }; }

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/stampchain-io/stampchain-mcp'

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