NestJS MCP 伺服器
npm i @nestjs/mcp @modelcontextprotocol/server zod
npm i -D @modelcontextprotocol/client # 記憶體內測試客戶端
Zod 4.2+ 與 ArkType 可直接使用;Valibot 需透過 toStandardJsonSchema();既有 JSON Schema 則使用 fromJsonSchema()。啟動時會拒絕 Zod 3 結構描述。
McpModule.forRoot({
name: 'cat-store', version: '1.0.0',
instructions: '在變更訂單前,請先使用 find-order 查詢訂單。', // 告訴模型如何使用此伺服器
}),
app.listen(3000) 後就會在 /mcp 路徑提供端點。可用 npx @modelcontextprotocol/inspector 檢查。啟動時模組會掃描所有提供者(所有模組,或只掃描 include 指定的模組)中的裝飾方法。如果遇到重複名稱、無效結構描述,或在未啟用 auth 的情況下使用 scopes,會快速失敗。
基本元件:提供者上的方法
| 基本元件 | 裝飾器 | 使用者 | 呼叫方式 |
|---|---|---|---|
| Tool(動作) | @Tool({ name, description, inputSchema, outputSchema?, annotations?, scopes? }) | 模型 | 驗證後的輸入、ctx |
| Resource(資料) | @Resource({ uri, mimeType }) / @ResourceTemplate({ uriTemplate: 'orders://{orderId}' }) | 客戶端(通常由使用者附加) | {} 或模板變數、ctx(ctx.uri) |
| Prompt(模板) | @Prompt({ name, description, argsSchema }) | 使用者選取 | 驗證後的引數、ctx |
@Injectable() // 一般提供者,在模組的 providers 中列出
export class OrdersTools {
constructor(private readonly orders: OrdersService) {}
@Tool({
name: 'find-order',
description: '根據訂單 ID 查詢訂單,或根據客戶 Email 查詢最新訂單',
inputSchema: z.object({
orderId: z.string().optional().describe('訂單 ID,例如 ord_123'),
email: z.email().optional().describe('客戶的 Email 位址'),
}),
annotations: { readOnlyHint: true }, // 客戶端可能無需詢問使用者即可執行
})
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('找不到此訂單'); // → 回傳 isError: true 的工具結果
return found;
}
@Tool({
name: 'change-address',
description: '變更尚未出貨的訂單的送貨地址',
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 文字
}
}
回傳值:工具回傳的字串會變成文字區塊。物件在存在 outputSchema 時會變成 structuredContent,否則變成 JSON 文字。如果要回傳圖片、音訊或內嵌資源,請自行回傳 CallToolResult;undefined 會回傳空結果。資源的字串會以文字傳送,Buffer/Uint8Array 以二進位 blob 傳送,物件以 JSON 傳送。提示的字串會變成一個使用者訊息;陣列會回傳 PromptMessage[]。工具名稱長度為 1–128 個字元,只能包含 A-Z a-z 0-9 _ - .,預設為方法名稱。
請為模型撰寫名稱、描述與 .describe() 文字。 模型只知道這些資訊:說明工具的功能、使用時機、是否會變更資料,並為每個引數提供範例值。使用 Resource 處理直接載入的內容;使用 Tool 處理需要引數的查詢。
呼叫上下文
ctx: McpContext 包含以下屬性:
kind、name、uriauthInfo(clientId、scopes、expiresAt、extra)request(平台請求;在 stdio 模式下為undefined)requestInfotransport('http' | 'stdio')era('modern'2026-07-28 /'legacy'2025)signal:傳遞給fetch和資料庫呼叫;在迴圈中呼叫throwIfAborted()reportProgress(progress, total?, message?)server(SDK 上下文)
若要詢問使用者(確認、缺少的值),請回傳 inputRequired({ inputRequests: { confirm: inputRequired.elicit({ message, requestedSchema }) } })。在重試時,使用 acceptedContent(ctx.server.mcpReq.inputResponses, 'confirm', Schema) 讀取答案。透過 HTTP 時,這個功能僅適用於 2026-07-28 客戶端,因為端點是無狀態的。透過 stdio 則兩種時期都適用。
提供者維持單例模式。請從 ctx 讀取每次呼叫的資料,而不是從請求範圍的提供者讀取。
增強器與錯誤
- 守衛、攔截器、管道與過濾器的執行方式與路由相同,使用
context.getType() === 'mcp'。McpExecutionContext.create(context)提供getInput()、getContext()、getKind()、getName()、getRequest()和getAuthInfo()。 - 全域增強器會對 MCP 呼叫以及攜帶的 HTTP 請求兩者都執行。 僅限 HTTP 的增強器必須在呼叫
switchToHttp()之前檢查context.getType() === 'http'。 - 全域的
ValidationPipe({ transform: true })會驗證 DTO 型別的輸入。自訂參數裝飾器應放在(input, ctx, @CurrentUser() user)之後。 - 工具拋出的錯誤會變成
isError: true的結果,模型可從中恢復。HttpException會保留其訊息;其他任何錯誤都會變成Internal server error並記錄下來。 - Resource 與 Prompt 會以 JSON-RPC 錯誤回應:
NotFoundException→ 資源找不到,BadRequestException→ 無效參數。 - 過濾器:請檢查
host.getType<McpContextType>() === 'mcp'。過濾器回傳的內容即成為結果;若要發送 JSON-RPC 錯誤,請拋出ProtocolError。
授權(可網路連線的伺服器)
McpModule.forRootAsync({
imports: [ConfigModule, AuthModule],
inject: [ConfigService, IdentityProviderTokenVerifier],
useFactory: (config: ConfigService, verifier: IdentityProviderTokenVerifier) => ({
name: 'cat-store', version: config.get('VERSION'),
auth: {
verifier, // 實作 McpTokenVerifier.verifyAccessToken(token) → AuthInfo | null
resource: config.get('MCP_RESOURCE'), // 規範公開 URL = 權杖的目標對象
authorizationServers: [config.get('IDENTITY_PROVIDER_URL')],
scopesSupported: ['orders:read', 'orders:write'],
},
}),
}),
- 此伺服器是一個 OAuth 2.1 資源伺服器:它驗證權杖,但從不核發權杖。
- 它在
/.well-known/oauth-protected-resource/mcp提供 RFC 9728 中繼資料。沒有權杖的請求會收到 401 狀態碼與WWW-Authenticate: Bearer挑戰,從而啟動客戶端的登入流程。 - 驗證器:
- JWT:使用
@nestjs/authentication的JwtVerifier(檢查簽章、發行者、目標對象與到期時間)。 - 不透明權杖:呼叫 RFC 7662 內省端點。
- 回傳的
AuthInfo必須包含resource === auth.resource以及未來的expiresAt,否則權杖會被拒絕並回傳 401。 - 拋出 401 錯誤以拒絕權杖;其他任何錯誤都會變成 500。
- JWT:使用
- 基本元件缺少
scopes→ 回傳 403 並要求升級權限。auth.requiredScopes會套用至每個請求。 - 搭配
@nestjs/authentication≥ 0.1.3 與@nestjs/authorization≥ 0.1.1 使用時:- 它們的全域守衛會驗證每個呼叫,並且
@CurrentUser()與@Can(Policy, 'action')可在工具中運作。 - 在端點上設定
decorators: [Public()],讓auth處理挑戰。 - 沒有
@Can()的工具會被拒絕。
- 它們的全域守衛會驗證每個呼叫,並且
- 僅供內部使用的伺服器可以跳過 OAuth,改為在端點上使用
decorators: [UseGuards(ApiKeyGuard)]。
傳輸方式
- Streamable HTTP 為預設方式;設定
path: false可停用它。端點是一個一般控制器,因此全域前置詞、版本化(endpointVersion)、中介軟體與 CORS 都適用。它是無狀態的:不需要黏性工作階段。兩種協議時期都會提供服務(http.legacy: 'reject'只提供 2026-07-28 版本)。http.responseMode可以是'auto'、'json'或'sse'。 - stdio,適用於啟動本機程序的輔助工具:建立
stdio.ts,使用NestFactory.createApplicationContext(AppModule, { logger: new McpStdioLogger() }),然後執行await app.get(McpStdioServer).listen(); await app.close();。任何寫入 stdout 的其他內容都會破壞協議。憑證來自process.env;不會檢查範圍,但守衛仍會執行。
完整選項表、生產設定、stdio 客戶端設定、測試模式與遷移對照表請參閱 references/mcp-reference.md。
工作流程
- 1. 安裝;檢查 Zod 4.2+;使用
name、version、instructions設定McpModule.forRoot。 - 2. 在提供者上加入工具、資源與提示,使用模型導向的描述,對每個引數加上
.describe(),對唯讀工具加上readOnlyHint,並在結果有結構時加上outputSchema。 - 3. 確保僅限 HTTP 的全域增強器檢查
getType();視需要加入 MCP 守衛與過濾器。 - 4. 可網路連線的伺服器:加入
auth(驗證器、資源、授權伺服器、範圍)、allowedHosts/allowedOrigins、CORS 標頭與主體大小限制。本機伺服器:使用McpStdioLogger加入stdio.ts。 - 5. 測試:直接對方法進行單元測試;使用
connectMcpClient(moduleRef, { token })搭配測試驗證器進行端到端測試;透過 Socket 測試 HTTP 路由。建置、檢查、執行測試、啟動應用程式並使用 Inspector 列出工具。
參考檔案
references/mcp-reference.md:模組選項、裝飾器選項、HTTP 強化(Origin、Host、CORS 標頭、主體大小)、stdio 客戶端設定、NestJS Observe 命名、生產檢查清單、測試(權杖與範圍、協議時期、McpRegistry 檢查、HTTP 與 stdio 測試),以及從@rekog/mcp-nest1.x/2.x 遷移。在設定生產環境、撰寫測試或進行遷移時閱讀。