Plaid API网关与文档搜索服务

提供多租户Plaid API网关和文档搜索服务的云服务,支持金融数据访问和文档检索。

  • by crmchattie
  • TypeScript
  • 云端部署
  • 更新于
在 GitHub 查看 crmchattie/plaid-mcp

使用说明

格子mcp

主控程序 服务器 格子状API,在Cloudflare Workers上运行。

尝试现场演示→ --无需凭据,包括沙盒测试帐户。

两台服务器:

  • api服务器 -多租户Plaid API网关。每个客户端都使用自己的Plaid进行身份验证 client_id 和 secret 通过Basic Auth,并获得一个与其自己的令牌库隔离的会话。
  • 文档服务器 --Plaid文档搜索和检索。无需身份验证。

服务器

api服务器

将Plaid API操作公开为工具的可流式HTTP MCP服务器。部署地点:

https://plaid-api-mcp.myplaid.workers.dev/mcp

25工具 在这些类别中:

类别 工具
链接 create_link_token, exchange_public_token
沙箱 sandbox_create_public_token, sandbox_reset_login, sandbox_fire_webhook
项目 get_item, remove_item
账户 get_accounts
认证 get_auth
交易记录 get_transactions_sync
平衡 get_balance
身份 get_identity
投资 get_investments_holdings, get_investments_transactions
负债 get_liabilities
转账 create_transfer, get_transfer, list_transfers
保险库 list_items, remove_stored_item
披露 get_disclosure_settings, set_disclosure_level

文档服务器

MCP服务器,用于搜索和阅读Plaid文档,由KV存储支持 llms.txt 内容。无需身份验证。部署地点:

https://plaid-docs-mcp.myplaid.workers.dev/mcp

工具: search_docs, get_doc_page, list_sections

身份验证(api服务器)

每个请求(CORS飞行前除外)都需要 Authorization: Basic <base64(client_id:secret)> 头球服务器本身不会验证凭据-如果凭据错误,Plaid API调用将失败,并显示Plaid自己的错误消息。文档服务器不需要身份验证。

保留凭证 仅在内存中。该框架对持久对象存储的默认道具持久性被覆盖,因此Plaid机密永远不会写入磁盘。每次请求都会通过Authorization标头收到新的凭据。

客户端配置

克劳德桌面/克劳德代码

{
  "mcpServers": {
    "plaid-api": {
      "url": "https://plaid-api-mcp.myplaid.workers.dev/mcp",
      "headers": {
        "Authorization": "Basic <base64(client_id:secret)>"
      }
    },
    "plaid-docs": {
      "url": "https://plaid-docs-mcp.myplaid.workers.dev/mcp"
    }
  }
}

MCP SDK(TypeScript)

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

// API server (requires Plaid credentials)
const apiTransport = new StreamableHTTPClientTransport(
  new URL("https://plaid-api-mcp.myplaid.workers.dev/mcp"),
  {
    requestInit: {
      headers: {
        Authorization: `Basic ${btoa(`${clientId}:${secret}`)}`,
      },
    },
  },
);

const apiClient = new Client({ name: "my-agent", version: "1.0" });
await apiClient.connect(apiTransport);

// Docs server (no auth required)
const docsTransport = new StreamableHTTPClientTransport(
  new URL("https://plaid-docs-mcp.myplaid.workers.dev/mcp"),
);

const docsClient = new Client({ name: "my-agent", version: "1.0" });
await docsClient.connect(docsTransport);

MCP SDK(Python)

from base64 import b64encode
from mcp.client.streamable_http import streamablehttp_client
from mcp import ClientSession

credentials = b64encode(f"{client_id}:{secret}".encode()).decode()

# API server (requires Plaid credentials)
async with streamablehttp_client(
    "https://plaid-api-mcp.myplaid.workers.dev/mcp",
    headers={"Authorization": f"Basic {credentials}"},
) as (read, write, _):
    async with ClientSession(read, write) as session:
        await session.initialize()

# Docs server (no auth required)
async with streamablehttp_client(
    "https://plaid-docs-mcp.myplaid.workers.dev/mcp",
) as (read, write, _):
    async with ClientSession(read, write) as session:
        await session.initialize()

会话隔离

每个MCP会话都有自己的持久对象实例(由会话ID键控):

  • 凭证 --每节课 PlaidClient 使用该用户的 client_id/secret,仅保存在内存中
  • 代币库 --每个会话DO存储访问令牌。LLM只能看到不透明的 item_ref 别名,从不原始访问令牌
  • 披露偏好 --对数据详细级别(摘要与详细)的每会话控制

不可能发生跨租户数据泄漏——DO存储是针对每个实例的。

安全模型

API服务器处理多个租户之间的真实财务凭证和数据。安全设计遵循深度防御:多个独立层必须全部失效,敏感数据才会泄露。

1.通过持久对象隔离会话

每个MCP会话都映射到自己的会话 Cloudflare持久对象 实例,键入方式 mcp-session-id。每次会话都会得到:

  • 它自己 PlaidClient 使用该用户的凭据
  • 它自己 TokenVault 在单独的DO储存中
  • 其自身的披露偏好

会话之间没有共享状态。持久对象存储是按实例设计的——一个会话无法读取另一个会话的保管库或凭据。

2.凭据从未写入磁盘

MCP框架(McpAgent)通常持续 props 到 ctx.storage 因此耐用对象可以从休眠中恢复。API服务器 覆盖两者 onStart() 和 updateProps() 要仅将凭据保存在内存中,请执行以下操作:

// workers/api-server/src/index.ts
async onStart(props: PlaidProps) {
  if (props) this.props = props;   // in-memory only
  // ... framework wiring without storage.put()
}

async updateProps(props: PlaidProps) {
  this.props = props;              // in-memory only, no persistence
}

如果DO休眠,下一个HTTP请求将在 Authorization 标头,因此会话恢复时不需要存储机密。

3.代币库——不透明别名

当公共令牌被交换为访问令牌时。, "item-a1b2c3d4").LLM只会看到并引用这些别名。

关键设计决策 TokenVault:

  • store(alias, entry) --保存完整 VaultEntry (包括 access_token)DO存储
  • resolve(alias) --仅返回 access_token 字符串,由工具在服务器端用于进行Plaid API调用
  • list() --返回元数据 随着 access_token 明确剥离:
// workers/api-server/src/lib/token-vault.ts
async list(): Promise<Record<string, Omit<VaultEntry, "access_token">>> {
  const entries = await this.storage.list<VaultEntry>({ prefix: KEY_PREFIX });
  const result: Record<string, Omit<VaultEntry, "access_token">> = {};
  for (const [key, entry] of entries) {
    const alias = key.slice(KEY_PREFIX.length);
    const { access_token: _, ...metadata } = entry;  // strip token
    result[alias] = metadata;
  }
  return result;
}

LLM可以询问“我有什么项目?”并返回别名和元数据,但永远不会返回原始令牌。

4.双重受众响应过滤

简单的API回复包含LLM并不总是需要查看完整的财务数据。服务器使用 MCP注释 向LLM和最终用户发送不同的内容:

// workers/api-server/src/lib/tool-helpers.ts
return {
  content: [
    {
      type: "text",
      text: summarize(data, level),          // safe summary
      annotations: { audience: ["assistant"] },
    },
    {
      type: "text",
      text: JSON.stringify(data, null, 2),   // full response
      annotations: { audience: ["user"] },
    },
  ],
};
  • audience: ["assistant"] --LLM看到一个包含掩码帐号、编辑后的PII和汇总计数的摘要版本
  • audience: ["user"] --客户端UI接收完整的JSON响应以供显示

这意味着LLM可以对数据进行推理(“您有3个支票账户”),而无需处理原始账号、SSN或路由号。

5.按类别数据汇总

每个数据类别都有自己的摘要功能,可以控制LLM看到的内容:

类别 LLM查看(摘要) LLM看到(详细) 始终对LLM隐藏
账户 计数、姓名、蒙面数字(****1234) 相同 完整帐号
认证 账户计数、ACH计数 相同 路由号、账号
平衡 账户名称、当前/可用余额 相同 完整账号
身份 所有者计数 仅限名字 全名、社会安全号码、地址、电话、电子邮件
交易 计数(添加/修改/删除) 日期、商家、金额、类别 完整描述(摘要模式)
投资 持有/证券计数 票据、数量、价值 成本基础、证券ID
负债 类型计数(信用/学生/抵押贷款) 余额、年利率、最低还款额 债权人详细信息、账号

6.用户控制的披露级别

用户可以通过两个工具控制LLM看到每个类别的详细程度:

  • get_disclosure_settings --显示当前级别
  • set_disclosure_level --在以下之间更改类别 "summary" 和 "detailed"

默认为 "summary" 适用于所有可配置的类别(交易、投资、负债、身份)。身份验证、帐户和余额是不可配置的——它们总是使用固定的摘要格式。

这为用户提供了明确的控制:“我希望LLM看到我的交易详细信息,这样它就可以帮助我制定预算”,而不是“告诉我计数”

7.输入验证

所有工具参数在执行前都会使用Zod模式进行验证。枚举类型将值限制为有效选项(例如,将网络传输到 ach | same-day-ach | wire).MCP框架在格式错误的输入到达工具处理程序之前拒绝它们。

摘要

层 它保护什么 机制
持久对象隔离 跨租户数据访问 每个会话单独的DO实例
内存中的凭据 静止的Plaid secrets 覆盖持久性,唤醒时重新认证
令牌库别名 从LLM访问令牌 不透明引用, list() 剥离代币
双音频注释 来自LLM的完整API响应 MCP audience 注释
类别摘要 PII、帐号、SSN 按类别 summarize() 功能
披露偏好 粒度数据暴露 用户控制的摘要与详细
Zod验证 输入格式错误/注入 所有参数的模式验证

项目结构

plaid-mcp/
  packages/shared/       # Shared types (PlaidEnv, DocEntry, etc.)
  workers/api-server/    # Plaid API MCP server (Cloudflare Worker)
  workers/docs-server/   # Plaid docs MCP server (Cloudflare Worker)
  apps/playground/       # Web playground (Next.js) — chat UI with MCP tools

pnpm-onrepo。工人部署到Cloudflare,操场部署到Vercel。

发展

pnpm install

# Run locally
pnpm dev:api
pnpm dev:docs

# Type check
pnpm typecheck

# Deploy
pnpm deploy:api
pnpm deploy:docs

验证

# No credentials -> 401
curl -s -X POST https://plaid-api-mcp.myplaid.workers.dev/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"0.1"}}}'

# With credentials -> MCP initialize response
curl -s -X POST https://plaid-api-mcp.myplaid.workers.dev/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Authorization: Basic $(echo -n '<client_id>:<secret>' | base64)" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"0.1"}}}'