从 0 开始实操 MCP:用 TypeScript 接入 CoinGecko,并在 Inspector / Codex 中验证
从 0 开始实操 MCP:用 TypeScript 接入 CoinGecko,并在 Inspector / Codex 中验证
如果你已经看过不少 MCP 介绍,却始终没有真正跑通过一个完整例子,那么这篇文章会更适合你。
这篇文章不讲太多抽象概念,也不一开始堆很多工具,而是只做一件事:
从零写一个最小的 TypeScript MCP Server,接入 CoinGecko 的真实市场数据接口,并在 Inspector 和 Codex 中完成验证。
整个过程只围绕一个工具展开:fetch_market_data。
你可以把它理解成一条完整的入门链路:
- 先理解 MCP 在这个案例里到底是什么
- 再动手写一个最小可运行的 MCP Server
- 然后接入真实 API
- 最后验证这个工具确实能被宿主发现和调用
这样学的好处是,你不会停留在“知道 MCP 是什么”,而是能真正完成一次从开发到验证的闭环。
本文配套示例代码已上传 GitHub,方便你边看边对照实践:
- GitHub 仓库:mcp-tooling-example
一、这篇文章要解决什么问题
学 MCP 时,很多人最容易卡住的地方,不是概念看不懂,而是不知道一个最小可运行项目到底长什么样。
所以先把本文的目标说清楚。读完并跑通后,你会完成下面几件事:
- 理解 MCP 在这个例子里的定位
- 用 TypeScript 写一个最小 MCP Server
- 注册一个可调用的工具:
fetch_market_data - 让这个工具真正调用 CoinGecko API 获取市场价格
- 用 MCP Inspector 验证工具是否可用
- 把它接入 Codex CLI,验证宿主确实能够调用它
也就是说,这不是一个只打印 hello world 的示例,而是一个真正有外部数据输入、有工具注册、有调试验证的完整入门项目。
二、先建立一个最小认知:MCP 在这个例子里是什么
在开始搭项目之前,先把最关键的问题讲清楚:
MCP 在这里不是业务逻辑本身,而是一层标准化的工具协议。
你可以先看这条链路:
Codex / ChatGPT / Inspector
-> MCP Client
-> 你的 MCP Server
-> 你的 TypeScript 逻辑
-> CoinGecko API
这里面每一层的职责都不一样:
宿主(Host):比如 Codex、Inspector、ChatGPT
它们负责发现工具、调用工具、消费结果MCP Client:负责按 MCP 协议和 Server 通信
你的 MCP Server:负责把你的能力暴露成标准工具
你的业务逻辑:真正做事的部分,比如“根据 symbol 拉取市场价格”
第三方 API:比如 CoinGecko,提供真实数据来源
所以,本文里你真正要实现的核心能力,其实是:
输入一个币种 symbol,比如 BTC,返回对应的实时市场数据。
而 MCP 做的事,是让这个能力能够被宿主以标准方式调用。
这点对新手非常重要。因为很多人一开始会把 MCP 和 Agent、业务逻辑、应用框架混在一起。其实更准确的理解应该是:
MCP 解决的是“能力如何暴露和被调用”,不是“业务本身如何实现”。
为什么这篇文章先从 STDIO 开始,而不是 HTTP
MCP 支持不同传输方式,但对新手来说,最适合入门的通常不是远程 HTTP,而是本地 STDIO。
原因很简单,STDIO 足够直接:
- 不需要先搭 Web 服务
- 不需要处理端口和路由
- 不需要 HTTPS 或公网暴露
- 本地调试链路更短,更容易定位问题
也就是说,你现在最需要的是先把一个工具从“写出来”到“被调用成功”跑通,而不是一上来解决部署问题。
所以这篇文章的策略是:
先用 STDIO 完成最小闭环,再考虑远程服务化。
这个顺序对初学者更稳,也更容易建立对 MCP 的直觉。
三、开始动手:从 0 搭一个最小可运行的 MCP Server
1. 准备工作
开始之前,先准备好本地环境:
- Node.js 18 或更高版本
- npm
- 一个 CoinGecko API Key
- 已安装 Codex CLI(后文用于扩展验证,可选)
获取 CoinGecko API Key
CoinGecko 提供 API Key 认证。常见方式是通过请求头传递:
- Demo Key:
x-cg-demo-api-key - Pro Key:
x-cg-pro-api-key
本文先使用 Demo Key 即可。因为我们的目标是跑通 MCP 链路,不是做高并发或生产级接入。
2. 创建项目
先初始化一个最小项目:
mkdir mcp-coingecko-demo
cd mcp-coingecko-demo
npm init -y
安装依赖:
npm install @modelcontextprotocol/sdk zod dotenv
npm install -D typescript tsx @types/node
初始化 TypeScript 配置:
npx tsc --init
到这里,你就有了一个可以写 MCP Server 的基础项目。
3. 设计项目结构
为了让示例保持简单,同时又不过度把所有逻辑塞进一个文件,这里建议先用下面这套结构:
mcp-coingecko-demo/
package.json
.env
src/
server.ts
domain/
market.ts
各文件职责如下:
src/server.ts
MCP Server 入口,负责注册工具、定义输入输出、连接传输层src/domain/market.ts
业务逻辑层,负责调用 CoinGecko API,处理市场数据
这样拆的好处很明显:
协议层和业务层分开
你能更清楚地理解:什么是 MCP,什么是业务逻辑后续更容易扩展
将来无论接 CLI、HTTP、ChatGPT,甚至测试脚本,都可以复用domain层更符合真实项目演进方式
哪怕这是一个入门项目,也最好从一开始建立基本的分层意识
对新手来说,这种拆分不是“为了架构而架构”,而是为了以后少踩坑。
4. 配置环境变量
在项目根目录创建 .env 文件:
COINGECKO_API_KEY=your_demo_key_here
COINGECKO_API_PLAN=demo
这两个环境变量分别表示:
COINGECKO_API_KEY:你的 CoinGecko API KeyCOINGECKO_API_PLAN:当前使用的套餐类型,本文先填demo
这样做的意义是把配置和代码分开。以后如果你切换到 Pro Key,或者在不同环境使用不同配置,就不用改业务代码。
5. 先写业务逻辑:获取市场数据
先新建 src/domain/market.ts:
import "dotenv/config";
export type MarketData = {
id: string;
symbol: string;
price: number;
currency: string;
source: string;
change24h?: number | null;
lastUpdatedAt?: number | null;
};
const SYMBOL_TO_COINGECKO_ID: Record<string, string> = {
BTC: "bitcoin",
ETH: "ethereum",
SOL: "solana",
};
function getCoinGeckoConfig() {
const plan = (process.env.COINGECKO_API_PLAN || "demo").toLowerCase();
const apiKey = process.env.COINGECKO_API_KEY;
if (!apiKey) {
throw new Error("Missing COINGECKO_API_KEY");
}
if (plan === "pro") {
return {
baseUrl: "https://pro-api.coingecko.com/api/v3",
headers: {
"x-cg-pro-api-key": apiKey,
},
source: "coingecko-pro",
};
}
return {
baseUrl: "https://api.coingecko.com/api/v3",
headers: {
"x-cg-demo-api-key": apiKey,
},
source: "coingecko-demo",
};
}
function resolveCoinId(symbol: string): string {
const normalizedSymbol = symbol.trim().toUpperCase();
if (!normalizedSymbol) {
throw new Error("symbol is required");
}
const coinId = SYMBOL_TO_COINGECKO_ID[normalizedSymbol];
if (!coinId) {
throw new Error(`Unsupported symbol: ${normalizedSymbol}`);
}
return coinId;
}
export async function fetchMarketData(symbol: string): Promise<MarketData> {
const normalizedSymbol = symbol.trim().toUpperCase();
const coinId = resolveCoinId(normalizedSymbol);
const { baseUrl, headers, source } = getCoinGeckoConfig();
const url = new URL(`${baseUrl}/simple/price`);
url.searchParams.set("ids", coinId);
url.searchParams.set("vs_currencies", "usd");
url.searchParams.set("include_24hr_change", "true");
url.searchParams.set("include_last_updated_at", "true");
const response = await fetch(url.toString(), {
method: "GET",
headers,
});
if (!response.ok) {
const text = await response.text();
throw new Error(`CoinGecko API error: ${response.status} ${text}`);
}
const json = (await response.json()) as Record<
string,
{
usd?: number;
usd_24h_change?: number;
last_updated_at?: number;
}
>;
const data = json[coinId];
if (!data || typeof data.usd !== "number") {
throw new Error(`No price data returned for ${coinId}`);
}
return {
id: coinId,
symbol: normalizedSymbol,
price: data.usd,
currency: "USD",
source,
change24h: typeof data.usd_24h_change === "number" ? data.usd_24h_change : null,
lastUpdatedAt:
typeof data.last_updated_at === "number" ? data.last_updated_at : null,
};
}
这段业务代码到底做了什么
这段代码看起来不长,但已经覆盖了一个真实工具最关键的几个环节。
它主要做了 5 件事:
- 读取环境变量,确定用 Demo 还是 Pro 配置
- 把输入的 symbol 映射成 CoinGecko 识别的 coin id
- 调用 CoinGecko 的
/simple/price接口 - 从响应里提取价格、24 小时涨跌幅、更新时间
- 把结果整理成统一的数据结构返回
换句话说,这一层解决的是:
“怎么把一个用户能理解的输入,变成第三方 API 能理解的请求,再把返回值整理成自己可控的输出格式。”
这就是典型的业务逻辑层职责。
为什么这里先手写 symbol 映射,而不是自动支持所有币种
你会发现这里没有一开始就去拉 /coins/list,而是先手写了几个映射:
BTC -> bitcoinETH -> ethereumSOL -> solana
这样做不是因为它更高级,而是因为它更适合入门阶段。
原因有两个:
先降低复杂度,优先跑通链路
新手最重要的不是“支持所有币种”,而是先确认整条链路能正常工作。如果一开始就引入动态映射、缓存、搜索、容错等问题,排错成本会明显升高。把问题拆小,更容易理解
在这一阶段,你只需要先理解工具如何注册、请求如何发出、数据如何返回、宿主如何调用。至于“如何支持所有 symbol”,那是下一阶段的问题。
技术学习里,一个很重要的原则就是:
先完成最小可运行版本,再逐步扩展能力。
6. 再写 MCP Server:把业务能力暴露成工具
当业务逻辑已经能独立工作后,再来写 src/server.ts:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { fetchMarketData } from "./domain/market.js";
const server = new McpServer({
name: "market-data-server",
version: "1.1.0",
});
server.registerTool(
"fetch_market_data",
{
description:
"Fetch crypto market data from CoinGecko for symbols like BTC, ETH, or SOL",
inputSchema: {
symbol: z
.string()
.min(1, "symbol cannot be empty")
.describe("Crypto asset symbol, for example BTC, ETH, or SOL"),
},
},
async ({ symbol }) => {
try {
const result = await fetchMarketData(symbol);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
const message =
error instanceof Error ? error.message : "Unknown error occurred";
return {
content: [
{
type: "text",
text: `Failed to fetch market data: ${message}`,
},
],
isError: true,
};
}
}
);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP server is running...");
}
main().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});
这段 MCP Server 代码应该怎么看
到这里,文章最关键的一层连接出现了:
业务逻辑已经有了,现在我们要把它变成一个标准工具。
上面这段代码主要做了 4 件事:
创建一个 MCP Server
这一步是在声明你的服务身份。宿主连接时,会把它当成一个 MCP Server 来识别。注册一个工具
这里注册了一个名为fetch_market_data的工具。从宿主视角看,它不关心你内部怎么实现,它只关心工具叫什么、接收什么参数、返回什么结果。用 Zod 描述输入参数
这一步不是为了“写得更正式”,而是为了让工具输入更可验证、更清晰。这样在 Inspector 或其他宿主里,也更容易看到工具需要什么参数。把业务逻辑接进来
MCP 层只负责协议和工具暴露,真正的业务仍然交给domain/market.ts。
这就是为什么前面要先拆文件。如果你把所有代码都写在一个地方,新手很容易分不清“协议代码”和“业务代码”分别在干什么。
7. 启动服务并理解运行细节
接下来给 package.json 增加脚本:
{
"type": "commonjs",
"scripts": {
"dev": "tsx watch src/server.ts",
"build": "tsc src/server.ts --outDir dist --module commonjs --target es2020 --esModuleInterop",
}
}
然后启动:
npm run dev
如果没有报错,而且进程保持运行、等待输入,那么通常说明你的 MCP Server 已经启动成功了。
这里要注意一点:
对 STDIO Server 来说,“启动后挂起等待输入”是正常现象,不是程序卡死。
因为它本来就是在等宿主通过标准输入和它通信。
为什么日志要写到 stderr,而不是 stdout
如果你使用的是 STDIO 传输,那么:
stdout 是 MCP 协议消息通道,不能随便输出普通日志。
也就是说,下面这种做法是有风险的:
console.log("server started");
因为这类普通日志会混进协议输出,可能直接破坏通信。
所以在 STDIO 模式下,普通日志应该写到:
console.error(...)
也就是 stderr。
这类细节很适合在入门文章里强调一下。因为很多人第一次跑不通,不是 MCP 本身有问题,而是被日志输出干扰了。
四、验证这个 MCP 工具是否真的可用
1. 用 MCP Inspector 验证
在真正接入 Codex 前,最好的做法是先用 MCP Inspector 验证。因为 Inspector 更适合做“工具级调试”。
启动 Inspector
如果你已经编译成 JS,可以这样启动:
npm run build
npx @modelcontextprotocol/inspector node dist/server.js
如果你更习惯直接用 tsx 跑,也可以按自己的本地执行方式配置。
在 Inspector 里重点验证什么
你不需要一上来测试很多场景,先抓住 3 个核心问题:
- Server 是否连接成功
- 是否能看到
fetch_market_data - 这个工具是否真的可以被调用
也就是说,Inspector 阶段的重点不是“功能多强”,而是“工具链路是否成立”。
测试正常输入
输入:
{
"symbol": "BTC"
}
预期返回类似:
{
"id": "bitcoin",
"symbol": "BTC",
"price": 123456,
"currency": "USD",
"source": "coingecko-demo",
"change24h": 1.23,
"lastUpdatedAt": 1712345678
}
其中价格会变化,这正说明你拿到的是实时数据,而不是写死的 mock 数据。
测试异常输入
除了成功场景,建议至少测下面几种失败输入。
空字符串
{
"symbol": ""
}
不支持的 symbol
{
"symbol": "DOGE"
}
缺少字段
{}
这一步的目的不是追求“所有输入都成功”,而是确认下面三件事:
- 合法输入能正常返回
- 非法输入不会把 Server 打崩
- 错误信息对人来说是可理解的
一个入门项目做到这里,其实已经比很多纯演示性质的示例更接近真实开发了。
执行结果证明图
下面这张图是 fetch_market_data 在 MCP Inspector 中连接成功并调用成功的结果。可以看到:
- 左侧已成功连接本地
market-data-server - 工具列表中已经出现
fetch_market_data - 右侧输入
ETH后,工具返回了来自 CoinGecko 的真实市场数据 Tool Result: Success说明这条本地 MCP 调用链路已经跑通

2. 接入 Codex CLI 验证
当 Inspector 验证通过后,再接入 Codex 才更稳。因为这时候你已经知道问题不在工具本身,而只可能在宿主配置或调用方式上。
准备 Codex 配置文件
创建或编辑:
~/.codex/config.toml
加入 MCP Server 配置:
model = "gpt-5.4"
[mcp_servers.market_data]
command = "npx"
args = ["tsx", "/绝对路径/mcp-coingecko-demo/src/server.ts"]
[mcp_servers.market_data.env]
COINGECKO_API_KEY = "your_demo_key_here"
COINGECKO_API_PLAN = "demo"
注意把路径替换成你自己的绝对路径。
启动 Codex
codex
第一次测试时,尽量显式要求调用工具
建议用比较明确的提示词,例如:
Use the MCP tool fetch_market_data with symbol BTC. Return the raw result first, then explain it.
为什么要写得这么明确?因为在验证阶段,你的目标不是“写一个自然对话提示词”,而是先确认宿主是否真的触发了工具调用。
如何判断 Codex 不是在“猜答案”
你可以重点看返回结果里是否出现你自己定义过的字段,比如:
source: coingecko-demoid: bitcoinchange24hlastUpdatedAt
这些字段如果都出现了,通常就可以比较确定:
它不是在凭语言模型常识回答,而是真的通过 MCP 调用了你的工具。
执行结果证明图
下面这张图展示的是把 MCP Server 接入 Codex 后的调用结果。可以看到 Codex 明确调用了:
market_data.fetch_market_data({"symbol":"BTC"})
并返回了你在工具中定义的字段,例如:
id: bitcoinsymbol: BTCpricesource: coingecko-demochange24hlastUpdatedAt
这说明 Codex 并不是在“猜答案”,而是真的通过 MCP 工具拿到了返回结果。

五、总结:跑通这条链路后,你真正掌握了什么
如果前面的步骤都已经跑通,那么你完成的其实不是一个简单 demo,而是一个完整的入门闭环:
- 你写了一个 TypeScript MCP Server
- 你注册了一个真实可用的工具
fetch_market_data - 你接入了第三方 API,而不是只返回静态内容
- 你用 Inspector 验证了工具层是否正常
- 你用 Codex 验证了宿主能否真正调用工具
这一步很重要,因为它意味着你已经跨过了 MCP 入门最难的那道坎:
从“知道概念”进入“能做出一个可验证的工具”。
很多人学到这里,后面再增加第二个工具、替换数据源、接入更多宿主,都会顺很多。
这篇文章本质上只做了一件事:
用一个真实的 CoinGecko 市场数据工具,把 MCP 的完整入门流程跑通。
如果你刚接触 MCP,其实不需要一开始就把所有协议细节都啃透。更重要的是先建立这几个直觉:
- MCP 是一层工具协议
- 业务逻辑仍然是你自己写
- 宿主通过 MCP 发现并调用你的工具
- Inspector 适合做调试验证
- Codex 可以作为真实宿主来消费你的工具能力
只要这条链路你亲手跑通过一次,后面再去扩展更多工具、更多数据源,甚至更完整的 agent 体系,理解都会快很多。