MCP 服务链本地搭建:把 MCPClient 的 BASE_URL 改到 TaoToken 后,mcp.json 不用动
2026/9/17 2:47:22 网站建设 项目流程

1. mcp.json 不用动——真正没着落的是 .env 里的对话通道

自己动手搭一条 MCP 服务链,前面几步其实都很顺:mcp.json 里声明 weather-http 和 amap-amap-sse,MCPClient 也能把工具列表读进来。真正让人卡住的是最后一步。很多教程只丢下一句「需要提前在 .env 文件中设置相关环境变量」,可 API_KEY 去哪申请、BASE_URL 填什么、MODEL 该写哪个,一个字都没提。这篇文章把这段补完:mcp.json 保持原样,打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 创建 API Key,把对话通道指到 TaoToken。

MCP 社区现在已经很成熟,官方和第三方提供了大量现成的 MCP 服务器,想调哪个就调哪个。但如果你和我一样喜欢钻研,看完现成实现总会冒出那个问题:能不能自己写一个客户端?答案是可以,而且代码量不大。真正写起来你就会发现,MCP 侧的工具发现、连接、调用都有人帮你封装好了,最难的反而是初始化大模型客户端那三行——API_KEY、BASE_URL、MODEL,缺一个就报错。

这三个变量为什么要单独准备?因为 MCP 只是「工具调度层」,负责把 weather-http 这样的本地服务和 amap-amap-sse 这样的远程服务接到你的程序里;而真正理解你提问、决定是否调用工具、把结果组织成人话的,是背后的大模型。那部分请求走的是 OpenAI 兼容协议,需要一个独立的 API 通道。TaoToken 就是专门为这类场景准备的统一接入通道,Key 从 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 创建,Base URL 填 https://taotoken.net/api 即可。

2. 先看完整链路,再决定动哪个文件

这条链路从下往上分成四层。最底层是两个 MCP 服务:weather-http 是你自己起的本地程序,监听 127.0.0.1:8002;amap-amap-sse 是高德地图的远程 MCP 服务,走 SSE 协议。往上一层是 mcp.json,它只负责告诉客户端「有哪些服务、用什么协议、地址是什么」。再往上是 MCPClient,它读取配置、建立连接、把工具列表注入系统提示词。最上面才是大模型对话通道,也就是 OpenAI 客户端初始化的部分。

搞清楚分层之后,该动哪个文件就一目了然了。mcp.json 是服务发现层,里面的 url、type、name 都是描述 MCP 服务的,和对话通道没有任何关系,所以原样保留。MCP_Prompt.txt 是系统提示词,里面放着$MCP_INFO$占位符和工具调用格式说明,也不需要改。需要动的只有 .env——把 API_KEY 换成 TaoToken 的 Key,把 BASE_URL 换成 https://taotoken.net/api,MODEL 按模型广场的实际 ID 填。mcp.json 一行不用改,weather-http 和 amap-amap-sse 的连接逻辑也不会受影响。

3. 第一步:mcp.json 保持原样,weather-http 与 amap-amap-sse 逐个确认

3.1 配置文件与字段说明

先看配置文件本身。这个文件描述了两个 MCP 服务,类型分别是 streamable_http 和 sse:

{ "mcpServers": { "weather-http": { "isActive": true, "type": "streamable_http", "url": "http://127.0.0.1:8002/mcp", "name": "weather-http" }, "amap-amap-sse": { "isActive": true, "type": "sse", "url": "https://mcp.amap.com/sse?key={高德key}", "name": "amap-amap-sse" } } }

四个字段的职责用一个表说清楚:

字段含义本次是否需要改
isActive是否激活该 MCP 服务不需要
typeMCP 服务类型:stdio / sse / streamable_http不需要
url远程或本地服务的地址不需要
name服务别名,用于日志和工具注入标识不需要

3.2 两个服务各自的启动前提

这里有两个容易踩的坑,和 TaoToken 无关,但会影响验证。第一个,amap 的 url 里带着{高德key}占位符,这是高德开放平台申请的 Key,和 TaoToken 的 API Key 是两回事,别混在一起。第二个,weather-http 的 type 是 streamable_http,这个类型要求 8002 端口上真的有服务在监听。如果跑验证时报 ConnectionError,先确认本地服务是否启动,而不是急着去改 mcp.json。

4. 第二步:去 TaoToken 创建 Key,把 .env 的 BASE_URL 指过去

4.1 申请 Key 与确认模型 ID

原文第 3 步只有一句话:「需要提前在 .env 文件中设置相关环境变量」。这句话对第一次搭链路的开发者来说信息量几乎为零。这里给出可落地的操作:打开 TaoToken 注册账号,在控制台创建一个 API Key,然后去模型广场确认你要用的模型 ID(以广场当时列表为准,不要凭记忆填日期后缀)。接下来在项目根目录创建 .env 文件:

API_KEY=YOUR_API_KEY BASE_URL=https://taotoken.net/api MODEL=

4.2 .env 的参数对照

变量来源注意事项
API_KEYTaoToken 控制台创建用占位符 YOUR_API_KEY 时代码会报 401,换成真实 Key
BASE_URL固定填 https://taotoken.net/api末尾不要加 /v1,也不要加任何 UTM 参数
MODEL模型广场模型 ID 以实际列表为准,不能自己猜

这里最容易出的错是把 Base URL 填成官网地址。官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 是给人注册、看模型、看用量用的;填进 .env 的接口地址必须是 https://taotoken.net/api,这两个地址分工不同,混了就会连不上。TaoToken 的定位是统一 API 兼容通道,所有消耗 Token 的请求都走这个接口,而 MCP 服务本身仍然走 mcp.json 里原来的 url。

5. 第三步:MCPClient 读取 .env,用 OpenAIClient 走 TaoToken

5.1 核心代码

MCPClient 的核心改动就在__init__里:不再硬编码任何模型参数,而是从 .env 加载。下面的版本支持 sse、stdio、streamable_http 三种类型,mcp.json 原文不需要任何调整:

import asyncio import json import os import re from typing import Optional from dotenv import load_dotenv from mcp import ClientSession, StdioServerParameters from mcp.client.sse import sse_client from mcp.client.stdio import stdio_client from mcp.client.streamable_http import streamable_http_client from openai import OpenAI load_dotenv() class MCPClient: def __init__(self): self.session: Optional[ClientSession] = None self.exit_stack = asyncio.AsyncExitStack() self.api_key = os.getenv("API_KEY") self.base_url = os.getenv("BASE_URL") self.model = os.getenv("MODEL") if not self.api_key or not self.base_url or not self.model: raise RuntimeError("请在 .env 中填写 API_KEY、BASE_URL、MODEL") self.client = OpenAI(api_key=self.api_key, base_url=self.base_url) self.sessions = {} self.messages = [] with open("./MCP_Prompt.txt", "r", encoding="utf-8") as f: self.system_prompt = f.read() async def connect_from_config(self, mcp_json_file: str): with open(mcp_json_file, "r", encoding="utf-8") as f: config = json.load(f) for name, cfg in config.get("mcpServers", {}).items(): if not cfg.get("isActive", False): continue server_type = cfg.get("type", "stdio").lower() url = cfg.get("url") try: if server_type == "sse": await self._connect_sse(name, url) elif server_type == "streamable_http": await self._connect_http(name, url) elif server_type == "stdio": await self._connect_stdio(name, cfg.get("command"), cfg.get("args", []), cfg.get("env", {})) else: print(f"{name} 的类型 {server_type} 不受支持,已跳过") except Exception as exc: print(f"{name} 连接失败: {exc}") async def _connect_sse(self, name: str, url: str): read, write = await self.exit_stack.enter_async_context(sse_client(url)) await self._register_session(name, read, write) async def _connect_http(self, name: str, url: str): read, write, _ = await self.exit_stack.enter_async_context(streamable_http_client(url)) await self._register_session(name, read, write) async def _connect_stdio(self, name: str, command: str, args: list, env: dict): params = StdioServerParameters(command=command, args=args, env=env) read, write = await self.exit_stack.enter_async_context(stdio_client(params)) await self._register_session(name, read, write) async def _register_session(self, name: str, read, write): session = await self.exit_stack.enter_async_context(ClientSession(read, write)) await session.initialize() self.sessions[name] = session tools = (await session.list_tools()).tools lines = [f"## {name}", "### Available Tools"] lines += [f"- {t.name}: {t.description}" for t in tools] self.system_prompt = self.system_prompt.replace("$MCP_INFO$", "\n".join(lines) + "\n$MCP_INFO$") print(f"Successfully connected to {name} with tools: {[t.name for t in tools]}") async def ask(self, query: str) -> str: self.messages = [ {"role": "system", "content": self.system_prompt}, {"role": "user", "content": query}, ] answer = self.client.chat.completions.create( model=self.model, max_tokens=1024, messages=self.messages ).choices[0].message.content tag = re.search(r"<use_mcp_tool>.*?</use_mcp_tool>", answer, re.S) if not tag: return answer raw = tag.group(0) server_name = re.search(r"<server_name>(.*?)</server_name>", raw).group(1) tool_name = re.search(r"<tool_name>(.*?)</tool_name>", raw).group(1) tool_args = json.loads(re.search(r"<arguments>(.*?)</arguments>", raw).group(1)) result = await self.sessions[server_name].call_tool(tool_name, tool_args) self.messages.append({"role": "assistant", "content": answer}) self.messages.append({"role": "user", "content": f"[工具 {tool_name} 返回: {result}]"}) return self.client.chat.completions.create( model=self.model, max_tokens=1024, messages=self.messages ).choices[0].message.content async def chat_loop(self): print("MCP Client Started!\n") while True: query = input("Query: ").strip() if query.lower() == "quit": break if not query: continue print(await self.ask(query)) async def close(self): await self.exit_stack.aclose() async def main(): client = MCPClient() try: await client.connect_from_config("./mcp.json") await client.chat_loop() finally: await client.close() if __name__ == "__main__": asyncio.run(main())

5.2 服务端与客户端的职责参考

这段代码里,_connect_http_connect_sse负责和服务端握手,握手成功后调list_tools()拿工具清单。服务端把工具包成 MCP 协议,客户端负责连接与对话,TaoToken 不参与这两步,它只出现在OpenAI(api_key=..., base_url=...)那一行。如果想验证「服务端和客户端到底通没通」,可以直接注释掉chat_loop()之前的初始化逻辑,只跑connect_from_config,看两个服务是否都打印出工具列表。这一步通过之后再谈对话,能省掉很多来回猜的时间。

6. 验证:先看 weather-http 工具注入,再输入 Query

6.1 预期输出

在项目目录运行:

python mcp_client.py

预期输出像这样:

Successfully connected to weather-http with tools: [...] Successfully connected to amap-amap-sse with tools: [...] MCP Client Started! Query:

看到 weather-http 的工具列表被打印出来,说明 mcp.json 的 streamable_http 配置在你当前的 MCPClient 版本里是能用的,本地服务链没有因为 .env 的改动而受影响。这一步是整个改造的底线:mcp.json 原样保留,MCP 服务链照常工作。

6.2 两种 Query 的验证顺序

接下来输入 Query,顺序有讲究。先问一个不依赖工具的问题,比如「用一句话解释 MCP 协议」,这一步只验证 TaoToken 的对话通道通不通。如果返回正常,再问需要调用天气或地图服务的问题,确认工具调用链路也通。这样万一报错,你能立刻判断是 MCP 服务的问题还是大模型通道的问题,而不是两头一起排查。

确认整个链路走的是 TaoToken,最直接的办法是回到 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 的用量页面,看刚才那几条 Query 是否产生了记录。有记录说明 MCPClient 的请求确实发到了 TaoToken,而不是某个本地代理或默认地址。这一步值得做,因为在 .env 里填错 Base URL 时,有些 OpenAI SDK 会静默回退到默认 api.openai.com,这时候对话看起来正常,但 Key 根本不是你想用的那个。

7. 排障:只聊这次改动可能引入的错

如果验证时遇到问题,按下面的顺序排查,别急着改 mcp.json。

7.1 401 Unauthorized:API_KEY 不对

检查 .env 里是否还留着YOUR_API_KEY占位符,或者 Key 复制时多了一个空格。Key 需要从 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 控制台创建,别拿 MCP 服务自己的 Key 来替代。

7.2 Model Not Found:MODEL 填了不存在的 ID

这种报错通常出现在client.chat.completions.create那一步。模型 ID 以 TaoToken 模型广场当时列表为准,代码里任何地方都不要写死模型名,统一从 .env 读。如果刚换过模型,先回 .env 改 MODEL,再重启脚本,不要只改代码里临时传的那个参数。

7.3 ConnectionError:MCP 服务本身没就绪

这个错和 TaoToken 无关。weather-http 是本地服务,8002 端口必须真的有程序在跑;amap-amap-sse 的 url 里如果带着未替换的{高德key}也会连不上。处理方式是回到 mcp.json 检查服务地址和 Key,而不是去动 BASE_URL。

8. 跑通之后,去控制台对一下这次调用

配置保存后,先在 TaoToken 模型对话 里用同一把 Key 发一条消息,确认模型 ID 和 Base URL 都没填错。若打算长期用这套链路写代码,可以看看 Coding Plan 能不能覆盖日常消耗;Key 的统一管理入口在 控制台 API Keys。如果之后想把同一个 Key 用到 Claude Code 里,环境变量对照见 接入文档。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询