mcp-connector-pattern

mcp-connector-pattern

A reference MCP server for a fictional bike shop that enables managing customers, inventory, orders, and outbound messages, demonstrating best practices for building MCP servers.

Category
访问服务器

README

mcp-connector-pattern

A small, complete reference MCP server for a fictional bike shop, Northwind Cycles. It exists to show how we build MCP servers, not to sell bikes: customers, inventory, orders, and outbound messages, backed by an in-memory fake upstream API. Read this in five minutes to see the shape of the pattern; read the source (it's short) to see it's real code, not a sketch.

What this demonstrates

1. One tool registry, two transports. src/server.ts exports a single createServer() factory. src/transports/stdio.ts and src/transports/http.ts both call it and hand the result to a different SDK Transport. MCP_TRANSPORT=stdio or MCP_TRANSPORT=http picks which one runs -- nothing about the tools changes. tests/dual-transport.test.ts proves this by spawning the real stdio process and a real HTTP server side by side and asserting they list the identical tool set.

2. Tool descriptions are the real interface. Every tool description in src/tools/ spells out what it does, what it doesn't do, and an example call -- see draft_order or confirm_order for the clearest case. A vague description ("place an order") gives a model no way to tell "draft it" from "commit it, charge the card, ship it" apart, and it will still answer confidently with the wrong tool -- you find out from a wrong result, not an exception. Precise descriptions are the cheapest fix available, and they're free at runtime.

3. Structured, not prose, responses. Every tool declares an outputSchema and returns matching structuredContent alongside a short text summary (see src/lib/result.ts). A caller -- model or code -- reads result.structuredContent.order.status, it doesn't parse a sentence.

4. Read/write separated, with a human-approval seam. This is the load-bearing idea. draft_order and draft_customer_message (src/tools/orders.ts, src/tools/messages.ts) only ever write to our own draft state -- no stock is touched, nothing is sent. confirm_order and send_customer_message are the only code paths that reach the outside world (decrementing stock, dispatching a message), and each requires the exact draft id from the step before. There is no single call that goes from "customer wants 2 bikes" to "stock decremented" -- a human has to be in that gap. tests/approval-seam.test.ts asserts this directly: it drafts an order, checks inventory hasn't moved, confirms it, and only then checks the stock changed.

5. Every side-effect tool reports exactly what it changed. confirm_order returns stockChanges: [{ unitId, before, after }] for every unit it touched; send_customer_message returns the sentAt timestamp. A model relaying "done!" to a user is only as honest as what the tool actually handed back -- so the tool hands back specifics, not a boolean.

6. One audit line per call. src/lib/audit.ts wraps every handler and writes [audit] {tool, actor, args, ok, durationMs} to stderr on every call, success or failure. Never stdout -- on the stdio transport stdout is the JSON-RPC channel, and one stray log line there corrupts every message after it. no-console is enforced by lint in src/ (console.error only) so this can't regress silently.

7. Secrets stay out of the repo. .env.example documents every variable; .env is gitignored. The HTTP transport requires Authorization: Bearer <token> on every request (src/lib/auth.ts, constant-time compare) and refuses to start without MCP_BEARER_TOKEN set. src/lib/redact.ts masks anything shaped like a token/secret/password before it reaches a log line -- see the audit output in the run below, where the bearer token shows as cu***23.

8. Registering as a custom connector -- see below.

What this deliberately does not do

Scope is the point. This is a pattern, not a starter kit:

  • No database -- state is an in-memory array (src/upstream/) that resets every restart. Swap that module for a real API client; nothing in src/tools/ has to change.
  • No OAuth -- the HTTP transport uses one shared bearer token, not per-user auth. Fine for a demo or an internal tool; a multi-tenant product needs real auth in front of /mcp.
  • No Docker, no CI, no MCP resources or prompts. Just the tool layer, two transports, and tests that prove both actually run.

Run it

npm install
MCP_TRANSPORT=stdio npm run start:stdio

That's the whole stdio path. In a second terminal, the same registry over HTTP:

cp .env.example .env   # edit MCP_BEARER_TOKEN to a real random value
npm run start:http
curl -s -X POST http://localhost:8787/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'Authorization: Bearer <your MCP_BEARER_TOKEN>' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Requests without a valid bearer token get 401; try the same curl without the Authorization header to see it.

Register as a custom connector

Both Claude.ai and Claude Desktop can add an MCP server as a custom connector under Settings -> Connectors -> Add custom connector:

  1. Run the server with MCP_TRANSPORT=http (above). For a connector reachable from claude.ai (not just localhost), put a public HTTPS URL in front of it -- a tunnel like ngrok http 8787 is enough for a demo; a real deployment needs a real host and TLS.
  2. In "Add custom connector", set the URL to https://<your-host>/mcp.
  3. If your client supports a custom header for the connector, set Authorization: Bearer <your MCP_BEARER_TOKEN>. Clients that only support OAuth will need a real OAuth flow in front of /mcp instead of the bearer check here -- out of scope for this demo, in scope for a production build.
  4. Save. The client calls initialize, then tools/list; you should see all ten Northwind Cycles tools with their descriptions.

For a local-only client (Claude Desktop, or any stdio-based MCP host), point it at MCP_TRANSPORT=stdio node --import tsx src/index.ts from this directory instead -- no network, no token, same tools.

Project layout

src/
  server.ts              single tool-registry factory (point 1)
  index.ts               entry point, picks a transport from MCP_TRANSPORT
  transports/
    stdio.ts             StdioServerTransport
    http.ts              StreamableHTTPServerTransport + bearer auth
  tools/
    customers.ts         read-only
    inventory.ts         read-only
    orders.ts            read/write split + approval seam (points 4, 5)
    messages.ts          read/write split + approval seam (points 4, 5)
  lib/
    audit.ts             per-call audit logging to stderr (point 6)
    redact.ts            secret masking for log lines (point 7)
    auth.ts              bearer token check (point 7)
    result.ts            structuredContent + text summary helper (point 3)
  upstream/
    db.ts                in-memory fake upstream data
    client.ts            async client over db.ts (swap this for a real API)
tests/
  approval-seam.test.ts  proves point 4
  dual-transport.test.ts proves point 1
scripts/
  smoke-stdio.ts         manual end-to-end check over stdio
  smoke-http.ts          manual end-to-end check over HTTP

Gates

npm run lint       # eslint, zero warnings
npm run typecheck   # tsc --noEmit, strict
npm test             # vitest

Stack

TypeScript, Node 22, ESM, @modelcontextprotocol/sdk 1.30, Zod 3, Vitest.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选