Skip to main content
Glama

RS.ge Waybill MCP Server

MCP_DEVELOPMENT_GUIDE.md19.7 kB
# How to Build an MCP Server - Complete Guide ## Table of Contents 1. [What is MCP?](#what-is-mcp) 2. [Prerequisites](#prerequisites) 3. [Step-by-Step Tutorial](#step-by-step-tutorial) 4. [MCP Protocol Basics](#mcp-protocol-basics) 5. [Tool Development](#tool-development) 6. [Testing Your MCP Server](#testing-your-mcp-server) 7. [Common Patterns](#common-patterns) 8. [Troubleshooting](#troubleshooting) 9. [Best Practices](#best-practices) --- ## What is MCP? **MCP (Model Context Protocol)** is a protocol that allows AI assistants like Claude to interact with external tools and services. It enables: - **Tool Calling** - Claude can call functions you define - **Resource Access** - Provide data Claude can read - **Prompts** - Define reusable prompt templates - **Sampling** - Request AI completions from your code ### Why Build an MCP Server? - Extend Claude's capabilities with custom functionality - Integrate with APIs, databases, or internal systems - Create domain-specific tools for your organization - Automate complex workflows through natural language --- ## Prerequisites ### Required Knowledge - **TypeScript/JavaScript** - MCP servers are typically written in TypeScript - **Node.js** - Runtime environment - **Async/Await** - For handling asynchronous operations - **HTTP/APIs** - If integrating with external services ### Required Tools ```bash # Node.js 18 or later node --version # Should be 18.x or higher # npm (comes with Node.js) npm --version # TypeScript (will be installed as dependency) # Claude Desktop (for testing) ``` --- ## Step-by-Step Tutorial ### Step 1: Project Setup ```bash # Create project directory mkdir my-mcp-server cd my-mcp-server # Initialize npm project npm init -y # Install MCP SDK npm install @modelcontextprotocol/sdk # Install TypeScript and types npm install -D typescript @types/node # Install build tools npm install -D ts-node nodemon # Create tsconfig.json npx tsc --init ``` ### Step 2: Configure TypeScript Edit `tsconfig.json`: ```json { "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "declaration": true, "declarationMap": true, "sourceMap": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } ``` ### Step 3: Create Basic Server Structure Create `src/index.ts`: ```typescript #!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { ListToolsRequestSchema, CallToolRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; // Create MCP server instance const server = new Server( { name: 'my-mcp-server', version: '1.0.0', }, { capabilities: { tools: {}, // We support tools }, } ); // Define available tools server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'echo', description: 'Echoes back the input text', inputSchema: { type: 'object', properties: { text: { type: 'string', description: 'Text to echo back', }, }, required: ['text'], }, }, ], }; }); // Handle tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; if (name === 'echo') { const text = args.text as string; return { content: [ { type: 'text', text: `You said: ${text}`, }, ], }; } throw new Error(`Unknown tool: ${name}`); }); // Start server async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('MCP server running on stdio'); } main().catch((error) => { console.error('Server error:', error); process.exit(1); }); ``` ### Step 4: Update package.json Add scripts and make it executable: ```json { "name": "my-mcp-server", "version": "1.0.0", "type": "module", "bin": { "my-mcp-server": "./dist/index.js" }, "scripts": { "build": "tsc && chmod +x dist/index.js", "dev": "npm run build && node dist/index.js", "watch": "nodemon --watch src --ext ts --exec \"npm run dev\"" }, "dependencies": { "@modelcontextprotocol/sdk": "^0.5.0" }, "devDependencies": { "@types/node": "^20.0.0", "nodemon": "^3.0.0", "ts-node": "^10.0.0", "typescript": "^5.0.0" } } ``` ### Step 5: Build and Test ```bash # Build npm run build # The dist/index.js file should now exist and be executable # Test manually (will wait for input on stdin) node dist/index.js ``` ### Step 6: Configure Claude Desktop Edit Claude Desktop config file: **Windows:** `%APPDATA%\Claude\claude_desktop_config.json` **Mac:** `~/Library/Application Support/Claude/claude_desktop_config.json` ```json { "mcpServers": { "my-mcp-server": { "command": "node", "args": [ "C:\\absolute\\path\\to\\my-mcp-server\\dist\\index.js" ] } } } ``` **Important:** Use absolute paths! ### Step 7: Restart Claude Desktop 1. Quit Claude Desktop completely 2. Start Claude Desktop 3. Test in chat: "Use the echo tool to say hello" --- ## MCP Protocol Basics ### Communication Flow ``` Claude Desktop ←→ MCP Server (stdio) ``` MCP uses **JSON-RPC 2.0** over **stdio** (standard input/output). ### Message Types 1. **Request** - Claude sends to server ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} } ``` 2. **Response** - Server sends back to Claude ```json { "jsonrpc": "2.0", "id": 1, "result": { "tools": [...] } } ``` 3. **Notification** - One-way message (no response expected) ### Key Request Handlers ```typescript // List available tools server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [...] }; }); // Execute a tool server.setRequestHandler(CallToolRequestSchema, async (request) => { // Handle tool execution }); // List resources (optional) server.setRequestHandler(ListResourcesRequestSchema, async () => { return { resources: [...] }; }); // Read a resource (optional) server.setRequestHandler(ReadResourceRequestSchema, async (request) => { // Return resource content }); // List prompts (optional) server.setRequestHandler(ListPromptsRequestSchema, async () => { return { prompts: [...] }; }); ``` --- ## Tool Development ### Anatomy of a Tool ```typescript { name: 'tool_name', // Unique identifier description: 'What it does', // Clear description for Claude inputSchema: { // JSON Schema for parameters type: 'object', properties: { param1: { type: 'string', description: 'Parameter description' } }, required: ['param1'] } } ``` ### Example: API Integration Tool ```typescript // Tool definition { name: 'get_weather', description: 'Get current weather for a city', inputSchema: { type: 'object', properties: { city: { type: 'string', description: 'City name' }, units: { type: 'string', enum: ['celsius', 'fahrenheit'], description: 'Temperature units', default: 'celsius' } }, required: ['city'] } } // Tool implementation if (name === 'get_weather') { const { city, units = 'celsius' } = args; try { // Call weather API const response = await fetch( `https://api.weather.com/current?city=${city}&units=${units}` ); const data = await response.json(); return { content: [ { type: 'text', text: JSON.stringify({ city: data.city, temperature: data.temp, conditions: data.conditions, units: units }, null, 2) } ] }; } catch (error) { return { content: [ { type: 'text', text: `Error fetching weather: ${error.message}` } ], isError: true }; } } ``` ### Example: Database Query Tool ```typescript import { Client } from 'pg'; // PostgreSQL client const dbClient = new Client({ host: 'localhost', database: 'mydb', user: 'user', password: 'password' }); await dbClient.connect(); // Tool definition { name: 'query_customers', description: 'Search customers by name or email', inputSchema: { type: 'object', properties: { search: { type: 'string', description: 'Search term' }, limit: { type: 'number', description: 'Max results', default: 10 } }, required: ['search'] } } // Tool implementation if (name === 'query_customers') { const { search, limit = 10 } = args; const result = await dbClient.query( 'SELECT id, name, email FROM customers WHERE name ILIKE $1 OR email ILIKE $1 LIMIT $2', [`%${search}%`, limit] ); return { content: [ { type: 'text', text: JSON.stringify({ count: result.rows.length, customers: result.rows }, null, 2) } ] }; } ``` ### Example: File System Tool ```typescript import fs from 'fs/promises'; import path from 'path'; // Tool definition { name: 'list_files', description: 'List files in a directory', inputSchema: { type: 'object', properties: { directory: { type: 'string', description: 'Directory path' } }, required: ['directory'] } } // Tool implementation if (name === 'list_files') { const { directory } = args; try { const files = await fs.readdir(directory); const fileDetails = await Promise.all( files.map(async (file) => { const filePath = path.join(directory, file); const stats = await fs.stat(filePath); return { name: file, size: stats.size, modified: stats.mtime, isDirectory: stats.isDirectory() }; }) ); return { content: [ { type: 'text', text: JSON.stringify({ directory, count: files.length, files: fileDetails }, null, 2) } ] }; } catch (error) { return { content: [ { type: 'text', text: `Error reading directory: ${error.message}` } ], isError: true }; } } ``` --- ## Testing Your MCP Server ### 1. Manual Testing with stdio ```typescript // test.ts import { spawn } from 'child_process'; const server = spawn('node', ['./dist/index.js']); // Send request server.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} }) + '\n'); // Listen for response server.stdout.on('data', (data) => { console.log('Response:', data.toString()); }); server.stderr.on('data', (data) => { console.log('Log:', data.toString()); }); ``` ### 2. Claude Desktop Testing Best way to test: 1. Configure in `claude_desktop_config.json` 2. Restart Claude Desktop 3. Open dev tools (View → Toggle Developer Tools) 4. Check console for MCP server logs 5. Test in chat: "Use [tool_name] to..." ### 3. Unit Testing ```typescript // __tests__/tools.test.ts import { describe, it, expect } from '@jest/globals'; import { myTool } from '../src/tools/my-tool'; describe('myTool', () => { it('should return expected result', async () => { const result = await myTool({ param: 'value' }); expect(result.content[0].text).toContain('expected'); }); }); ``` --- ## Common Patterns ### Pattern 1: Configuration Management ```typescript // src/config.ts import fs from 'fs'; import path from 'path'; interface Config { apiKey: string; baseUrl: string; timeout: number; } export function loadConfig(): Config { const configPath = path.join(__dirname, '../config.json'); const configData = fs.readFileSync(configPath, 'utf-8'); const config = JSON.parse(configData); // Replace environment variables return { apiKey: process.env.API_KEY || config.apiKey, baseUrl: config.baseUrl, timeout: config.timeout }; } ``` ### Pattern 2: Error Handling ```typescript async function handleToolCall(name: string, args: any) { try { // Tool implementation const result = await someTool(args); return { content: [{ type: 'text', text: JSON.stringify(result) }] }; } catch (error) { console.error(`Error in ${name}:`, error); return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true }; } } ``` ### Pattern 3: Logging ```typescript import winston from 'winston'; const logger = winston.createLogger({ level: 'info', format: winston.format.json(), transports: [ new winston.transports.File({ filename: 'error.log', level: 'error' }), new winston.transports.File({ filename: 'combined.log' }) ] }); // Use in tools logger.info('Tool called', { name, args }); logger.error('Tool failed', { name, error: error.message }); ``` ### Pattern 4: Retry Logic ```typescript async function retryWithBackoff<T>( fn: () => Promise<T>, maxRetries: number = 3, initialDelay: number = 1000 ): Promise<T> { let lastError: any; let delay = initialDelay; for (let i = 0; i <= maxRetries; i++) { try { return await fn(); } catch (error) { lastError = error; if (i < maxRetries) { await new Promise(resolve => setTimeout(resolve, delay)); delay *= 2; } } } throw lastError; } // Usage const result = await retryWithBackoff(() => apiCall(), 3, 1000); ``` ### Pattern 5: Input Validation ```typescript import { z } from 'zod'; const WeatherArgsSchema = z.object({ city: z.string().min(1, 'City is required'), units: z.enum(['celsius', 'fahrenheit']).default('celsius') }); if (name === 'get_weather') { // Validate input const validatedArgs = WeatherArgsSchema.parse(args); // Use validated args const result = await getWeather(validatedArgs.city, validatedArgs.units); } ``` --- ## Troubleshooting ### Issue: MCP Server Not Appearing in Claude Desktop **Symptoms:** - Server configured in `claude_desktop_config.json` - Claude doesn't show tools in chat **Solutions:** 1. Check absolute path in config (no relative paths!) 2. Ensure `dist/index.js` has shebang: `#!/usr/bin/env node` 3. Verify file is executable: `chmod +x dist/index.js` 4. Restart Claude Desktop completely (quit, not just close window) 5. Check Claude Desktop logs: View → Toggle Developer Tools ### Issue: Server Crashes on Startup **Symptoms:** - Server starts then immediately exits - Claude shows "Server disconnected" **Solutions:** 1. Check `console.error()` logs (goes to Claude dev tools) 2. Run manually: `node dist/index.js` to see errors 3. Check for missing dependencies 4. Verify TypeScript compiled correctly: `npm run build` ### Issue: Tool Not Being Called **Symptoms:** - Claude acknowledges tool exists - But doesn't use it when asked **Solutions:** 1. Improve tool description (be specific!) 2. Add examples in description 3. Check inputSchema matches Claude's expectations 4. Try explicit: "Use the [tool_name] tool to..." ### Issue: JSON Parse Errors **Symptoms:** - "Unexpected token" errors - Server crashes when Claude calls tool **Solutions:** 1. Ensure all responses are valid JSON 2. Use `JSON.stringify()` for complex objects 3. Handle undefined/null values 4. Escape special characters in strings --- ## Best Practices ### 1. Tool Design ✅ **DO:** - Give tools clear, descriptive names - Write detailed descriptions with examples - Use JSON Schema validation - Return structured data - Handle errors gracefully ❌ **DON'T:** - Make tools too generic - Return unstructured text when JSON is better - Ignore error handling - Use unclear parameter names ### 2. Performance ✅ **DO:** - Cache frequent requests - Use connection pooling for databases - Implement timeouts - Use streaming for large responses ❌ **DON'T:** - Make synchronous blocking calls - Load entire files into memory - Retry indefinitely - Skip resource cleanup ### 3. Security ✅ **DO:** - Validate all inputs - Use environment variables for secrets - Sanitize file paths - Rate limit expensive operations - Log security events ❌ **DON'T:** - Commit secrets to git - Trust user input - Allow arbitrary file access - Expose internal errors to Claude ### 4. Maintainability ✅ **DO:** - Organize code into modules - Use TypeScript for type safety - Write tests - Document your tools - Version your server ❌ **DON'T:** - Put everything in one file - Skip type definitions - Ignore linting warnings - Leave TODO comments forever --- ## Real-World Example: RS.ge Waybill MCP Server This server integrates a SOAP API with Claude. Key learnings: ### 1. Complex API Integration ```typescript // Bad: Direct SOAP calls in tool handlers if (name === 'get_waybills') { const xml = buildSoapXml(args); // Complex logic here const response = await axios.post(url, xml); return parseXml(response.data); // More complex logic } // Good: Separate API client class ApiClient { async getWaybills(start, end) { const xml = this.buildRequest(start, end); const response = await this.post(xml); return this.parseResponse(response); } } if (name === 'get_waybills') { const result = await apiClient.getWaybills(args.start, args.end); return { content: [{ type: 'text', text: JSON.stringify(result) }] }; } ``` ### 2. Date Handling ```typescript // Learned through trial and error: // - API requires ISO datetime (not just date) // - End date needs +1 day to be inclusive // - Format must be exactly YYYY-MM-DDTHH:MM:SS function prepareDates(start: string, end: string) { const startDateTime = `${start}T00:00:00`; const endDate = new Date(end); endDate.setDate(endDate.getDate() + 1); const endDateTime = endDate.toISOString().slice(0, 19); return { startDateTime, endDateTime }; } ``` ### 3. XML Parsing Gotchas ```typescript // Problem: XML attributes break simple parsers // <Response xmlns="..."> ← This creates @_xmlns key! const parser = new XMLParser({ ignoreAttributes: false, // Need to see them attributeNamePrefix: '@_' // Prefix them }); const parsed = parser.parse(xml); // Filter out attributes when looking for data const keys = Object.keys(parsed).filter(k => !k.startsWith('@_')); ``` --- ## Summary Building an MCP server: 1. **Setup** - Initialize Node.js project with TypeScript 2. **Define Tools** - Create clear, focused tools with good schemas 3. **Implement Handlers** - Write robust, error-handling tool implementations 4. **Test Thoroughly** - Test manually, with Claude, and with unit tests 5. **Deploy** - Configure in Claude Desktop and monitor logs **Key Takeaways:** - MCP uses JSON-RPC over stdio - Tools must have clear descriptions and schemas - Error handling is critical - Test with real Claude Desktop - Look at working examples (like this project!) **Next Steps:** - Study the code in `src/tools/` for real examples - Read the MCP SDK documentation - Build a simple server first, then add complexity - Join the MCP community for help --- **Resources:** - MCP SDK: https://github.com/modelcontextprotocol/sdk - This Project: Example of production MCP server - Claude Desktop: For testing

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/BorisSolomonia/MCPWaybill'

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