MCP构建AI应用学习笔记(6)
理解 MCP 客户端的核心价值:“翻译用户操作与服务器 API,降低使用门槛”;掌握两种客户端的开发流程:桌面客户端(PyQt)的 GUI 组件与异步线程,Web 客户端(Vue)的 API 请求与响应式界面;学会客户端与服务器的联调技巧:解决跨域、会话 ID 一致性、异步处理等关键问题。
MCP 客户端开发:让用户轻松玩转远程 AI 服务
一、MCP 客户端的核心定位:连接用户与服务器的 “桥梁”
前序章节搭建的 MCP 服务器已具备多用户远程访问能力,但仅通过 API 接口(如 Postman 调用)无法满足普通用户的使用需求 —— 用户需要直观、易用的交互界面,无需了解 “API 参数”“请求格式” 等技术细节。MCP 客户端的核心价值,就是为用户提供可视化操作界面,将服务器的 “文档上传、提问、答案导出” 等功能转化为 “点击按钮、选择文件、输入文本” 等简单操作,彻底降低 MCP 服务的使用门槛。
简单来说,MCP 客户端的作用是 “翻译”:把用户的直观操作(如点击 “上传文档”)翻译成服务器能理解的 API 请求,再把服务器返回的 JSON 数据(如答案内容)翻译成用户能看懂的界面展示(如格式化文本、可下载按钮),实现 “用户无需技术背景,也能流畅使用 AI 服务”。
二、MCP 客户端技术选型:兼顾易用性与跨平台
课程针对不同用户场景(“本地桌面使用”“网页在线访问”)提供两种技术方案,平衡开发效率、用户体验与跨平台能力:
客户端类型 | 技术栈 | 核心优势 | 适用场景 |
---|---|---|---|
桌面客户端 | PyQt6(GUI 框架)+ Requests(API 请求库) | 1. 跨平台(Windows/macOS/Linux);2. 支持本地文件操作(如直接选择本地 PDF);3. GUI 组件丰富(可实现复杂交互,如进度条、弹窗提示) | 需频繁使用本地文件、偏好桌面应用的用户(如企业内部员工) |
Web 客户端 | Vue3(前端框架)+ Axios(HTTP 请求库)+ Tailwind CSS(样式库) | 1. 无需安装,通过浏览器访问(跨设备:PC / 手机);2. 轻量化,加载速度快;3. 样式美观,易适配不同屏幕尺寸 | 需多设备访问、无需安装应用的场景(如外部客户、临时用户) |
两种方案均遵循 “最小依赖” 原则:桌面客户端无需用户额外安装 Python 环境(可打包为 exe/mac 应用),Web 客户端无需用户配置任何环境(打开浏览器即可使用)。
三、MCP 客户端核心功能设计:对齐服务器 API 能力
客户端功能需严格对齐 MCP 服务器的 API 接口(文档上传、提问、答案导出、会话管理),同时增加 “用户体验增强功能”(如历史对话记录、错误提示、加载动画)。核心功能模块与交互流程如下:
1. 核心功能模块
功能模块 | 用户操作流程 | 背后技术逻辑 |
---|---|---|
会话初始化 | 打开客户端后,自动生成 / 输入 “会话 ID” | 1. 首次使用自动生成唯一会话 ID(如 “user_8f2d7c”);2. 已使用过可手动输入历史会话 ID(恢复之前的文档与对话);3. 会话 ID 关联所有用户数据(确保与服务器数据匹配) |
文档上传 | 点击 “上传文档”→选择本地 PDF/Word→等待上传完成 | 1. 通过 Requests/Axios 调用服务器/api/upload 接口;2. 显示上传进度(基于文件大小与已上传字节数计算);3. 上传成功后显示 “文档解析完成” 提示,列出文档名称与页数 |
提问交互 | 输入问题→点击 “提交”→查看答案 | 1. 校验输入(如问题非空);2. 调用服务器/api/answer 接口,携带 “会话 ID、文档名称、问题内容”;3. 显示 “加载中” 动画;4. 接收服务器返回的格式化答案,在界面展示(区分用户问题与 AI 答案) |
答案导出 | 点击 “导出对话”→选择保存路径→下载文件 | 1. 调用服务器/api/export 接口,获取导出文件的下载 URL;2. 通过浏览器下载(Web 客户端)或本地文件写入(桌面客户端);3. 提示 “导出成功” 并显示文件保存路径 |
历史对话管理 | 滚动查看 “历史对话” 面板→点击可重新编辑问题 | 1. 本地缓存近期对话记录(避免频繁请求服务器);2. 支持 “清空历史”(仅清空本地缓存,服务器数据保留);3. 点击历史问题可自动填充到输入框,方便修改后重新提交 |
2. 关键用户体验增强功能
- 加载状态提示:所有 API 请求(上传、提问、导出)过程中显示 “加载动画”(如旋转图标、进度条),避免用户误以为 “操作未响应”;
- 错误提示:请求失败时(如服务器连接超时、文档格式不支持),弹出清晰的错误原因(如 “服务器连接失败,请检查网络”“仅支持 PDF/Word 文件”),而非技术报错(如 “500 Internal Server Error”);
- 对话格式化展示:用不同颜色 / 样式区分 “用户问题”(如蓝色背景)与 “AI 答案”(如白色背景),答案中保留 “参考文档、页数” 等关键信息,提升可读性;
- 响应式适配:Web 客户端支持手机 / PC 屏幕适配(如手机端隐藏侧边历史面板,点击按钮展开;PC 端显示双栏布局:左侧历史对话,右侧输入与答案)。
四、分步开发:两种客户端的实现示例
方案 1:桌面客户端(PyQt6)实现
1. 环境准备与依赖安装
bash
# 安装PyQt6(GUI框架)、Requests(API请求)、PyInstaller(打包工具,可选)
pip install pyqt6 requests pyinstaller
2. 核心代码实现(关键模块)
python
import sys
import requests
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QLineEdit, QPushButton, QTextEdit,
QFileDialog, QLabel, QProgressBar)
from PyQt6.QtCore import QThread, pyqtSignal # 用于异步处理API请求(避免界面卡顿)
# MCP服务器基础URL(需替换为实际服务器地址)
SERVER_URL = "http://127.0.0.1:8000"
class MCPClient(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("MCP AI问答客户端")
self.setGeometry(100, 100, 800, 600) # 窗口位置与大小
# 初始化会话ID(首次使用自动生成,可手动修改)
self.session_id = "user_" + str(hash(str(sys.time()))) # 简单生成唯一ID
self.current_doc_name = None # 当前选中的文档名称
# 初始化界面组件
self.init_ui()
def init_ui(self):
# 中心部件与主布局
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
# 1. 会话ID模块(输入框+标签)
session_layout = QHBoxLayout()
session_label = QLabel("会话ID:")
self.session_input = QLineEdit(self.session_id)
session_layout.addWidget(session_label)
session_layout.addWidget(self.session_input)
main_layout.addLayout(session_layout)
# 2. 文档上传模块(按钮+文档信息标签)
upload_layout = QHBoxLayout()
self.upload_btn = QPushButton("上传文档(PDF/Word)")
self.upload_btn.clicked.connect(self.choose_file) # 绑定文件选择事件
self.doc_info_label = QLabel("未选择文档")
upload_layout.addWidget(self.upload_btn)
upload_layout.addWidget(self.doc_info_label)
main_layout.addLayout(upload_layout)
# 3. 上传进度条(默认隐藏)
self.upload_progress = QProgressBar()
self.upload_progress.setVisible(False)
main_layout.addWidget(self.upload_progress)
# 4. 对话展示区域(文本编辑框,不可编辑)
self.chat_display = QTextEdit()
self.chat_display.setReadOnly(True)
main_layout.addWidget(self.chat_display)
# 5. 提问输入与提交模块(输入框+按钮)
input_layout = QHBoxLayout()
self.question_input = QLineEdit()
self.question_input.setPlaceholderText("请输入您的问题...")
self.submit_btn = QPushButton("提交")
self.submit_btn.clicked.connect(self.send_question) # 绑定提问事件
input_layout.addWidget(self.question_input)
input_layout.addWidget(self.submit_btn)
main_layout.addLayout(input_layout)
# 6. 导出按钮
self.export_btn = QPushButton("导出当前对话")
self.export_btn.clicked.connect(self.export_chat) # 绑定导出事件
main_layout.addWidget(self.export_btn)
# 选择本地文件并上传
def choose_file(self):
# 打开文件选择对话框,仅允许PDF/Word
file_path, _ = QFileDialog.getOpenFileName(
self, "选择文档", "", "Documents (*.pdf *.docx)"
)
if not file_path:
return # 用户取消选择
# 获取文件名(用于后续API请求)
self.current_doc_name = file_path.split("/")[-1]
# 启动异步线程上传文件(避免界面卡顿)
self.upload_thread = UploadThread(
server_url=SERVER_URL,
session_id=self.session_input.text(),
file_path=file_path
)
# 绑定线程信号(进度更新、上传完成)
self.upload_thread.progress_signal.connect(self.update_upload_progress)
self.upload_thread.result_signal.connect(self.upload_result_handler)
self.upload_thread.start()
# 显示进度条
self.upload_progress.setVisible(True)
self.upload_progress.setValue(0)
# 更新上传进度
def update_upload_progress(self, progress):
self.upload_progress.setValue(progress)
# 处理上传结果
def upload_result_handler(self, result):
self.upload_progress.setVisible(False)
if result["status"] == "success":
# 显示文档信息
doc_name = result["data"]["doc_name"]
page_count = result["data"]["page_count"]
self.doc_info_label.setText(f"已上传:{doc_name}({page_count}页)")
# 提示上传成功
self.chat_display.append(f"【系统提示】文档 {doc_name} 上传并解析成功,可开始提问!")
else:
# 显示错误信息
error_msg = result["error"]
self.chat_display.append(f"【错误】文档上传失败:{error_msg}")
# 发送问题到服务器
def send_question(self):
question = self.question_input.text().strip()
if not question:
self.chat_display.append("【错误】请输入问题后再提交!")
return
if not self.current_doc_name:
self.chat_display.append("【错误】请先上传文档再提问!")
return
# 清空输入框
self.question_input.clear()
# 在对话区显示用户问题
self.chat_display.append(f"【您】:{question}")
self.chat_display.append("【系统提示】正在生成答案...")
# 启动异步线程发送提问请求
self.question_thread = QuestionThread(
server_url=SERVER_URL,
session_id=self.session_input.text(),
doc_name=self.current_doc_name,
question=question
)
self.question_thread.result_signal.connect(self.question_result_handler)
self.question_thread.start()
# 处理提问结果
def question_result_handler(self, result):
# 先删除“正在生成答案”的提示
current_text = self.chat_display.toPlainText()
current_text = current_text.replace("【系统提示】正在生成答案...", "")
self.chat_display.setPlainText(current_text)
if result["status"] == "success":
answer = result["data"]["formatted_answer"]
self.chat_display.append(f"【AI】:{answer}\n")
else:
error_msg = result["error"]
self.chat_display.append(f"【错误】答案生成失败:{error_msg}\n")
# 导出对话
def export_chat(self):
session_id = self.session_input.text()
# 选择导出路径
save_path, _ = QFileDialog.getSaveFileName(
self, "保存对话", "", "Markdown Files (*.md)"
)
if not save_path:
return
# 启动异步线程导出
self.export_thread = ExportThread(
server_url=SERVER_URL,
session_id=session_id,
save_path=save_path
)
self.export_thread.result_signal.connect(self.export_result_handler)
self.export_thread.start()
self.chat_display.append("【系统提示】正在导出对话...")
# 处理导出结果
def export_result_handler(self, result):
if result["status"] == "success":
self.chat_display.append(f"【系统提示】对话已成功导出至:{result['data']['save_path']}\n")
else:
self.chat_display.append(f"【错误】对话导出失败:{result['error']}\n")
# 异步上传线程(避免界面卡顿)
class UploadThread(QThread):
progress_signal = pyqtSignal(int) # 进度信号(0-100)
result_signal = pyqtSignal(dict) # 结果信号(服务器返回的JSON)
def __init__(self, server_url, session_id, file_path):
super().__init__()
self.server_url = server_url
self.session_id = session_id
self.file_path = file_path
def run(self):
try:
url = f"{self.server_url}/api/upload"
# 读取文件并构造请求
with open(self.file_path, "rb") as f:
file_size = os.path.getsize(self.file_path)
files = {"file": (os.path.basename(self.file_path), f, "application/octet-stream")}
params = {"session_id": self.session_id}
# 用流式上传并计算进度
response = requests.post(
url, files=files, params=params, stream=True
)
# 简化进度计算(实际可根据已上传字节数/总大小计算)
self.progress_signal.emit(50)
response.raise_for_status() # 检查请求是否成功
result = response.json()
self.progress_signal.emit(100)
self.result_signal.emit(result)
except Exception as e:
self.result_signal.emit({"status": "failed", "error": str(e)})
# 异步提问线程
class QuestionThread(QThread):
result_signal = pyqtSignal(dict)
def __init__(self, server_url, session_id, doc_name, question):
super().__init__()
self.server_url = server_url
self.session_id = session_id
self.doc_name = doc_name
self.question = question
def run(self):
try:
url = f"{self.server_url}/api/answer"
data = {
"session_id": self.session_id,
"doc_name": self.doc_name,
"user_question": self.question
}
response = requests.post(url, json=data)
response.raise_for_status()
self.result_signal.emit(response.json())
except Exception as e:
self.result_signal.emit({"status": "failed", "error": str(e)})
# 异步导出线程
class ExportThread(QThread):
result_signal = pyqtSignal(dict)
def __init__(self, server_url, session_id, save_path):
super().__init__()
self.server_url = server_url
self.session_id = session_id
self.save_path = save_path
def run(self):
try:
# 1. 请求导出接口获取下载URL
url = f"{self.server_url}/api/export"
data = {"session_id": self.session_id}
response = requests.post(url, json=data)
response.raise_for_status()
export_result = response.json()
if export_result["status"] != "success":
raise Exception(export_result["error"])
# 2. 下载文件并保存到本地
download_url = f"{self.server_url}{export_result['data']['download_url']}"
download_response = requests.get(download_url)
download_response.raise_for_status()
with open(self.save_path, "wb") as f:
f.write(download_response.content)
self.result_signal.emit({
"status": "success",
"data": {"save_path": self.save_path}
})
except Exception as e:
self.result_signal.emit({"status": "failed", "error": str(e)})
# 启动客户端
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MCPClient()
window.show()
sys.exit(app.exec())
3. 打包为可执行文件(可选)
将 Python 脚本打包为无需 Python 环境的 exe(Windows)/app(macOS)文件,方便用户使用:
bash
# Windows打包为exe(--onefile表示单文件,--windowed表示无控制台窗口)
pyinstaller --onefile --windowed --name MCPClient main.py
# macOS打包为app
pyinstaller --onefile --windowed --name MCPClient main.py
方案 2:Web 客户端(Vue3 + Axios)实现
1. 环境准备与项目初始化
bash
# 安装Vue CLI(若未安装)
npm install -g @vue/cli
# 创建Vue项目
vue create mcp-web-client
# 进入项目目录并安装依赖(Axios、Tailwind CSS)
cd mcp-web-client
npm install axios tailwindcss postcss autoprefixer
# 初始化Tailwind CSS
npx tailwindcss init -p
2. 核心代码实现(关键组件:src/components/ChatClient.vue)
vue
<template>
<div class="min-h-screen bg-gray-50 flex flex-col">
<!-- 顶部导航 -->
<nav class="bg-blue-600 text-white p-4">
<div class="container mx-auto">
<h1 class="text-2xl font-bold">MCP AI问答客户端</h1>
</div>
</nav>
<!-- 主内容区 -->
<div class="container mx-auto flex-grow p-4 md:flex md:gap-4">
<!-- 左侧:历史对话(仅PC端显示,移动端隐藏) -->
<div class="md:w-1/4 bg-white rounded-lg shadow p-4 hidden md:block">
<h2 class="text-lg font-semibold mb-2">历史对话</h2>
<div v-if="chatHistory.length === 0" class="text-gray-500 text-sm">暂无对话记录</div>
<div
v-for="(item, index) in chatHistory"
:key="index"
class="mb-2 p-2 rounded cursor-pointer hover:bg-gray-100"
@click="fillQuestion(item.question)"
>
<p class="text-sm font-medium">您:{{ item.question }}</p>
</div>
</div>
<!-- 右侧:主要交互区(上传、提问、答案展示) -->
<div class="md:w-3/4 bg-white rounded-lg shadow p-4">
<!-- 会话ID配置 -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">会话ID</label>
<div class="flex gap-2">
<input
v-model="sessionId"
type="text"
class="flex-grow border border-gray-300 rounded px-3 py-2 text-sm"
placeholder="自动生成的会话ID"
/>
<button
@click="generateSessionId"
class="bg-blue-500 text-white px-3 py-2 rounded text-sm hover:bg-blue-600"
>
重新生成
</button>
</div>
</div>
<!-- 文档上传 -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">上传文档(PDF/Word)</label>
<div class="flex gap-2">
<input
type="file"
accept=".pdf,.docx"
@change="handleFileSelect"
class="flex-grow text-sm"
/>
<button
@click="uploadFile"
v-if="selectedFile"
class="bg-green-500 text-white px-3 py-2 rounded text-sm hover:bg-green-600"
>
{{ isUploading ? "上传中..." : "上传" }}
</button>
</div>
<!-- 上传进度条 -->
<div v-if="isUploading" class="mt-2">
<progress :value="uploadProgress" max="100" class="w-full"></progress>
</div>
<!-- 文档信息提示 -->
<p v-if="docInfo" class="mt-1 text-sm text-green-600">{{ docInfo }}</p>
</div>
<!-- 答案展示区 -->
<div class="mb-4 h-64 overflow-y-auto border border-gray-200 rounded p-3 bg-gray-50">
<div v-if="chatMessages.length === 0" class="text-gray-500 text-sm text-center mt-10">
上传文档后开始提问吧!
</div>
<div v-for="(msg, index) in chatMessages" :key="index" class="mb-3">
<!-- 用户问题 -->
<div v-if="msg.role === 'user'" class="mb-2">
<p class="text-sm font-medium text-blue-600">您:{{ msg.content }}</p>
</div>
<!-- AI答案 -->
<div v-if="msg.role === 'ai'" class="bg-white p-3 rounded shadow-sm">
<p class="text-sm font-medium text-green-600">AI:</p>
<p class="text-sm whitespace-pre-wrap">{{ msg.content }}</p>
</div>
<!-- 系统提示 -->
<div v-if="msg.role === 'system'" class="text-sm text-gray-500 italic">{{ msg.content }}</div>
</div>
</div>
<!-- 提问输入区 -->
<div class="flex gap-2">
<input
v-model="question"
type="text"
class="flex-grow border border-gray-300 rounded px-3 py-2 text-sm"
placeholder="请输入您的问题..."
@keyup.enter="sendQuestion"
/>
<button
@click="sendQuestion"
:disabled="!question || !selectedFile || isGenerating"
class="bg-blue-500 text-white px-3 py-2 rounded text-sm hover:bg-blue-600 disabled:bg-gray-400"
>
{{ isGenerating ? "生成中..." : "提交" }}
</button>
</div>
<!-- 导出按钮 -->
<button
@click="exportChat"
class="mt-4 bg-purple-500 text-white px-3 py-2 rounded text-sm hover:bg-purple-600"
>
导出当前对话(Markdown)
</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import axios from 'axios';
// 服务器基础URL(替换为实际服务器地址)
const SERVER_URL = "http://127.0.0.1:8000";
// 响应式变量
const sessionId = ref("");
const selectedFile = ref(null);
const isUploading = ref(false);
const uploadProgress = ref(0);
const docInfo = ref("");
const question = ref("");
const isGenerating = ref(false);
const chatMessages = ref([]);
const chatHistory = ref([]); // 仅用于历史对话列表
// 初始化:生成会话ID
onMounted(() => {
generateSessionId();
});
// 生成唯一会话ID
const generateSessionId = () => {
sessionId.value = "user_" + Math.random().toString(36).substr(2, 9);
};
// 处理文件选择
const handleFileSelect = (e) => {
const file = e.target.files[0];
if (file) {
selectedFile.value = file;
docInfo.value = `已选择:${file.name}`;
}
};
// 上传文件到服务器
const uploadFile = async () => {
if (!selectedFile.value) return;
const formData = new FormData();
formData.append("file", selectedFile.value);
try {
isUploading.value = true;
uploadProgress.value = 0;
chatMessages.value.push({ role: "system", content: "正在上传并解析文档..." });
const response = await axios.post(
`${SERVER_URL}/api/upload`,
formData,
{
params: { session_id: sessionId.value },
headers: { "Content-Type": "multipart/form-data" },
onUploadProgress: (progressEvent) => {
// 计算上传进度
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
uploadProgress.value = percentCompleted;
},
}
);
const result = response.data;
if (result.status === "success") {
chatMessages.value.push({
role: "system",
content: `文档 ${result.data.doc_name} 上传并解析成功(${result.data.page_count}页),可开始提问!`
});
docInfo.value = `已上传:${result.data.doc_name}(${result.data.page_count}页)`;
} else {
throw new Error(result.error);
}
} catch (error) {
chatMessages.value.push({ role: "system", content: `文档上传失败:${error.message}` });
} finally {
isUploading.value = false;
}
};
// 发送问题到服务器
const sendQuestion = async () => {
const questionText = question.value.trim();
if (!questionText || !selectedFile.value) return;
try {
isGenerating.value = true;
// 添加用户问题到对话列表
chatMessages.value.push({ role: "user", content: questionText });
chatHistory.value.push({ question: questionText }); // 加入历史对话
question.value = ""; // 清空输入框
const response = await axios.post(`${SERVER_URL}/api/answer`, {
session_id: sessionId.value,
doc_name: selectedFile.value.name,
user_question: questionText,
});
const result = response.data;
if (result.status === "success") {
chatMessages.value.push({ role: "ai", content: result.data.formatted_answer });
} else {
throw new Error(result.error);
}
} catch (error) {
chatMessages.value.push({ role: "system", content: `答案生成失败:${error.message}` });
} finally {
isGenerating.value = false;
}
};
// 填充历史问题到输入框
const fillQuestion = (questionText) => {
question.value = questionText;
};
// 导出对话
const exportChat = async () => {
try {
chatMessages.value.push({ role: "system", content: "正在导出对话..." });
// 请求导出接口
const response = await axios.post(`${SERVER_URL}/api/export`, {
session_id: sessionId.value,
});
const result = response.data;
if (result.status !== "success") {
throw new Error(result.error);
}
// 下载文件
const downloadUrl = `${SERVER_URL}${result.data.download_url}`;
const link = document.createElement("a");
link.href = downloadUrl;
link.download = result.data.file_name;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
chatMessages.value.push({ role: "system", content: "对话导出成功!" });
} catch (error) {
chatMessages.value.push({ role: "system", content: `对话导出失败:${error.message}` });
}
};
</script>
3. 运行与部署
bash
# 本地运行Web客户端(开发模式)
npm run serve
# 构建生产版本(用于部署到服务器)
npm run build
构建完成后,将dist
目录下的文件部署到 Nginx/Apache 服务器,即可通过浏览器访问 Web 客户端。
四、客户端与服务器的联调要点
- 跨域问题排查:若 Web 客户端调用服务器 API 时提示 “跨域错误”,需检查服务器 CORS 配置(确保
allow_origins
包含 Web 客户端的域名,如"http://localhost:8080"
); - 会话 ID 一致性:客户端的 “会话 ID” 必须与服务器请求中的
session_id
完全一致,否则会导致 “找不到文档 / 对话记录”(建议客户端自动生成会话 ID,避免用户手动输入错误); - 文件名称匹配:客户端上传文档后,提问时的
doc_name
必须与服务器存储的 “文档名称” 完全一致(如包含扩展名.pdf
),否则服务器无法匹配到对应的文档; - 异步处理:所有 API 请求(上传、提问、导出)必须用异步处理(如 PyQt 的线程、Vue 的 async/await),避免 “界面卡顿”(用户误以为操作未响应)。
五、总结与后续扩展
1. 本集核心收获
- 理解 MCP 客户端的核心价值:“翻译用户操作与服务器 API,降低使用门槛”;
- 掌握两种客户端的开发流程:桌面客户端(PyQt)的 GUI 组件与异步线程,Web 客户端(Vue)的 API 请求与响应式界面;
- 学会客户端与服务器的联调技巧:解决跨域、会话 ID 一致性、异步处理等关键问题。
2. 后续扩展方向
- 用户认证功能:添加 “账号密码登录”(替代当前的 “会话 ID”),实现用户数据隔离(如不同用户登录后只能查看自己的文档与对话);
- 离线缓存功能:桌面客户端支持 “离线保存对话记录”,网络恢复后同步到服务器;
- 多文档管理:支持同时上传多个文档,提问时可选择 “基于哪个文档回答”;
- 界面个性化:Web 客户端支持切换主题(如浅色 / 深色模式),桌面客户端支持自定义窗口大小、字体。
更多推荐
所有评论(0)