MCP(模型上下文協議)無疑是當今人工智慧領域最令人興奮的趨勢之一。不僅每個人都在談論它,而且每個人都在爭相開發自己的版本,以便在人工智慧時代佔有一席之地。你不這麼認為嗎?猜猜看[MCP 伺服器目錄](https://www.pulsemcp.com/servers) 中列出了多少個 MCP 伺服器?
)?
現在的數字是4684,別忘了,MCP還只是個六個月大的嬰兒。不過,你有沒有註意到左邊那個唯一的濾鏡:
猜猜用了這個濾鏡之後還剩下多少?只有 59 個,約佔總數的 1%。對於不熟悉這個濾鏡的朋友,我會做一個簡短的解釋,如果你已經理解了,可以跳過。
MCP 最初主要被設想為一個本地執行協議,AI 模型可以與在同一台機器上執行的本地工具和資料來源進行交互,例如官方文件中列出的範例 MCP FileSystem 和 Fetch。這意味著您必須在本機上安裝並執行 MCP 伺服器,無論底層傳輸是 stdio 還是 HTTP SSE。這種設計選擇在早期是合理的,因為它簡化了安全問題並降低了延遲。該協議的簡潔性使其非常適合開發人員在個人機器上嘗試 AI 工具整合。然而,隨著 MCP 的普及和 AI 生態系統的發展,對安全遠端執行的需求日益凸顯:
{% 嵌入 https://x.com/kentcdodds/status/1907218594624372868
%}
透過遠端 MCP 伺服器,組織可以透過網路將其資料和服務公開給 AI 模型,因此最重要的是 Auth(身份驗證和授權)。最初,這個問題的解決方法是要求使用者在產品中產生 API 金鑰,然後在 MCP 設定中進行設定。以下是 Neon MCP 伺服器的一個範例:
{
"mcpServers": {
"neon": {
"command": "npx",
"args": [
"-y",
"@neondatabase/mcp-server-neon",
"start",
"<YOUR_NEON_API_KEY>"
]
}
}
}
開發人員都熟悉這個過程。但是,由於擔心“SaaS 已死”,越來越多的 SaaS 產品開始提供 MCP 伺服器(如果你不明白,可以搜尋“SaaS 已死”😁),這對於非開發人員來說絕對不是一個好的用戶體驗。此外,用戶需要不時手動更新 NPM 套件才能保持最新狀態。
肯特在他的推文中表達了同樣的擔憂:
目前我看到的所有範例都要求使用者產生一個令牌,然後將該令牌放入 MCP 配置中。這是我們目前最好的方法嗎?我希望找到一個支援 OAuth 流程的 MCP 用戶端範例,該流程需要使用託管的 MCP 伺服器。
>
MCP 雖然還很年輕,但發展非常迅速。 OAuth 支持於 2025 年 3 月推出,當時 MCP 才剛誕生 3 個月左右。以下是官方規範中的授權流程步驟:
該規範為客戶端和伺服器提供了 SDK 支持,使更多 MCP 伺服器能夠採用並利用它。甚至一些依賴 API 金鑰的現有伺服器也開始採用這種新的現代化解決方案。例如,上面提到的 Neon MCP 伺服器也增加了對它的支援。
{
"mcpServers": {
"Neon": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://mcp.neon.tech/sse"]
}
}
}
💡
mcp-remote
適用於不支援 OAuth 的客戶端。對於像 Cursor 這樣支援 OAuth 的客戶端來說,沒有必要使用它。
新增上述 MCP 配置後,MCP 用戶端將自動開啟瀏覽器進行標準 OAuth 流程,不再需要 API 金鑰:
有了這個現代化的 MCP 伺服器,你就不用擔心產品經理或 CEO 突然跑到你辦公桌前,請求你幫忙連接公司某個 SaaS 產品的 MCP 伺服器了。相信我,這種情況真的會發生。 😂
由於 OAuth 支援帶來的無縫體驗,許多公司渴望提供 MCP 伺服器來增強其現有產品或服務,尤其是在 SaaS 領域。平衡靈活性和簡單性始終是 SaaS 的核心挑戰——按鈕太多會讓用戶不知所措,按鈕太少又會限制進階用戶。例如,我一直很喜歡 Trello 簡潔又有效率的 UX/UI 設計:
然而,每當我做一些例行工作時,它都會讓我感到刺痛。
上週完成了多少張卡片?
誰的卡片不完整最多?
哪個清單中的卡片不完整最多?
Trello 無法直接顯示答案;你必須手動計算。而 MCP-LLM 組合正好能提供一個很好的解決方案,讓使用者可以使用自然語言來完成工作。我認為這才是微軟 CEO 薩蒂亞·納德拉在所謂的「SaaS 已死」採訪中試圖表達的真正觀點。
這似乎是顯而易見的選擇。然而,由於這些 API 很可能是在 LLM 誕生之前很久就設計的,它們可能不適合 LLM 使用。一方面,參數和傳回資料的語意可能不夠清晰,LLM 難以理解;另一方面,這些 API 可能不夠靈活,無法提供使用者想要實現的功能。
對於考慮這種方法的人來說,最大的擔憂可能是時間。別擔心,我們從來不會真正從零開始建置,不是嗎? MCP 發展迅速,整個生態系統也是如此。讓我向你展示一個現代堆疊,它可以顯著減少你需要寫的程式碼,從而加快速度。
It fully implements the MCP specification, including Authorization. It provides a strong abstraction, freeing you from wrestling with low-level protocol implementation so you can focus on the business logic.
ZenStack is a schema-first TypeScript toolkit on top of Prisma ORM. The core part is a ZModel DSL that unifies data modeling and access control. Here is an example of what a simple blog post looks like:
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
password String @password @omit
posts Post[]
@@allow('read', true)
}
model Post {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
title String
content String?
published Boolean @default(false)
viewCount Int @default(0)
author User? @relation(fields: [authorId], references: [id])
authorId Int? @default(auth().id)
@@allow('all', auth() == author)
// logged-in users can view published posts
@@allow('read', auth() != null && published)
}
Based on the ZModel schema, ZenStack automatically creates well-structured, type-safe, and authorized CRUD APIs for your database. MCP's core value lies in its tools, which fundamentally consist of schema definitions and executable operations—specifically, the CRUD operations on your database for most SaaS applications. ZenStack can generate both directly from its schema, which can be safely called by LLM thanks to the access control policy. Thus, your primary task is to define the schema, and ZenStack manages everything else.
那麼,讓我從頭開始,帶你了解如何將資料庫轉變為基於 OAuth 身份驗證和授權的 MCP 伺服器。我將以上面提到的部落格文章為例;你可以將其調整到適合你的應用,只需相應地更改 ZModel 架構即可。
您可以在下面找到已完成的專案:
https://github.com/jiashengguo/zenstack-mcp-auth
使用以下腳本透過 Prisma 和 Express 初始化專案:
npx try-prisma@latest -t orm/express -n blog-app-mcp
安裝MCP SDK、bcrypt(用於雜湊密碼):
npm install @modelcontextprotocol/sdk, bcrypt, @types/bcrypt
初始化 ZenStack:
npx zenstack@latest init
為了支援OAuth,伺服器需要儲存以下資訊:
OAuth 用戶端
授權碼
存取令牌
刷新令牌
因此,讓我們將它們新增到 ZModel 模式檔案中,以便我們可以使用產生的類型安全 API 對它們進行操作:
model OAuthClient {
id Int @id @default(autoincrement())
client_id String @unique
client_secret String?
...
}
model AuthorizationCode {
id Int @id @default(autoincrement())
code String @unique
...
}
model AccessToken {
id Int @id @default(autoincrement())
token String @unique
...
}
model RefreshToken {
id Int @id @default(autoincrement())
token String @unique
...
}
根據 MCP 規範,伺服器必須實作以下三個端點:
| 端點 | 網址 | 用法 |
| --- | --- | --- |
| 授權 | /authorize | 用於授權請求 |
| Token | /token | 用於令牌交換和刷新 |
| 註冊 | /register | 用於動態用戶端註冊 |
MCP SDK 提供了一個實用函數mcpAuthRouter
來安裝這些標準的授權伺服器端點。我們需要實作一個OAuthServerProvider
介面,並將其傳遞給mcpAuthRouter
。
讓我們建立一個AuthMiddleware
類別來處理所有的 Auth 邏輯,並建立一個實作OAuthServerProvider
PasswordAuthProvider
來執行實際邏輯。
export class AuthMiddleware {
private authProvider: PasswordAuthProvider;
private authRouter: express.Router = express.Router();
...
private setupRouter() {
// Add OAuth router using the mcpAuthRouter function
this.authRouter.use(
'/',
mcpAuthRouter({
provider: this.authProvider,
issuerUrl: new URL(config.baseUrl),
baseUrl: new URL(config.baseUrl),
scopesSupported: ['read', 'write'],
})
);
...
}
}
以下是實際會被呼叫的三個函數,分別對應上述的三個必要的端點:
export interface OAuthServerProvider {
// /register
get clientsStore(): OAuthRegisteredClientsStore;
// /authorize
authorize(client: OAuthClientInformationFull, params: AuthorizationParams, res: Response): Promise<void>;
// /token
exchangeAuthorizationCode(client: OAuthClientInformationFull, authorizationCode: string, codeVerifier?: string, redirectUri?: string): Promise<OAuthTokens>;
}
clientStore
透過registerClient
和getClient
函數處理註冊和讀取,它只是從資料庫寫入和讀取OAuthClient
模型。
實際的授權流程始於authorize
。它的作用是將所有必要的參數重新導向到實際的授權伺服器。由於在本例中,授權伺服器就是它自己,因此我們將其重新導向到/auth/login
路徑:
const loginUrl = new URL('/auth/login', config.baseUrl);
...
res.redirect(loginUrl.toString());
因此,我們需要建立一個login.html
來提供服務。這是一個典型的登入表單,如下所示:
點擊登入後,它將從授權流程傳遞的所有 URL 參數與電子郵件和密碼結合起來,發佈到伺服器端點/auth/login
。
伺服器端點/auth/login
最後會呼叫PasswordAuthProvider
的handlelogin
函數。它會驗證使用者身份。如果正確,則建立授權碼,傳回給用戶端,並將其儲存在資料庫中的AuthorizationCode
實例中。
// Generate authorization code
const authCode = crypto.randomBytes(32).toString('hex');
// Store authorization code in database
await this.prisma.authorizationCode.create({
data: {
code: authCode,
clientId,
userId: user.id,
codeChallenge,
redirectUri,
expiresAt: new Date(Date.now() + 600000), // 10 minutes
scopes,
},
});
在取得伺服器傳回的授權碼後, login.html
會重新導向至 MCP 用戶端並攜帶授權碼。最終,MCP 用戶端會使用授權碼呼叫/token
介面來取得存取令牌,並呼叫exchangeAuthorizationCode
函數。
在exchangeAuthorizationCode
中,伺服器會先驗證剛剛授予的授權碼是否有效,以及客戶端的身份資訊(例如 PKCE)。如果一切正確,伺服器會產生存取權杖 (Access token) 和刷新令牌 (Refresh token),並將它們存入資料庫,然後傳回給 MCP 用戶端。
// Store tokens in database
await this.prisma.accessToken.create({
data: {
token: accessToken,
clientId: client.client_id,
userId: authData.userId,
scopes: authData.scopes as any,
expiresAt,
},
});
await this.prisma.refreshToken.create({
data: {
token: refreshToken,
clientId: client.client_id,
userId: authData.userId,
scopes: authData.scopes as any,
expiresAt: new Date(Date.now() + 86400 * 30 * 1000), // 30 days
},
});
// Clean up authorization code
await this.prisma.authorizationCode.delete({
where: { code: authorizationCode },
});
return {
access_token: accessToken,
token_type: 'Bearer',
expires_in: expiresIn,
refresh_token: refreshToken,
scope: (authData.scopes as string[]).join(' '),
};
MCP 用戶端取得存取權杖後,每次透過主端點與伺服器通訊時,都會將存取權杖放入 Authorization 頭中。伺服器應該使用它來驗證客戶端並獲取客戶端的身份。我們選擇常規的/mcp
端點作為主端點,並getFlexibleAuthMiddleware
來實現這一點。
MCP 用戶端向伺服器發送的第一個請求是initialize
請求。伺服器應回應伺服器資訊和產生的會話 ID,客戶端應始終包含該 ID 以維護會話。作為生產就緒的 MCP 伺服器,它肯定應該支援多個同時連接。因此,我們使用一個映射來按會話 ID 儲存每個客戶端。
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
為了管理每個使用者的會話,我們將為其建立一個專用的MCPServer
。由於它是每個用戶每個 MCP 伺服器的,我們可以在其中儲存userId
,以便在執行工具時用於授權。
if (sessionId && transports[sessionId]) {
transport = transports[sessionId];
}
else if (!sessionId && isInitializeRequest(req.body)) {
// Handle new MCP connection initialization
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => crypto.randomUUID(),
onsessioninitialized: (sessionId: string) => {
console.log(`New MCP session initialized: ${sessionId}, User ID: ${userId}`);
transports[sessionId] = transport!;
},
});
const mcpServer = createMCPServer(userId);
await mcpServer.connect(transport);
console.log(`MCP server connected for User ID: ${userId}`);
// Handle connection close
transport.onclose = () => {
if (transport?.sessionId) {
console.log(`MCP session closed: ${transport.sessionId}`);
delete transports[transport.sessionId];
}
};
}
else {
// invalid request
...
}
// Handle the request
await transport.handleRequest(req, res, req.body);
我們將完全依賴 ZenStack 來完成這項工作。記住該工具的兩個部分:架構 (Schema) 和執行 (Execution)。
ZenStack 有一個原生的 Zod 插件,可以為其 CRUD API 產生 Zod 模式。我們可以透過將以下內容新增至schema.zmodel
來啟用它:
plugin zod {
provider = '@core/zod'
}
然後,在執行zenstack generate
後,它會為@zenstackhq/runtime/zod/input
中的每個模型產生其支援的所有操作的 Zod 模式。例如,這是Post
的 Zod 模式:
import { z } from 'zod';
import type { Prisma } from '.zenstack/models';
declare type PostInputSchemaType = {
findUnique: z.ZodType<Prisma.PostFindUniqueArgs>;
findFirst: z.ZodType<Prisma.PostFindFirstArgs>;
findMany: z.ZodType<Prisma.PostFindManyArgs>;
create: z.ZodType<Prisma.PostCreateArgs>;
createMany: z.ZodType<Prisma.PostCreateManyArgs>;
delete: z.ZodType<Prisma.PostDeleteArgs>;
deleteMany: z.ZodType<Prisma.PostDeleteManyArgs>;
update: z.ZodType<Prisma.PostUpdateArgs>;
updateMany: z.ZodType<Prisma.PostUpdateManyArgs>;
upsert: z.ZodType<Prisma.PostUpsertArgs>;
aggregate: z.ZodType<Prisma.PostAggregateArgs>;
groupBy: z.ZodType<Prisma.PostGroupByArgs>;
count: z.ZodType<Prisma.PostCountArgs>;
};
因此,每個模型的每個操作都將成為我們 MCP 伺服器的工具。
每個函數的執行都非常簡單;只需使用 LLM 產生的參數動態呼叫函數即可。因此,整個 Tool 的建立邏輯只需一個 lambda 表達式,程式碼量不到 30 行:
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import crudInputSchema from '@zenstackhq/runtime/zod/input';
...
export function createMCPServer(userId: number) {
const server = new McpServer(
...
)
Object.entries(crudInputSchema)
.filter(([name]) => modelNames.includes(getModelName(name)))
.forEach(([name, functions]) => {
const modelName = getModelName(name);
Object.entries(functions as Record<string, any>)
.filter(([functionName]) => functionNames.includes(functionName))
.forEach(([functionName, schema]) => {
const toolName = `${modelName}_${functionName}`;
server.tool(
toolName,
`Prisma client API '${functionName}' function input argument for model '${modelName}'. ${currentUserPrompt}`,
{
args: schema,
},
async ({ args }) => {
console.log(`Calling tool: ${toolName} with args:`, JSON.stringify(args, null, 2));
const prisma = getPrisma(userId);
const data = await (prisma as any)[modelName][functionName](args);
console.log(`Tool ${toolName} returned:`, JSON.stringify(data, null, 2));
return {
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
};
}
);
});
});
return server;
}
需要指出的是,用於呼叫該操作的 Prisma 用戶端是 ZenStack enhanced
版,它包含當前用戶身份,即初始化時儲存在MCPServer
中的userId
。這將徹底杜絕未經授權的資料存取,無論 LLM 為該函數產生什麼參數,即使發生幻讀(hallucination):
import { enhance } from '@zenstackhq/runtime';
// Gets a Prisma client bound to the current user identity
export function getPrisma(userId: number | null) {
const user = userId ? { id: userId } : undefined;
return enhance(prisma, { user });
}
還有一個好處是,由於這些工具實際上是標準的 Prisma 用戶端 API,因此它非常適合 LLM:
首先,其聲明性和結構良好的特性使其易於理解和推理,從而使 LLM 能夠以最小的歧義生成準確且可預測的查詢
其次,API 的靈活性允許一次呼叫存取多個模型(資料庫表),使 LLM 能夠有效地導航複雜的資料關係並跨不同實體執行複雜的查詢,而無需單獨的 API 呼叫或複雜的編排邏輯。
第三,Prisma 多年來已被廣泛採用,提供了豐富的文件、範例和社群使用生態系統,LLM 可以從中學習,大大增加了產生正確且上下文感知程式碼的機會。
範例專案包含種子資料供您使用。只需執行以下命令即可準備就緒:
npx prisma db push
npx prisma db seed
它建立了三個有帖子的用戶。所有使用者的密碼均為password123
。
使用您選擇的任何 MCP 用戶端盡情暢玩:
我非常期待聽到你的應用程式所取得的成果!如果你正在尋找編寫 ZModel 架構的技巧,請隨時與我聯繫或加入我們的 Discord 。我隨時樂意提供協助!
原文出處:https://dev.to/zenstack/turning-your-database-into-an-mcp-server-with-auth-32mp