MCP Server-Architektur verstehen
Nach dem Aufbau typsicherer APIs mit Zod-Verträgen besteht die nächste Herausforderung darin, Server-Architekturen zu erstellen, die erweiterbar, wartbar und skalierbar sind. Das Model Context Protocol (MCP) bietet ein Framework zum Aufbau von Servern, die sich durch eine klar definierte Architektur an sich ändernde Anforderungen anpassen können.
Was ist das Model Context Protocol (MCP)?
MCP ist ein Architekturmuster zum Aufbau von Servern, die Kontext und Zustand über mehrere Interaktionen hinweg verwalten. Im Gegensatz zu traditionellen zustandslosen REST-APIs behalten MCP-Server den Konversationskontext bei, verarbeiten komplexe Workflows und bieten eine strukturierte Möglichkeit, Funktionalität durch Plugins zu erweitern.
Stellen Sie sich MCP als eine Protokollschicht vor, die zwischen Ihrer Anwendungslogik und der Transportschicht sitzt und Folgendes verwaltet:
- Kontextpersistenz über Anfragen hinweg
- Zustandsübergänge und Lifecycle-Management
- Protokollebene Belange wie Authentifizierung und Autorisierung
- Erweiterbarkeit durch eine Plugin-Architektur
interface MCPServer {
initialize(): Promise<void>
handleRequest(request: MCPRequest): Promise<MCPResponse>
shutdown(): Promise<void>
}
interface MCPRequest {
id: string
method: string
params: unknown
context?: Record<string, unknown>
}
interface MCPResponse {
id: string
result?: unknown
error?: MCPError
}
Kernkomponenten der Server-Architektur
Ein MCP-Server besteht aus mehreren Schlüsselkomponenten, die zusammenarbeiten:
Request Router: Leitet eingehende Anfragen basierend auf Methodennamen oder Mustern an geeignete Handler weiter.
Context Manager: Verwaltet Zustand und Kontext über mehrere Anfragen hinweg und ermöglicht zustandsbehaftete Interaktionen.
Handler Registry: Ordnet Methoden ihren Implementierungs-Handlern zu und unterstützt dynamische Registrierung.
Protocol Layer: Verarbeitet Serialisierung, Deserialisierung und protokollspezifische Belange.
Lifecycle Manager: Koordiniert Server-Initialisierung, Start, Herunterfahren und Bereinigung.
class MCPServerCore {
private router: RequestRouter
private contextManager: ContextManager
private handlerRegistry: HandlerRegistry
private protocol: ProtocolLayer
private lifecycle: LifecycleManager
constructor(config: MCPServerConfig) {
this.router = new RequestRouter()
this.contextManager = new ContextManager(config.contextOptions)
this.handlerRegistry = new HandlerRegistry()
this.protocol = new ProtocolLayer(config.protocolOptions)
this.lifecycle = new LifecycleManager()
}
async initialize(): Promise<void> {
await this.lifecycle.runPhase('initialize', async () => {
await this.contextManager.initialize()
await this.protocol.initialize()
this.registerCoreHandlers()
})
}
async handleRequest(request: MCPRequest): Promise<MCPResponse> {
const context = await this.contextManager.getContext(request)
const handler = this.handlerRegistry.getHandler(request.method)
if (!handler) {
return {
id: request.id,
error: {
code: -32601,
message: `Method not found: ${request.method}`
}
}
}
try {
const result = await handler(request.params, context)
await this.contextManager.updateContext(request, result)
return {
id: request.id,
result
}
} catch (error) {
return {
id: request.id,
error: this.formatError(error)
}
}
}
registerHandler(method: string, handler: RequestHandler): void {
this.handlerRegistry.register(method, handler)
}
private registerCoreHandlers(): void {
this.registerHandler('server.info', this.handleServerInfo.bind(this))
this.registerHandler('server.ping', this.handlePing.bind(this))
}
private async handleServerInfo(): Promise<ServerInfo> {
return {
name: 'MCP Server',
version: '1.0.0',
capabilities: ['context', 'plugins', 'streaming']
}
}
private async handlePing(): Promise<{ pong: boolean }> {
return { pong: true }
}
private formatError(error: unknown): MCPError {
if (error instanceof Error) {
return {
code: -32000,
message: error.message,
data: { stack: error.stack }
}
}
return {
code: -32000,
message: 'Unknown error occurred'
}
}
}
Request/Response-Lebenszyklus
Das Verständnis des Request-Lebenszyklus ist entscheidend für den Aufbau zuverlässiger MCP-Server:
- Request-Empfang: Server empfängt rohe Request-Daten über die Transportschicht
- Deserialisierung: Protokollschicht konvertiert rohe Daten in MCPRequest-Objekt
- Kontext-Laden: Context Manager ruft Kontext für die Anfrage ab oder erstellt ihn
- Routing: Router bestimmt, welcher Handler die Anfrage verarbeiten soll
- Validierung: Request-Parameter werden gegen erwartetes Schema validiert
- Ausführung: Handler verarbeitet die Anfrage mit Zugriff auf Kontext
- Kontext-Update: Context Manager persistiert alle Zustandsänderungen
- Serialisierung: Response wird in Transportformat konvertiert
- Response-Übertragung: Formatierte Response wird an Client zurückgesendet
class RequestLifecycle {
constructor(
private protocol: ProtocolLayer,
private contextManager: ContextManager,
private router: RequestRouter,
private validator: RequestValidator
) {}
async process(rawRequest: Buffer): Promise<Buffer> {
let request: MCPRequest | null = null
try {
request = await this.protocol.deserialize(rawRequest)
const context = await this.contextManager.loadContext(request)
const handler = this.router.route(request.method)
if (!handler) {
throw new Error(`No handler for method: ${request.method}`)
}
await this.validator.validate(request, handler.schema)
const result = await handler.execute(request.params, context)
await this.contextManager.saveContext(request, context)
const response: MCPResponse = {
id: request.id,
result
}
return await this.protocol.serialize(response)
} catch (error) {
const errorResponse: MCPResponse = {
id: request?.id || 'unknown',
error: this.formatError(error)
}
return await this.protocol.serialize(errorResponse)
}
}
private formatError(error: unknown): MCPError {
if (error instanceof ValidationError) {
return {
code: -32602,
message: 'Invalid params',
data: error.details
}
}
if (error instanceof Error) {
return {
code: -32000,
message: error.message
}
}
return {
code: -32000,
message: 'Unknown error'
}
}
}
Verbindungsverwaltung
MCP-Server unterhalten oft persistente Verbindungen mit Clients, was ein sorgfältiges Verbindungs-Lifecycle-Management erfordert:
class ConnectionManager {
private connections: Map<string, Connection> = new Map()
private heartbeatInterval: NodeJS.Timeout | null = null
async addConnection(connectionId: string, socket: WebSocket): Promise<void> {
const connection: Connection = {
id: connectionId,
socket,
createdAt: Date.now(),
lastActivity: Date.now(),
context: {}
}
this.connections.set(connectionId, connection)
socket.on('message', (data) => this.handleMessage(connectionId, data))
socket.on('close', () => this.removeConnection(connectionId))
socket.on('error', (error) => this.handleError(connectionId, error))
await this.sendWelcome(connection)
}
async removeConnection(connectionId: string): Promise<void> {
const connection = this.connections.get(connectionId)
if (!connection) return
try {
await this.cleanupContext(connection)
connection.socket.close()
} finally {
this.connections.delete(connectionId)
}
}
startHeartbeat(intervalMs: number = 30000): void {
this.heartbeatInterval = setInterval(() => {
this.checkConnections()
}, intervalMs)
}
stopHeartbeat(): void {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval)
this.heartbeatInterval = null
}
}
private checkConnections(): void {
const now = Date.now()
const timeout = 60000 // 60 seconds
for (const [id, connection] of this.connections) {
if (now - connection.lastActivity > timeout) {
this.removeConnection(id)
}
}
}
private async handleMessage(
connectionId: string,
data: WebSocket.Data
): Promise<void> {
const connection = this.connections.get(connectionId)
if (!connection) return
connection.lastActivity = Date.now()
// Process message through MCP server
}
private async sendWelcome(connection: Connection): Promise<void> {
const welcome = {
type: 'welcome',
connectionId: connection.id,
serverInfo: {
name: 'MCP Server',
version: '1.0.0'
}
}
connection.socket.send(JSON.stringify(welcome))
}
private async cleanupContext(connection: Connection): Promise<void> {
// Clean up any resources associated with this connection
}
private handleError(connectionId: string, error: Error): void {
console.error(`Connection ${connectionId} error:`, error)
this.removeConnection(connectionId)
}
}
Zustandsverwaltungsstrategien
MCP-Server benötigen robuste Zustandsverwaltung, um Kontext über Anfragen hinweg zu erhalten:
In-Memory-Zustand: Schnell, aber nicht persistent über Neustarts hinweg. Geeignet für Sitzungsdaten und temporären Kontext.
class InMemoryStateStore {
private state: Map<string, ContextState> = new Map()
async get(contextId: string): Promise<ContextState | null> {
return this.state.get(contextId) || null
}
async set(contextId: string, state: ContextState): Promise<void> {
this.state.set(contextId, state)
}
async delete(contextId: string): Promise<void> {
this.state.delete(contextId)
}
async clear(): Promise<void> {
this.state.clear()
}
}
Persistenter Zustand: Überlebt Neustarts, aber langsamer. Verwenden Sie dies für wichtigen Kontext, der erhalten bleiben muss.
class PersistentStateStore {
constructor(private db: Database) {}
async get(contextId: string): Promise<ContextState | null> {
const row = await this.db.query(
'SELECT state FROM context_state WHERE id = ?',
[contextId]
)
return row ? JSON.parse(row.state) : null
}
async set(contextId: string, state: ContextState): Promise<void> {
await this.db.query(
'INSERT OR REPLACE INTO context_state (id, state, updated_at) VALUES (?, ?, ?)',
[contextId, JSON.stringify(state), Date.now()]
)
}
async delete(contextId: string): Promise<void> {
await this.db.query('DELETE FROM context_state WHERE id = ?', [contextId])
}
}
Hybrid-Ansatz: Kombinieren Sie beide für optimale Leistung und Zuverlässigkeit.
class HybridStateStore {
constructor(
private memory: InMemoryStateStore,
private persistent: PersistentStateStore
) {}
async get(contextId: string): Promise<ContextState | null> {
let state = await this.memory.get(contextId)
if (!state) {
state = await this.persistent.get(contextId)
if (state) {
await this.memory.set(contextId, state)
}
}
return state
}
async set(contextId: string, state: ContextState): Promise<void> {
await this.memory.set(contextId, state)
await this.persistent.set(contextId, state)
}
async delete(contextId: string): Promise<void> {
await this.memory.delete(contextId)
await this.persistent.delete(contextId)
}
}
Sicherheit und Authentifizierung
Sicherheit ist in MCP-Servern von größter Bedeutung. Implementieren Sie mehrere Schutzebenen:
class SecurityLayer {
constructor(
private authProvider: AuthProvider,
private rateLimiter: RateLimiter,
private validator: InputValidator
) {}
async authenticate(request: MCPRequest): Promise<AuthContext> {
const token = this.extractToken(request)
if (!token) {
throw new AuthError('Missing authentication token')
}
const authContext = await this.authProvider.verify(token)
if (!authContext.isValid) {
throw new AuthError('Invalid authentication token')
}
return authContext
}
async authorize(
authContext: AuthContext,
method: string
): Promise<boolean> {
const permissions = authContext.permissions
const requiredPermission = this.getRequiredPermission(method)
return permissions.includes(requiredPermission)
}
async checkRateLimit(clientId: string): Promise<void> {
const allowed = await this.rateLimiter.checkLimit(clientId)
if (!allowed) {
throw new RateLimitError('Rate limit exceeded')
}
}
async validateInput(request: MCPRequest): Promise<void> {
await this.validator.validate(request.params)
}
private extractToken(request: MCPRequest): string | null {
return request.context?.authorization as string || null
}
private getRequiredPermission(method: string): string {
const methodPermissions: Record<string, string> = {
'user.create': 'user:write',
'user.read': 'user:read',
'user.update': 'user:write',
'user.delete': 'user:delete'
}
return methodPermissions[method] || 'default'
}
}
Leistungsüberlegungen
Optimieren Sie Ihren MCP-Server für Produktions-Workloads:
Request-Batching: Verarbeiten Sie mehrere Anfragen zusammen, um Overhead zu reduzieren.
Connection Pooling: Verwenden Sie Datenbankverbindungen und externe Service-Verbindungen wieder.
Caching: Cachen Sie häufig abgerufene Daten, um die Datenbanklast zu reduzieren.
Asynchrone Verarbeitung: Verwenden Sie async/await und nicht-blockierende I/O durchgehend.
Fazit
Die MCP-Server-Architektur bietet eine solide Grundlage für den Aufbau erweiterbarer, wartbarer Server-Systeme. Durch das Verständnis der Kernkomponenten—Request-Routing, Kontextverwaltung, Verbindungshandling, Zustandspersistenz und Sicherheit—können Sie Server erstellen, die mit den Anforderungen Ihrer Anwendung skalieren.
Die hier gezeigten Muster bilden das Rückgrat von Produktions-MCP-Servern. Im nächsten Beitrag werden wir untersuchen, wie diese Architektur mit einem Plugin-System erweitert werden kann, das dynamische Funktionalität ermöglicht, ohne den Kern-Server-Code zu ändern.
Schreiten Sie weiter voran und genießen Sie jeden Schritt Ihrer Programmierreise.
