NestJS MCP Server
npm i @nestjs/mcp @modelcontextprotocol/server zod
npm i -D @modelcontextprotocol/client # In-Memory-Test-Client
Zod 4.2+ und ArkType funktionieren direkt; Valibot über toStandardJsonSchema(); ein vorhandenes JSON-Schema über fromJsonSchema(). Zod 3-Schemas werden beim Start abgelehnt.
McpModule.forRoot({
name: 'cat-store', version: '1.0.0',
instructions: 'Look orders up with find-order before changing them.', // teilt dem Modell mit, wie der Server zu verwenden ist
}),
app.listen(3000 stellt den Endpunkt unter /mcp bereit. Untersuchen Sie ihn mit npx @modelcontextprotocol/inspector. Beim Start scannt das Modul die Provider (alle Module oder nur include) nach dekorierten Methoden. Es schlägt bei doppelten Namen, ungültigen Schemas oder deklarierten scopes ohne auth fehl.
Primitive: Methoden auf Providern
| Primitive | Decorator | Wer verwendet es | Aufgerufen mit |
|---|---|---|---|
| Tool (eine Aktion) | @Tool({ name, description, inputSchema, outputSchema?, annotations?, scopes? }) | das Modell | validierte Eingabe, ctx |
| Ressource (Daten) | @Resource({ uri, mimeType }) / @ResourceTemplate({ uriTemplate: 'orders://{orderId}' }) | der Client, oft vom Benutzer angehängt | {} oder die Template-Variablen, ctx (ctx.uri) |
| Prompt (eine Vorlage) | @Prompt({ name, description, argsSchema }) | der Benutzer wählt eine aus | validierte Argumente, ctx |
@Injectable() // ein einfacher Provider, der in den providers eines Moduls aufgeführt ist
export class OrdersTools {
constructor(private readonly orders: OrdersService) {}
@Tool({
name: 'find-order',
description: 'Find an order by its id, or the latest orders of a customer by email',
inputSchema: z.object({
orderId: z.string().optional().describe('The order id, such as ord_123'),
email: z.email().optional().describe("The customer's email address"),
}),
annotations: { readOnlyHint: true }, // Client kann es ohne Rückfrage beim Benutzer ausführen
})
async find({ orderId, email }: { orderId?: string; email?: string }) {
const found = orderId ? [await this.orders.findById(orderId)] : await this.orders.latestFor(email!);
if (!found[0]) throw new NotFoundException('No such order'); // → Tool-Ergebnis mit isError: true
return found;
}
@Tool({
name: 'change-address',
description: 'Change the delivery address of an order that has not shipped yet',
inputSchema: z.object({ orderId: z.string(), address: z.string().min(10) }),
outputSchema: z.object({ orderId: z.string(), address: z.string(), status: z.string() }),
scopes: ['orders:write'],
})
async changeAddress({ orderId, address }: { orderId: string; address: string }, ctx: McpContext) {
const o = await this.orders.changeAddress(orderId, address, ctx.authInfo?.clientId);
return { orderId: o.id, address: o.address, status: o.status }; // → structuredContent + JSON-Text
}
}
Rückgabewerte: Ein String eines Tools wird zu einem Textblock. Ein Objekt wird zu structuredContent, wenn ein outputSchema vorhanden ist, andernfalls zu JSON-Text. Geben Sie für Bilder, Audio oder eingebettete Ressourcen selbst ein CallToolResult zurück; undefined ergibt ein leeres Ergebnis. Ein String einer Ressource wird als Text gesendet, ein Buffer/Uint8Array als Blob, ein Objekt als JSON. Ein String eines Prompts wird zu einer Benutzernachricht; ein Array gibt PromptMessage[] zurück. Toolnamen sind 1–128 Zeichen aus A-Z a-z 0-9 _ - . und standardmäßig der Methodenname.
Schreiben Sie Namen, Beschreibungen und .describe()-Texte für das Modell. Das ist alles, was es weiß: Sagen Sie, was das Tool tut, wann es verwendet werden soll, ob es etwas ändert, und geben Sie einen Beispielwert für jedes Argument. Verwenden Sie Ressourcen für Inhalte, die unverändert geladen werden; verwenden Sie Tools für Suchvorgänge, die Argumente benötigen.
Aufrufkontext
ctx: McpContext hat diese Eigenschaften:
kind,name,uriauthInfo(clientId,scopes,expiresAt,extra)request(die Plattformanfrage;undefinedüber stdio)requestInfotransport('http' | 'stdio')era('modern'2026-07-28 /'legacy'2025)signal: übergeben Sie es anfetch- und DB-Aufrufe; rufen SiethrowIfAborted()in Schleifen aufreportProgress(progress, total?, message?)server(SDK-Kontext)
Um den Benutzer zu fragen (Bestätigungen, fehlende Werte), geben Sie inputRequired({ inputRequests: { confirm: inputRequired.elicit({ message, requestedSchema }) } }) zurück. Beim erneuten Versuch lesen Sie die Antwort mit acceptedContent(ctx.server.mcpReq.inputResponses, 'confirm', Schema). Über HTTP funktioniert dies nur für Clients von 2026-07-28, da der Endpunkt zustandslos ist. Über stdio funktioniert es für beide Epochen.
Provider bleiben Singletons. Lesen Sie aufrufspezifische Daten aus ctx, nicht aus anforderungsbezogenen Providern.
Enhancer und Fehler
- Guards, Interceptors, Pipes und Filter funktionieren wie bei Routen, mit
context.getType() === 'mcp'.McpExecutionContext.create(context)bietetgetInput(),getContext(),getKind(),getName(),getRequest()undgetAuthInfo(). - Globale Enhancer werden bei MCP-Aufrufen und der dazugehörigen HTTP-Anfrage ausgeführt. Nur-HTTP-Enhancer müssen vor dem Aufruf von
switchToHttp()context.getType() === 'http'prüfen. - Ein globaler
ValidationPipe({ transform: true })validiert DTO-typisierte Eingaben. Benutzerdefinierte Parameter-Decoratoren folgen auf(input, ctx, @CurrentUser() user). - Von Tools ausgelöste Fehler werden zu
isError: true-Ergebnissen, von denen sich das Modell erholen kann. EinHttpExceptionbehält seine Nachricht; jeder andere Fehler wird zuInternal server errorund wird protokolliert. - Ressourcen und Prompts antworten mit JSON-RPC-Fehlern:
NotFoundException→ Ressource nicht gefunden,BadRequestException→ ungültige Parameter. - Filter: Prüfen Sie
host.getType<McpContextType>() === 'mcp'. Was auch immer der Filter zurückgibt, wird zum Ergebnis; um einen JSON-RPC-Fehler zu senden, werfen Sie einenProtocolError.
Autorisierung (netzwerkerreichbare Server)
McpModule.forRootAsync({
imports: [ConfigModule, AuthModule],
inject: [ConfigService, IdentityProviderTokenVerifier],
useFactory: (config: ConfigService, verifier: IdentityProviderTokenVerifier) => ({
name: 'cat-store', version: config.get('VERSION'),
auth: {
verifier, // implementiert McpTokenVerifier.verifyAccessToken(token) → AuthInfo | null
resource: config.get('MCP_RESOURCE'), // kanonische öffentliche URL = Token-Audience
authorizationServers: [config.get('IDENTITY_PROVIDER_URL')],
scopesSupported: ['orders:read', 'orders:write'],
},
}),
}),
- Der Server ist ein OAuth 2.1 Ressourcenserver: Er verifiziert Tokens und gibt sie niemals aus.
- Er stellt RFC-9728-Metadaten unter
/.well-known/oauth-protected-resource/mcpbereit. Anfragen ohne Token erhalten einen 401-Status mit einerWWW-Authenticate: Bearer-Aufforderung, die das Anmeldeverfahren des Clients startet. - Verifier:
- JWTs: Verwenden Sie
JwtVerifiervon@nestjs/authentication(prüft Signatur, Aussteller, Audience und Ablauf). - Opaque-Tokens: Rufen Sie den RFC-7662-Introspection-Endpunkt auf.
- Das zurückgegebene
AuthInfomussresource === auth.resourceund ein zukünftigesexpiresAtenthalten, andernfalls wird das Token mit 401 abgelehnt. - Werfen Sie einen 401-Fehler, um ein Token abzulehnen; jeder andere Fehler wird zu einem 500er.
- JWTs: Verwenden Sie
- Fehlende
scopesbei einem Primitiv → 403 mit Step-Up-Challenge.auth.requiredScopesgilt für jede Anfrage. - Mit
@nestjs/authentication≥ 0.1.3 und@nestjs/authorization≥ 0.1.1:- Deren globale Guards authentifizieren jeden Aufruf, und
@CurrentUser()und@Can(Policy, 'action')funktionieren in Tools. - Setzen Sie
decorators: [Public()]auf den Endpunkt, damitauthdie Challenge bearbeitet. - Ein Tool ohne
@Can()wird verweigert.
- Deren globale Guards authentifizieren jeden Aufruf, und
- Nur-interne Server können OAuth überspringen und
decorators: [UseGuards(ApiKeyGuard)]auf den Endpunkt setzen.
Transporte
- Streamable HTTP wird standardmäßig bereitgestellt;
path: falsedeaktiviert es. Der Endpunkt ist ein normaler Controller, daher gelten globales Präfix, Versionierung (endpointVersion), Middleware und CORS. Er ist zustandslos: Es werden keine Sticky Sessions benötigt. Beide Protokoll-Epochen werden bedient (http.legacy: 'reject'bedient nur 2026-07-28).http.responseModekann'auto','json'oder'sse'sein. - stdio, für Assistenten, die einen lokalen Prozess starten: Erstellen Sie
stdio.tsmitNestFactory.createApplicationContext(AppModule, { logger: new McpStdioLogger() }), dannawait app.get(McpStdioServer).listen(); await app.close();. Alles andere, was auf stdout geschrieben wird, unterbricht das Protokoll. Anmeldeinformationen stammen ausprocess.env; Scopes werden nicht geprüft, aber Guards werden ausgeführt.
Siehe references/mcp-reference.md für die vollständige Optionstabelle, Produktionseinstellungen, die stdio-Client-Konfiguration, Testmuster und die Migrationskarte.
Arbeitsablauf
- 1. Installieren; auf Zod 4.2+ prüfen;
McpModule.forRootmitname,version,instructions. - 2. Tools, Ressourcen und Prompts auf Providern hinzufügen, mit modellorientierten Beschreibungen,
.describe()bei jedem Argument,readOnlyHintbei schreibgeschützten Tools undoutputSchema, wenn das Ergebnis strukturiert ist. - 3. Sicherstellen, dass nur-HTTP-globale Enhancer
getType()prüfen; MCP-Guards und -Filter nach Bedarf hinzufügen. - 4. Netzwerkerreichbare Server:
auth(Verifier, Ressource, Autorisierungsserver, Scopes),allowedHosts/allowedOrigins, CORS-Header und das Body-Limit hinzufügen. Lokale Server:stdio.tsmitMcpStdioLoggerhinzufügen. - 5. Tests: Methoden direkt unit-testen; e2e mit
connectMcpClient(moduleRef, { token })gegen einen Test-Verifier ausführen; die HTTP-Route über einen Socket testen. Bauen, linten, Tests ausführen, die App starten und Tools mit dem Inspector auflisten.
Referenzdateien
references/mcp-reference.md: Moduloptionen, Decorator-Optionen, HTTP-Härtung (Origin, Host, CORS-Header, Body-Größe), die stdio-Client-Konfiguration, NestJS Observe-Namensgebung, die Produktions-Checkliste, Tests (Tokens und Scopes, Protokoll-Epochen, McpRegistry-Prüfungen, HTTP- und stdio-Tests) und Migration von@rekog/mcp-nest1.x/2.x. Lesen Sie diese Datei beim Konfigurieren der Produktion, Schreiben von Tests oder bei der Migration.
Quellen: Übersicht · Primitive · Transporte · Testen · Migration