独立开发者写 MCP server 的第一版不要上来就做 OAuth、多租户和远程部署,把一个只读 SaaS demo 跑通:本地 stdio、一个 tool、一个 resource、Inspector 验证、Claude Desktop 配置。本文访问时间是 2026-05-22,@modelcontextprotocol/sdk 在 npm 上的 latest 为 1.29.0;如果你读到时版本变了,跑下面的版本检查命令,不要照抄旧文章里的底层 handler。

确认版本和 API 有无变化

MCP TypeScript SDK 迭代很快,旧文章常见问题是还在用 Server + setRequestHandler(ListToolsRequestSchema, ...) 当入门代码。新项目优先看官方 build-server 教程里的高层 McpServer,再用 npm 元数据确认版本。

npm view @modelcontextprotocol/sdk version
npm view @modelcontextprotocol/sdk peerDependencies --json

本文比对到的结果是:@modelcontextprotocol/sdk latest 为 1.29.0,peer dependency 支持 zod^3.25 || ^4.0。官方 build-server TypeScript 示例仍安装 zod@3,所以保守教程用 zod@3;如果你的项目已经全面迁到 Zod 4,用 tsc 验证 SDK 当前版本的类型。

场景2026 年建议验证方式
本地 Claude Desktop / Cursor 集成stdio transportInspector 能连接,客户端完整重启后能列出 tool
公开远程 MCP 服务Streamable HTTP加鉴权、会话、限流,再用 HTTP 客户端测初始化
旧客户端兼容HTTP + SSE只在目标客户端明确要求时保留
入门代码McpServer.registerTool / registerResourcenpm run build 无类型错误

项目初始化

下面命令在 macOS、Linux 和大多数 CI 环境都能跑。Windows 用户把路径写法换成 PowerShell 风格即可,MCP 本身不要求特定包管理器。

mkdir indie-mcp-server
cd indie-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod@3
npm install -D typescript tsx @types/node
npm pkg set type=module
npm pkg set scripts.build=tsc
npm pkg set scripts.dev="tsx src/index.ts"
npm pkg set scripts.inspect="npm run build && npx @modelcontextprotocol/inspector node build/index.js"
mkdir src

tsconfig.json 用 Node 16 模块解析,这是官方教程给 TypeScript server 的基线配置。Node 版本不用写死到文章里,在自己机器上跑 node --version;官方教程要求 Node.js 16 或更高,生产项目建议跟你的部署平台统一到当前 LTS。

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "types": ["node"]
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

最小 Server 写法

这个 demo 模拟一个独立 SaaS 的后台:tool 查询客户状态,resource 暴露定价规则。真实项目里,resource 更适合放只读文档、配置、schema、账单规则;tool 才适合触发查询、写入或外部 API 调用。

把下面内容保存为 src/index.ts

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({
  name: "indie-mcp-server",
  version: "0.1.0",
});

const customers = [
  { id: "demo_001", plan: "pro", mrr: 29, status: "active" },
  { id: "demo_002", plan: "free", mrr: 0, status: "trial" },
] as const;

server.registerResource(
  "pricing-policy",
  "app://pricing-policy",
  {
    title: "Pricing policy",
    description: "Read-only pricing notes for the SaaS support agent",
    mimeType: "text/markdown",
  },
  async (uri) => ({
    contents: [
      {
        uri: uri.href,
        mimeType: "text/markdown",
        text: "# Pricing\n\nFree plan: $0. Pro plan: $29/month. Refund window: 7 days.",
      },
    ],
  }),
);

server.registerTool(
  "lookup_customer",
  {
    title: "Lookup customer",
    description: "Return a demo customer's plan, MRR and account status by customer id.",
    inputSchema: {
      customerId: z.string().min(1).describe("Customer id, for example demo_001"),
    },
  },
  async ({ customerId }) => {
    const customer = customers.find((item) => item.id === customerId);

    if (!customer) {
      return {
        isError: true,
        content: [{ type: "text", text: `No customer found for ${customerId}` }],
      };
    }

    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(customer, null, 2),
        },
      ],
    };
  },
);

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("indie-mcp-server running on stdio");
}

main().catch((error) => {
  console.error("Fatal error in MCP server", error);
  process.exit(1);
});

注意最后两处都用 console.error。stdio transport 把 stdout 留给 JSON-RPC 协议,console.log 会污染协议流,表现成客户端连不上、初始化失败或莫名其妙的 JSON parse error。

本地验证 Tools 和 Resources

先不要急着接 Claude Desktop。用 Inspector 单独验证协议,能少排一层客户端配置问题。

npm run build
npx @modelcontextprotocol/inspector node build/index.js

Inspector 打开后做四件事:

  1. 确认连接成功,server 名称显示为 indie-mcp-server
  2. 在 Tools 里调用 lookup_customer,参数填 {"customerId":"demo_001"}
  3. 再填 {"customerId":"missing"},确认错误路径返回 isError: true
  4. 在 Resources 里读取 app://pricing-policy,确认 MIME type 和文本内容都正常。

这一步比直接问 Claude 更可靠,因为它会暴露 schema、返回值和 notification stream。等 Inspector 通过后,再接具体客户端。

接入 Claude Desktop

Claude Desktop 的本地 server 配置要写绝对路径。官方调试文档也提醒,客户端启动 stdio server 时工作目录可能不是你的项目目录,配置相对路径很容易在重启后失效。

macOS 配置文件路径通常是:

~/Library/Application Support/Claude/claude_desktop_config.json

示例配置:

{
  "mcpServers": {
    "indie-demo": {
      "command": "node",
      "args": ["/ABSOLUTE/PATH/indie-mcp-server/build/index.js"],
      "env": {
        "SAAS_API_BASE": "https://api.example.com"
      }
    }
  }
}

保存后要完整退出并重新打开 Claude Desktop,只关窗口不一定会重启 MCP 进程。连接失败时先看 Claude 日志:macOS 在 ~/Library/Logs/Claude,Windows 在 %APPDATA%\\Claude\\logs

密钥与权限存放

MCP server 的权限等于启动它的客户端进程权限。独立开发者最容易犯的错,是把 Stripe live secret、数据库 URL、内部 admin token 直接塞进本地配置,然后让 tool 按模型请求自由调用。

更稳的限制是三层:

限制做法不做什么
代码仓库.env.example 只放变量名不提交真实 .env、Claude 配置和客户数据
MCP tool默认只读、限字段、限返回条数不让模型直接拿全量用户表或 live secret
后端代理用短期 token、scope、审计日志不把支付平台主密钥交给本地客户端

如果必须做写操作,例如退款、改套餐、发邮件,把 tool 名字写得非常明确:create_refund_draftmanage_billing 安全。返回值先生成草稿,让真人在你的后台确认;不要把「客户端会弹确认」当成唯一权限模型。

日志与调试

本地 stdio server 用 stderr。最简单的结构化日志是:

console.error(JSON.stringify({
  level: "info",
  event: "tool_called",
  tool: "lookup_customer",
  customerId,
  ts: new Date().toISOString(),
}));

不要记录 API key、Authorization header、完整用户邮箱、支付卡信息和原始 prompt。需要排查时记录 request id、tool 名、耗时、状态码和被脱敏的实体 id 就够了。

远程 Streamable HTTP server 不受 stdout 协议限制,但要把日志接进平台侧:Cloudflare Workers Logs、Railway Logs、Fly.io Logs 或你自己的 OpenTelemetry。远程服务还要额外测 Mcp-Session-Id、并发请求、超时和鉴权失败路径。

什么时候做远程部署

本地 stdio 适合个人工作流、内部客服助手、连接本机文件或内网服务。想把 MCP server 给用户用,才考虑远程托管。

远程部署前至少补齐这些东西:

  • Streamable HTTP transport,而不是把 stdio 进程硬挂到 Web server 后面。
  • HTTPS、认证、限流、审计日志和版本化 endpoint。
  • 多租户隔离:tenant id 不能只靠模型传参决定。
  • 工具超时:外部 API 10 秒没返回就失败,不要让客户端一直等。
  • 向后兼容策略:SDK 升级后先跑 Inspector 和一组真实客户端回归。

如果你只是给自己或 3 人小团队用,先保持本地 stdio。远程 MCP 的维护成本不在代码量,而在权限、密钥轮换、日志留存和协议升级。

相关阅读