Skip to main content
Glama
MCP_SERVER_DEVELOPMENT_GUIDE.md13.4 kB
# MCP Server Development Guide A comprehensive guide for creating production-ready Model Context Protocol (MCP) servers, based on real-world development experience. ## Table of Contents 1. [Project Setup](#project-setup) 2. [API Integration](#api-integration) 3. [Testing Strategy](#testing-strategy) 4. [DXT Extension Packaging](#dxt-extension-packaging) 5. [Deployment](#deployment) 6. [Troubleshooting](#troubleshooting) 7. [Best Practices](#best-practices) ## Project Setup ### Initial Structure ``` mcp-server-template/ ├── .github/workflows/ │ ├── ci.yml │ ├── release.yml │ └── docker-publish.yml ├── src/ │ ├── index.ts # MCP server entrypoint │ ├── config/ │ │ ├── index.ts │ │ └── production.ts │ ├── services/ │ │ └── api-client.ts # API integration layer │ ├── tools/ │ │ ├── base.ts # Base tool class │ │ ├── index.ts │ │ └── [domain]/ # Domain-specific tools │ ├── types/ │ │ └── api.ts # API response types │ └── utils/ │ └── logger.ts ├── tests/ │ ├── integration/ │ ├── unit/ │ └── setup.ts ├── docker-compose.yml ├── Dockerfile ├── manifest.json # DXT manifest ├── .dxtignore ├── .prettierignore ├── jest.config.ts ├── tsconfig.json └── package.json ``` ### Key Dependencies ```json { "dependencies": { "@modelcontextprotocol/sdk": "^0.4.0", "zod": "^3.22.0", "axios": "^1.6.0", "dotenv": "^16.3.0" }, "devDependencies": { "@types/node": "^20.0.0", "typescript": "^5.0.0", "jest": "^29.0.0", "@types/jest": "^29.0.0", "prettier": "^3.0.0", "eslint": "^8.0.0" } } ``` ### Essential Configuration Files #### `.prettierignore` ``` node_modules/ dist/ coverage/ *.log .env ``` #### `.dxtignore` ``` plans/ coverage/ .taskmaster/ .roo/ .husky/ .cursor/ .github/ prompt_logs/ docs/ tests/ Dockerfile docker-compose.yml README.md CHANGELOG.md setup.sh scripts/ jest.config.ts jest.resolver.js package-lock.json *.log *.md *.test.* ``` ## API Integration ### Base API Client Pattern ```typescript // src/services/api-client.ts import axios, { AxiosInstance, AxiosResponse } from 'axios'; import { z } from 'zod'; export class ApiClient { private client: AxiosInstance; constructor(baseURL: string, apiKey: string) { this.client = axios.create({ baseURL, headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', }, timeout: 30000, }); // Add request/response interceptors for logging this.setupInterceptors(); } private setupInterceptors() { this.client.interceptors.request.use( (config) => { console.log(`[API] ${config.method?.toUpperCase()} ${config.url}`); return config; }, (error) => Promise.reject(error) ); this.client.interceptors.response.use( (response) => { console.log(`[API] ${response.status} ${response.config.url}`); return response; }, (error) => { console.error(`[API] Error: ${error.response?.status} ${error.config?.url}`); return Promise.reject(error); } ); } async get<T>(url: string, schema: z.ZodSchema<T>): Promise<T> { const response: AxiosResponse = await this.client.get(url); return schema.parse(response.data); } async post<T>(url: string, data: any, schema: z.ZodSchema<T>): Promise<T> { const response: AxiosResponse = await this.client.post(url, data); return schema.parse(response.data); } // Add other methods as needed } ``` ### Authentication Patterns #### API Key Authentication ```typescript // src/config/index.ts import { z } from 'zod'; const configSchema = z.object({ API_KEY: z.string().min(1, 'API key is required'), API_BASE_URL: z.string().url('Invalid API base URL'), LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'), }); export const config = configSchema.parse(process.env); ``` #### OAuth Authentication ```typescript // src/services/oauth-client.ts export class OAuthClient { private clientId: string; private clientSecret: string; private redirectUri: string; constructor(clientId: string, clientSecret: string, redirectUri: string) { this.clientId = clientId; this.clientSecret = clientSecret; this.redirectUri = redirectUri; } getAuthUrl(): string { return `https://api.example.com/oauth/authorize?client_id=${this.clientId}&redirect_uri=${this.redirectUri}&response_type=code`; } async exchangeCodeForToken(code: string): Promise<string> { // Implementation for OAuth token exchange } } ``` ## Testing Strategy ### Unit Testing Pattern ```typescript // tests/unit/tools/example.test.ts import { ExampleTool } from '../../../src/tools/example'; describe('ExampleTool', () => { let tool: ExampleTool; beforeEach(() => { tool = new ExampleTool(); }); describe('execute', () => { it('should handle valid input', async () => { const result = await tool.execute({ input: 'test' }); expect(result).toBeDefined(); }); it('should handle invalid input', async () => { await expect(tool.execute({ input: '' })) .rejects.toThrow('Input is required'); }); }); }); ``` ### Integration Testing Pattern ```typescript // tests/integration/api-client.test.ts import { ApiClient } from '../../src/services/api-client'; describe('ApiClient Integration', () => { let client: ApiClient; beforeAll(() => { client = new ApiClient( process.env.TEST_API_BASE_URL!, process.env.TEST_API_KEY! ); }); it('should fetch data successfully', async () => { const data = await client.get('/endpoint', responseSchema); expect(data).toBeDefined(); }); }); ``` ### Docker Testing Environment ```yaml # docker-compose.test.yml version: '3.8' services: test-api: image: mockserver/mockserver ports: - "1080:1080" environment: MOCKSERVER_INITIALIZATION_JSON_PATH: /config/initializerJson.json volumes: - ./tests/mocks:/config ``` ## DXT Extension Packaging ### Manifest Structure ```json { "name": "Your MCP Server", "version": "1.0.0", "description": "MCP server for Your API", "entry_point": "dist/index.js", "mcp_config": { "command": "node", "args": ["dist/index.js"], "env": { "API_KEY": "${user_config.API_KEY}", "API_BASE_URL": "${user_config.API_BASE_URL}", "LOG_LEVEL": "${user_config.LOG_LEVEL}" } }, "user_config": { "API_KEY": { "type": "string", "description": "Your API key", "required": true }, "API_BASE_URL": { "type": "string", "description": "API base URL", "default": "https://api.example.com" }, "LOG_LEVEL": { "type": "string", "enum": ["debug", "info", "warn", "error"], "default": "info" } } } ``` ### Debug Logging for DXT ```typescript // src/index.ts - Add at the very top process.on('uncaughtException', (err) => { console.error('Uncaught Exception:', err); process.exit(1); }); process.on('unhandledRejection', (reason) => { console.error('Unhandled Rejection:', reason); process.exit(1); }); console.error('[DEBUG] MCP server entrypoint reached:', __filename, process.cwd(), process.argv, process.env); if (!process.env.API_KEY) { console.error('[DEBUG] API_KEY is missing!'); process.exit(1); } ``` ## Deployment ### Docker Configuration ```dockerfile # Dockerfile FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . RUN npm run build FROM node:20-alpine AS runtime WORKDIR /app COPY --from=builder /app/dist ./dist COPY --from=builder /app/node_modules ./node_modules COPY package.json ./ EXPOSE 3000 CMD ["node", "dist/index.js"] ``` ### GitHub Actions Release Workflow ```yaml # .github/workflows/release.yml name: Release on: push: tags: - 'v*.*.*' jobs: release: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: '20' - name: Extract version from tag id: extract_version run: | echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT - name: Extract release notes from CHANGELOG.md id: extract_release_notes run: | version="${{ steps.extract_version.outputs.VERSION }}" echo "Extracting release notes for version $version" awk -v version="$version" ' BEGIN { found=0; content="" } /^## \[.*\] - / { if (found) exit if (match($0, /\[([^\]]+)\]/, arr) && arr[1] == version) found=1 next } /^## \[.*\] - / && found { exit } found && !/^#/ { if (content == "" && $0 == "") next content = content $0 "\n" } END { gsub(/\n+$/, "", content) print content }' CHANGELOG.md > release-notes.md echo "Release notes extracted to release-notes.md" - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: tag_name: ${{ github.ref_name }} name: "Release ${{ github.ref_name }}" body_path: release-notes.md draft: false prerelease: ${{ contains(steps.extract_version.outputs.VERSION, '-') }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ``` ## Troubleshooting ### Common Issues and Solutions #### 1. DXT Extension Not Starting in Claude Desktop **Symptoms**: Extension works in MCP Inspector but fails in Claude Desktop **Solutions**: - Ensure `dist/index.js` exists and is the entrypoint - Add robust debug logging at the top of entrypoint - Check that `node_modules/` is included in DXT package - Verify environment variables are properly mapped - Check Claude Desktop logs: `~/Library/Logs/Claude/mcp-*.log` #### 2. Prettier CI Errors **Symptoms**: CI fails on formatting checks **Solutions**: - Run `npx prettier --write .` locally - Ensure `.prettierignore` excludes generated files - Commit formatting fixes before pushing #### 3. API Rate Limiting **Symptoms**: API calls fail with 429 errors **Solutions**: - Implement exponential backoff - Add request queuing - Use API client with built-in rate limiting #### 4. Type Validation Errors **Symptoms**: Zod validation fails on API responses **Solutions**: - Log raw API responses for debugging - Make schema fields optional where appropriate - Add transform functions for data normalization ### Debug Checklist - [ ] Check Claude Desktop logs - [ ] Verify environment variables - [ ] Test entrypoint manually: `node dist/index.js` - [ ] Check file permissions in DXT package - [ ] Verify Node.js version compatibility - [ ] Test API connectivity - [ ] Review manifest.json configuration ## Best Practices ### Code Organization 1. **Separation of Concerns**: Keep API logic, MCP tools, and configuration separate 2. **Type Safety**: Use Zod for runtime validation and TypeScript for compile-time checks 3. **Error Handling**: Implement comprehensive error handling with meaningful messages 4. **Logging**: Use structured logging with configurable levels ### Performance 1. **Caching**: Implement caching for frequently accessed data 2. **Rate Limiting**: Respect API rate limits and implement backoff strategies 3. **Connection Pooling**: Reuse HTTP connections where possible 4. **Lazy Loading**: Load data only when needed ### Security 1. **Environment Variables**: Never hardcode sensitive data 2. **Input Validation**: Validate all inputs with Zod schemas 3. **Error Messages**: Don't expose sensitive information in error messages 4. **Dependencies**: Regularly update dependencies and audit for vulnerabilities ### Testing 1. **Test Coverage**: Aim for >90% test coverage 2. **Integration Tests**: Test against real or mocked APIs 3. **Docker Testing**: Use Docker for consistent test environments 4. **CI/CD**: Automate testing in CI/CD pipelines ### Documentation 1. **README**: Include setup, usage, and troubleshooting information 2. **API Documentation**: Document all tools and their parameters 3. **Changelog**: Maintain a detailed changelog following Keep a Changelog format 4. **Examples**: Provide usage examples for common scenarios ## Quick Start Checklist - [ ] Initialize project with TypeScript and essential dependencies - [ ] Set up API client with authentication - [ ] Create base tool class and implement domain-specific tools - [ ] Add comprehensive testing (unit + integration) - [ ] Configure Docker for development and deployment - [ ] Set up CI/CD with GitHub Actions - [ ] Create DXT manifest and test extension packaging - [ ] Implement release workflow with changelog extraction - [ ] Add comprehensive error handling and logging - [ ] Document setup and usage instructions This guide should provide a solid foundation for creating production-ready MCP servers quickly and efficiently.

Latest Blog Posts

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/asachs01/float-mcp'

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