Przejdź do głównej zawartości

Najlepsze praktyki i optymalizacja MCP

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.

  1. Przegląd kodu źródłowego

    • Sprawdź repozytorium GitHub serwera
    • Poszukaj najnowszych aktualizacji i aktywnej konserwacji
    • Przejrzyj problemy i ostrzeżenia bezpieczeństwa
    • Zweryfikuj reputację autora
  2. Analiza uprawnień

    • Do jakich danych serwer ma dostęp?
    • Jakie zewnętrzne API wywołuje?
    • Czy wymaga uprawnień do zapisu?
    • Czy może wykonywać polecenia systemowe?
  3. Testowanie w izolacji

    • Uruchom najpierw w kontenerze Docker
    • Użyj maszyny wirtualnej dla niezaufanych serwerów
    • Monitoruj ruch sieciowy podczas testowania
    • Sprawdź zmiany w systemie plików
KomponentŚrodek bezpieczeństwaPriorytet
UwierzytelnianieUżyj zmiennych środowiskowych dla sekretówKrytyczny
SiećOgranicz do localhost dla wrażliwych danychWysoki
UprawnieniaUruchamiaj z minimalnymi wymaganymi uprawnieniamiKrytyczny
Dostęp do danychUżywaj danych tylko do odczytu gdy to możliweWysoki
LogowanieAudituj wszystkie operacje bez ujawniania sekretówŚredni
AktualizacjeUtrzymuj serwery i zależności w aktualnej wersjiWysoki

Najlepsza praktyka: Nigdy nie wpisuj danych uwierzytelniających na stałe

Okno terminala
# .env.local (dodaj do .gitignore)
DATABASE_URL=postgresql://readonly:pass@localhost/db
API_KEY=sk-proj-...
OAUTH_CLIENT_SECRET=...
# Załaduj w shell
export $(cat .env.local | xargs)
# Użyj w konfiguracji MCP
claude mcp add db -e DATABASE_URL=$DATABASE_URL -- \
npx -y @modelcontextprotocol/server-postgres

Serwery lokalne (stdio)

Najbezpieczniejsza opcja

  • Brak narażenia sieciowego
  • Izolacja procesów
  • Bezpośrednia kontrola dostępu do systemu plików
  • Odpowiednie dla wrażliwych operacji

Serwery zdalne (SSE/HTTP)

Wymaga dodatkowej ostrożności

  • Zawsze używaj HTTPS/TLS
  • Implementuj ograniczenia częstotliwości
  • Dodaj warstwy uwierzytelniania
  • Monitoruj podejrzaną aktywność
Okno terminala
# Przykład: Ograniczenie serwera MCP tylko do dostępu lokalnego
# Reguła iptables dla Linux
sudo iptables -A INPUT -p tcp --dport 3845 -s 127.0.0.1 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 3845 -j DROP
# Reguła pf dla macOS
echo "block in proto tcp to any port 3845" | sudo pfctl -ef -
# Windows Firewall PowerShell
New-NetFirewallRule -DisplayName "Block MCP External" `
-Direction Inbound -LocalPort 3845 -Protocol TCP `
-Action Block -RemoteAddress !127.0.0.1
// Przykład: Oczyszczanie zewnętrznej zawartości
server.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),
}],
};
}
);
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 stanu
import 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 bazodanowymi
import { 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 zdrowia
setInterval(async () => {
try {
await pool.query('SELECT 1');
} catch (error) {
console.error('Check-up zdrowia bazy danych nieudany:', error);
}
}, 30000);
// Graceful shutdown
process.on('SIGTERM', async () => {
await pool.end();
process.exit(0);
});
// Monitorowanie i ograniczanie użycia pamięci
const 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ęciowego
server.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`,
}],
};
}
);
  1. 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ędzi
    server.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,
    });
    });
    });
  2. 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 Prometheusa
    app.get('/metrics', (req, res) => {
    res.set('Content-Type', register.contentType);
    res.end(register.metrics());
    });
  3. 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ści
server.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ąd
Zapewnia 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 zdalnych
const 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ędzi
const 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 API
server.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żania
const 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),
}),
}],
};
}
);
  • Bezpieczeństwo: Zmienne środowiskowe dla sekretów, waliduj wszystkie wejścia
  • Wydajność: Implementuj cache’owanie, paginuj duże wyniki
  • Niezawodność: Dodaj timeout’y, logikę retry, wyłączniki obwodu
  • Monitorowanie: Strukturalne logi, zbieraj metryki, implementuj kontrole zdrowia
  • Dokumentacja: Utrzymuj aktualny README, dokumentuj wszystkie narzędzia
  • Testowanie: Testy jednostkowe, testy integracyjne, testy obciążenia
  • Aktualizacje: Regularne aktualizacje zależności, łatki bezpieczeństwa

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.