fix: clarify MCP client setup and unify agent notifications

This commit is contained in:
陈煜 committed 2026-10-04 02:23:13 +08:00
1 parent 91c489513a
commit 06bee32863
6 files changed
+115 -23

No files matched your search

+50 -16
View File
@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { api } from './api'; import { api } from './api';
import { showToast, useToast } from './Toast';
type Connection = { type Connection = {
id: string; id: string;
@@ -87,8 +88,9 @@ const capabilityGroups = [
]; ];
function instructions(url: string) { function instructions(url: string) {
return [ return [
'以下是配置 MCP 连接后的操作说明,不会自动添加连接或授予工具。若本会话没有 WorthPath 工具,请先告知我,并指导我在客户端配置连接;不要仅凭网址或聊天中的令牌声称已连接。',
'请使用已连接的 WorthPath MCP 服务处理我的资产与负债,服务地址:' + url, '请使用已连接的 WorthPath MCP 服务处理我的资产与负债,服务地址:' + url,
'接入使用 Streamable HTTP。支持 OAuth 的客户端使用该地址发现授权服务,并由我在 WorthPath 网页登录授权;支持自定义 Bearer 头的客户端可在安全的凭据设置中填写个人令牌。不要让我把密码或令牌粘贴到对话中。', '接入使用 Streamable HTTP。支持 OAuth 的客户端使用该地址发现授权服务,并由我在 WorthPath 网页登录授权;个人令牌仅用于明确支持 Bearer 认证的客户端,按其文档配置环境变量或凭据存储。不要假设客户端有自定义请求头界面,不要让我把密码或令牌粘贴到对话中。localhost 仅适用于 MCP 请求在 WorthPath 所在电脑执行的客户端;云端客户端需要可访问的 HTTPS 地址。',
'先 tools/list 发现工具,调用 connection_info 核对权限:read 只读,draft 创建待网页确认的草稿,write 直接普通写入。隐藏账户读写以连接附加授权为准。', '先 tools/list 发现工具,调用 connection_info 核对权限:read 只读,draft 创建待网页确认的草稿,write 直接普通写入。隐藏账户读写以连接附加授权为准。',
'查询:用 positions_list 搜索对象;遇到同名先让我选择稳定 ID;按 limit/offset 或 cursor 翻页。金额和克数使用十进制字符串,业务时间是 UTC+8 的 YYYY-MM-DD 或 YYYY-MM-DDTHH:mm。', '查询:用 positions_list 搜索对象;遇到同名先让我选择稳定 ID;按 limit/offset 或 cursor 翻页。金额和克数使用十进制字符串,业务时间是 UTC+8 的 YYYY-MM-DD 或 YYYY-MM-DDTHH:mm。',
'写入:先 state_get 取得 state,使用 expectedState 与唯一 idempotencyKey 调用所需工具。网络重试保持键和全部参数一致;状态冲突则重新查询并换键。不要把估值更新、转账、还款或负债变化互相替代。', '写入:先 state_get 取得 state,使用 expectedState 与唯一 idempotencyKey 调用所需工具。网络重试保持键和全部参数一致;状态冲突则重新查询并换键。不要把估值更新、转账、还款或负债变化互相替代。',
@@ -113,6 +115,8 @@ export function AgentConnections() {
[consentLevel, setConsentLevel] = useState('read'), [consentLevel, setConsentLevel] = useState('read'),
[consentHiddenRead, setConsentHiddenRead] = useState(false), [consentHiddenRead, setConsentHiddenRead] = useState(false),
[consentHiddenWrite, setConsentHiddenWrite] = useState(false); [consentHiddenWrite, setConsentHiddenWrite] = useState(false);
useToast(error, 'error');
useToast(message, 'success');
const load = async () => setData(await api<Management>('/agent')); const load = async () => setData(await api<Management>('/agent'));
const act = async (work: () => Promise<unknown>) => { const act = async (work: () => Promise<unknown>) => {
if (busy) return; if (busy) return;
@@ -142,9 +146,9 @@ export function AgentConnections() {
const copy = async (text: string, label: string) => { const copy = async (text: string, label: string) => {
try { try {
await navigator.clipboard.writeText(text); await navigator.clipboard.writeText(text);
setMessage(label); showToast(label, 'success');
} catch { } catch {
setError('复制失败,请从下方可选中文本手动复制'); showToast('复制失败,请从下方可选中文本手动复制', 'error');
} }
}; };
return ( return (
@@ -156,16 +160,6 @@ export function AgentConnections() {
</div> </div>
{data && <span className="badge">{data.capabilities.length} 个工具 · 按连接授权</span>} {data && <span className="badge">{data.capabilities.length} 个工具 · 按连接授权</span>}
</div> </div>
{error && (
<p role="alert" className="danger-text">
{error}
</p>
)}
{message && (
<p role="status" className="agent-notice">
{message}
</p>
)}
{data && ( {data && (
<div className="agent-layout"> <div className="agent-layout">
<nav className="agent-tabs agent-wide" aria-label="Agent 功能分区"> <nav className="agent-tabs agent-wide" aria-label="Agent 功能分区">
@@ -207,9 +201,14 @@ export function AgentConnections() {
复制地址 复制地址
</button> </button>
</div> </div>
<p className="muted">在客户端添加地址,完成网页授权,再复制教程并描述任务。</p> <p className="muted">
先在客户端添加 MCP 连接并完成授权。复制下面的操作说明到聊天不会建立连接。
</p>
<details className="agent-details"> <details className="agent-details">
<summary>查看连接步骤</summary> <summary>查看连接步骤</summary>
<p>
localhost 只适用于本机执行 MCP 请求的客户端;云端客户端需要可访问的 HTTPS 地址。
</p>
<ol className="agent-steps"> <ol className="agent-steps">
<li> <li>
<strong>配置连接</strong>:在支持远程 MCP OAuth 的客户端中填写地址,选择 <strong>配置连接</strong>:在支持远程 MCP OAuth 的客户端中填写地址,选择
@@ -220,6 +219,10 @@ export function AgentConnections() {
或草稿模式开始,根据需要增加权限。OAuth 访问令牌 1 小时,刷新授权最多 30 或草稿模式开始,根据需要增加权限。OAuth 访问令牌 1 小时,刷新授权最多 30
天并轮换。 天并轮换。
</li> </li>
<li>
<strong>检查工具已加载</strong>:客户端须能列出 WorthPath 工具并成功调用
connection_info。只有网址、网页预览或“明白”回复不能证明已连接。
</li>
<li> <li>
<strong>把操作说明交给 Agent</strong> <strong>把操作说明交给 Agent</strong>
:复制下方教程,不包含任何密码或令牌,再描述具体任务。 :复制下方教程,不包含任何密码或令牌,再描述具体任务。
@@ -250,11 +253,42 @@ export function AgentConnections() {
rows={8} rows={8}
/> />
</details> </details>
<details className="agent-details">
<summary>本机 Codex:使用 OAuth,无需填写个人令牌</summary>
<p>
在运行 WorthPath 的电脑上打开 PowerShell。未添加连接时先运行第一行;已有
worthpath 连接时直接运行登录命令,在 WorthPath 网页登录并选择权限。
</p>
<pre>{`codex mcp add worthpath --url ${data.mcpUrl}\ncodex mcp login worthpath --scopes read --oauth-client-registration dcr\ncodex mcp list`}</pre>
<button
className="secondary"
type="button"
onClick={() =>
void copy(
`codex mcp login worthpath --scopes read --oauth-client-registration dcr`,
'OAuth 登录命令已复制',
)
}
>
复制 OAuth 登录命令
</button>
<p className="muted">
read 为只读;草稿使用 read,draft,直接普通写入使用
read,write。登录后重启客户端或新建会话,并检查 WorthPath 工具已加载。
</p>
<p className="muted">
已验证 Codex CLI 0.160.0 的 OAuth/DCR 登录及其凭据通过官方 SDK
的初始化、工具发现和 connection_info。尚未验证 Codex
桌面会话内调用;云端聊天无法直接使用本机 localhost。
</p>
</details>
<details className="agent-details"> <details className="agent-details">
<summary>支持 Bearer 头的客户端与已验证 SDK 配置</summary> <summary>支持 Bearer 头的客户端与已验证 SDK 配置</summary>
<p> <p>
在下方创建个人令牌,把完整值填入客户端的安全凭据设置;请求头为 Authorization: 仅在客户端明确支持 Bearer
Bearer &lt;令牌&gt;。个人令牌只显示一次,不能作为通用 OAuth 的替代。 认证时创建个人令牌,按其文档配置环境变量或凭据存储;不要放入聊天、URL
或教程。请求头为 Authorization: Bearer
&lt;令牌&gt;。个人令牌只显示一次,不能作为通用 OAuth 的替代。
</p> </p>
<pre>{`new StreamableHTTPClientTransport(new URL(${JSON.stringify(data.mcpUrl)}), {\n requestInit: { headers: { Authorization: 'Bearer ' + process.env.MCP_ACCESS_TOKEN } }\n});`}</pre> <pre>{`new StreamableHTTPClientTransport(new URL(${JSON.stringify(data.mcpUrl)}), {\n requestInit: { headers: { Authorization: 'Bearer ' + process.env.MCP_ACCESS_TOKEN } }\n});`}</pre>
<button <button
+2 -4
View File
@@ -307,6 +307,7 @@ export default function App() {
[clearConfirmation, setClearConfirmation] = useState(0); [clearConfirmation, setClearConfirmation] = useState(0);
useToast(error, 'error'); useToast(error, 'error');
useToast(success, 'success'); useToast(success, 'success');
useToast(fxStatus?.state === 'error' ? fxStatus.message : '', 'error');
const [settingsSection, setSettingsSection] = useState( const [settingsSection, setSettingsSection] = useState(
new URLSearchParams(location.search).has('agent_authorization') || new URLSearchParams(location.search).has('agent_authorization') ||
new URLSearchParams(location.search).has('agent_operation') new URLSearchParams(location.search).has('agent_operation')
@@ -2116,10 +2117,7 @@ export default function App() {
<section className="panel" hidden={settingsSection !== 'rates'}> <section className="panel" hidden={settingsSection !== 'rates'}>
<h2>{tr('每日汇率')}</h2> <h2>{tr('每日汇率')}</h2>
{fxStatus && ( {fxStatus && (
<p <p className="muted" role="status">
className={fxStatus.state === 'error' ? 'notice warning' : 'muted'}
role="status"
>
{tr(fxStatus.message)} {tr(fxStatus.message)}
{fxStatus.attemptedAt {fxStatus.attemptedAt
? ' · ' + ? ' · ' +
+5 -2
View File
@@ -5,12 +5,15 @@ type Kind = 'success' | 'error';
type Notice = { id: number; message: string; kind: Kind }; type Notice = { id: number; message: string; kind: Kind };
let sequence = 0; let sequence = 0;
const listeners = new Set<(notice: Notice) => void>(); const listeners = new Set<(notice: Notice) => void>();
export function useToast(message: string, kind: Kind) { export function showToast(message: string, kind: Kind) {
useEffect(() => {
if (message) { if (message) {
const notice = { id: ++sequence, message, kind }; const notice = { id: ++sequence, message, kind };
listeners.forEach((fn) => fn(notice)); listeners.forEach((fn) => fn(notice));
} }
}
export function useToast(message: string, kind: Kind) {
useEffect(() => {
showToast(message, kind);
}, [message, kind]); }, [message, kind]);
} }
function ToastItem({ notice, remove }: { notice: Notice; remove: (id: number) => void }) { function ToastItem({ notice, remove }: { notice: Notice; remove: (id: number) => void }) {
+29
View File
@@ -0,0 +1,29 @@
# MCP 接入排查与提示统一
2026-10-04 02:23(UTC+8)。使用临时账号验证,真实用户账目、令牌及 Codex 配置未修改。
## 发现
本机已有 enabled 的 worthpath Streamable HTTP 配置,URL 为 http://localhost:3100/mcp。该配置未指定 Bearer 环境变量或 Authorization 头;CLI auth_status=unknown,不能以该状态断言 OAuth 凭据存在或不存在。本次会话没有加载 WorthPath 工具。服务 OAuth 资源发现返回正常元数据,未认证 MCP 请求返回 401。
教程贴入聊天不会注册 MCP 服务。客户端必须先完成配置、认证和工具加载。localhost 只指请求执行端所在电脑,远端客户端需要可达 HTTPS 服务,不能根据网页预览声称已连接。
## 修改
- apps/web/src/AgentConnections.tsx:教程前置连接检查、网络边界、Bearer 配置限制;折叠显示已验证 Codex OAuth 命令;成功和错误反馈统一右上角 Toast。
- apps/web/src/Toast.tsx:提取 showToast,重复复制相同命令也刷新通知,复用已有自动消失、去重、关闭和模态框兼容机制。
- apps/web/src/App.tsx:汇率更新失败使用相同 Toast;汇率状态仍作为可查询信息显示。缺少汇率造成总额不完整等持续业务说明保留。
- docs/mcp.md:实际接入步骤、已验证客户端范围、环境变量 PAT 的进程继承限制。
- update.md:完成提示统一要求并记录接入修正。
## 实际验证
- Codex CLI 0.160.0,独立临时 CODEX_HOME,OAuth/DCR + S256 PKCE;临时用户 read 授权成功,CLI 回调与凭据保存成功。
- 使用 CLI 发行的 OAuth 凭据通过官方 SDK 1.31.0 初始化、发现 39 个工具、调用 connection_info;权限 read,隐藏读写均 false。
- 临时用户、DCR 注册、授权和临时客户端凭据已清理,没有使用聊天中暴露的个人令牌。
- 前端 TypeScript 检查及生产构建通过。
- 浏览器验证新说明和命令、复制后右上角成功提示、重复复制刷新、错误密码请求拒绝且右上角错误提示。令牌值不出现在 Toast 中。
本次没有数据库模型或业务服务变更,无新增迁移。没有重复运行此前 REST/MySQL 全业务回归;此前结果见更新记录。本次尚未验证 Codex 桌面会话直接工具调用、其他商业客户端或公网 HTTPS 部署。CLI 登录测试与 SDK 工具调用测试不等于这些验证。
具体接入步骤见 [MCP 文档](mcp.md)。没有部署或推送。
+27 -1
View File
@@ -4,6 +4,10 @@
## 开始连接 ## 开始连接
操作教程仅用于连接完成后的业务指引。把地址和教程贴进聊天不会注册 MCP 服务,也不会为当前会话授予工具;网页预览、Agent 回复“明白”均不能证明已连接。先在客户端配置服务并完成认证,再检查工具列表与 `connection_info`。
`localhost` 是 MCP 客户端执行请求的电脑。本机客户端可以访问本机 WorthPath;远端或云端客户端需要能访问的 HTTPS 服务地址,不能直接访问你的电脑上的 `localhost:3100`。不要把个人令牌贴在聊天、URL、教程或 Git 配置中。
在「设置与备份 → 连接 Agent」复制实际 MCP 地址和“Agent 使用教程”。支持远程 OAuth 的客户端填写此地址,选择 Streamable HTTP;在 WorthPath 网页登录,审核客户端名称、回调地址、资源,并选择连接权限。网页会回到已注册的精确回调地址并保留 state。客户端自行验证 state。 在「设置与备份 → 连接 Agent」复制实际 MCP 地址和“Agent 使用教程”。支持远程 OAuth 的客户端填写此地址,选择 Streamable HTTP;在 WorthPath 网页登录,审核客户端名称、回调地址、资源,并选择连接权限。网页会回到已注册的精确回调地址并保留 state。客户端自行验证 state。
OAuth 使用发现元数据、动态注册的公开客户端、授权码、S256 PKCE、RFC 8707 resource。每次 MCP 请求独立验证 Bearer;网页登录 Cookie、MCP 会话 ID 均不作为认证凭证。发现入口为 `/.well-known/oauth-protected-resource/mcp`,未授权响应提供 `WWW-Authenticate`。 OAuth 使用发现元数据、动态注册的公开客户端、授权码、S256 PKCE、RFC 8707 resource。每次 MCP 请求独立验证 Bearer;网页登录 Cookie、MCP 会话 ID 均不作为认证凭证。发现入口为 `/.well-known/oauth-protected-resource/mcp`,未授权响应提供 `WWW-Authenticate`。
@@ -22,6 +26,28 @@ OAuth 使用发现元数据、动态注册的公开客户端、授权码、S256
旧连接有 write 时,现在直接执行普通修改;旧全局策略不再阻止它。只想创建草稿时,撤销旧连接并重新授权 read draft。历史策略行保留但不参与任何权限判断;旧敏感工具草稿不可提交。 旧连接有 write 时,现在直接执行普通修改;旧全局策略不再阻止它。只想创建草稿时,撤销旧连接并重新授权 read draft。历史策略行保留但不参与任何权限判断;旧敏感工具草稿不可提交。
## 本机 Codex OAuth 接入
以下命令已核对本机 Codex CLI 0.160.0 帮助和[官方 MCP 文档](https://learn.chatgpt.com/docs/extend/mcp?surface=cli)。在 WorthPath 所在电脑的 PowerShell 执行;已有名为 worthpath 的连接时跳过 add。CLI 与桌面/IDE 的配置共享不代表当前会话已经加载工具,授权后重启客户端或新建会话,并检查工具发现。
```powershell
codex mcp add worthpath --url http://localhost:3100/mcp
codex mcp login worthpath --scopes read --oauth-client-registration dcr
codex mcp list
```
浏览器打开 WorthPath 登录与授权页面;用户选择连接权限后客户端取得 OAuth 凭据,无需个人令牌。请求草稿改为 `--scopes read,draft`;请求普通写入改为 `--scopes read,write`。隐藏账户权限另行显式申请与授权。DCR 是当前服务实际支持并验证的客户端注册方式。
使用独立临时 CODEX_HOME 和临时账号,真实通过 CLI OAuth/DCR 登录。CLI 保存的凭据随后通过官方 SDK 初始化、发现 39 个工具和调用 connection_info,权限 read、隐藏读写 false。已清理临时账号、注册及凭据;未更改真实用户的授权或 Codex 配置。尚未验证 Codex 桌面聊天内实际调用,不能把 CLI 登录测试等同于桌面端端到端验证。
若客户端明确需要 PAT,Codex HTTP 配置支持 `bearer_token_env_var`,不是假定存在一个“安全自定义头”界面:
```powershell
codex mcp add worthpath --url http://localhost:3100/mcp --bearer-token-env-var WORTHPATH_MCP_TOKEN
```
令牌通过客户端进程可读取的 WORTHPATH_MCP_TOKEN 环境变量提供,不写入该命令或聊天。已经启动的桌面进程不会自动获得后来在另一个终端设置的环境变量。优先 OAuth,避免此进程环境问题;PAT 配置命令语法已核对,尚未通过 Codex 桌面端验证。
## Agent 操作 ## Agent 操作
查询使用稳定 UUID;名称重名时先让用户选择。列表 limit 最大 100;图标固定 60;历史和资金往来使用 cursor。日期为 UTC+8 业务日期 `YYYY-MM-DD` 或 `YYYY-MM-DDTHH:mm`,返回时间戳使用 UTC ISO。金额、克数、汇率与报价为十进制字符串,沿用 Decimal 和 DECIMAL 规则。 查询使用稳定 UUID;名称重名时先让用户选择。列表 limit 最大 100;图标固定 60;历史和资金往来使用 cursor。日期为 UTC+8 业务日期 `YYYY-MM-DD` 或 `YYYY-MM-DDTHH:mm`,返回时间戳使用 UTC ISO。金额、克数、汇率与报价为十进制字符串,沿用 Decimal 和 DECIMAL 规则。
@@ -60,7 +86,7 @@ pnpm --filter @worthpath/api mcp:probe
Remove-Item Env:MCP_ACCESS_TOKEN Remove-Item Env:MCP_ACCESS_TOKEN
``` ```
未验证 MCP Inspector、Claude、ChatGPT、Cursor、Codex 等商业客户端的配置和 OAuth 实际行为,不提供推测配置。生产 HTTPS 代理未部署验证。 除上文 Codex CLI OAuth/DCR 登录外,未验证 MCP Inspector、Claude、ChatGPT、Cursor 或 Codex 桌面端实际工具调用。生产 HTTPS 代理未部署验证。
## 启动与部署 ## 启动与部署
+2
View File
@@ -88,3 +88,5 @@
设置与交互验收完成:2026-10-03 17:54。33 项单元检查、15 项真实 MySQL 集成检查、前后端类型检查和生产构建通过;京东金融图标修正后另通过 4 项图标与转账专项回归。15 个迁移已应用,9 张表与 92 个字段注释齐全。登录与隐私、总览卡片、计划弹窗、右上角提示、京东金融 App 图标和快速转账详情见 [更新说明](docs/update-settings-interaction-2026-10-03.md)。复查未发现新增未完成要求。 设置与交互验收完成:2026-10-03 17:54。33 项单元检查、15 项真实 MySQL 集成检查、前后端类型检查和生产构建通过;京东金融图标修正后另通过 4 项图标与转账专项回归。15 个迁移已应用,9 张表与 92 个字段注释齐全。登录与隐私、总览卡片、计划弹窗、右上角提示、京东金融 App 图标和快速转账详情见 [更新说明](docs/update-settings-interaction-2026-10-03.md)。复查未发现新增未完成要求。
账户转账与估价验收完成:2026-10-03 18:56。35 项单元检查、17 项真实 MySQL 集成检查、类型检查和生产构建通过;最终估值去重另通过 4 项专项回归。17 个迁移已应用,10 张表与 106 个字段注释完整。计入开关、黄金/白银估价、ZIP v8 和第三方接口清单详见 [更新说明](docs/update-inclusion-metals-2026-10-03.md) 与 [README](README.md#第三方接口与外部素材来源)。复查未发现新增未完成要求。 账户转账与估价验收完成:2026-10-03 18:56。35 项单元检查、17 项真实 MySQL 集成检查、类型检查和生产构建通过;最终估值去重另通过 4 项专项回归。17 个迁移已应用,10 张表与 106 个字段注释完整。计入开关、黄金/白银估价、ZIP v8 和第三方接口清单详见 [更新说明](docs/update-inclusion-metals-2026-10-03.md) 与 [README](README.md#第三方接口与外部素材来源)。复查未发现新增未完成要求。
- ~~修改提示框,全部提示直接在右上角出现弹出提示框~~ — 已完成并通过浏览器成功/认证错误反馈验证:2026-10-04 02:23(UTC+8)
- ~~修正 MCP 接入教程误导,补充已验证的 Codex OAuth 配置和 localhost 限制~~ — 已完成:2026-10-04 02:23(UTC+8);CLI 0.160.0 OAuth/DCR 登录及官方 SDK 39 工具发现通过,桌面会话内调用尚未验证。