从零开始:如何开发一个MCP Client
手把手教你如何使用Cursor工具快速、高效开发一个天气 MCP Client 服务,并调用本地MCP Server服务。

上一篇文章中,我们介绍了如何开发一个MCP Server,具体细节详情可见【从零开始:如何开发一个MCP Server】,同时也在Cursor中集成使用了。
今天主要介绍如何开发一个MCP Client,仍以天气服务为例,构建一个由LLM-驱动的聊天机器人客户端,该客户端能够与本地的天气MCP服务集成,以此来深入了解下,MCP客户端与MCP服务端之间是如何交互的。
通过这次分享,你可以收获到:
- 一个简单的MCP Client 开发框架,灵活的LLM服务提供商架构,可插拔的工厂设计模式,通过命令行参数轻松切换不同的LLM服务,以便后续扩展新的LLM服务提供商
- 只需提供MCP工具的json配置文件,类似Cursor的MCP配置操作,即可完成工具的动态发现和调用,快速集成MCP服务的使用
- 好记性不如烂笔头,站在岸上学不会游泳,通过代码实践,深刻理解MCP的开发交互过程
一、开发环境要求
- Node.js >= 16.0.0
- npm >= 8.0.0
- TypeScript >= 4.5.0
- MacOS:15.3
- Cursor版本: 0.47.8
二、搭建基础开发框架
良好的开发框架往往能让你获取到意想不到的开发效率。
项目github地址:GitHub - fist-maestro/mcp-client-typescript
1、项目结构说明
```
├ mcp-client-typescript/
├── src/ # 源代码目录
│ ├── index.ts # 应用程序入口文件(程序启动配置、命令行参数处理、服务初始化)
│ ├── controllers/ # 控制器层:处理用户交互(用户输入处理、会话管理、响应展示)
│ │ └── ChatController.ts # 聊天控制器
│ ├── services/ # 服务层:核心业务逻辑
│ │ ├── ManagerService.ts # 服务管理器(MCP服务协调、工具调用管理、LLM服务路由)
│ │ ├── ManagerInfa.ts # 服务管理器接口定义
│ │ └── llm-providers/ # LLM服务提供商实现
│ │ ├── DeepSeekService.ts # DeepSeek服务
│ │ └── AnthropicService.ts # Anthropic服务
│ └── common/ # 公共代码
│ ├── config/ # 配置管理
│ │ ├── llm-config/ # LLM服务配置
│ │ │ ├── deepseek.ts # DeepSeek配置
│ │ │ └── anthropic.ts # Anthropic配置
│ │ └── mcp-servers-config/ # MCP服务器配置
│ │ └── weather.json # 天气服务配置
│ ├── types/ # 类型定义
│ │ └── tool.ts # 工具相关接口和类型
│ └── utils/ # 工具函数
│ ├── logger.ts # 日志工具
│ └── cityNameMap.ts # 城市名称映射
│ ├── package.json # 项目配置文件:依赖管理、脚本命令
│ ├── package-lock.json # 依赖版本锁定文件
│ ├── tsconfig.json # TypeScript编译配置
│ ├── .gitignore # Git忽略规则配置
│ └── README.md # 项目说明文档
```
2、代码分层架构
框架核心采用四层架构设计:表示层、服务层、工具层、基础设施层
2.1. 表示层(Presentation Layer)
- ChatController
- 处理用户输入输出
- 管理交互会话
- 格式化响应数据
- 职责:用户交互界面的管理和展示
2.2. 服务层(Service Layer)
- ManagerService
- 服务生命周期管理
- 服务间通信协调
- 工具调用管理
- LLM服务提供商
- DeepSeek服务实现
- Anthropic服务实现
- 职责:业务逻辑的核心处理
2.3. 工具层(Tool Layer)
- 外部工具服务
- 天气查询服务
- 工具调用接口
- 职责:提供具体的功能实现
2.4. 基础设施层(Infrastructure Layer)
- 配置管理
- LLM服务配置
- MCP服务器配置
- 工具函数
- 日志记录
- 城市名称映射
- 职责:提供基础支持服务
三、自建MCP Client
接下来我们基于以上开发框架快速新建一个MCP Client,并调用本地的Weather MCP Server。
1、基础设施层开发
首先开发基础设施层,为整个应用提供基础支持:
1.1、配置管理
-
LLM配置管理
以deepseek为例,一个LLM服务商为一个配置文件,方便维护和管理
// /mcp-client-typescript/src/common/config/llm-config/deepseek.ts
export interface DeepSeekConfig {
apiKey: string;
model: string;
maxTokens: number;
apiBase: string;
}
export const defaultDeepSeekConfig: DeepSeekConfig = {
apiKey: '此处填写你的API Key',
model: 'deepseek-chat',
maxTokens: 2048,
apiBase: 'https://api.deepseek.com/v1'
};
-
MCP Server json配置管理
以本地的Weather MCP Server 为例,一个MCP Server 为一个json配置文件
// /mcp-client-typescript/src/common/config/mcp-servers-config/weather.json
{
"weather": {
"command": "/opt/homebrew/bin/node",
"args": [
"/Users/xxx/webser/mcp-servers/build/weather/index.js"
],
"env": {
"OPENWEATHER_API_KEY": "此处填写你的API Key"
}
}
}
1.2、日志Logger工具类实现
按天维度记录系统调用日志信息
// /mcp-client-typescript/src/common/utils/logger.ts
import fs from 'fs';
import path from 'path';
export class Logger {
private logDir: string;
private currentDate: string;
private logStream: fs.WriteStream | null;
constructor(logDir: string = 'logs') {
this.logDir = logDir;
this.currentDate = this.getFormattedDate();
this.logStream = null;
this.initLogStream();
}
private getFormattedDate(): string {
const now = new Date();
return `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`;
}
private initLogStream(): void {
if (!fs.existsSync(this.logDir)) {
fs.mkdirSync(this.logDir, { recursive: true });
}
const logFile = path.join(this.logDir, `${this.currentDate}.log`);
this.logStream = fs.createWriteStream(logFile, { flags: 'a' });
}
private formatMessage(level: string, message: string): string {
const timestamp = new Date().toISOString();
return `[${timestamp}] [${level}] ${message}\n`;
}
public info(message: string): void {
const formattedMessage = this.formatMessage('INFO', message);
console.log(formattedMessage.trim());
this.logStream?.write(formattedMessage);
}
public error(message: string): void {
const formattedMessage = this.formatMessage('ERROR', message);
console.error(formattedMessage.trim());
this.logStream?.write(formattedMessage);
}
public close(): void {
this.logStream?.end();
}
}
2、MCP Server tools 开发
定义统一的tools工具类,用于规范化不同的MCP Server 提供的tools 信息,以便在LLM上下文进行统一交互。
2.1、定义
// /mcp-client-typescript/src/common/types/tool.ts
export interface Tool {
name: string;
description: string;
input_schema: Record<string, any>;
}
2.2、实现
// src/services/ManagerService.ts
export class ManagerService implements MCPManager {
async connectToServer(serverName: string): Promise<Tool[]> {
try {
// ...
const toolsResult = await this.mcp.listTools();
this.tools = toolsResult.tools.map((tool) => ({
name: tool.name,
description: tool.description || '无描述',
input_schema: tool.inputSchema,
}));
// ...
} catch (error) {
this.logger.error(`Error connecting to server ${serverName}: ${error}`);
throw error;
}
}
}
3、服务层开发
服务层实现核心业务逻辑,采用工厂设计模式,将核心工作流程进行抽象,具体实现是由具体的LLM Service实现类来处理
3.1、核心service管理
-
核心接口定义
// src/services/ManagerInfa.ts
export interface MCPManager {
loadServers(): Promise<void>;
connectToServer(serverName: string): Promise<Tool[]>;
processQuery(query: string): Promise<string>;
cleanup(): Promise<void>;
}
-
核心接口实现
// src/services/ManagerService.ts
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { Tool } from '../common/types/tool.js';
import fs from 'fs/promises';
import path from 'path';
import { MCPManager, MCPServerConfigs, LLMProvider } from './ManagerInfa.js';
import { DeepSeekService } from './llm-providers/DeepSeekService.js';
import { AnthropicService } from './llm-providers/AnthropicService.js';
import { Logger } from '../common/utils/logger.js';
import { translateToolArguments } from '../common/utils/cityNameMap.js';
export class ManagerService implements MCPManager {
private mcp: Client;
private transport: StdioClientTransport | null = null;
private tools: Tool[] = [];
private llmProvider: LLMProvider;
private logger: Logger;
private serverConfigs: MCPServerConfigs = {};
constructor(provider: string = 'deepseek') {
this.mcp = new Client({ name: 'mcp-client-cli', version: '1.0.0' });
// 根据provider参数选择LLM服务商
switch (provider.toLowerCase()) {
case 'deepseek':
this.llmProvider = new DeepSeekService();
break;
case 'anthropic':
this.llmProvider = new AnthropicService();
break;
default:
throw new Error(`不支持的LLM服务商: ${provider}`);
}
this.llmProvider.setMCPClient(this.mcp);
this.logger = new Logger();
}
/**
* 获取所有已加载的服务器名称
* @returns 服务器名称数组
*/
getServerNames(): string[] {
return Object.keys(this.serverConfigs);
}
async loadServers(): Promise<void> {
// ...
}
async connectToServer(serverName: string): Promise<Tool[]> {
// ...
}
private async callTool(name: string, args: any): Promise<any> {
// ...
}
async processQuery(query: string): Promise<string> {
// ...
}
async cleanup(): Promise<void> {
// ...
}
setLLMProvider(provider: LLMProvider): void {
this.llmProvider = provider;
this.llmProvider.setMCPClient(this.mcp);
}
}
-
动态加载MCP Server 配置
// src/services/ManagerService.ts
async loadServers(): Promise<void> {
try {
const configDir = path.join(process.cwd(), 'src/common/config/mcp-servers-config');
const files = await fs.readdir(configDir);
for (const file of files) {
if (file.endsWith('.json')) {
const configContent = await fs.readFile(path.join(configDir, file), 'utf-8');
const config = JSON.parse(configContent);
this.serverConfigs = { ...this.serverConfigs, ...config };
}
}
this.logger.info(`Loaded server configs: ${Object.keys(this.serverConfigs).join(', ')}`);
} catch (error) {
this.logger.error(`Error loading server configs: ${error}`);
throw error;
}
}
-
连接MCP Server,获取tools
// src/services/ManagerService.ts
async connectToServer(serverName: string): Promise<Tool[]> {
try {
const config = this.serverConfigs[serverName];
if (!config) {
throw new Error(`Server configuration not found for: ${serverName}`);
}
this.transport = new StdioClientTransport({
command: config.command,
args: config.args,
env: config.env,
});
this.mcp.connect(this.transport);
const toolsResult = await this.mcp.listTools();
this.tools = toolsResult.tools.map((tool) => ({
name: tool.name,
description: tool.description || '无描述',
input_schema: tool.inputSchema,
}));
this.logger.info(`Connected to server ${serverName} with tools: ${this.tools.map(t => t.name).join(', ')}`);
return this.tools;
} catch (error) {
this.logger.error(`Error connecting to server ${serverName}: ${error}`);
throw error;
}
}
-
抽象不同的LLM服务商进行上下文交互
// src/services/ManagerService.ts
async processQuery(query: string): Promise<string> {
try {
const response = await this.llmProvider.processQuery(query, this.tools);
this.logger.info(`Processed query: ${query}`);
return response;
} catch (error) {
this.logger.error(`Error processing query: ${error}`);
throw error;
}
}
async cleanup(): Promise<void> {
if (this.transport) {
await this.mcp.close();
}
this.logger.close();
}
setLLMProvider(provider: LLMProvider): void {
this.llmProvider = provider;
this.llmProvider.setMCPClient(this.mcp);
}
3.2、LLM服务实现
-
LLM服务接口定义
// src/services/ManagerInfa.ts
export interface LLMProvider {
setMCPClient(client: Client): void;
processQuery(query: string, tools: Tool[]): Promise<string>;
cleanup(): Promise<void>;
}
-
LLM服务接口实现
// src/services/llm-providers/DeepSeekService.ts
import { Tool } from '../../common/types/tool.js';
import { LLMProvider } from '../ManagerInfa.js';
import { defaultDeepSeekConfig } from '../../common/config/llm-config/deepseek.js';
import { Logger } from '../../common/utils/logger.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { translateToolArguments } from '../../common/utils/cityNameMap.js';
export class DeepSeekService implements LLMProvider {
private logger: Logger;
private mcp: Client | null = null;
constructor() {
this.logger = new Logger();
}
setMCPClient(client: Client): void {
this.mcp = client;
}
async processQuery(query: string, tools: Tool[]): Promise<string> {
if (!this.mcp) {
throw new Error('MCP client not set');
}
try {
const response = await fetch(`${defaultDeepSeekConfig.apiBase}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${defaultDeepSeekConfig.apiKey}`,
},
body: JSON.stringify({
model: defaultDeepSeekConfig.model,
messages: [
{
role: 'system',
content: '你是一个天气助手,可以帮助用户查询天气信息。请使用提供的工具来获取天气数据,并用中文自然语言回复用户。'
},
{
role: 'user',
content: query
}
],
max_tokens: defaultDeepSeekConfig.maxTokens,
tools: tools.map(tool => ({
type: 'function',
function: {
name: tool.name,
description: tool.description,
parameters: tool.input_schema
}
}))
}),
});
if (!response.ok) {
throw new Error(`DeepSeek API error: ${response.statusText}`);
}
const data = await response.json();
// 检查是否有工具调用
if (data.choices[0].message.tool_calls) {
const toolCalls = data.choices[0].message.tool_calls;
this.logger.info(`[DeepSeek] 调用工具: ${toolCalls.map((t: any) => `weather服务的${t.function.name}`).join(', ')}`);
// 执行所有工具调用
const toolResults = await Promise.all(
toolCalls.map(async (toolCall: any) => {
if (!this.mcp) {
throw new Error('MCP client not set');
}
// 转换参数中的城市名
const args = JSON.parse(toolCall.function.arguments);
const originalCity = args.city; // 保存原始城市名
const translatedArgs = translateToolArguments(args);
if (originalCity !== translatedArgs.city) {
this.logger.info(`[DeepSeek] 参数转换: ${originalCity} -> ${translatedArgs.city}`);
}
const result = await this.mcp.callTool({
name: toolCall.function.name,
arguments: translatedArgs
});
// 记录MCP server的返回结果
this.logger.info(`[DeepSeek] weather服务的${toolCall.function.name}调用完成`);
return {
role: 'tool',
content: JSON.stringify(result),
tool_call_id: toolCall.id
};
})
);
// 发送第二次请求,包含工具调用结果
const secondResponse = await fetch(`${defaultDeepSeekConfig.apiBase}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${defaultDeepSeekConfig.apiKey}`,
},
body: JSON.stringify({
model: defaultDeepSeekConfig.model,
messages: [
{
role: 'system',
content: '你是一个天气助手,可以帮助用户查询天气信息。请使用提供的工具来获取天气数据,并用中文自然语言回复用户。'
},
{
role: 'user',
content: query
},
data.choices[0].message,
...toolResults
],
max_tokens: defaultDeepSeekConfig.maxTokens
}),
});
if (!secondResponse.ok) {
throw new Error(`DeepSeek API error: ${secondResponse.statusText}`);
}
const secondData = await secondResponse.json();
return secondData.choices[0].message.content;
}
return data.choices[0].message.content;
} catch (error) {
this.logger.error(`Error processing query with DeepSeek: ${error}`);
throw error;
}
}
async cleanup(): Promise<void> {
this.logger.close();
}
}
4、控制层开发
4.1、服务初始化和连接
// src/controllers/ChatController.ts
import readline from 'readline/promises';
import { ManagerService } from '../services/ManagerService.js';
import { Logger } from '../common/utils/logger.js';
export class ChatController {
private manager: ManagerService;
private logger: Logger;
constructor(provider: string = 'deepseek') {
this.manager = new ManagerService(provider);
this.logger = new Logger();
}
async initialize(): Promise<void> {
try {
await this.manager.loadServers();
// 获取所有已加载的服务器名称
const serverNames = this.manager.getServerNames();
this.logger.info(`Found servers: ${serverNames.join(', ')}`);
// 连接所有服务器
for (const serverName of serverNames) {
try {
await this.manager.connectToServer(serverName);
this.logger.info(`Successfully connected to server: ${serverName}`);
} catch (error) {
this.logger.error(`Failed to connect to server ${serverName}: ${error}`);
// 继续尝试连接其他服务器,而不是直接失败
}
}
this.logger.info('Chat controller initialized successfully');
} catch (error) {
this.logger.error(`Error initializing chat controller: ${error}`);
throw error;
}
}
async startChat(): Promise<void> {
// ...
}
private async cleanup(): Promise<void> {
// ...
}
}
4.2、聊天交互方法
// src/controllers/ChatController.ts
import readline from 'readline/promises';
import { ManagerService } from '../services/ManagerService.js';
import { Logger } from '../common/utils/logger.js';
export class ChatController {
private manager: ManagerService;
private logger: Logger;
constructor(provider: string = 'deepseek') {
this.manager = new ManagerService(provider);
this.logger = new Logger();
}
async initialize(): Promise<void> {
// ...
}
async startChat(): Promise<void> {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
try {
console.log('\nMCP Client Started!');
console.log('Type your queries or "quit" to exit.');
while (true) {
const query = await rl.question('\nQuery: ');
if (query.toLowerCase() === 'quit') {
break;
}
try {
const response = await this.manager.processQuery(query);
console.log('\n' + response);
} catch (error) {
console.error(`Error processing query: ${error}`);
this.logger.error(`Error processing query: ${error}`);
}
}
} finally {
rl.close();
await this.cleanup();
}
}
private async cleanup(): Promise<void> {
// ...
}
}
4.3、资源清理方法
// src/controllers/ChatController.ts
import readline from 'readline/promises';
import { ManagerService } from '../services/ManagerService.js';
import { Logger } from '../common/utils/logger.js';
export class ChatController {
private manager: ManagerService;
private logger: Logger;
constructor(provider: string = 'deepseek') {
this.manager = new ManagerService(provider);
this.logger = new Logger();
}
async initialize(): Promise<void> {
// ...
}
async startChat(): Promise<void> {
// ...
}
private async cleanup(): Promise<void> {
await this.manager.cleanup();
this.logger.close();
}
}
5、入口文件实现
最后,实现应用程序入口文件,将所有层次整合在一起
// src/index.ts
import { ChatController } from './controllers/ChatController.js';
import { parseArgs } from 'node:util';
async function main() {
try {
// 解析命令行参数
const { values } = parseArgs({
options: {
provider: {
type: 'string',
short: 'p',
default: 'deepseek'
}
}
});
const provider = values.provider?.toLowerCase();
if (provider !== 'deepseek' && provider !== 'anthropic') {
throw new Error('不支持的LLM服务商。请使用 --provider=deepseek 或 --provider=anthropic');
}
const controller = new ChatController(provider);
await controller.initialize();
await controller.startChat();
} catch (error) {
console.error('Error:', error);
process.exit(1);
}
}
main();
6、启动服务

7、运行结果

至此,一个完整的MCP Client 就开发完了,只要一步即可添加其他MCP Server,实现本地Client调用MCP Server,如果是想要集成其他LLM,比如通义千文,也仅需两步即可,添加LLM接口配置和实现指定service接口即可。
四、MCP Client 开发总结
经过上述开发过程,我们稍微总结下,整个过程大致分为两个核心要点:
-
三个角色
-
六步交互
三个角色
在 MCP 开发范式中,主要涉及三个角色之间的协同工作:
-
MCP Client:接受用户输入,与 LLM 交互,发起请求并接收响应。
-
LLM:处理 MCP Client 的请求,进行逻辑推理并选择合适的 MCP Server 和 MCP Tool。
-
MCP Server:提供具体的tools工具和功能,执行实际的任务。
六步交互
基于 MCP 的调用过程分为六个核心步骤:
1、用户提问
用户通过界面向 MCP Client输入查询,例如"深圳天气怎么样?"。此时,MCP Client 会接收这个输入,并将查询内容连同已配置的MCP Tools信息一起,传递给选定的 LLM 服务提供商。
2、LLM 推理分析
LLM 服务提供商接收到查询后,会分析用户的问题和 MCP Server 信息,识别并筛选出最合适的 MCP Server 和 MCP Tool 来解决问题,包括工具名称和所需参数,并将结果反馈给MCP Client。
3、工具调用
MCP Client收到 LLM 的建议后,会调用MCP Server提供的MCP Tools工具。
4、结果返回
MCP Server处理请求后,会返回具体结果给到MCP Client。
5、内容整合
MCP Client 会将原始的用户查询和MCP Server返回的数据再次发送给 LLM 服务提供商,目的是让 LLM 将专业的天气数据转换成用户友好的自然语言描述。LLM 会考虑用户的原始问题,组织一个完整、易懂的回答。
6、最终响应
LLM 将整合后的自然语言响应,返回给MCP Client,并在界面上展示给用户。
在整个 MCP 调用过程中,MCP Server 及 MCP Tool 的信息至关重要。从第一步和第二步可以看出,这些信息为 LLM 提供了解决问题的关键线索。这些信息本质上就是 MCP 中的 System Prompt(系统提示词),其核心作用是为 LLM 提供清晰的指导,帮助其更好地理解用户需求并选择合适的工具来解决问题。
五、相关资源
- MCP Client 官方开发指南:For Client Developers - Model Context Protocol
- 项目github地址:GitHub - fist-maestro/mcp-client-typescript
更多资源获取,猛戳这里领取👇👇👇:
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐

所有评论(0)