Skip to main content
Glama

XMTP MCP Server

by kwaude
index.ts15.5 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { Client, type Dm, type Group, type DecodedMessage, type Identifier, type IdentifierKind, type Conversation } from "@xmtp/node-sdk"; import { generatePrivateKey, privateKeyToAccount, type PrivateKeyAccount } from "viem/accounts"; import { toBytes } from "viem"; import * as dotenv from "dotenv"; // Load environment variables dotenv.config(); interface XMTPServerState { client: Client | null; walletAddress: string | null; } class XMTPMCPServer { private server: Server; private state: XMTPServerState; constructor() { this.server = new Server( { name: "xmtp-mcp-server", version: "0.1.0", }, { capabilities: { resources: {}, tools: {}, }, } ); this.state = { client: null, walletAddress: null, }; this.setupHandlers(); } private setupHandlers() { // List available resources this.server.setRequestHandler(ListResourcesRequestSchema, async () => { return { resources: [ { uri: "xmtp://conversations", name: "XMTP Conversations", description: "List all conversations in XMTP inbox", mimeType: "application/json", }, { uri: "xmtp://inbox", name: "XMTP Inbox", description: "Access XMTP inbox messages", mimeType: "application/json", }, ], }; }); // Read resources this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const { uri } = request.params; if (!this.state.client) { throw new Error("XMTP client not connected. Use connect_xmtp tool first."); } switch (uri) { case "xmtp://conversations": const conversations = await this.getConversations(); return { contents: [ { uri, mimeType: "application/json", text: JSON.stringify(conversations, null, 2), }, ], }; case "xmtp://inbox": const inbox = await this.getInboxMessages(); return { contents: [ { uri, mimeType: "application/json", text: JSON.stringify(inbox, null, 2), }, ], }; default: throw new Error(`Unknown resource: ${uri}`); } }); // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "connect_xmtp", description: "Connect to XMTP network with wallet key", inputSchema: { type: "object", properties: { privateKey: { type: "string", description: "Wallet private key (optional, uses env WALLET_KEY if not provided)", }, environment: { type: "string", description: "XMTP environment: local, dev, or production", enum: ["local", "dev", "production"], default: "production", }, }, }, }, { name: "send_message", description: "Send a message to an address via XMTP", inputSchema: { type: "object", properties: { recipient: { type: "string", description: "Wallet address or ENS name to send message to", }, message: { type: "string", description: "Message content to send", }, }, required: ["recipient", "message"], }, }, { name: "get_messages", description: "Get messages from a conversation with an address", inputSchema: { type: "object", properties: { address: { type: "string", description: "Wallet address to get conversation with", }, limit: { type: "number", description: "Maximum number of messages to retrieve", default: 50, }, }, required: ["address"], }, }, { name: "list_conversations", description: "List all active XMTP conversations", inputSchema: { type: "object", properties: {}, }, }, { name: "check_can_message", description: "Check if an address can receive XMTP messages", inputSchema: { type: "object", properties: { address: { type: "string", description: "Wallet address to check", }, }, required: ["address"], }, }, { name: "stream_messages", description: "Start streaming new messages from all conversations", inputSchema: { type: "object", properties: { callback: { type: "string", description: "Optional callback function name for message handling", }, }, }, }, ], }; }); // Handle tool calls this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case "connect_xmtp": return await this.connectXMTP(args); case "send_message": return await this.sendMessage(args); case "get_messages": return await this.getMessages(args); case "list_conversations": return await this.listConversations(); case "check_can_message": return await this.checkCanMessage(args); case "stream_messages": return await this.streamMessages(args); default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: `Error: ${errorMessage}`, }, ], }; } }); } private async connectXMTP(args: any) { try { const privateKey = args.privateKey || process.env.WALLET_KEY; const environment = args.environment || process.env.XMTP_ENV || "production"; if (!privateKey) { throw new Error("Private key required. Provide via parameter or WALLET_KEY env variable."); } // Create proper EOA signer for XMTP using viem account (exact same as working debug script) const account = privateKeyToAccount(privateKey as `0x${string}`); const signer = { type: "EOA" as const, signMessage: async (message: string) => { const signature = await account.signMessage({ message }); return toBytes(signature); }, getIdentifier: () => { return { identifier: account.address, identifierKind: 0, // IdentifierKind.Ethereum }; }, getChainId: () => BigInt(1), // Ethereum mainnet as bigint }; // Initialize XMTP client with proper signer this.state.client = await Client.create(signer, { env: environment as "local" | "dev" | "production", }); this.state.walletAddress = account.address; return { content: [ { type: "text", text: `Successfully connected to XMTP ${environment} network with address: ${account.address}`, }, ], }; } catch (error) { throw new Error(`XMTP connection failed: ${error}`); } } private async sendMessage(args: any) { if (!this.state.client) { throw new Error("XMTP client not connected. Use connect_xmtp tool first."); } const { recipient, message } = args; try { // Create recipient identifier for XMTP operations const recipientIdentifier = { identifier: recipient, identifierKind: 0, // IdentifierKind.Ethereum }; // Check if we can message this address (try both original and lowercase) const canMessage = await this.state.client.canMessage([recipientIdentifier]); const canMessageResult = canMessage.get(recipient) || canMessage.get(recipient.toLowerCase()); if (!canMessageResult) { throw new Error(`Address ${recipient} is not on the XMTP network`); } // Create DM conversation with identifier (back to original method) const conversation = await this.state.client.conversations.newDmWithIdentifier(recipientIdentifier); // Send message await conversation.send(message); // Sync to ensure message is propagated await this.state.client.conversations.syncAll(); return { content: [ { type: "text", text: `Message sent to ${recipient}: "${message}"`, }, ], }; } catch (error) { throw new Error(`Failed to send message: ${error}`); } } private async getMessages(args: any) { if (!this.state.client) { throw new Error("XMTP client not connected. Use connect_xmtp tool first."); } const { address, limit = 50 } = args; try { // Create conversation with proper identifier (back to original method) const addressIdentifier = { identifier: address, identifierKind: 0, // IdentifierKind.Ethereum }; const conversation = await this.state.client.conversations.newDmWithIdentifier(addressIdentifier); // Sync conversations to ensure we get latest messages await this.state.client.conversations.syncAll(); // Add brief delay after sync await new Promise(resolve => setTimeout(resolve, 500)); const messages = await conversation.messages({ limit }); const messageList = messages.map((msg: DecodedMessage<any>) => ({ id: msg.id, sender: msg.senderInboxId, content: msg.content, timestamp: msg.sentAt?.toISOString(), })); return { content: [ { type: "text", text: JSON.stringify(messageList, null, 2), }, ], }; } catch (error) { throw new Error(`Failed to get messages: ${error}`); } } private async listConversations() { if (!this.state.client) { throw new Error("XMTP client not connected. Use connect_xmtp tool first."); } try { const conversations = await this.state.client.conversations.list(); const conversationList = conversations.map((conv) => { return { id: conv.id, createdAt: conv.createdAt?.toISOString(), } }); return { content: [ { type: "text", text: JSON.stringify(conversationList, null, 2), }, ], }; } catch (error) { throw new Error(`Failed to list conversations: ${error}`); } } private async checkCanMessage(args: any) { if (!this.state.client) { throw new Error("XMTP client not connected. Use connect_xmtp tool first."); } const { address } = args; try { // Convert address to proper identifier format const identifier = { identifier: address, identifierKind: 0, // IdentifierKind.Ethereum }; const canMessage = await this.state.client.canMessage([identifier]); return { content: [ { type: "text", text: `Address ${address} can receive XMTP messages: ${canMessage.get(address)}`, }, ], }; } catch (error) { throw new Error(`Failed to check messaging capability: ${error}`); } } private async streamMessages(args: any) { if (!this.state.client) { throw new Error("XMTP client not connected. Use connect_xmtp tool first."); } try { // Start streaming all messages const stream = await this.state.client.conversations.streamAllMessages(); let messageCount = 0; const messages: any[] = []; // Collect first few messages for demonstration for await (const message of stream) { messages.push({ id: message.id, sender: message.senderInboxId, content: message.content, timestamp: message.sentAt?.toISOString(), conversationId: message.conversationId, }); messageCount++; if (messageCount >= 10) break; // Limit for demonstration } return { content: [ { type: "text", text: `Started streaming messages. Recent messages:\n${JSON.stringify(messages, null, 2)}`, }, ], }; } catch (error) { throw new Error(`Failed to stream messages: ${error}`); } } private async getConversations() { if (!this.state.client) return []; try { const conversations = await this.state.client.conversations.list(); return conversations.map((conv) => { return { id: conv.id, createdAt: conv.createdAt?.toISOString(), } }); } catch (error) { console.error("Error getting conversations:", error); return []; } } private async getInboxMessages() { if (!this.state.client) return []; try { const conversations = await this.state.client.conversations.list(); const allMessages: any[] = []; for (const conv of conversations) { const messages = await conv.messages({ limit: 10 }); messages.forEach((msg: DecodedMessage<any>) => { allMessages.push({ id: msg.id, sender: msg.senderInboxId, content: msg.content, timestamp: msg.sentAt?.toISOString(), conversationId: msg.conversationId, }); }); } // Sort by timestamp (newest first) return allMessages.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime() ); } catch (error) { console.error("Error getting inbox messages:", error); return []; } } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error("XMTP MCP server running on stdio"); } } async function main() { const server = new XMTPMCPServer(); await server.run(); } main().catch((error) => { console.error("Server error:", error); process.exit(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/kwaude/xmtp-mcp'

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