Kluczowa zasada: Używaj tylko serwerów MCP z zaufanych źródeł. Serwery zewnętrzne mają taki sam dostęp jak każdy kod, który uruchamiasz lokalnie.
Opanuj najlepsze praktyki wdrażania, zabezpieczania i optymalizacji serwerów MCP w środowiskach produkcyjnych. Ucz się z rzeczywistych doświadczeń i unikaj typowych pułapek.
Kluczowa zasada: Używaj tylko serwerów MCP z zaufanych źródeł. Serwery zewnętrzne mają taki sam dostęp jak każdy kod, który uruchamiasz lokalnie.
Przegląd kodu źródłowego
Analiza uprawnień
Testowanie w izolacji
Komponent | Środek bezpieczeństwa | Priorytet |
---|---|---|
Uwierzytelnianie | Użyj zmiennych środowiskowych dla sekretów | Krytyczny |
Sieć | Ogranicz do localhost dla wrażliwych danych | Wysoki |
Uprawnienia | Uruchamiaj z minimalnymi wymaganymi uprawnieniami | Krytyczny |
Dostęp do danych | Używaj danych tylko do odczytu gdy to możliwe | Wysoki |
Logowanie | Audituj wszystkie operacje bez ujawniania sekretów | Średni |
Aktualizacje | Utrzymuj serwery i zależności w aktualnej wersji | Wysoki |
Najlepsza praktyka: Nigdy nie wpisuj danych uwierzytelniających na stałe
# .env.local (dodaj do .gitignore)DATABASE_URL=postgresql://readonly:pass@localhost/dbAPI_KEY=sk-proj-...OAUTH_CLIENT_SECRET=...
# Załaduj w shellexport $(cat .env.local | xargs)
# Użyj w konfiguracji MCPclaude mcp add db -e DATABASE_URL=$DATABASE_URL -- \ npx -y @modelcontextprotocol/server-postgres
Dla produkcji: Użyj odpowiedniego zarządzania sekretami
// Używając AWS Secrets Managerimport { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager";
async function getSecret(secretName: string) { const client = new SecretsManagerClient({ region: "us-east-1" }); const command = new GetSecretValueCommand({ SecretId: secretName }); const response = await client.send(command); return JSON.parse(response.SecretString);}
// Inicjalizuj MCP z bezpiecznymi danymiconst secrets = await getSecret("mcp-server-credentials");process.env.API_KEY = secrets.apiKey;
Dla serwerów zdalnych: Zaimplementuj odpowiedni przepływ OAuth
// Bezpieczne przechowywanie tokenówimport { encrypt, decrypt } from './crypto';
class TokenStore { async saveToken(userId: string, token: string) { const encrypted = await encrypt(token); await db.saveUserToken(userId, encrypted); }
async getToken(userId: string) { const encrypted = await db.getUserToken(userId); return decrypt(encrypted); }
async refreshIfNeeded(userId: string) { const token = await this.getToken(userId); if (isExpired(token)) { const newToken = await refreshOAuthToken(token); await this.saveToken(userId, newToken); return newToken; } return token; }}
Serwery lokalne (stdio)
Najbezpieczniejsza opcja
Serwery zdalne (SSE/HTTP)
Wymaga dodatkowej ostrożności
# Przykład: Ograniczenie serwera MCP tylko do dostępu lokalnego# Reguła iptables dla Linuxsudo iptables -A INPUT -p tcp --dport 3845 -s 127.0.0.1 -j ACCEPTsudo iptables -A INPUT -p tcp --dport 3845 -j DROP
# Reguła pf dla macOSecho "block in proto tcp to any port 3845" | sudo pfctl -ef -
# Windows Firewall PowerShellNew-NetFirewallRule -DisplayName "Block MCP External" ` -Direction Inbound -LocalPort 3845 -Protocol TCP ` -Action Block -RemoteAddress !127.0.0.1
// Przykład: Oczyszczanie zewnętrznej zawartościserver.tool( 'fetch_webpage', 'Bezpieczne pobieranie i przetwarzanie zawartości strony', { url: z.string().url() }, async ({ url }) => { // Walidacja domeny URL const allowedDomains = ['docs.company.com', 'api.company.com']; const urlObj = new URL(url);
if (!allowedDomains.includes(urlObj.hostname)) { throw new Error('Domena URL niedozwolona'); }
// Pobieranie z limitem czasu i rozmiaru const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000);
try { const response = await fetch(url, { signal: controller.signal, headers: { 'User-Agent': 'MCP-Server/1.0' }, });
// Ograniczenie rozmiaru odpowiedzi const contentLength = response.headers.get('content-length'); if (contentLength && parseInt(contentLength) > 1_000_000) { throw new Error('Odpowiedź zbyt duża'); }
const text = await response.text();
// Oczyszczanie zawartości const sanitized = text .replace(/<script[^>]*>.*?<\/script>/gi, '') // Usuń skrypty .replace(/\bignore previous instructions\b/gi, '[ZREDAGOWANE]') // Podstawowy filtr wstrzykiwania .substring(0, 50000); // Ogranicz do rozsądnego rozmiaru
return { content: [{ type: 'text', text: `Zawartość z ${url}:\n\n${sanitized}`, }], }; } finally { clearTimeout(timeout); } });
Wyzwanie: Duże odpowiedzi mogą wypełnić okna kontekstu AI, zmniejszając skuteczność i zwiększając koszty.
server.tool( 'search_large_dataset', 'Wyszukiwanie z automatyczną paginacją', { query: z.string(), page: z.number().optional().default(1), pageSize: z.number().optional().default(20), }, async ({ query, page, pageSize }) => { const results = await searchDatabase(query, { offset: (page - 1) * pageSize, limit: pageSize, });
const hasMore = results.total > page * pageSize;
return { content: [{ type: 'text', text: JSON.stringify({ results: results.items, page, pageSize, total: results.total, hasMore, nextPage: hasMore ? page + 1 : null, }, null, 2), }], }; });
server.tool( 'analyze_logs', 'Analiza logów z automatycznym podsumowywaniem', { timeRange: z.string(), includeRaw: z.boolean().optional().default(false), }, async ({ timeRange, includeRaw }) => { const logs = await fetchLogs(timeRange);
// Zawsze dostarcz podsumowanie const summary = { totalEntries: logs.length, errorCount: logs.filter(l => l.level === 'ERROR').length, warningCount: logs.filter(l => l.level === 'WARN').length, topErrors: groupByFrequency(logs.filter(l => l.level === 'ERROR')), timeDistribution: groupByHour(logs), };
let response = `Podsumowanie analizy logów:\n${JSON.stringify(summary, null, 2)}`;
// Dołącz surowe logi tylko jeśli specjalnie poproszono i rozmiar jest rozsądny if (includeRaw && logs.length < 100) { response += `\n\nSurowe logi:\n${JSON.stringify(logs, null, 2)}`; } else if (includeRaw) { response += `\n\nUwaga: Znaleziono ${logs.length} logów. Zbyt dużo do wyświetlenia. Użyj filtrów, aby zawęzić wyniki.`; }
return { content: [{ type: 'text', text: response }], }; });
// Zwracaj najpierw najważniejsze daneserver.tool( 'get_user_profile', 'Pobieranie profilu użytkownika z progresywnymi szczegółami', { userId: z.string(), detail: z.enum(['basic', 'full', 'complete']).optional().default('basic'), }, async ({ userId, detail }) => { const basicInfo = await getUserBasicInfo(userId);
if (detail === 'basic') { return { content: [{ type: 'text', text: JSON.stringify(basicInfo) }], }; }
const activitySummary = await getUserActivitySummary(userId);
if (detail === 'full') { return { content: [{ type: 'text', text: JSON.stringify({ ...basicInfo, activitySummary }), }], }; }
// Complete zawiera wszystko const fullHistory = await getUserFullHistory(userId); return { content: [{ type: 'text', text: JSON.stringify({ ...basicInfo, activitySummary, fullHistory }), }], }; });
class MCPCache { private cache = new Map<string, { data: any; expires: number }>(); private readonly DEFAULT_TTL = 5 * 60 * 1000; // 5 minut
set(key: string, data: any, ttl = this.DEFAULT_TTL) { this.cache.set(key, { data, expires: Date.now() + ttl, }); }
get(key: string) { const entry = this.cache.get(key); if (!entry) return null;
if (Date.now() > entry.expires) { this.cache.delete(key); return null; }
return entry.data; }
// Implementuj generowanie kluczy cache static key(tool: string, params: any): string { return `${tool}:${JSON.stringify(params, Object.keys(params).sort())}`; }}
const cache = new MCPCache();
server.tool( 'expensive_operation', 'Kosztowna operacja z cache', { input: z.string() }, async ({ input }) => { const cacheKey = MCPCache.key('expensive_operation', { input }); const cached = cache.get(cacheKey);
if (cached) { return { content: [{ type: 'text', text: `[Z cache] ${cached}`, }], }; }
const result = await performExpensiveOperation(input); cache.set(cacheKey, result);
return { content: [{ type: 'text', text: result }], }; });
// Cache oparty na Redis dla współdzielonego stanuimport Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
server.tool( 'get_team_metrics', 'Pobieranie metryki teamowych z rozproszonym cache', { teamId: z.string() }, async ({ teamId }) => { const cacheKey = `metrics:${teamId}`;
// Najpierw sprawdź cache const cached = await redis.get(cacheKey); if (cached) { return { content: [{ type: 'text', text: cached, }], }; }
// Oblicz metryki const metrics = await computeTeamMetrics(teamId); const result = JSON.stringify(metrics, null, 2);
// Cache z wygaśnięciem await redis.setex(cacheKey, 300, result); // 5 minut TTL
return { content: [{ type: 'text', text: result }], }; });
// Efektywne zarządzanie połączeniami bazodanowymiimport { Pool } from 'pg';
const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 20, // Maksymalna liczba połączeń idleTimeoutMillis: 30000, // Zamknij bezczynne połączenia po 30s connectionTimeoutMillis: 2000, // Szybko przerwij przy błędzie połączenia});
// Check-up zdrowiasetInterval(async () => { try { await pool.query('SELECT 1'); } catch (error) { console.error('Check-up zdrowia bazy danych nieudany:', error); }}, 30000);
// Graceful shutdownprocess.on('SIGTERM', async () => { await pool.end(); process.exit(0);});
// Monitorowanie i ograniczanie użycia pamięciconst v8 = require('v8');
function getMemoryUsage() { const heap = v8.getHeapStatistics(); return { used: Math.round(heap.used_heap_size / 1024 / 1024), total: Math.round(heap.total_heap_size / 1024 / 1024), limit: Math.round(heap.heap_size_limit / 1024 / 1024), };}
// Wykrywanie ciśnienia pamięciowegoserver.tool( 'process_large_file', 'Przetwarzanie pliku z zarządzaniem pamięcią', { filePath: z.string() }, async ({ filePath }) => { const memory = getMemoryUsage();
// Sprawdź pamięć przed przetwarzaniem if (memory.used / memory.limit > 0.8) { // Wymuś garbage collection jeśli dostępne if (global.gc) { global.gc(); }
// Jeśli nadal wysokie, odmów operacji const newMemory = getMemoryUsage(); if (newMemory.used / newMemory.limit > 0.8) { return { content: [{ type: 'text', text: 'Serwer pod ciśnieniem pamięciowym. Spróbuj ponownie później.', }], }; } }
// Przetwarzaj strumieniowo aby minimalizować pamięć const stream = createReadStream(filePath); const chunks = [];
for await (const chunk of stream) { // Przetwarzaj kawałek po kawałku chunks.push(processChunk(chunk));
// Okresowo oddawaj kontrolę if (chunks.length % 100 === 0) { await new Promise(resolve => setImmediate(resolve)); } }
return { content: [{ type: 'text', text: `Pomyślnie przetworzono ${chunks.length} kawałków`, }], }; });
Strukturalne logowanie
import winston from 'winston';
const logger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json() ), transports: [ new winston.transports.Console(), new winston.transports.File({ filename: 'mcp-server.log' }), ],});
// Loguj użycie narzędziserver.use((tool, params, next) => { const start = Date.now(); logger.info('Wywołano narzędzie', { tool, params: sanitizeParams(params) });
next().then(result => { logger.info('Narzędzie ukończone', { tool, duration: Date.now() - start, success: true, }); }).catch(error => { logger.error('Narzędzie nieudane', { tool, duration: Date.now() - start, error: error.message, }); });});
Zbieranie metryk
import { Counter, Histogram, register } from 'prom-client';
const toolCallCounter = new Counter({ name: 'mcp_tool_calls_total', help: 'Łączna liczba wywołań narzędzi', labelNames: ['tool', 'status'],});
const toolDuration = new Histogram({ name: 'mcp_tool_duration_seconds', help: 'Czas wykonania narzędzia', labelNames: ['tool'], buckets: [0.1, 0.5, 1, 2, 5, 10],});
// Endpoint metryk dla Prometheusaapp.get('/metrics', (req, res) => { res.set('Content-Type', register.contentType); res.end(register.metrics());});
Kontrola zdrowia
server.tool( 'health_check', 'Stan zdrowia systemu', {}, async () => { const checks = await Promise.allSettled([ checkDatabase(), checkExternalAPI(), checkDiskSpace(), checkMemory(), ]);
const results = checks.map((check, index) => ({ component: ['database', 'api', 'disk', 'memory'][index], status: check.status === 'fulfilled' ? 'healthy' : 'unhealthy', details: check.status === 'fulfilled' ? check.value : check.reason, }));
const allHealthy = results.every(r => r.status === 'healthy');
return { content: [{ type: 'text', text: JSON.stringify({ status: allHealthy ? 'healthy' : 'degraded', timestamp: new Date().toISOString(), checks: results, }, null, 2), }], }; });
// Strategie fallback dla zewnętrznych zależnościserver.tool( 'get_weather', 'Pobieranie pogody z dostawcami fallback', { location: z.string() }, async ({ location }) => { const providers = [ { name: 'primary', fn: () => getPrimaryWeather(location) }, { name: 'secondary', fn: () => getSecondaryWeather(location) }, { name: 'cache', fn: () => getCachedWeather(location) }, ];
for (const provider of providers) { try { const result = await withTimeout(provider.fn(), 3000); if (result) { return { content: [{ type: 'text', text: `Pogoda z ${provider.name}: ${JSON.stringify(result)}`, }], }; } } catch (error) { logger.warn(`Dostawca pogody ${provider.name} nieudany`, { error }); continue; } }
// Wszyscy dostawcy nieudani return { content: [{ type: 'text', text: 'Dane pogodowe tymczasowo niedostępne. Spróbuj ponownie później.', }], }; });
class CircuitBreaker { private failures = 0; private lastFailTime = 0; private state: 'closed' | 'open' | 'half-open' = 'closed';
constructor( private threshold = 5, private timeout = 60000 // 1 minuta ) {}
async call<T>(fn: () => Promise<T>): Promise<T> { if (this.state === 'open') { if (Date.now() - this.lastFailTime > this.timeout) { this.state = 'half-open'; } else { throw new Error('Wyłącznik obwodu jest otwarty'); } }
try { const result = await fn(); if (this.state === 'half-open') { this.state = 'closed'; this.failures = 0; } return result; } catch (error) { this.failures++; this.lastFailTime = Date.now();
if (this.failures >= this.threshold) { this.state = 'open'; }
throw error; } }}
const apiBreaker = new CircuitBreaker();
server.tool( 'call_external_api', 'Wywołanie zewnętrznego API z wyłącznikiem obwodu', { endpoint: z.string() }, async ({ endpoint }) => { try { const result = await apiBreaker.call(() => fetch(endpoint).then(r => r.json()) );
return { content: [{ type: 'text', text: JSON.stringify(result) }], }; } catch (error) { if (error.message === 'Wyłącznik obwodu jest otwarty') { return { content: [{ type: 'text', text: 'Usługa tymczasowo niedostępna z powodu powtarzających się błędów', }], }; } throw error; } });
// .mcp.json - Konfiguracja zespołowa w kontroli wersji{ "mcpServers": { "team-database": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-postgres"], "env": { "DATABASE_URL": "${DATABASE_URL}", // Ze środowiska "POOL_SIZE": "10", "STATEMENT_TIMEOUT": "30s" } }, "team-apis": { "command": "npx", "args": ["-y", "@ourcompany/mcp-internal-apis"], "env": { "API_BASE_URL": "${INTERNAL_API_URL}", "API_KEY": "${INTERNAL_API_KEY}" } } }}
# Serwer MCP: Narzędzia wewnętrzne
## PrzeglądZapewnia dostęp do wewnętrznych narzędzi i usług firmy.
## Dostępne narzędzia
### `search_knowledge_base`Wyszukiwanie wewnętrznej dokumentacji i wiki.
**Parametry:**- `query` (string, wymagany): Zapytanie wyszukiwania- `category` (string, opcjonalny): Filtruj według kategorii- `limit` (number, opcjonalny): Maksymalna liczba wyników (domyślnie: 10)
**Przykład:**
“Wyszukaj procedury wdrażania dla usługi płatności”
### `get_service_status`Sprawdź stan zdrowia i status usług wewnętrznych.
**Parametry:**- `service` (string, opcjonalny): Konkretna nazwa usługi
**Przykład:**
“Sprawdź status usługi uwierzytelniania”
## Uwagi bezpieczeństwa- Wymaga połączenia VPN- Używa konta usługowego z dostępem tylko do odczytu- Loguje wszystkie zapytania w celach audytowych
## Rozwiązywanie problemów- Błąd "Connection refused": Upewnij się, że VPN jest podłączony- Błąd "Unauthorized": Sprawdź wygaśnięcie klucza API
Kompresuj odpowiedzi
Usuń niepotrzebne białe znaki i formatowanie z odpowiedzi JSON gdy to odpowiednie.
Używaj podsumowań
Domyślnie dostarczaj podsumowania, z opcjami szczegółowych danych tylko gdy potrzebne.
Implementuj filtry
Pozwól na filtrowanie u źródła aby zmniejszyć transfer i przetwarzanie danych.
Cache agresywnie
Cache’uj często żądane dane aby uniknąć redundantnego przetwarzania.
import rateLimit from 'express-rate-limit';
// Ograniczanie częstości per-użytkownik dla serwerów zdalnychconst limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minut max: 100, // Ogranicz każdego użytkownika do 100 żądań na okno message: 'Zbyt wiele żądań, spróbuj ponownie później', standardHeaders: true, legacyHeaders: false, keyGenerator: (req) => req.user?.id || req.ip,});
// Ograniczanie częstości specyficzne dla narzędziconst toolLimiters = new Map();
function getToolLimiter(tool: string, maxRequests = 10) { if (!toolLimiters.has(tool)) { toolLimiters.set(tool, new Map()); }
return (userId: string) => { const userLimiters = toolLimiters.get(tool); const now = Date.now(); const windowStart = now - 60000; // Okno 1 minuty
const requests = userLimiters.get(userId) || []; const recentRequests = requests.filter(t => t > windowStart);
if (recentRequests.length >= maxRequests) { throw new Error(`Przekroczono limit częstości dla ${tool}`); }
recentRequests.push(now); userLimiters.set(userId, recentRequests); };}
// Obsługuj wiele wersji APIserver.tool( 'api_call', 'Endpoint API z wersjami', { endpoint: z.string(), version: z.enum(['v1', 'v2']).optional().default('v2'), }, async ({ endpoint, version }) => { const handlers = { v1: () => callV1API(endpoint), v2: () => callV2API(endpoint), };
const result = await handlers[version]();
// Dodaj ostrzeżenie o deprecation dla starych wersji if (version === 'v1') { return { content: [{ type: 'text', text: `[OSTRZEŻENIE DEPRECATION] API v1 zostanie usunięte 2025-12-31. Użyj v2.\n\n${JSON.stringify(result)}`, }], }; }
return { content: [{ type: 'text', text: JSON.stringify(result) }], }; });
// Feature flags dla stopniowego wdrażaniaconst features = { enhancedSearch: process.env.FEATURE_ENHANCED_SEARCH === 'true', aiSummaries: process.env.FEATURE_AI_SUMMARIES === 'true', caching: process.env.FEATURE_CACHING !== 'false', // Domyślnie włączone};
server.tool( 'search', 'Wyszukiwanie z feature flags', { query: z.string() }, async ({ query }) => { let results;
if (features.enhancedSearch) { results = await enhancedSearch(query); } else { results = await basicSearch(query); }
if (features.aiSummaries && results.length > 0) { results = await addAISummaries(results); }
return { content: [{ type: 'text', text: JSON.stringify({ results, features: Object.entries(features) .filter(([_, enabled]) => enabled) .map(([name]) => name), }), }], }; });
Pamiętaj: Serwery MCP to potężne narzędzia wymagające takiej samej doskonałości operacyjnej jak każda usługa produkcyjna. Traktuj je z taką samą starannością jak każdy krytyczny komponent infrastruktury.