将您的数据库转换为带身份验证的 MCP 服务器
MCP在人工智能领域很热门,但是……
MCP授权的出现
MCP 是 SaaS 的一项良好增强功能
构建 MCP 服务器的两种方法
从零开始构建 MCP 服务器
测试
MCP在人工智能领域很热门,但是……
MCP(模型上下文协议)无疑是目前人工智能领域最令人兴奋的趋势之一。不仅人人都在谈论它,而且每个人都在争相开发自己的版本,以期在人工智能时代占据一席之地。不信?猜猜看MCP服务器目录中列出了多少个MCP服务器?
现在这个数字是 4684,别忘了,MCP 才六个月大。不过,你有没有注意到左边那个独一无二的滤镜?
猜猜应用此筛选条件后还剩下多少?只有 59 个——约占总数的 1%。如果您不熟悉此筛选条件,我将提供简要说明;如果您已经了解,则可以跳过此部分。
MCP 最初主要被设计为一种本地执行协议,允许 AI 模型与运行在同一台机器上的本地工具和数据源进行交互,例如官方文档中列出的 MCP 文件系统和 Fetch 示例。这意味着无论底层传输方式是标准 I/O 还是 HTTP SSE,都必须在本地机器上安装并运行 MCP 服务器。这种设计选择在早期是合理的,因为它简化了安全问题并降低了延迟。该协议的简洁性使其非常适合开发人员在个人机器上进行 AI 工具集成实验。然而,随着 MCP 的普及和 AI 生态系统的发展,对安全远程执行的需求变得越来越明显:
借助远程 MCP 服务器,企业可以通过互联网将数据和服务暴露给 AI 模型,因此最重要的环节是身份验证(认证和授权)。最初,解决方案是要求用户在产品内生成 API 密钥,然后在 MCP 设置中进行配置。以下是 Neon MCP 服务器的一个示例:
{
"mcpServers": {
"neon": {
"command": "npx",
"args": [
"-y",
"@neondatabase/mcp-server-neon",
"start",
"<YOUR_NEON_API_KEY>"
]
}
}
}
开发者们对这个过程都很熟悉。但由于越来越多的SaaS产品出于对“SaaS已死”的恐惧(如果你不明白,可以搜索一下“SaaS已死”😁),开始提供MCP服务器,这对非开发者用户来说绝对不是什么好的体验。此外,用户还需要不时手动更新NPM包才能保持最新状态。
肯特在推特上也表达了同样的担忧:
目前我看到的所有示例都是用户生成令牌,然后将该令牌添加到 MCP 配置中。这就是我们目前能做到的最好方案吗?我希望能找到一个支持 OAuth 流程并连接到托管 MCP 服务器的 MCP 客户端示例。
MCP授权的出现
MCP 虽然还很年轻,但发展非常迅速。OAuth 支持是在 2025 年 3 月推出的,当时它大约只有 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 服务器了。相信我,这种情况确实会发生。😂
MCP 是 SaaS 的一项良好增强功能
凭借 OAuth 支持的无缝体验,许多公司都渴望提供 MCP 服务器来增强其现有产品或服务,尤其是在 SaaS 领域。平衡灵活性和简洁性始终是 SaaS 的核心挑战——按钮过多会让用户感到不知所措,按钮过少则会限制高级用户的操作。例如,我一直很喜欢 Trello 简洁高效的 UX/UI 设计:
然而,每次遇到某些例行工作时,它都会让我感到不舒服。
- 上周完成了多少张卡片?
- 谁的牌组里不完整的牌最多?
- 哪个列表包含的卡片不完整最多?
Trello 没有提供直接显示答案的方法;你必须手动计算。而 MCP-LLM 组合或许能提供一个绝佳的解决方案,让用户能够使用自然语言完成工作。我认为这正是微软 CEO 萨蒂亚·纳德拉在他那次被认为是“SaaS 已死”的采访中试图传达的真正观点。
构建 MCP 服务器的两种方法
1. 依赖现有 API
这似乎是显而易见的选择。然而,由于这些 API 的设计时间很可能远早于 LLM 的诞生,它们可能并不适合 LLM 使用。一方面,参数和返回数据的语义可能不够清晰,LLM 难以理解;另一方面,这些 API 的功能可能不够灵活,无法满足用户的需求。
2. 从零开始
对于考虑采用这种方法的人来说,最大的顾虑可能就是时间。别担心,我们从来不会真的从零开始构建,对吧?MCP 正在快速发展,整个生态系统也是如此。让我向您展示一个现代化的技术栈,它可以显著减少您需要编写的代码量,从而加快开发速度。
-
它完全实现了MCP规范,包括授权机制。它提供了强大的抽象层,使您无需费力处理底层协议实现,从而可以专注于业务逻辑。
-
ZenStack 是一个基于 Prisma ORM 的、以模式为先的 TypeScript 工具包。其核心是一个 ZModel DSL,它统一了数据建模和访问控制。以下是一个简单的博客文章示例:
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) }基于 ZModel 模式,ZenStack 会自动为您的数据库创建结构良好、类型安全且经过授权的 CRUD API。MCP 的核心价值在于其工具,这些工具主要由模式定义和可执行操作组成——具体来说,就是大多数 SaaS 应用的数据库 CRUD 操作。ZenStack 可以直接从其模式生成这些 API,并且由于访问控制策略的存在,LLM 可以安全地调用这些 API。因此,您的主要任务是定义模式,而 ZenStack 会处理其他一切。
从零开始构建 MCP 服务器
接下来,我将带您逐步了解如何从零开始将您的数据库转换为基于 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 规范,服务器必须实现以下三个端点:
| 端点 | 网址 | 用法 |
|---|---|---|
| 授权 | /授权 | 用于授权请求 |
| 令牌 | /token | 用于代币兑换和刷新 |
| 登记 | /登记 | 用于动态客户端注册 |
MCP SDK 提供了一个实用函数,mcpAuthRouter用于安装这些标准授权服务器端点。我们需要做的是实现一个函数,OAuthServerProvider以便将其传递给该函数mcpAuthRouter。
让我们创建一个AuthMiddleware类来处理所有身份验证逻辑,并PasswordAuthProvider实现该类OAuthServerProvider来执行实际逻辑。
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
实际的授权流程始于 `<authorization_server>` authorize。它的作用是将所有必要的参数重定向到实际的授权服务器。由于在本例中,授权服务器就是它本身,因此我们重定向到以下/auth/login路径:
const loginUrl = new URL('/auth/login', config.baseUrl);
...
res.redirect(loginUrl.toString());
因此,我们需要创建一个login.html服务来提供这项功能。这是一个典型的登录表单,如下所示:
点击“登录”后,它会将从授权流程中传递的所有 URL 参数以及电子邮件和密码组合起来,发布到服务器端点/auth/login。
服务器端点/auth/login最终会调用该函数handlelogin。PasswordAuthProvider它会验证用户身份。如果身份正确,则生成一个授权码,将其返回给客户端,并将其存储在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)。如果一切正确,服务器会生成访问令牌和刷新令牌,并将它们存储在数据库中,然后返回给 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(' '),
};
有状态多连接流式HTTP服务器
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 来完成这项工作。请记住该工具的两个组成部分:模式和执行。
模式
ZenStack 有一个原生的 Zod 插件,用于为其 CRUD API 生成 Zod schema。我们可以通过将以下代码添加到配置中来启用它schema.zmodel:
plugin zod {
provider = '@core/zod'
}
然后,运行后zenstack generate,它会为模型中的每个模型生成其支持的所有操作的 Zod schema @zenstackhq/runtime/zod/input。例如,以下是模型的 Zod schema Post:
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 生成的参数动态调用该函数即可。因此,整个工具创建逻辑实际上就是一个 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其中包含当前用户身份信息,该信息userId在MCPServer初始化期间存储。无论 LLM 为该函数生成什么参数,即使出现幻觉,这也能完全消除未经授权的数据访问:
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:
- 首先,其声明式和结构化的特性使其易于理解和推理,从而使语言学习模型能够生成准确、可预测且歧义最小的查询。
- 其次,API 的灵活性允许通过一次调用访问多个模型(数据库表),使 LLM 能够高效地处理复杂的数据关系,并在不同的实体之间执行复杂的查询,而无需单独的 API 调用或复杂的编排逻辑。
- 第三,Prisma 多年来被广泛采用,提供了丰富的文档、示例和社区使用经验,LLM 可以从中学习,大大提高了生成正确且具有上下文感知能力的代码的可能性。
测试
示例项目包含可供您尝试的种子数据。只需运行以下命令即可使其准备就绪:
npx prisma db push
npx prisma db seed
它会创建三个用户并发布帖子。所有用户的密码均为 password123:
尽情使用你选择的任何MCP客户端进行游戏吧:
我非常期待了解您的应用程序取得的成果!如果您在编写 ZModel schema 时需要任何帮助,请随时联系我或加入我们的 Discord 服务器。我非常乐意提供帮助!
文章来源:https://dev.to/zenstack/turning-your-database-into-an-mcp-server-with-auth-32mp





