Skip to main content
Glama

Stampchain MCP Server

Official
mcp-server.test.ts17.1 kB
/* eslint-disable @typescript-eslint/unbound-method */ import { vi, describe, it, expect, beforeEach } from 'vitest'; import type { CallToolRequest } from '@modelcontextprotocol/sdk/types.js'; /** * Integration tests for the complete MCP server */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import type { StampchainClient } from '../../api/stampchain-client.js'; import { ToolRegistry } from '../../tools/registry.js'; import { createAllTools, toolMetadata } from '../../tools/index.js'; import { loadConfiguration } from '../../config/index.js'; import { createMockStamp, createMockCollection, createMockToken } from '../utils/test-helpers.js'; import type { ToolContext, ToolResponse } from '../../interfaces/tool.js'; import type { Stamp, CollectionResponse, TokenResponse } from '../../api/types.js'; // Mock the transport and external dependencies vi.mock('@modelcontextprotocol/sdk/server/stdio.js'); vi.mock('../../api/stampchain-client.js'); describe('MCP Server Integration', () => { let server: Server; let mockApiClient: StampchainClient; let toolRegistry: ToolRegistry; let config: ReturnType<typeof loadConfiguration>; type CallToolHandler = (request: CallToolRequest) => Promise<ToolResponse>; type ListToolsHandler = () => { tools: Array<{ name: string; description: string; inputSchema: unknown }>; }; let callToolHandler: CallToolHandler; let listToolsHandler: ListToolsHandler; beforeEach(() => { // Load test configuration config = loadConfiguration({ cliArgs: { logLevel: 'error' }, // Suppress logs during tests }); // Initialize tool registry toolRegistry = new ToolRegistry(config.registry); // Create mock API client with proper typing const mockedClient = { getStamp: vi.fn<[number], Promise<Stamp>>(), searchStamps: vi.fn<[unknown], Promise<Stamp[]>>(), getRecentStamps: vi.fn<[number?], Promise<Stamp[]>>(), getCollection: vi.fn<[string], Promise<CollectionResponse>>(), searchCollections: vi.fn<[unknown], Promise<CollectionResponse[]>>(), getToken: vi.fn<[string], Promise<TokenResponse>>(), searchTokens: vi.fn<[unknown], Promise<TokenResponse[]>>(), getBlock: vi.fn(), getBalance: vi.fn(), customRequest: vi.fn(), }; mockApiClient = mockedClient as unknown as StampchainClient; // Create and register all tools const tools = createAllTools(mockApiClient); for (const metadata of Object.values(toolMetadata)) { for (const toolName of metadata.tools) { const tool = tools[toolName]; if (tool) { toolRegistry.register(tool, { category: metadata.category, version: '1.0.0', }); } } } // Initialize MCP server server = new Server( { name: config.name, version: config.version, }, { capabilities: { tools: {}, }, } ); // Register handlers (simplified version of main server logic) listToolsHandler = (): { tools: Array<{ name: string; description: string; inputSchema: unknown }>; } => { const tools = toolRegistry.getMCPTools(); return { tools }; }; server.setRequestHandler(ListToolsRequestSchema, listToolsHandler); callToolHandler = async (request: CallToolRequest): Promise<ToolResponse> => { const { name: toolName, arguments: args } = request.params; try { const tool = toolRegistry.get(toolName); const context: ToolContext = { logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }, apiClient: mockApiClient, config, }; const result = await tool.execute(args || {}, context); return result; } catch (error) { return { content: [ { type: 'text', text: error instanceof Error ? error.message : 'Unknown error', }, ], isError: true, }; } }; server.setRequestHandler(CallToolRequestSchema, callToolHandler); }); describe('Server Initialization', () => { it('should initialize with all expected tools', () => { const stats = toolRegistry.getStats(); expect(stats.totalTools).toBe(13); // All 13 tools should be registered (10 original + 3 analysis tools) expect(stats.enabledTools).toBe(13); expect(stats.categories).toBe(4); // stamps, collections, tokens, analysis }); it('should have correct tool categories', () => { const categories = toolRegistry.getCategories(); expect(categories).toContain('Bitcoin Stamps'); expect(categories).toContain('Stamp Collections'); expect(categories).toContain('SRC-20 Tokens'); }); it('should register all expected tools', () => { const tools = toolRegistry.list(); const toolNames = tools.map((t) => t.name); // Stamp tools expect(toolNames).toContain('get_stamp'); expect(toolNames).toContain('search_stamps'); expect(toolNames).toContain('get_recent_stamps'); // Collection tools expect(toolNames).toContain('get_collection'); expect(toolNames).toContain('search_collections'); // Token tools expect(toolNames).toContain('get_token_info'); expect(toolNames).toContain('search_tokens'); }); }); describe('List Tools Handler', () => { it('should return all available tools', () => { // Get tools from registry const mcpTools = toolRegistry.getMCPTools(); expect(mcpTools).toHaveLength(13); expect(mcpTools.every((tool) => tool.name && tool.description && tool.inputSchema)).toBe( true ); }); it('should return tools with correct MCP format', () => { // Get tools from registry const mcpTools = toolRegistry.getMCPTools(); const stampTool = mcpTools.find((t) => t.name === 'get_stamp'); expect(stampTool).toBeDefined(); expect(stampTool!.description).toContain( 'Retrieve detailed information about a specific Bitcoin stamp' ); expect(stampTool!.inputSchema.type).toBe('object'); expect(stampTool!.inputSchema.properties).toBeDefined(); }); }); describe('Call Tool Handler', () => { describe('Stamp Tools', () => { it('should execute get_stamp tool successfully', async () => { const mockStamp = createMockStamp(); const getStampMock = vi.mocked(mockApiClient.getStamp); getStampMock.mockResolvedValueOnce(mockStamp); const result = await callToolHandler({ params: { name: 'get_stamp', arguments: { stamp_id: 12345 }, }, }); expect(mockApiClient.getStamp).toHaveBeenCalledWith(12345); expect(result.content.length).toBeGreaterThanOrEqual(1); expect(result.content[0].text).toContain('Stamp #12345'); expect(result.isError).toBeUndefined(); }); it('should execute search_stamps tool with filters', async () => { const mockStamps = [createMockStamp(), createMockStamp({ stamp: 12346 })]; const searchStampsMock = vi.mocked(mockApiClient.searchStamps); searchStampsMock.mockResolvedValueOnce(mockStamps); // Use the handler from beforeEach const result = await callToolHandler({ params: { name: 'search_stamps', arguments: { creator: 'bc1qtest123456789012345678901234567890', limit: 10, }, }, }); expect(mockApiClient.searchStamps).toHaveBeenCalledWith({ creator: 'bc1qtest123456789012345678901234567890', sort_order: 'DESC', page: 1, page_size: 20, }); expect(result.content[0].text).toContain('Found 2 stamps'); }); it('should execute get_recent_stamps tool', async () => { const mockStamps = Array.from({ length: 5 }, (_, i) => createMockStamp({ stamp: 12345 + i }) ); const searchStampsMock = vi.mocked(mockApiClient.searchStamps); searchStampsMock.mockResolvedValueOnce(mockStamps); // Use the handler from beforeEach const result = await callToolHandler({ params: { name: 'get_recent_stamps', arguments: { limit: 5 }, }, }); expect(mockApiClient.searchStamps).toHaveBeenCalledWith({ sort_order: 'DESC', page: 1, page_size: 5, is_cursed: undefined, }); expect(result.content[0].text).toContain('5 Most Recent Stamps'); }); }); describe('Collection Tools', () => { it('should execute get_collection tool successfully', async () => { const mockCollections = [createMockCollection()]; const searchCollectionsMock = vi.mocked(mockApiClient.searchCollections); searchCollectionsMock.mockResolvedValueOnce(mockCollections); // Use the handler from beforeEach const result = await callToolHandler({ params: { name: 'get_collection', arguments: { collection_id: 'test-collection' }, }, }); expect(mockApiClient.searchCollections).toHaveBeenCalledWith({ query: 'test-collection', page: 1, page_size: 1, }); expect(result.content[0].text).toContain('Collection: Test Collection'); }); it('should execute search_collections tool', async () => { const mockCollections = [createMockCollection()]; const searchCollectionsMock = vi.mocked(mockApiClient.searchCollections); searchCollectionsMock.mockResolvedValueOnce(mockCollections); // Use the handler from beforeEach const result = await callToolHandler({ params: { name: 'search_collections', arguments: { creator: 'bc1qtest123456789012345678901234567890' }, }, }); expect(mockApiClient.searchCollections).toHaveBeenCalledWith({ creator: 'bc1qtest123456789012345678901234567890', sort_by: 'created_at', sort_order: 'DESC', page: 1, page_size: 20, }); expect(result.content[0].text).toContain('Found 1 collection'); }); }); describe('Token Tools', () => { it('should execute get_token_info tool successfully', async () => { const mockTokens = [createMockToken()]; const searchTokensMock = vi.mocked(mockApiClient.searchTokens); searchTokensMock.mockResolvedValueOnce(mockTokens); // Use the handler from beforeEach const result = await callToolHandler({ params: { name: 'get_token_info', arguments: { tick: 'TEST' }, }, }); expect(mockApiClient.searchTokens).toHaveBeenCalledWith({ query: 'TEST', page: 1, page_size: 1, }); expect(result.content[0].text).toContain('Token: TEST'); }); it('should execute search_tokens tool', async () => { const mockTokens = [createMockToken(), createMockToken({ tick: 'OTHER' })]; const searchTokensMock = vi.mocked(mockApiClient.searchTokens); searchTokensMock.mockResolvedValueOnce(mockTokens); // Use the handler from beforeEach const result = await callToolHandler({ params: { name: 'search_tokens', arguments: { min_holders: 100 }, }, }); expect(mockApiClient.searchTokens).toHaveBeenCalledWith({ sort_by: 'deploy_timestamp', sort_order: 'DESC', page: 1, page_size: 20, }); expect(result.content[0].text).toContain('Found 2 tokens'); }); }); describe('Error Handling', () => { it('should handle tool not found errors', async () => { // Use the handler from beforeEach const result = await callToolHandler({ params: { name: 'non_existent_tool', arguments: {}, }, }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Tool not found: non_existent_tool'); }); it('should handle validation errors', async () => { // Use the handler from beforeEach const result = await callToolHandler({ params: { name: 'get_stamp', arguments: { stamp_id: -1 }, // Invalid negative ID }, }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Validation failed'); }); it('should handle API errors', async () => { const getStampMock = vi.mocked(mockApiClient.getStamp); getStampMock.mockRejectedValueOnce(new Error('API temporarily unavailable')); // Use the handler from beforeEach const result = await callToolHandler({ params: { name: 'get_stamp', arguments: { stamp_id: 12345 }, }, }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('API temporarily unavailable'); }); it('should handle missing required parameters', async () => { // Use the handler from beforeEach const result = await callToolHandler({ params: { name: 'get_stamp', arguments: {}, // Missing stamp_id }, }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Validation failed'); }); }); }); describe('End-to-End Workflows', () => { it('should support stamp discovery workflow', async () => { // Use the handler from beforeEach // Step 1: Get recent stamps const recentStamps = [createMockStamp({ stamp: 12345 }), createMockStamp({ stamp: 12346 })]; const searchStampsMock = vi.mocked(mockApiClient.searchStamps); searchStampsMock.mockResolvedValueOnce(recentStamps); await callToolHandler({ params: { name: 'get_recent_stamps', arguments: { limit: 2 } }, }); // Step 2: Search for more stamps in the collection const collectionStamps = [ createMockStamp({ stamp: 12340 }), createMockStamp({ stamp: 12341 }), ]; searchStampsMock.mockResolvedValueOnce(collectionStamps); await callToolHandler({ params: { name: 'search_stamps', arguments: { collection_id: 'popular-collection', limit: 10 }, }, }); // Step 3: Get collection details const collectionInfo = [createMockCollection({ collection_id: 'popular-collection' })]; const searchCollectionsMock = vi.mocked(mockApiClient.searchCollections); searchCollectionsMock.mockResolvedValueOnce(collectionInfo); await callToolHandler({ params: { name: 'get_collection', arguments: { collection_id: 'popular-collection' }, }, }); // Verify all API calls were made correctly expect(mockApiClient.searchStamps).toHaveBeenCalledWith({ sort_order: 'DESC', page: 1, page_size: 2, is_cursed: undefined, }); expect(mockApiClient.searchStamps).toHaveBeenCalledWith({ collection_id: 'popular-collection', sort_order: 'DESC', page: 1, page_size: 20, }); expect(mockApiClient.searchCollections).toHaveBeenCalledWith({ query: 'popular-collection', page: 1, page_size: 1, }); }); it('should support token research workflow', async () => { // Use the handler from beforeEach // Step 1: Search for tokens with high holder count const popularTokens = [ createMockToken({ tick: 'UNCOMMON' }), createMockToken({ tick: 'RARE' }), ]; const searchTokensMock = vi.mocked(mockApiClient.searchTokens); searchTokensMock.mockResolvedValueOnce(popularTokens); await callToolHandler({ params: { name: 'search_tokens', arguments: { min_holders: 1000, limit: 10 }, }, }); // Step 2: Get detailed info for specific token const tokenInfo = [createMockToken({ tick: 'UNCOMMON' })]; searchTokensMock.mockResolvedValueOnce(tokenInfo); await callToolHandler({ params: { name: 'get_token_info', arguments: { tick: 'UNCOMMON' }, }, }); // Verify API calls expect(mockApiClient.searchTokens).toHaveBeenCalledWith({ sort_by: 'deploy_timestamp', sort_order: 'DESC', page: 1, page_size: 20, }); expect(mockApiClient.searchTokens).toHaveBeenCalledWith({ query: 'UNCOMMON', page: 1, page_size: 1, }); }); }); });

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