Przejdź do głównej zawartości

Budowanie niestandardowych serwerów MCP: rozszerzaj Cursor o swoje narzędzia

Budowanie niestandardowych serwerów MCP: rozszerzaj Cursor o swoje narzędzia

Dział zatytułowany „Budowanie niestandardowych serwerów MCP: rozszerzaj Cursor o swoje narzędzia”

Serwery Model Context Protocol (MCP) to pomost między Cursor a twoimi zewnętrznymi narzędziami. Chociaż Cursor jest dostarczany z wieloma gotowymi serwerami MCP, prawdziwa moc leży w tworzeniu niestandardowych serwerów dostosowanych do unikalnych potrzeb twojej organizacji.

MCP stosuje model klient-serwer, gdzie:

  • Cursor działa jako klient, wysyłając żądania narzędzi i danych
  • Twój serwer MCP odpowiada z możliwościami i wykonuje działania
  • Komunikacja odbywa się przez transporty stdio, SSE lub HTTP

Zbudujmy prosty serwer MCP, który integruje się z hipotetycznym wewnętrznym API.

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// Stwórz instancję serwera
const server = new McpServer({
name: "internal-api-server",
version: "1.0.0",
description: "Łączy Cursor z naszymi wewnętrznymi systemami"
});
// Zdefiniuj narzędzie
server.tool(
"get_user_info",
"Pobierz informacje o użytkowniku z wewnętrznego API",
{
userId: z.string().describe("ID użytkownika")
},
async ({ userId }) => {
// Twoje wywołanie API tutaj
const response = await fetch(`https://api.internal.com/users/${userId}`);
const data = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(data, null, 2)
}]
};
}
);
// Uruchom serwer
const transport = new StdioServerTransport();
await server.connect(transport);

Dodaj do swojego ~/.cursor/mcp.json:

{
"mcpServers": {
"internal-api": {
"command": "node",
"args": ["/path/to/your/server.js"],
"env": {
"API_KEY": "your-api-key-here"
}
}
}
}

Twórz serwery, które udostępniają wiele powiązanych narzędzi:

// Serwer MCP analizatora bazy danych
const dbServer = new McpServer({
name: "database-analyzer",
version: "1.0.0"
});
// Narzędzie 1: inspektor schematu
dbServer.tool(
"inspect_schema",
"Pobierz informacje o schemacie bazy danych",
{ table: z.string() },
async ({ table }) => {
const schema = await db.getTableSchema(table);
return formatSchemaResponse(schema);
}
);
// Narzędzie 2: analizator zapytań
dbServer.tool(
"analyze_query",
"Przeanalizuj wydajność zapytania SQL",
{ query: z.string() },
async ({ query }) => {
const plan = await db.explainQuery(query);
return formatQueryPlan(plan);
}
);
// Narzędzie 3: profiler danych
dbServer.tool(
"profile_data",
"Profiluj rozkład danych w tabeli",
{
table: z.string(),
column: z.string().optional()
},
async ({ table, column }) => {
const profile = await db.profileData(table, column);
return formatDataProfile(profile);
}
);

Buduj serwery, które utrzymują stan między żądaniami:

class StatefulMcpServer {
private sessions: Map<string, SessionData> = new Map();
private server: McpServer;
constructor() {
this.server = new McpServer({
name: "stateful-workflow",
version: "1.0.0"
});
this.setupTools();
}
private setupTools() {
// Rozpocznij sesję
this.server.tool(
"start_session",
"Zainicjuj nową sesję przepływu pracy",
{ workflowType: z.string() },
async ({ workflowType }) => {
const sessionId = generateId();
this.sessions.set(sessionId, {
type: workflowType,
state: 'initialized',
data: {}
});
return {
content: [{
type: "text",
text: `Sesja rozpoczęta: ${sessionId}`
}]
};
}
);
// Wykonaj kroki przepływu pracy
this.server.tool(
"execute_step",
"Wykonaj następny krok w przepływie pracy",
{
sessionId: z.string(),
input: z.any()
},
async ({ sessionId, input }) => {
const session = this.sessions.get(sessionId);
if (!session) throw new Error("Sesja nie znaleziona");
// Przetwarzaj na podstawie obecnego stanu
const result = await this.processWorkflowStep(session, input);
return {
content: [{
type: "text",
text: JSON.stringify(result)
}]
};
}
);
}
}

Udostępniaj dynamiczne zasoby, które Cursor może przeglądać:

// Serwer dokumentacji zapewniający przeglądalne zasoby
server.resource(
"api_docs",
"Przeglądaj dokumentację API",
async (uri) => {
// Parsuj URI, aby określić, którą dokumentację pokazać
const path = uri.replace("api-docs://", "");
if (path === "/") {
// Zwróć listę dostępnych API
return {
content: [{
type: "text",
text: await getApiIndex()
}]
};
}
// Zwróć konkretną dokumentację API
const doc = await getApiDoc(path);
return {
content: [{
type: "text",
text: doc
}]
};
}
);
// Zasoby mogą być hierarchiczne
server.listResources = async () => {
return [
{ uri: "api-docs://users", name: "API użytkowników" },
{ uri: "api-docs://payments", name: "API płatności" },
{ uri: "api-docs://analytics", name: "API analityki" }
];
};

Zespół bezpieczeństwa zbudował serwer MCP do automatycznej analizy bezpieczeństwa:

  • Foldersecurity-scanner-mcp/
    • Foldersrc/
      • index.ts
      • Folderscanners/
        • dependencies.ts
        • secrets.ts
        • vulnerabilities.ts
      • Folderreporters/
        • markdown.ts
        • json.ts
    • package.json
    • mcp.json
// Główna implementacja skanera bezpieczeństwa
class SecurityScannerMcp {
private scanners = [
new DependencyScanner(),
new SecretScanner(),
new VulnerabilityScanner()
];
async setupTools() {
// Kompleksowe skanowanie bezpieczeństwa
this.server.tool(
"security_scan",
"Uruchom kompleksową analizę bezpieczeństwa",
{
directory: z.string(),
scanTypes: z.array(z.enum(['deps', 'secrets', 'vulns'])).optional()
},
async ({ directory, scanTypes }) => {
const results = [];
for (const scanner of this.scanners) {
if (!scanTypes || scanTypes.includes(scanner.type)) {
const findings = await scanner.scan(directory);
results.push(...findings);
}
}
// Wygeneruj raport
const report = new SecurityReport(results);
return {
content: [{
type: "text",
text: report.toMarkdown()
}]
};
}
);
// Sprawdź konkretny plik
this.server.tool(
"check_file_security",
"Kontrola bezpieczeństwa konkretnego pliku",
{
filePath: z.string()
},
async ({ filePath }) => {
const issues = await this.quickScan(filePath);
return formatIssues(issues);
}
);
}
}
// MCP monitorowania wydajności łączący z narzędziami APM
class PerformanceMcp {
private apmClient: APMClient;
constructor() {
this.apmClient = new APMClient({
endpoint: process.env.APM_ENDPOINT,
apiKey: process.env.APM_API_KEY
});
}
setupTools() {
// Pobierz metryki wydajności
this.server.tool(
"get_performance_metrics",
"Pobierz obecne metryki wydajności",
{
service: z.string(),
timeRange: z.enum(['1h', '24h', '7d']).default('1h')
},
async ({ service, timeRange }) => {
const metrics = await this.apmClient.getMetrics({
service,
from: getTimeFromRange(timeRange),
to: new Date()
});
return {
content: [{
type: "text",
text: this.formatMetrics(metrics)
}]
};
}
);
// Analizuj wolne punkty końcowe
this.server.tool(
"analyze_slow_endpoints",
"Znajdź i przeanalizuj wolne punkty końcowe API",
{
threshold: z.number().default(1000),
limit: z.number().default(10)
},
async ({ threshold, limit }) => {
const slowEndpoints = await this.apmClient.getSlowTransactions({
threshold,
limit
});
const analysis = await this.analyzeEndpoints(slowEndpoints);
return {
content: [{
type: "text",
text: analysis
}]
};
}
);
}
}
// MCP automatyzacji wdrożenia
class DeploymentMcp {
setupTools() {
// Wdróż do środowiska
this.server.tool(
"deploy",
"Wdróż aplikację do określonego środowiska",
{
environment: z.enum(['dev', 'staging', 'prod']),
version: z.string().optional(),
dryRun: z.boolean().default(false)
},
async ({ environment, version, dryRun }) => {
// Kontrole przed wdrożeniem
const checks = await this.runPreDeployChecks(environment);
if (!checks.passed) {
return {
content: [{
type: "text",
text: `Kontrole przed wdrożeniem nie powiodły się:\n${checks.failures.join('\n')}`
}]
};
}
// Wykonaj wdrożenie
if (!dryRun) {
const result = await this.deploy(environment, version);
await this.notifyTeam(environment, result);
return {
content: [{
type: "text",
text: this.formatDeployResult(result)
}]
};
}
return {
content: [{
type: "text",
text: "Próba na sucho ukończona pomyślnie"
}]
};
}
);
}
}
// Implementuj uwierzytelnianie w swoim serwerze MCP
class SecureMcpServer {
private async authenticate(headers: Headers): Promise<boolean> {
const token = headers.get('Authorization')?.replace('Bearer ', '');
if (!token) return false;
// Zweryfikuj token z twoim serwisem uwierzytelniania
return await this.authService.verifyToken(token);
}
async handleRequest(request: Request) {
if (!await this.authenticate(request.headers)) {
throw new Error('Nieautoryzowany');
}
// Przetwórz uwierzytelnione żądanie
}
}
// Zawsze waliduj i oczyszczaj wejścia
server.tool(
"execute_query",
"Uruchom zapytanie do bazy danych",
{
query: z.string()
.min(1)
.max(1000)
.refine(
(q) => !q.match(/DROP|DELETE|TRUNCATE/i),
"Operacje destruktywne nie są dozwolone"
)
},
async ({ query }) => {
// Dodatkowa walidacja
const sanitized = sanitizeSQL(query);
const result = await db.query(sanitized);
return formatResult(result);
}
);
// Używaj zmiennych środowiskowych dla sekretów
const config = {
apiKey: process.env.API_KEY,
dbPassword: process.env.DB_PASSWORD,
// Nigdy nie hardkoduj sekretów!
};
// Waliduj wymagane sekrety przy uruchomieniu
function validateEnvironment() {
const required = ['API_KEY', 'DB_PASSWORD'];
const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
throw new Error(`Brakujące wymagane zmienne środowiskowe: ${missing.join(', ')}`);
}
}
import { MockMcpClient } from '@mcp/testing';
describe('SecurityScannerMcp', () => {
let server: SecurityScannerMcp;
let client: MockMcpClient;
beforeEach(() => {
server = new SecurityScannerMcp();
client = new MockMcpClient(server);
});
test('security_scan znajduje luki', async () => {
const result = await client.callTool('security_scan', {
directory: './test-project',
scanTypes: ['vulns']
});
expect(result.content[0].text).toContain('Znaleziono 3 luki');
});
});
// Test z rzeczywistym połączeniem Cursor
async function testWithCursor() {
// Uruchom swój serwer MCP
const serverProcess = spawn('node', ['./dist/index.js']);
// Skonfiguruj instancję testową Cursor
const testConfig = {
mcpServers: {
"test-server": {
command: "node",
args: ["./dist/index.js"]
}
}
};
// Uruchom scenariusze testowe
// ...
}
Okno terminala
# Konfiguracja rozwoju
npm run build:watch
# Skonfiguruj swój lokalny serwer MCP w ustawieniach Cursor
# Ustawienia > Funkcje > MCP > Dodaj serwer
{
"mcpServers": {
"my-server-dev": {
"command": "node",
"args": ["./dist/index.js"],
"env": {
"NODE_ENV": "development",
"DEBUG": "mcp:*"
}
}
}
}
package.json
{
"name": "@company/mcp-internal-tools",
"version": "1.0.0",
"bin": {
"mcp-internal": "./dist/index.js"
},
"publishConfig": {
"registry": "https://npm.company.com"
}
}
// Zespół instaluje globalnie
npm install -g @company/mcp-internal-tools
// Konfiguracja w Cursor
{
"mcpServers": {
"internal-tools": {
"command": "mcp-internal"
}
}
}

Dla dużych odpowiedzi używaj strumieniowania:

server.tool(
"analyze_large_dataset",
"Przeanalizuj duży zestaw danych",
{ datasetId: z.string() },
async function* ({ datasetId }) {
const dataset = await loadDataset(datasetId);
// Strumieniuj wyniki podczas przetwarzania
for await (const batch of processBatches(dataset)) {
yield {
content: [{
type: "text",
text: `Przetworzono partię: ${JSON.stringify(batch.summary)}\n`
}]
};
}
}
);
class CachedMcpServer {
private cache = new LRUCache<string, any>({
max: 100,
ttl: 1000 * 60 * 5 // 5 minut
});
async handleExpensiveOperation(params: any) {
const cacheKey = JSON.stringify(params);
// Sprawdź cache najpierw
const cached = this.cache.get(cacheKey);
if (cached) return cached;
// Wykonaj kosztowną operację
const result = await this.expensiveOperation(params);
// Buforuj wynik
this.cache.set(cacheKey, result);
return result;
}
}
// Używaj pakietu debug do szczegółowego logowania
import debug from 'debug';
const log = debug('mcp:my-server');
server.on('tool_call', (tool, params) => {
log('Wywołane narzędzie:', tool, params);
});
// Uruchom z DEBUG=mcp:* aby zobaczyć logi
Okno terminala
# Zainstaluj inspektor MCP
npm install -g @mcp/inspector
# Przetestuj swój serwer
mcp-inspect ./my-server.js
# Interfejs testów interaktywnych
> list_tools
> call_tool get_user_info {"userId": "123"}

Problemy z timeoutem

Problem: Długotrwałe operacje przekraczają timeout Rozwiązanie: Implementuj raportowanie postępu lub podziel na mniejsze operacje

Wycieki pamięci

Problem: Użycie pamięci serwera rośnie z czasem Rozwiązanie: Właściwie czyść zasoby, używaj WeakMaps dla cache’y

Obsługa błędów

Problem: Błędy krasują serwer Rozwiązanie: Owijaj wszystkie handlery narzędzi w try-catch, zwracaj przyjazne błędy

Teraz, gdy potrafisz budować niestandardowe serwery MCP:

  1. Zidentyfikuj możliwości integracji: Wylistuj najczęściej używane narzędzia zespołu
  2. Zacznij od prostego: Zbuduj serwer MCP tylko do odczytu najpierw
  3. Iteruj na podstawie użycia: Dodawaj funkcje w miarę potrzeb
  4. Udostępniaj zespołowi: Spakuj i dystrybuuj, aby pomnożyć wpływ

Pamiętaj: najlepsze serwery MCP rozwiązują prawdziwe problemy. Zacznij od swojego największego problemu i buduj od tego. Każda niestandardowa integracja czyni Cursor potężniejszym dla twojego konkretnego przepływu pracy.