Skip to main content
Glama

Stampchain MCP Server

Official
stampchain-client.ts17.1 kB
import axios, { AxiosInstance, AxiosError, type AxiosRequestConfig } from 'axios'; import axiosRetry from 'axios-retry'; import { Logger } from '../utils/logger.js'; import { type MCPError, InvalidParametersError, InternalError, ResourceNotFoundError, RateLimitError, NetworkError, TimeoutError, StampchainAPIError, } from '../utils/errors.js'; import type { Stamp, StampResponse, StampListResponse, CollectionResponse, CollectionListResponse, TokenResponse, TokenListResponse, BlockResponse, BalanceResponse, APIErrorResponse, StampQueryParams, CollectionQueryParams, TokenQueryParams, RecentSale, RecentSalesResponse, RecentSalesQueryParams, MarketDataQueryParams, StampMarketData, } from './types.js'; export interface StampchainClientConfig { baseURL?: string; apiKey?: string; timeout?: number; retries?: number; retryDelay?: number; apiVersion?: string; } export class StampchainClient { private client: AxiosInstance; private logger: Logger; private apiVersion: string; private requestTimers: Map<string, { startTime: number; requestId: string }> = new Map(); constructor(config?: StampchainClientConfig) { const { baseURL = 'https://stampchain.io/api/v2', apiKey, timeout = 30000, retries = 3, retryDelay = 1000, apiVersion = '2.3', } = config || {}; this.logger = new Logger('StampchainClient'); this.apiVersion = apiVersion; // Create axios instance with base configuration this.client = axios.create({ baseURL, timeout, headers: { 'Content-Type': 'application/json', 'X-API-Version': apiVersion, ...(apiKey && { 'X-API-Key': apiKey }), }, }); // Configure retry logic with exponential backoff axiosRetry(this.client, { retries, retryDelay: (retryCount) => { const delay = retryDelay * Math.pow(2, retryCount - 1); this.logger.info(`Retrying request (attempt ${retryCount})...`, { delay }); return delay; }, retryCondition: (error) => { // Retry on network errors or 5xx status codes return ( axiosRetry.isNetworkOrIdempotentRequestError(error) || (error.response?.status !== undefined && error.response.status >= 500) ); }, }); // Add request interceptor for logging and performance tracking this.client.interceptors.request.use( (config) => { // Start performance timer for this request const requestId = `${config.method}_${config.url}_${Date.now()}_${Math.random()}`; const startTime = Date.now(); this.requestTimers.set(requestId, { startTime, requestId }); this.logger.startTimer(requestId); // Store requestId in headers for retrieval later config.headers['X-Request-ID'] = requestId; this.logger.debug('Making API request', { method: config.method, url: config.url, params: config.params, requestId, }); return config; }, (error: unknown) => { this.logger.error('Request error', { error: String(error) }); return Promise.reject(error); } ); // Add response interceptor for logging, error handling, and performance tracking this.client.interceptors.response.use( (response) => { const requestId = response.config.headers['X-Request-ID'] as string; const timerData = this.requestTimers.get(requestId); if (requestId && timerData) { this.logger.endTimer(requestId, { status: response.status, url: response.config.url, method: response.config.method, dataSize: JSON.stringify(response.data).length, }); this.requestTimers.delete(requestId); } this.logger.debug('API response received', { status: response.status, url: response.config.url, requestId, duration: timerData ? `${Date.now() - timerData.startTime}ms` : undefined, }); return response; }, (error: AxiosError<APIErrorResponse>) => { const requestId = error.config?.headers?.['X-Request-ID'] as string; const timerData = this.requestTimers.get(requestId); if (requestId && timerData) { this.logger.endTimer(requestId, { error: true, status: error.response?.status, url: error.config?.url, method: error.config?.method, }); this.requestTimers.delete(requestId); } this.logger.error('API request failed', { status: error.response?.status, url: error.config?.url, requestId, duration: timerData ? `${Date.now() - timerData.startTime}ms` : undefined, error: error.message, }); return Promise.reject(this.handleApiError(error)); } ); } /** * Handle API error responses with version-specific logic */ private handleApiError(error: AxiosError<APIErrorResponse>): MCPError { const { response } = error; const endpoint = response?.config?.url || 'unknown'; if (!response) { // Network error if (error.code === 'ECONNABORTED' || error.message.includes('timeout')) { return new TimeoutError(30000, endpoint); } return new NetworkError(`Network error for ${endpoint}`, error.code); } const { status, data } = response; const errorMessage = data?.error || data?.message || error.message || 'Unknown error'; // Version-specific error handling if (status === 400 && errorMessage.includes('version')) { this.logger.warn(`API version ${this.apiVersion} may not be supported`, { error: errorMessage, }); // Could trigger automatic version fallback here } switch (status) { case 400: return new InvalidParametersError(errorMessage); case 404: return new ResourceNotFoundError('resource', endpoint); case 429: return new RateLimitError(); case 500: case 502: case 503: case 504: return new InternalError(errorMessage, data); default: return new StampchainAPIError(errorMessage, status, endpoint, data); } } /** * Perform automatic version fallback if current version fails */ private async attemptVersionFallback(originalError: AxiosError): Promise<void> { const fallbackVersions = ['2.2', '2.1']; for (const version of fallbackVersions) { if (version === this.apiVersion) continue; try { const isCompatible = await this.testVersionCompatibility(version); if (isCompatible) { this.logger.warn( `Falling back to API version ${version} due to error with ${this.apiVersion}` ); this.setApiVersion(version); return; } } catch (fallbackError) { this.logger.debug(`Version ${version} fallback failed`, { error: fallbackError }); } } throw originalError; } /** * Initialize client with automatic version negotiation */ public async initializeWithVersionNegotiation(): Promise<void> { try { // First, try to get available versions const versionInfo = await this.getAvailableVersions(); // Check if our current version is supported const currentVersionInfo = versionInfo.versions.find((v) => v.version === this.apiVersion); if (!currentVersionInfo) { this.logger.warn(`API version ${this.apiVersion} not found in available versions`); // Fall back to the current version from server this.setApiVersion(versionInfo.current); return; } // Check if current version is deprecated or end-of-life if ( currentVersionInfo.status === 'deprecated' || currentVersionInfo.status === 'end-of-life' ) { this.logger.warn( `API version ${this.apiVersion} is ${currentVersionInfo.status}, consider upgrading` ); // If deprecated, optionally upgrade to current version if (currentVersionInfo.status === 'end-of-life') { this.logger.info(`Upgrading to current API version ${versionInfo.current}`); this.setApiVersion(versionInfo.current); } } // Test compatibility with current version const isCompatible = await this.testVersionCompatibility(this.apiVersion); if (!isCompatible) { this.logger.warn(`API version ${this.apiVersion} compatibility test failed`); await this.attemptVersionFallback( new Error('Version compatibility test failed') as AxiosError ); } this.logger.info(`Successfully initialized with API version ${this.apiVersion}`); } catch (error) { this.logger.error('Version negotiation failed', { error }); // Continue with original version if negotiation fails } } /** * Get version-specific feature availability */ public getFeatureAvailability(): { marketData: boolean; recentSales: boolean; enhancedFiltering: boolean; dispenserInfo: boolean; cacheStatus: boolean; } { const majorVersion = parseFloat(this.apiVersion); return { marketData: majorVersion >= 2.3, recentSales: majorVersion >= 2.3, enhancedFiltering: majorVersion >= 2.3, dispenserInfo: majorVersion >= 2.3, cacheStatus: majorVersion >= 2.3, }; } /** * Execute a request with automatic version fallback on failure */ private async executeWithFallback<T>( requestFn: () => Promise<T>, fallbackFn?: () => Promise<T> ): Promise<T> { try { return await requestFn(); } catch (error) { if (error instanceof AxiosError && error.response?.status === 400) { // Try version fallback try { await this.attemptVersionFallback(error); // Retry with new version return await requestFn(); } catch (fallbackError) { // If fallback function is provided, try it if (fallbackFn) { this.logger.info('Attempting fallback implementation'); return await fallbackFn(); } throw fallbackError; } } throw error; } } /** * Get a specific stamp by ID */ async getStamp(stampId: number): Promise<Stamp> { const response = await this.client.get<StampResponse>(`/stamps/${stampId}`); return response.data.data.stamp; } /** * Search stamps with query parameters */ async searchStamps(params: StampQueryParams = {}): Promise<Stamp[]> { const response = await this.client.get<StampListResponse>('/stamps', { params }); return response.data.data; } /** * Get recent stamps */ async getRecentStamps(limit: number = 20): Promise<Stamp[]> { const params: StampQueryParams = { limit, }; const response = await this.client.get<StampListResponse>('/stamps', { params }); return response.data.data; } /** * Get recent sales data (v2.3 feature) */ async getRecentSales(params: RecentSalesQueryParams = {}): Promise<RecentSalesResponse> { const features = this.getFeatureAvailability(); if (!features.recentSales) { // Fallback for older API versions - use regular stamps endpoint this.logger.info('Recent sales not available in this API version, using fallback'); const stamps = await this.searchStamps({ limit: params.page_size || 20, sort_order: params.sort_order || 'DESC', }); // Transform to match RecentSalesResponse format return { data: stamps.map((stamp) => ({ tx_hash: stamp.tx_hash, block_index: stamp.block_index, stamp_id: stamp.stamp || 0, price_btc: typeof stamp.floorPrice === 'number' ? stamp.floorPrice : 0, price_usd: stamp.floorPriceUSD || null, timestamp: stamp.block_index, // Use block_index as timestamp approximation })), metadata: { dayRange: params.dayRange || 30, lastUpdated: Date.now(), total: stamps.length, }, last_block: stamps[0]?.block_index || 0, }; } return this.executeWithFallback( () => this.client.get<RecentSalesResponse>('/stamps/recentSales', { params }).then((r) => r.data), async () => { // Fallback implementation for when v2.3 endpoint fails const stamps = await this.searchStamps({ limit: params.page_size || 20, sort_order: params.sort_order || 'DESC', }); return { data: stamps.map((stamp) => ({ tx_hash: stamp.tx_hash, block_index: stamp.block_index, stamp_id: stamp.stamp || 0, price_btc: typeof stamp.floorPrice === 'number' ? stamp.floorPrice : 0, price_usd: stamp.floorPriceUSD || null, timestamp: stamp.block_index, })), metadata: { dayRange: params.dayRange || 30, lastUpdated: Date.now(), total: stamps.length, }, last_block: stamps[0]?.block_index || 0, }; } ); } /** * Get market data for stamps (v2.3 feature) */ async getMarketData(params: MarketDataQueryParams = {}): Promise<{ data: StampMarketData[]; last_block: number; page: number; limit: number; total: number; }> { const response = await this.client.get('/stamps/marketData', { params }); return response.data; } /** * Get market data for a specific stamp (v2.3 feature) */ async getStampMarketData(stampId: number): Promise<StampMarketData> { const response = await this.client.get<{ data: StampMarketData }>( `/stamps/${stampId}/marketData` ); return response.data.data; } /** * Get a specific collection by ID */ async getCollection(collectionId: string): Promise<CollectionResponse> { const response = await this.client.get<CollectionResponse>(`/collections/${collectionId}`); return response.data; } /** * Search collections with query parameters */ async searchCollections(params: CollectionQueryParams = {}): Promise<CollectionResponse[]> { const response = await this.client.get<CollectionListResponse>('/collections', { params }); return response.data.data; } /** * Get a specific SRC-20 token by ticker */ async getToken(tick: string): Promise<TokenResponse> { const response = await this.client.get<TokenResponse>(`/src20/${tick}`); return response.data; } /** * Search SRC-20 tokens with query parameters */ async searchTokens(params: TokenQueryParams = {}): Promise<TokenResponse[]> { const response = await this.client.get<TokenListResponse>('/src20', { params }); return response.data.data; } /** * Get stamps from a specific block */ async getBlock(blockIndex: number): Promise<BlockResponse> { const response = await this.client.get<BlockResponse>(`/block/${blockIndex}`); return response.data; } /** * Get stamps owned by a specific address */ async getBalance(address: string): Promise<BalanceResponse> { const response = await this.client.get<BalanceResponse>(`/balance/${address}`); return response.data; } /** * Make a custom request to the API */ async customRequest<T>(config: AxiosRequestConfig): Promise<T> { const response = await this.client.request<T>(config); return response.data; } /** * Get the current API version being used by the client */ getApiVersion(): string { return this.apiVersion; } /** * Set the API version for future requests */ setApiVersion(version: string): void { this.apiVersion = version; if (this.client.defaults.headers) { this.client.defaults.headers['X-API-Version'] = version; } this.logger.info(`API version updated to ${version}`); } /** * Get available API versions from the server */ async getAvailableVersions(): Promise<{ current: string; requestedVersion: string; versions: Array<{ version: string; status: string; releaseDate: string; endOfLife?: string; }>; }> { const response = await this.client.get('/versions'); return response.data; } /** * Test API version compatibility */ async testVersionCompatibility(version: string): Promise<boolean> { try { const tempClient = axios.create({ baseURL: this.client.defaults.baseURL, timeout: this.client.defaults.timeout, headers: { 'Content-Type': 'application/json', 'X-API-Version': version, }, }); await tempClient.get('/health'); return true; } catch (error) { this.logger.warn(`Version ${version} compatibility test failed`, { error }); return false; } } /** * Handle API error responses with version-specific logic */ }

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