发布于 2026-01-06 5 阅读
0

将您的数据库转换为带身份验证的 MCP 服务器 MCP 在人工智能领域正处于发展趋势,但是…… MCP 授权的出现 MCP 是 SaaS 的一项良好增强功能 构建 MCP 服务器的两种方法 从头开始​​构建 MCP 服务器 测试

将您的数据库转换为带身份验证的 MCP 服务器

MCP在人工智能领域很热门,但是……

MCP授权的出现

MCP 是 SaaS 的一项良好增强功能

构建 MCP 服务器的两种方法

从零开始构建 MCP 服务器

测试

MCP在人工智能领域很热门,但是……

MCP(模型上下文协议)无疑是目前人工智能领域最令人兴奋的趋势之一。不仅人人都在谈论它,而且每个人都在争相开发自己的版本,以期在人工智能时代占据一席之地。不信?猜猜看MCP服务器目录中列出了多少个MCP服务器?

现在这个数字是 4684,别忘了,MCP 才六个月大。不过,你有没有注意到左边那个独一无二的滤镜?

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>"
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

开发者们对这个过程都很熟悉。但由于越来越多的SaaS产品出于对“SaaS已死”的恐惧(如果你不明白,可以搜索一下“SaaS已死”😁),开始提供MCP服务器,这对非开发者用户来说绝对不是什么好的体验。此外,用户还需要不时手动更新NPM包才能保持最新状态。

肯特在推特上也表达了同样的担忧:

目前我看到的所有示例都是用户生成令牌,然后将该令牌添加到 MCP 配置中。这就是我们目前能做到的最好方案吗?我希望能找到一个支持 OAuth 流程并连接到托管 MCP 服务器的 MCP 客户端示例。

MCP授权的出现

MCP 虽然还很年轻,但发展非常迅速。OAuth 支持是在 2025 年 3 月推出的,当时它大约只有 3 个月大。以下是官方规范中的授权流程步骤:

MCP-Auth-Flow

该规范为客户端和服务端都提供了 SDK 支持,使得更多 MCP 服务器能够采用并利用它。甚至一些依赖 API 密钥的现有服务器也开始采用这种新的现代化解决方案。例如,上文提到的 Neon MCP 服务器也增加了对该规范的支持。

{
  "mcpServers": {
    "Neon": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://mcp.neon.tech/sse"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

💡 这mcp-remote适用于不支持 OAuth 的客户端。对于像 Cursor 这样已经支持 OAuth 的客户端来说,这是不必要的。

添加上述 MCP 配置后,MCP 客户端将自动打开浏览器,完成标准的 OAuth 流程,不再需要 API 密钥:

Neon-OAuth

有了这款现代化的 MCP 服务器,您就不用担心产品经理或 CEO 跑到您办公桌前,求您帮忙连接公司使用的某个 SaaS 产品的 MCP 服务器了。相信我,这种情况确实会发生。😂

MCP 是 SaaS 的一项良好增强功能

凭借 OAuth 支持的无缝体验,许多公司都渴望提供 MCP 服务器来增强其现有产品或服务,尤其是在 SaaS 领域。平衡灵活性和简洁性始终是 SaaS 的核心挑战——按钮过多会让用户感到不知所措,按钮过少则会限制高级用户的操作。例如,我一直很喜欢 Trello 简洁高效的 UX/UI 设计:

Trello

然而,每次遇到某些例行工作时,它都会让我感到不舒服。

  • 上周完成了多少张卡片?
  • 谁的牌组里不完整的牌最多?
  • 哪个列表包含的卡片不完整最多?

Trello 没有提供直接显示答案的方法;你必须手动计算。而 MCP-LLM 组合或许能提供一个绝佳的解决方案,让用户能够使用自然语言完成工作。我认为这正是微软 CEO 萨蒂亚·纳德拉在他那次被认为是“SaaS 已死”的采访中试图传达的真正观点。

构建 MCP 服务器的两种方法

1. 依赖现有 API

这似乎是显而易见的选择。然而,由于这些 API 的设计时间很可能远早于 LLM 的诞生,它们可能并不适合 LLM 使用。一方面,参数和返回数据的语义可能不够清晰,LLM 难以理解;另一方面,这些 API 的功能可能不够灵活,无法满足用户的需求。

2. 从零开始

对于考虑采用这种方法的人来说,最大的顾虑可能就是时间。别担心,我们从来不会真的从零开始构建,对吧?MCP 正在快速发展,整个生态系统也是如此。让我向您展示一个现代化的技术栈,它可以显著减少您需要编写的代码量,从而加快开发速度。

  • MCP TypeScript SDK

    它完全实现了MCP规范,包括授权机制。它提供了强大的抽象层,使您无需费力处理底层协议实现,从而可以专注于业务逻辑。

  • ZenStack

    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
Enter fullscreen mode Exit fullscreen mode

安装 MCP SDK 和 bcrypt(用于对密码进行哈希处理):

npm install @modelcontextprotocol/sdk, bcrypt, @types/bcrypt
Enter fullscreen mode Exit fullscreen mode

初始化 ZenStack:

npx zenstack@latest init
Enter fullscreen mode Exit fullscreen mode

身份验证

为了支持 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
  ...
}
Enter fullscreen mode Exit fullscreen mode

根据 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'],
            })
        );
        ...
    }
}
Enter fullscreen mode Exit fullscreen mode

密码验证提供程序

以下是实际将要调用的三个函数,分别对应于上面提到的三个必要端点:

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>;
}
Enter fullscreen mode Exit fullscreen mode

clientStore处理注册和读取registerClient功能,它只是简单地从数据库中getClient写入和读取模型。OAuthClient

实际的授权流程始于 `<authorization_server>` authorize。它的作用是将所有必要的参数重定向到实际的授权服务器。由于在本例中,授权服务器就是它本身,因此我们重定向到以下/auth/login路径:

      const loginUrl = new URL('/auth/login', config.baseUrl);
      ...
      res.redirect(loginUrl.toString());
Enter fullscreen mode Exit fullscreen mode

因此,我们需要创建一个login.html服务来提供这项功能。这是一个典型的登录表单,如下所示:

登录页面

点击“登录”后,它会将从授权流程中传递的所有 URL 参数以及电子邮件和密码组合起来,发布到服务器端点/auth/login

服务器端点/auth/login最终会调用该函数handleloginPasswordAuthProvider它会验证用户身份。如果身份正确,则生成一个授权码,将其返回给客户端,并将其存储在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,
      },
  });
Enter fullscreen mode Exit fullscreen mode

从服务器获取到授权码后,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(' '),
};
Enter fullscreen mode Exit fullscreen mode

有状态多连接流式HTTP服务器

MCP客户端获取访问令牌后,每次通过主端点与服务器通信时,都会将访问令牌放在Authorization标头中。服务器应使用该令牌来验证客户端并获取客户端身份。我们选择常规/mcp端点作为主端点getFlexibleAuthMiddleware来实现这一点。

MCP客户端向服务器发送的第一个请求是一个initialize请求。服务器应响应服务器信息和一个生成的会话ID,客户端始终需要包含该会话ID以维护会话。作为生产环境可用的MCP服务器,它必须支持多个并发连接。因此,我们使用一个映射表,根据会话ID存储每个客户端。

const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
Enter fullscreen mode Exit fullscreen mode

为了管理每个用户的会话,我们将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);
Enter fullscreen mode Exit fullscreen mode

工具

我们将完全依赖 ZenStack 来完成这项工作。请记住该工具的两个组成部分:模式和执行。

模式

ZenStack 有一个原生的 Zod 插件,用于为其 CRUD API 生成 Zod schema。我们可以通过将以下代码添加到配置中来启用它schema.zmodel

plugin zod {
    provider = '@core/zod'
}
Enter fullscreen mode Exit fullscreen mode

然后,运行后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>;
};
Enter fullscreen mode Exit fullscreen mode

因此,每个模型的每个操作都将成为我们 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;
}
Enter fullscreen mode Exit fullscreen mode

需要说明的是,用于调用该操作的 Prisma 客户端是 ZenStack,enhanced其中包含当前用户身份信息,该信息userIdMCPServer初始化期间存储。无论 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 });
}
Enter fullscreen mode Exit fullscreen mode

另一个好处是,由于这些工具实际上是标准的 Prisma 客户端 API,因此它们非常适合 LLM:

  • 首先,其声明式和结构化的特性使其易于理解和推理,从而使语言学习模型能够生成准确、可预测且歧义最小的查询。
  • 其次,API 的灵活性允许通过一次调用访问多个模型(数据库表),使 LLM 能够高效地处理复杂的数据关系,并在不同的实体之间执行复杂的查询,而无需单独的 API 调用或复杂的编排逻辑。
  • 第三,Prisma 多年来被广泛采用,提供了丰富的文档、示例和社区使用经验,LLM 可以从中学习,大大提高了生成正确且具有上下文感知能力的代码的可能性。

测试

示例项目包含可供您尝试的种子数据。只需运行以下命令即可使其准备就绪:

npx prisma db push
npx prisma db seed
Enter fullscreen mode Exit fullscreen mode

它会创建三个用户并发布帖子。所有用户的密码均为 password123

尽情使用你选择的任何MCP客户端进行游戏吧:

VSCode-copilot

我非常期待了解您的应用程序取得的成果!如果您在编写 ZModel schema 时需要任何帮助,请随时联系或加入我们的 Discord 服务器。我非常乐意提供帮助!

文章来源:https://dev.to/zenstack/turning-your-database-into-an-mcp-server-with-auth-32mp