MCP Snapshot Server

  • src
import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { config } from 'dotenv'; import { SnapshotService } from './services/snapshotService.js'; import { z } from 'zod'; import { ListToolsRequestSchema, CallToolRequestSchema, ListToolsResultSchema, CallToolResultSchema, } from "@modelcontextprotocol/sdk/types.js"; // Load environment variables config(); // Define parameter schemas for tool arguments const SpacesParamsSchema = z.object({ limit: z.number().optional(), skip: z.number().optional() }); const ProposalsParamsSchema = z.object({ spaceId: z.string(), state: z.string().optional(), limit: z.number().optional() }); const ProposalParamsSchema = z.object({ proposalId: z.string() }); const UserParamsSchema = z.object({ address: z.string() }); const RankedSpacesParamsSchema = z.object({ first: z.number().optional(), skip: z.number().optional(), category: z.string().optional(), search: z.string().optional() }); export class MCPServer { private server: Server; private snapshotService: SnapshotService; constructor() { this.server = new Server({ name: "snapshot-mcp", version: "1.0.0" }, { capabilities: { tools: { list: true, // Declare that we support tools/list call: true // Declare that we support tools/call } } }); this.snapshotService = new SnapshotService(); this.setupHandlers(); } private setupHandlers() { // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "getSpaces", description: "Get list of Snapshot spaces", inputSchema: { // Changed from parameters to inputSchema type: "object", properties: { limit: { type: "number", description: "Number of spaces to fetch" }, skip: { type: "number", description: "Number of spaces to skip" } } } }, { name: "getProposals", description: "Get proposals for a Snapshot space", inputSchema: { // Changed from parameters to inputSchema type: "object", properties: { spaceId: { type: "string", description: "ID of the space" }, state: { type: "string", description: "Filter by proposal state (active, closed, pending, all)" }, limit: { type: "number", description: "Number of proposals to fetch" } }, required: ["spaceId"] } }, { name: "getProposal", description: "Get details of a specific proposal", inputSchema: { // Changed from parameters to inputSchema type: "object", properties: { proposalId: { type: "string", description: "ID of the proposal" } }, required: ["proposalId"] } }, { name: "getUser", description: "Get information about a Snapshot user", inputSchema: { // Changed from parameters to inputSchema type: "object", properties: { address: { type: "string", description: "Ethereum address of the user" } }, required: ["address"] } }, { name: "getRankedSpaces", description: "Get ranked list of Snapshot spaces with detailed information", inputSchema: { type: "object", properties: { first: { type: "number", description: "Number of spaces to fetch (default: 18)" }, skip: { type: "number", description: "Number of spaces to skip (default: 0)" }, category: { type: "string", description: "Category to filter by (default: 'all')" }, search: { type: "string", description: "Search term to filter spaces" } } } } ] }; }); // Handle tool calls this.server.setRequestHandler(CallToolRequestSchema, async (request) => { console.error('Received tool call request:', JSON.stringify(request, null, 2)); if (!request.params || !request.params.name) { throw new Error('Invalid tool call request: missing tool name'); } const toolName = request.params.name; const args = request.params.arguments || {}; try { switch (toolName) { case "getSpaces": { const parsedArgs = SpacesParamsSchema.parse(args); const spaces = await this.snapshotService.getSpaces( parsedArgs.limit || 20, parsedArgs.skip || 0 ); return { content: [{ type: "text", text: JSON.stringify(spaces, null, 2) }] }; } case "getProposals": { const parsedArgs = ProposalsParamsSchema.parse(args); const proposals = await this.snapshotService.getProposals( parsedArgs.spaceId, parsedArgs.state || "all", parsedArgs.limit || 20 ); return { content: [{ type: "text", text: JSON.stringify(proposals, null, 2) }] }; } case "getProposal": { const parsedArgs = ProposalParamsSchema.parse(args); const proposal = await this.snapshotService.getProposal(parsedArgs.proposalId); return { content: [{ type: "text", text: JSON.stringify(proposal, null, 2) }] }; } case "getUser": { const parsedArgs = UserParamsSchema.parse(args); const user = await this.snapshotService.getUser(parsedArgs.address); return { content: [{ type: "text", text: JSON.stringify(user, null, 2) }] }; } case "getRankedSpaces": { const parsedArgs = RankedSpacesParamsSchema.parse(args); const spaces = await this.snapshotService.getRankedSpaces( parsedArgs.first || 18, parsedArgs.skip || 0, parsedArgs.category || "all", parsedArgs.search ); return { content: [{ type: "text", text: JSON.stringify(spaces, null, 2) }] }; } default: throw new Error(`Tool not found: ${toolName}`); } } catch (error) { console.error("Tool execution error:", error); return { content: [{ type: "text", text: error instanceof Error ? error.message : 'Unknown error' }] }; } }); } async start() { const transport = new StdioServerTransport(); await this.server.connect(transport); } }