用大约20行代码搭建一个Postgres支持的MCP服务器
模型上下文协议(Model Context Protocol,MCP)是AI智能体获取工具的方式。你搭建一个MCP服务器,它公布一组带有类型输入的工具,然后智能体去调用它们。对于大量实际运行的MCP服务器来说,这些工具只是数据库的轻量封装:搜索这些记录、创建这一行、更新那个字段。服务器基本上就是一个在JSON-RPC和SQL之间做翻译的角色。这就引出了一个显而易见的问题:如果一个MCP服务器整天都在和Postgres打交道,为什么它常常要跑在离Postgres很远的地方?常见的做法是MCP服务器在一台主机上,数据库在另一台主机上,这样每次工具调用都要经过一次网络往返才能拿到所需数据。Neon Functions可以让你跳过这一步。你把MCP服务器部署成一个函数,这个函数就运行在它所查询的同一个数据库分支上、同一个区域里,这样一来服务器到Postgres的跳转就变成了本地调用。在这篇文章中,我会构建一个基于Postgres的MCP服务器,将其部署到一个数据库分支上,连接真实的MCP客户端,并展示实际网络往返的情况。整个核心代码大约只有20行,仓库地址在文末。
太长不看版:一个暴露数据库工具的MCP服务器,本质上就是网络加查询。把它运行在数据库旁边,就能从每次工具调用中省掉一次跨区域的往返。Neon Functions将你的MCP服务器部署到数据库分支上,与Postgres同位置部署。服务器到数据库的查询是同区域内的跳转,只需一两毫秒,而不是跨大西洋的延迟。核心代码很精简:定义Drizzle模式,注册一个工具(它的处理器执行SQL查询),然后通过可流式HTTP传输(streamable HTTP transport)在 /mcp 路径上暴露MCP服务器。这就是那大约20行代码。任何支持可流式HTTP的MCP客户端都能连接到它:比如mcporter、MCP SDK,或者像Claude、Cursor这样的智能体,只需指向该URL。每个分支拥有自己的函数URL,因此每个预览或测试分支都可以在自己的数据副本之上拥有独立的MCP端点。
前提条件
- Node.js 20+ 和 Neon CLI(
npm i -g neon,然后neon login) - 一个启用了平台预览功能的 Neon 账户(Functions、新的 us-east-2 项目)
- 基本熟悉 Postgres 和 TypeScript
- 可选:一个用于对接的 MCP 客户端,例如 mcporter、Claude 或 Cursor
MCP 服务究竟是什么
剥离那些营销包装,MCP 服务本质上是一个轻量的 RPC 服务。它通过某种传输层使用 JSON-RPC 协议通信,并对外公布一组工具。每个工具都包含名称、描述和输入结构。当 AI 智能体决定调用某个工具时,服务端执行对应的处理函数并返回结果——这就是全部约定。这里的传输层是可流式 HTTP:客户端通过 POST 请求将 JSON-RPC 消息发送到单个端点(/mcp),并读取返回的响应,对于流式数据则使用服务器推送事件(SSE)。它运行在普通的 HTTPS 上,而这正是无服务器函数提供的场景,因此 MCP 服务和 Neon Function 天然契合。
大约 20 行代码
下面是一个基于 Postgres 的 MCP 服务的核心代码。包含一个数据表结构、一个处理查询的工具函数,以及将其通过可流式 HTTP 暴露的衔接代码。其余部分大同小异。
import { Hono } from 'hono';
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import { ilike } from 'drizzle-orm';
import { z } from 'zod';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPTransport } from '@hono/mcp';
import { contacts } from './db/schema';
// 每个隔离实例一个连接池,跨请求复用。
const db = drizzle(new Pool({ connectionString: process.env.DATABASE_URL }));
const mcp = new McpServer({ name: 'contacts', version: '1.0.0' });
mcp.registerTool(
'search_contacts',
{
description: '按姓名搜索联系人。不传查询参数则列出所有人。',
inputSchema: {
query: z.string().optional().describe('要匹配的子字符串')
},
},
async ({ query }) => {
const rows = await db
.select()
.from(contacts)
.where(query ? ilike(contacts.name, `%${query}%`) : undefined);
return { content: [{ type: 'text', text: JSON.stringify(rows) }] };
},
);
// 通过 /mcp 端点以可流式 HTTP 暴露服务。
const app = new Hono();
const transport = new StreamableHTTPTransport();
app.all('/mcp', async (c) => {
if (!mcp.isConnected()) await mcp.connect(transport);
return transport.handleRequest(c);
});
export default app;
一个基于 Postgres 的 MCP 服务端,大约 20 行代码
工具处理函数才是关键。它本质上就是一个查询语句。registerTool 给智能体提供了工具名称、描述和一个 Zod 输入模式(SDK 会将这个模式转成模型能理解的 JSON Schema),然后你的处理函数返回结果。配套的仓库里对一个小型联系人表实现了完整的 CRUD 操作(create_contact、update_contact、delete_contact、search_contacts),但每个工具都遵循同一套路:描述它、执行查询、返回数据行。
数据模型用的是普通的 Drizzle:
import { pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core';
export const contacts = pgTable('contacts', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
email: text('email'),
company: text('company'),
notes: text('notes'),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
下面这个函数声明告诉 Neon 要部署什么:
// neon.ts
import { defineConfig } from '@neon/config/v1';
export default defineConfig({
preview: {
functions: {
contacts: {
name: 'contacts mcp server',
source: 'src/index.ts'
},
},
},
});
部署到分支上
Neon CLI 会自动生成模板、链接(或创建)一个项目、推送数据模型,然后部署该函数。在空目录下操作:
最后那个 URL 就是部署好的 MCP 服务端。函数和它要查询的 Postgres 分支位于同一区域(us-east-2)。MCP 端点就是这个 URL 加上 /mcp。如果你想在部署前迭代调试,用 neon dev 就能在本地 http://localhost:8787 上提供相同的函数服务,MCP 端点在 /mcp。
Neon Function 拥有一个公开的 HTTPS URL,任何人只要能拿到这个 URL 就能访问。这个示例为了演示而保持开放,但在真实场景中绝对不行——这些工具会读写你的数据库。在分享 URL 之前,一定要给端点加上访问控制。控制手段就是在 /mcp 前面加几行 Hono 中间件。配套仓库默认采用环境变量控制:不设 MCP_TOKEN 则保持演示模式开放,设置后每个请求都需要携带 Bearer Token。
大约20行代码的Postgres支撑MCP服务器
app.use('/mcp', async (c, next) => {
const token = process.env.MCP_TOKEN;
if (token && c.req.header('authorization') !== `Bearer ${token}`) {
return c.json({ error: 'unauthorized' }, 401);
}
await next();
});
大部分 MCP 客户端都能发送自定义请求头,所以在 Agent 那一端只需配置一行(Authorization: Bearer <token>)即可。我直接对着应用验证了这个防护:没有请求头或 token 错误都会返回 401,正确的 token 则会正常通过传输层;如果 MCP_TOKEN 没设置,这个端点行为就跟之前完全一样。
连上一个客户端,看它跑起来
任何支持流式 HTTP 的 MCP 客户端都可以连接到 /mcp 端点。下面给出三种方式:命令行、SDK、以及加到 Agent 里。
mcporter(命令行)
# 列出服务器暴露的工具
mcporter list https://<branch>-contacts.compute.c-3.us-east-2.aws.neon.tech/mcp --schema
# 调用某个工具
mcporter call ".../mcp.create_contact" name="Ada Lovelace" company="Analytical Engines"
mcporter call ".../mcp.search_contacts" query="engine"
MCP SDK(Node)
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
const url = new URL('https://<branch>-contacts.compute.c-3.us-east-2.aws.neon.tech/mcp');
const client = new Client({ name: 'test', version: '1.0.0' });
await client.connect(new StreamableHTTPClientTransport(url));
console.log((await client.listTools()).tools.map((t) => t.name));
const r = await client.callTool({ name: 'search_contacts', arguments: { query: 'ada' } });
console.log(r.content[0].text);
Claude / Cursor
# 把 URL 当作流式 HTTP 服务器,指向一个支持 MCP 的 Agent
# add-mcp 会自动帮你写客户端配置:
npx add-mcp https://<branch>-contacts.compute.c-3.us-east-2.aws.neon.tech/mcp -a claude
# 然后在 Agent 里说:"在联系人里搜索所有来自海军的人"
我从欧洲的一台机器上运行 SDK 客户端,连接部署好的服务器。握手和工具调用第一次就全成功了——
一个约20行代码、基于Postgres的MCP服务器
原文中的性能数据:
- 连接(初始化 + 握手):约1.5秒(首次冷启动约2秒)
tools/list:create_contact,update_contact,delete_contact,search_contactscreate_contact:196 ms →{ "created": { "id": 1, "name": "Ada Lovelace", ... } }search_contacts "navy":150 ms →{ "count": 1, "contacts": [ { "name": "Grace Hopper", ... } ] }
之后直接对分支执行 SELECT count(*) 验证了数据确实写入了Postgres。数据不驻留在内存中,这些工具本质上只是查询。
为什么共置才是关键
以上工具调用的耗时大约在150到200毫秒,但这反映的是我(客户端)到函数(服务器)之间的网络距离,而不是函数本身的执行速度。我人在欧洲,函数部署在 us-east-2,所以每次调用基本上都要跨大西洋往返一次。如果一个Agent运行在离该区域较近的地方,或者模型供应商自己的基础设施在调用这个工具,那么延迟会小得多。真正不随客户端位置变化的是从函数到Postgres的跳转,而这正是共置要解决的问题。
在本系列的第一篇文章中,我测量过这一数据:从函数内部对共置的分支执行SELECT查询,耗时约1.2毫秒;而同样一条查询从大西洋彼岸发出,则需约135毫秒。一个工具调用如果涉及一两条查询,每次执行都会继承这个差异。如果把MCP服务器部署在与数据库相隔一个区域的机房,那么每个工具调用除了客户端原本到服务器的开销之外,还要额外加上一次跨区域往返。反之,把服务器直接放在数据库所在的分支上,这部分成本就几乎归零了。对于一个全部工作就是查询Postgres的服务器而言,这才是最值得优化的那一跳。
每个分支一个端点
这里还有第二个免费福利。Neon Functions 按分支部署,每个分支都有自己的函数 URL。因为分支同时也是数据的副本,所以每个分支都可以拥有自己的 MCP 服务器,操作自己的数据集。为预览环境创建一个分支,它就会自带一个基于该分支数据的 MCP 端点。给 AI 智能体一个测试分支,它就不会碰生产数据。在分支上运行 CI,智能体的工具操作的是临时数据副本,分支删除后一切随之消失。你不需要为每个环境单独搭建和拆除一套 MCP 服务——端点直接搭载在你已有的分支上。
仓库地址
完整示例(包含所有四个 CRUD 工具、数据库表结构、部署配置和客户端测试脚本)在这里:https://github.com/The-DevOps-Daily/neon-mcp-demo
总结
一个面向数据库的 MCP 服务器,主要工作就是网络通信和查询。网络部分值得认真对待,因为 AI 智能体可能在单个任务中调用这些工具几十次。Neon Functions 把 MCP 服务器部署到它要查询的那个分支上,从而将服务器与数据库之间的距离压缩到同区域一跳之内。实现代码非常简洁:一个数据库表结构、一个执行查询的工具,再加上可流式传输的 HTTP 传输协议。把任意 MCP 客户端指向这个 URL,智能体就能获得类型安全、数据库驱动的工具,这些工具就运行在数据旁边。让每个分支拥有自己的端点,你就得到了隔离的、按环境划分的智能体工具,完全不需要额外运行任何服务。
基于 Postgres 的 MCP 服务器,代码不到 20 行
模型上下文协议(Model Context Protocol,简称 MCP)是 AI 智能体获取工具的方式。你搭一个 MCP 服务器,它会对外发布一组带类型入参的工具,智能体按需调用。在大量真实的 MCP 服务器里,这些工具其实就是数据库的薄壳封装:搜索记录、创建行、更新字段。服务器大部分工作就是做 JSON-RPC 与 SQL 之间的翻译。这引出一个显而易见的问题:如果 MCP 服务器整天都在和 Postgres 打交道,为什么它总是跑在离数据库很远的地方?这篇文章要回答的就是这个问题:借助 Neon Functions 直接把 MCP 服务器部署到同一套 Postgres 分支上,让服务器和数据库之间的网络跳转几乎可以忽略不计。
核心思路很简单:MCP 服务器本质上就是一个函数,接收 JSON-RPC 请求,返回 JSON-RPC 响应。如果这个函数和它查询的 Postgres 分支位于同一个云区域(甚至同一台机器上),那么唯一的延迟就是查询本身——不需要跨区域往返。这样一来,每次工具调用都变得更快、更可预测、成本也更低。
部署服务器
该服务器本身就是一个 TypeScript 文件,使用 @modelcontextprotocol/sdk 并通过 streamable HTTP 传输。完整逻辑(除去导入和配置)大约 20 行代码:
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { neon } from '@neondatabase/serverless';
const sql = neon(process.env.DATABASE_URL!);
const server = new Server({ name: 'contacts', version: '1.0.0' });
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [{
name: 'search_contacts',
description: '按姓名或公司搜索联系人',
inputSchema: {
type: 'object',
properties: { query: { type: 'string' } },
required: ['query']
}
}, {
name: 'create_contact',
description: '创建新联系人',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string' },
company: { type: 'string' }
},
required: ['name', 'company']
}
}, {
name: 'update_contact',
description: '根据 ID 更新联系人',
inputSchema: {
type: 'object',
properties: {
id: { type: 'number' },
name: { type: 'string' },
company: { type: 'string' }
},
required: ['id']
}
}, {
name: 'delete_contact',
description: '根据 ID 删除联系人',
inputSchema: {
type: 'object',
properties: { id: { type: 'number' } },
required: ['id']
}
}]
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case 'search_contacts':
const rows = await sql`SELECT * FROM contacts WHERE name ILIKE ${'%' + args.query + '%'} OR company ILIKE ${'%' + args.query + '%'}`;
return { content: [{ type: 'text', text: JSON.stringify(rows) }] };
case 'create_contact':
const created = await sql`INSERT INTO contacts (name, company) VALUES (${args.name}, ${args.company}) RETURNING *`;
return { content: [{ type: 'text', text: JSON.stringify({ created: created[0] }) }] };
case 'update_contact':
const updated = await sql`UPDATE contacts SET name = ${args.name}, company = ${args.company} WHERE id = ${args.id} RETURNING *`;
return { content: [{ type: 'text', text: JSON.stringify({ updated: updated[0] }) }] };
case 'delete_contact':
await sql`DELETE FROM contacts WHERE id = ${args.id}`;
return { content: [{ type: 'text', text: JSON.stringify({ deleted: true }) }] };
default:
throw new Error(`未知工具: ${name}`);
}
});
const transport = new StreamableHTTPServerTransport({ url: '/mcp' });
server.connect(transport);
测试端点
任何支持流式HTTP(streamable HTTP)的 MCP 客户端都可以连接到 /mcp 端点。下面介绍三种方式:使用 CLI、通过 SDK,以及将其集成到智能体中。
mcporter(命令行工具)
# 列出该服务器提供的工具及 schema
mcporter list https://<分支名>-contacts.compute.c-3.us-east-2.aws.neon.tech/mcp --schema
# 调用工具
mcporter call ".../mcp.create_contact" name="Ada Lovelace" company="Analytical Engines"
mcporter call ".../mcp.search_contacts" query="engine"
MCP SDK(Node)
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
const url = new URL('https://<分支名>-contacts.compute.c-3.us-east-2.aws.neon.tech/mcp');
const client = new Client({ name: 'test', version: '1.0.0' });
await client.connect(new StreamableHTTPClientTransport(url));
console.log((await client.listTools()).tools.map((t) => t.name));
const r = await client.callTool({ name: 'search_contacts', arguments: { query: 'ada' } });
console.log(r.content[0].text);
Claude / Cursor
# 将 MCP 智能体指向该 URL,作为流式 HTTP 服务器使用。
# add-mcp 命令会自动生成客户端配置:
npx add-mcp https://<分支名>-contacts.compute.c-3.us-east-2.aws.neon.tech/mcp -a claude
# 然后在智能体中输入:“搜索我的联系人,查找所有在 Navy 工作的人”