Skip to main content
Glama
index.ts10.2 kB
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import axios from 'axios'; import crypto from 'crypto'; import { config } from 'dotenv'; import express from 'express'; import type { Hex } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; import { withPaymentInterceptor } from 'x402-axios'; import { z } from 'zod'; config(); const transportMode = process.env.TRANSPORT_MODE || 'stdio'; // Type for API client type ApiClient = ReturnType<typeof withPaymentInterceptor>; // In HTTP mode, API clients are stored per session ID // In STDIO mode, use environment variable for single global client let api: ApiClient | undefined; const apiClients = new Map<string, ApiClient>(); if (transportMode === 'stdio') { const privateKey = process.env.PRIVATE_KEY as Hex; if (!privateKey) { throw new Error('Missing PRIVATE_KEY environment variable for STDIO mode'); } const account = privateKeyToAccount(privateKey); api = withPaymentInterceptor(axios.create({ baseURL: 'https://api-x402.asksally.xyz' }), account); } // Helper function to set up prompts const setupPrompts = (server: McpServer) => { // Prompt 1: Ask Sally about metabolic health server.prompt( 'ask-sally-metabolic', 'Generate a well-formed question for Sally about metabolic health', { topic: z.string().describe('The metabolic health topic to ask about'), }, async (args) => { return { messages: [ { role: 'user', content: { type: 'text', text: `Please use the chat-with-sally tool to ask: "${args.topic}"`, }, }, ], }; }, ); // Prompt 2: General chat with Sally server.prompt( 'ask-sally-general', 'Start a general conversation with Sally', async () => { return { messages: [ { role: 'user', content: { type: 'text', text: 'Please use the chat-with-sally tool to start a conversation about health and wellness.', }, }, ], }; }, ); }; // Helper function to set up resources const setupResources = (server: McpServer, getApiClient: () => ApiClient | undefined) => { // Resource 1: Server configuration info server.resource( 'server-config', 'sally://config/info', { description: 'Information about the Sally MCP server configuration and status', mimeType: 'application/json', }, async () => { const apiClient = getApiClient(); return { contents: [ { uri: 'sally://config/info', mimeType: 'application/json', text: JSON.stringify( { serverName: 'x402 MCP Sally Server', version: '1.0.0', apiConnected: apiClient !== undefined, apiEndpoint: 'https://api-x402.asksally.xyz', availableTools: ['chat-with-sally'], transportMode: process.env.TRANSPORT_MODE || 'stdio', }, null, 2, ), }, ], }; }, ); // Resource 2: Sally API documentation server.resource( 'sally-docs', 'sally://docs/api', { description: 'Documentation for chatting with Sally and the x402 payment protocol', mimeType: 'text/markdown', }, async () => { return { contents: [ { uri: 'sally://docs/api', mimeType: 'text/markdown', text: `# Sally MCP Server Documentation ## Overview This MCP server provides access to Sally, an AI assistant specializing in metabolic health, through the x402 blockchain-based payment protocol. ## Available Tools ### chat-with-sally Chat with Sally about metabolic health topics. **Parameters:** - \`message\` (string, required): Your question or message for Sally **Requirements:** Valid privateKey configuration **Returns:** Sally's response in JSON format **Example topics:** - Metabolic health and wellness - Nutrition advice - Exercise recommendations - Health goal setting ## x402 Payment Protocol All interactions with Sally use the x402 protocol for micropayments. Each API call is automatically paid for using your configured wallet. ## Security Best Practices - Always use a dedicated wallet for MCP interactions - Never use your main wallet's private key - Keep your private key secure and never share it - Monitor your wallet balance regularly ## Getting Started 1. Configure your privateKey in the MCP client settings 2. Use the chat-with-sally tool to start a conversation 3. Ask Sally about metabolic health, nutrition, or wellness topics ## Support For issues or questions about Sally, visit the Sally Labs documentation. `, }, ], }; }, ); }; // Create an MCP server factory function // getApiClient: function to retrieve API client for the current session const createServer = (getApiClient: () => ApiClient | undefined) => { const server = new McpServer({ name: 'x402 MCP Sally Server', version: '1.0.0', }); // Add tools to the server server.tool( 'chat-with-sally', 'Chat with Sally to talk about metabolic health (requires privateKey configuration)', { message: z.string().describe('The message to send to Sally'), }, { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true, }, async (args) => { const apiClient = getApiClient(); if (!apiClient) { return { content: [{ type: 'text', text: 'Error: API client not initialized. Please provide a valid privateKey.' }], }; } const { message } = args; try { const res = await apiClient.post('/chats', { message }); return { content: [{ type: 'text', text: JSON.stringify(res.data) }], }; } catch (err: any) { console.error(err); return { content: [ { type: 'text', text: `Failed to chat with Sally. Error: ${err.response?.data?.message || err.message}`, }, ], }; } }, ); // Set up prompts and resources setupPrompts(server); setupResources(server, getApiClient); return server; }; // For STDIO mode, create a single server instance with global API client export const server = createServer(() => api); // Connect with appropriate transport based on mode if (transportMode === 'http') { // HTTP transport for Smithery hosted deployment const app = express(); const port = process.env.PORT || 8081; // Add middleware for parsing JSON and URL-encoded data app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.get('/health', (_req, res) => { res.json({ status: 'ok' }); }); // Handle all HTTP methods on /mcp endpoint app.all('/mcp', async (req, res) => { console.log(`Received ${req.method} /mcp request`); // Extract privateKey from query parameters or request body (optional for scanning) const privateKeyStr = (req.query.privateKey as string) || (req.body?.privateKey as string); console.log('Request with privateKey:', privateKeyStr ? 'present' : 'missing'); // Initialize API client for this request if valid privateKey provided let requestApiClient: ApiClient | undefined; if (privateKeyStr) { // Validate hex format: must start with 0x and be 66 characters (0x + 64 hex digits) const isValidFormat = privateKeyStr.startsWith('0x') && privateKeyStr.length === 66; if (!isValidFormat) { console.log('Invalid or incomplete privateKey provided - proceeding without API client'); console.log(`PrivateKey validation failed: length=${privateKeyStr.length}, starts_with_0x=${privateKeyStr.startsWith('0x')}`); // Don't fail the request - just continue without API client // Tools will return appropriate errors when invoked without credentials } else { try { // Cast to Hex after validation const privateKey = privateKeyStr as Hex; // Create API client with the provided privateKey for this request const account = privateKeyToAccount(privateKey); requestApiClient = withPaymentInterceptor(axios.create({ baseURL: 'https://api-x402.asksally.xyz' }), account); console.log('Successfully created API client with provided privateKey'); } catch (error: any) { console.error('Failed to initialize API client:', error); // Log error but don't fail the request - allow scan to proceed console.log('Continuing without API client - tools will require valid credentials'); } } } else { console.log('No privateKey provided - tools will require credentials when invoked'); } try { // Create a new server and transport for each request // Pass the API client directly via closure const requestServer = createServer(() => requestApiClient); const requestTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, // Stateless mode enableJsonResponse: true, // Use JSON responses instead of SSE streams }); await requestServer.connect(requestTransport); // Let the transport handle the request await requestTransport.handleRequest(req, res, req.body); } catch (error: any) { console.error('Failed to handle MCP request:', error); if (!res.headersSent) { res.status(500).json({ error: 'Failed to handle request', details: error.message }); } } }); app.listen(port, () => { console.log(`Sally MCP server listening on port ${port}`); }); } else { // STDIO transport for local Smithery CLI deployment const transport = new StdioServerTransport(); await server.connect(transport); }

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/sally-labs/sally-mcp'

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