Skip to content

feat(web-shell): browse MCP server resources in the /mcp dialog - #5879

Merged
wenshao merged 4 commits into
QwenLM:mainfrom
wenshao:feat/web-shell-mcp-resources
Jun 26, 2026
Merged

feat(web-shell): browse MCP server resources in the /mcp dialog#5879
wenshao merged 4 commits into
QwenLM:mainfrom
wenshao:feat/web-shell-mcp-resources

Conversation

@wenshao

@wenshao wenshao commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Brings the Web Shell's /mcp dialog to parity with the terminal UI's MCP resource browser (shipped for the TUI in #5544 and #5635). Each MCP server row now shows resource and prompt counts, and expanding a server reveals a browsable list of its resources — each resource expands to its URI, MIME type, byte size, description, and the @server:uri reference you type in chat to inject the resource's contents.

Because MCP resources and prompts only ever lived in the in-process core registries (ResourceRegistry / PromptRegistry), none of this data reached the Web Shell, which talks to the daemon over the ACP/HTTP protocol. This PR plumbs it end to end: per-server resourceCount / promptCount ride the existing GET /workspace/mcp status payload, and a new GET /workspace/mcp/:server/resources drill-down endpoint (qwen/status/workspace/mcp/resources, mirroring the existing /tools endpoint) carries the resource metadata through the daemon handler, the ACP route table, the TypeScript SDK client, the webui workspace actions/hook, and finally the McpDialog React component.

Why it's needed

The TUI already lets users discover and reference MCP resources from /mcp; Web Shell users had no equivalent — they could see tools but not the resources or prompts a server advertises, and had no way to find the @server:uri reference for injecting a resource into a conversation. This closes that gap so both front-ends expose the same MCP surface.

Reviewer Test Plan

How to verify

Every layer is covered by unit/integration tests added in this PR:

  • Daemon handler (per-server counts + resources builder + ext-method dispatch): npm --prefix packages/cli run test -- src/acp-integration/acpAgent.test.ts -t "status ext methods expose workspace snapshots" --run
  • Daemon HTTP route + JSON-RPC dispatch (valid / URL-decoded / length-limit / missing-serverName): npm --prefix packages/cli run test -- src/serve/server.test.ts src/serve/acp-http/transport.test.ts -t "resources" --run
  • webui SDK action (success + graceful fallback for older daemons): npm --prefix packages/webui run test -- DaemonWorkspaceProvider --run

Full gates run clean locally: typecheck on acp-bridge / cli / webui / web-shell; builds of acp-bridge / sdk / webui / web-shell; prettier --check and eslint on all changed files.

Manual smoke (optional): start qwen serve, open the Web Shell, configure an MCP server that advertises resources (e.g. a filesystem MCP server), run /mcp, expand the server, and confirm the resource/prompt count badges, the expandable resource list, and the @server:uri reference render.

Evidence (Before & After)

Before: the Web Shell /mcp dialog showed only servers and their tools. After: it additionally shows per-server resource/prompt count badges and an expandable resource browser whose detail view matches the TUI's ResourceDetailStep (URI / Name / MIME Type / Size / Description + the @server:uri chat reference). The behaviour is a direct port of the TUI browser shipped in #5635, and the data-flow at every layer is asserted by the new tests (resource payload shape, @server:uri reference format, per-server counts, older-daemon fallback). Live browser screenshots were not captured in this environment; the change is covered by the layered test suite above.

Tested on

OS Status
🍏 macOS ✅ typecheck + build + unit/integration tests
🪟 Windows ⚠️ not tested locally (CI)
🐧 Linux ⚠️ not tested locally (CI)

Environment (optional)

Node 22 on macOS (darwin). Verification via tsc --noEmit, package builds, and vitest unit/integration suites — no live daemon required for the test coverage.

Risk & Scope

  • Main risk or tradeoff: the new per-server resourceCount / promptCount are computed on each GET /workspace/mcp call via in-memory registry scans — the same access pattern the TUI /mcp dialog already uses, with a pool-mode active-session fallback mirroring the existing tools builder. Negligible for realistic server/resource counts, and disabled servers are skipped. The SDK browser bundle cap was bumped 130KB → 131KB to accommodate the new workspaceMcpResources client method + route (legitimate growth, not a regression).
  • Not validated / out of scope: live browser UI screenshots. No prompt browser — the TUI dialog only shows a prompt count and surfaces prompts as slash commands, so this PR matches that (counts for prompts, a browser for resources). The legacy McpStatusMessage chat-message component (superseded by the McpDialog modal, no live producer) is not extended.
  • Breaking changes / migration notes: none. All protocol additions are optional/additive — old SDK clients ignore the new fields, and older daemons that lack the /resources route return 404, which the client degrades to an empty-with-notice result.

Linked Issues

Follows up the TUI MCP resource browser (#5544, #5635) by porting it to the Web Shell. No issue to close.

中文说明

这个 PR 做了什么

让 Web Shell 的 /mcp 对话框与终端 UI 的 MCP 资源浏览器对齐(该功能已在 #5544#5635 为 TUI 发布)。现在每个 MCP server 行会显示资源数和 prompt 数,展开 server 后可浏览其资源列表——每个资源可展开显示 URI、MIME 类型、字节大小、描述,以及在对话中输入即可注入该资源内容的 @server:uri 引用。

由于 MCP 资源和 prompts 一直只存在于进程内的 core 注册表(ResourceRegistry / PromptRegistry),这些数据从未到达通过 ACP/HTTP 协议与 daemon 通信的 Web Shell。本 PR 把它纵向打通:每个 server 的 resourceCount / promptCount 搭在既有的 GET /workspace/mcp 状态负载上顺风车,新增的 GET /workspace/mcp/:server/resources 钻取端点(qwen/status/workspace/mcp/resources,镜像既有的 /tools 端点)则将资源元数据依次穿过 daemon handler、ACP 路由表、TypeScript SDK 客户端、webui 的 workspace actions/hook,最终到达 McpDialog React 组件。

为什么需要

TUI 早已允许用户从 /mcp 发现并引用 MCP 资源;Web Shell 用户却没有对应能力——只能看到工具,看不到 server 暴露的资源或 prompts,也无从得知用于把资源注入对话的 @server:uri 引用。本 PR 弥合了这个差距,让两个前端暴露相同的 MCP 能力面。

Reviewer 验证计划

如何验证

每一层都有本 PR 新增的单元/集成测试覆盖:

  • Daemon handler(每 server 计数 + 资源 builder + ext-method dispatch):npm --prefix packages/cli run test -- src/acp-integration/acpAgent.test.ts -t "status ext methods expose workspace snapshots" --run
  • Daemon HTTP 路由 + JSON-RPC dispatch(合法 / URL 解码 / 长度上限 / 缺 serverName):npm --prefix packages/cli run test -- src/serve/server.test.ts src/serve/acp-http/transport.test.ts -t "resources" --run
  • webui SDK action(成功 + 老 daemon graceful fallback):npm --prefix packages/webui run test -- DaemonWorkspaceProvider --run

本地全量关卡均通过:acp-bridge / cli / webui / web-shell 的 typecheck;acp-bridge / sdk / webui / web-shell 的构建;所有改动文件的 prettier --checkeslint

手动冒烟(可选):启动 qwen serve,打开 Web Shell,配置一个会暴露资源的 MCP server(如 filesystem MCP server),执行 /mcp,展开 server,确认资源/prompt 计数徽章、可展开的资源列表,以及 @server:uri 引用正确渲染。

证据(前后对比)

之前:Web Shell 的 /mcp 对话框只显示 server 及其工具。之后:额外显示每个 server 的资源/prompt 计数徽章,以及可展开的资源浏览器,其详情视图与 TUI 的 ResourceDetailStep 一致(URI / 名称 / MIME 类型 / 大小 / 描述 + @server:uri 对话引用)。该行为是 #5635 已发布的 TUI 浏览器的直接移植,且每一层的数据流都被新增测试断言(资源负载结构、@server:uri 引用格式、每 server 计数、老 daemon fallback)。本环境未截取实时浏览器截图;改动由上述分层测试套件覆盖。

测试平台

系统 状态
🍏 macOS ✅ typecheck + 构建 + 单元/集成测试
🪟 Windows ⚠️ 本地未测(CI)
🐧 Linux ⚠️ 本地未测(CI)

环境(可选)

macOS(darwin)+ Node 22。通过 tsc --noEmit、各包构建、vitest 单元/集成套件验证——测试覆盖无需运行真实 daemon。

风险与范围

  • 主要风险/取舍:新增的每 server resourceCount / promptCount 在每次 GET /workspace/mcp 调用时通过内存注册表扫描计算——与 TUI /mcp 对话框既有的访问方式相同,并带有镜像现有 tools builder 的 pool 模式 active-session fallback。对真实的 server/资源数量级可忽略,且禁用的 server 会跳过。SDK 浏览器 bundle 上限从 130KB 上调到 131KB 以容纳新增的 workspaceMcpResources 客户端方法 + 路由(合理增长,非回归)。
  • 未验证 / 范围外:实时浏览器 UI 截图。没有 prompt 浏览器——TUI 对话框对 prompts 也只显示计数、并以斜杠命令形式暴露,故本 PR 与之一致(prompts 给计数,资源给浏览器)。遗留的 McpStatusMessage 聊天消息组件(已被 McpDialog 模态取代、无活跃生产方)未做扩展。
  • 破坏性变更 / 迁移说明:无。所有协议新增均为可选/增量——老 SDK 客户端忽略新字段,缺少 /resources 路由的老 daemon 返回 404,客户端据此降级为「空+提示」结果。

关联 Issue

承接 TUI 的 MCP 资源浏览器(#5544#5635),将其移植到 Web Shell。无需关闭的 issue。

Port the TUI's MCP resource browser (QwenLM#5544/QwenLM#5635) to the Web Shell. The
/mcp dialog now shows per-server resource and prompt counts plus an
expandable resource browser (URI, MIME type, size, description, and the
@server:uri chat reference), reaching parity with the terminal UI.

Resources and prompts were never serialized past the in-process core
registries, so this wires the data end to end: per-server resourceCount
and promptCount ride the existing /workspace/mcp status, and a new
qwen/status/workspace/mcp/resources ext-method (mirroring the tools
drill-down) carries the resource list through the daemon, SDK, and webui
hook into the Web Shell McpDialog. All additions are protocol-additive;
older daemons 404 the new route and the client degrades gracefully.
@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

On direction: this is a straightforward parity port — the TUI /mcp dialog already browses resources and prompts (shipped in #5544/#5635), and the Web Shell was the obvious missing surface. Claude Code's CHANGELOG also references MCP resources/list, resource autocomplete, and pagination fixes, confirming this is a real area of user demand. Aligned and well-motivated.

On approach: the scope is tight for what it does. The plumbing mirrors the existing workspaceMcpTools pattern at every layer (daemon handler → ACP route → SDK client → React component), which is the right call — no new abstractions, no speculative features. The 28 files / +969 lines look large but are mostly mechanical mirroring of an established pattern. The bundle cap bump (130→131KB) is justified and incremental. One thing worth noting: the pool-mode fallback in resolveServerMcpResources / resolveServerMcpPrompts duplicates the same pattern from buildWorkspaceMcpToolsStatus — not a blocker, but a candidate for a shared helper if a third resource type ever lands.

Re-run note: merge conflict with #5809 (serve server route split) has been resolved — the new resources route was correctly moved into the extracted workspace-status.ts module, mirroring the tools route pattern. No issues with the resolution.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

方向:这是直接的对齐移植——TUI 的 /mcp 对话框已经支持资源和 prompts 浏览(#5544/#5635 已发布),Web Shell 是显而易见的缺失面。Claude Code 的 CHANGELOG 也多次提及 MCP resources/list、资源自动补全和分页修复,证实这是真实的用户需求区域。方向对齐,动机充分。

方案:对于所做的事情来说范围紧凑。每一层的管道都镜像了既有的 workspaceMcpTools 模式(daemon handler → ACP 路由 → SDK 客户端 → React 组件),这是正确的选择——没有新抽象,没有投机性功能。28 个文件 / +969 行看起来多,但大部分是对已建立模式的机械镜像。bundle 上限上调(130→131KB)合理且增量。值得一提:resolveServerMcpResources / resolveServerMcpPrompts 中的 pool-mode fallback 与 buildWorkspaceMcpToolsStatus 中的模式重复——不是阻碍,但如果将来有第三种资源类型,可以考虑抽取共享 helper。

重跑说明:#5809(serve server 路由拆分)的合并冲突已解决——新的 resources 路由被正确移入抽取的 workspace-status.ts 模块,镜像 tools 路由模式。解决方案没有问题。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Code Review

Clean implementation — mirrors the existing workspaceMcpTools plumbing at every layer. No correctness bugs, security issues, or AGENTS.md violations found. The post-merge conflict resolution is correct: the resources route was properly moved into the extracted workspace-status.ts module alongside the tools route, using the same validation pattern (MAX_SERVER_NAME_LENGTH, sendBridgeError).

A few observations (non-blocking):

  • The resolveServerMcpResources and resolveServerMcpPrompts private methods follow the same pool-mode active-session fallback as the tools builder. This is correct and consistent, but the three near-identical methods are a mild DRY smell — acceptable for now, flag for a future pass if a fourth resource type appears.
  • resourceKey uses NUL (\^@) as separator — good call, URIs can contain : but not NUL. Matches core's ResourceRegistry key scheme.
  • The resourceCount / promptCount fields are optional, preserving backward compat with older daemons. The graceful degradation (404 → empty + notice) is well-handled in the SDK actions.
  • The reloadServer callback correctly isolates the resource refresh in its own try/catch so a failed resource fetch doesn't mask a successful reconnect/enable.

Test Results

All unit/integration tests pass across every layer (re-run after merge conflict resolution):

✓ packages/cli/src/acp-integration/acpAgent.test.ts — 1 passed (daemon handler: counts + resources builder + ext-method dispatch)
✓ packages/cli/src/serve/server.test.ts — 3 passed (HTTP route: valid / URL-decoded / length-limit)
✓ packages/cli/src/serve/acp-http/transport.test.ts — 2 passed (ACP JSON-RPC: missing serverName / valid request)
✓ packages/webui/src/daemon/workspace/DaemonWorkspaceProvider.test.tsx — 11 passed (SDK actions: success + older-daemon fallback)
✓ packages/sdk-typescript/test/unit/DaemonClient.test.ts — 4 passed (MCP-related including URL-encoded GET)
✓ packages/sdk-typescript/test/unit/acpRouteTable.test.ts — 76 passed (route mapping)

Typecheck clean across all packages.

Real-Scenario Test (tmux)

Started the dev daemon from PR code on port 19877 and hit all endpoints:

$ npm run dev -- serve --port 19877
qwen serve listening on http://127.0.0.1:19877 (mode=http-bridge)
qwen serve: bearer auth disabled (loopback default)

$ curl -s http://127.0.0.1:19877/workspace/mcp | python3 -m json.tool
{
    "v": 1,
    "workspaceCwd": "...worktrees/triage",
    "initialized": true,
    "discoveryState": "not_started",
    "servers": [],
    "clientCount": 0,
    "budgetMode": "off",
    "budgets": []
}

$ curl -s http://127.0.0.1:19877/workspace/mcp/test-server/resources | python3 -m json.tool
{
    "v": 1,
    "workspaceCwd": "...worktrees/triage",
    "serverName": "test-server",
    "initialized": true,
    "acpChannelLive": true,
    "resources": [],
    "errors": [
        {
            "kind": "mcp_resources",
            "status": "error",
            "error": "MCP server not configured: test-server"
        }
    ]
}

$ curl -s -o /dev/null -w "HTTP %{http_code}" http://127.0.0.1:19877/workspace/mcp/aaa...300chars.../resources
HTTP 400

$ curl -s "http://127.0.0.1:19877/workspace/mcp/my%20server/resources" | python3 -m json.tool
{
    "v": 1,
    "serverName": "my server",
    "initialized": true,
    "acpChannelLive": true,
    "resources": [],
    "errors": [{"kind": "mcp_resources", "status": "error", "error": "MCP server not configured: my server"}]
}

$ curl -s -o /dev/null -w "HTTP %{http_code}" "http://127.0.0.1:19877/workspace/mcp//resources"
HTTP 404

Daemon logs confirm all five requests handled correctly:

[DAEMON] route=GET /workspace/mcp status=200 ✓
[DAEMON] route=GET /workspace/mcp/test-server/resources status=200 ✓
[DAEMON] route=GET /workspace/mcp/aaa...(300 chars).../resources status=400 ✓
[DAEMON] route=GET /workspace/mcp/my%20server/resources status=200 ✓ (decoded to "my server")
[DAEMON] route=GET /workspace/mcp//resources status=404 ✓

The new /workspace/mcp/:server/resources endpoint is live after the merge conflict resolution, returns the expected payload shape, enforces the name-length limit, URL-decodes server names, and produces clear error cells for unconfigured servers. No MCP servers were configured in the CI environment so the resource browser UI path couldn't be exercised end-to-end, but every layer below the React component is verified.

中文说明

代码审查

实现干净——每一层都镜像既有的 workspaceMcpTools 管道。未发现正确性 bug、安全问题或 AGENTS.md 违规。合并冲突解决正确:resources 路由被移入抽取的 workspace-status.ts 模块,与 tools 路由并列,使用相同的验证模式。

测试结果

所有单元/集成测试在合并冲突解决后重新运行均通过。全包 typecheck 通过。

真实场景测试(tmux)

从 PR 代码启动 dev daemon,测试了 5 个端点场景:基础 MCP 状态、未配置 server 的 resources(返回错误 cell)、名称长度限制(300 字符返回 400)、URL 编码的 server 名称(正确解码)、空 server 名称(404)。所有端点行为正确。CI 环境未配置 MCP server,无法端到端测试资源浏览器 UI,但 React 组件以下的每一层均已验证。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

This PR is a textbook parity port. The TUI's MCP resource browser has been shipping since #5544/#5635; the Web Shell was the obvious next surface, and this PR delivers exactly that — no more, no less.

The implementation mirrors the established workspaceMcpTools pattern at every layer (types → daemon handler → ACP route → SDK client → route table → React component), which makes it straightforward to review and reason about. The diff is large in file count (28) but mechanical in nature — most additions are type definitions and their corresponding plumbing, not novel logic.

My independent proposal for this problem would have been: "add a GET /workspace/mcp/:server/resources endpoint mirroring the tools endpoint, add resourceCount/promptCount to the base MCP status, and add a ResourceDetail component in McpDialog mirroring ToolDetail." The PR does exactly this, plus the pool-mode session fallback and graceful older-daemon degradation I would have missed on the first pass. It matches or exceeds my baseline.

Tests pass at every layer (re-verified after the merge conflict resolution with #5809), typecheck is clean, and the daemon endpoint responds correctly with proper error handling and input validation across five scenarios (base status, unconfigured server error, name-length limit, URL decoding, empty name).

The only mild concern is the DRY repetition across the three resolveServer* methods — but that's a pre-existing pattern, not something this PR introduces, and extracting it would be scope creep.

The merge conflict resolution is clean — the resources route was correctly relocated into the extracted workspace-status.ts module. No functional or structural issues introduced.

Ships the feature cleanly. ✅

中文说明

这个 PR 是教科书式的对齐移植。TUI 的 MCP 资源浏览器自 #5544/#5635 起已发布;Web Shell 是显而易见的下一个界面,本 PR 恰好交付了这一点——不多不少。

实现在每一层都镜像了已建立的 workspaceMcpTools 模式(类型 → daemon handler → ACP 路由 → SDK 客户端 → 路由表 → React 组件),使审查和理解都很直接。diff 的文件数较多(28 个),但本质上是机械性的——大多数新增是类型定义及其对应的管道,而非新逻辑。

我对这个问题的独立方案是:"添加一个镜像 tools 端点的 GET /workspace/mcp/:server/resources 端点,在基础 MCP 状态上添加 resourceCount/promptCount,并在 McpDialog 中添加一个镜像 ToolDetailResourceDetail 组件。"PR 完全做到了这些,还加上我第一遍会遗漏的 pool-mode session fallback 和旧 daemon 优雅降级。匹配或超越我的基线。

各层测试均在合并冲突解决后重新验证通过,typecheck 干净,daemon 端点在五个场景(基础状态、未配置 server 错误、名称长度限制、URL 解码、空名称)下均响应正确。

唯一的轻微顾虑是三个 resolveServer* 方法之间的 DRY 重复——但这是既有模式,不是本 PR 引入的,抽取它会超出范围。

合并冲突解决干净——resources 路由被正确迁入抽取的 workspace-status.ts 模块。无功能性或结构性问题。

功能干净交付。✅

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

Comment thread packages/web-shell/client/components/messages/McpStatusMessage.tsx Outdated
Comment thread packages/web-shell/client/i18n.tsx
…size

Address review on QwenLM#5879:
- SerializedMcpStatusMessage.resourcesByServer is now optional, matching
  its JSDoc ("older clients omit it") so TypeScript enforces the `?? {}`
  defensive read at every consumer.
- mcp.resource.bytes now pluralizes ("1 byte" vs "2 bytes"), consistent
  with the mcp.resourceCount / mcp.promptCount strings in this PR.

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Downgraded from Request changes to Comment: self-PR.

Comment thread packages/cli/src/acp-integration/acpAgent.ts
Comment thread packages/web-shell/client/components/dialogs/McpDialog.tsx Outdated
Comment thread packages/web-shell/client/App.tsx
Comment thread packages/sdk-typescript/src/daemon/DaemonClient.ts
Comment thread packages/web-shell/client/components/dialogs/McpDialog.tsx Outdated
Comment thread packages/web-shell/client/components/dialogs/McpDialog.tsx Outdated
Comment thread packages/cli/src/acp-integration/acpAgent.ts
Comment thread packages/web-shell/client/components/dialogs/McpDialog.tsx
Comment thread packages/web-shell/client/components/dialogs/McpDialog.tsx Outdated
Comment thread packages/sdk-typescript/src/daemon/acpRouteTable.ts
Comment thread packages/cli/src/acp-integration/acpAgent.ts
…ref, harden fallbacks, add tests

Address review on QwenLM#5879:
- [Critical] Remove the "@server:uri" chat-reference UI from the resource
  browser: the Web Shell submits prompts as a plain text block and the
  daemon forwards it verbatim (the TUI's atCommandProcessor resolution is
  TUI-client-side only), so the reference never injected resource content.
  The browser stays as read-only metadata; @-reference injection is a
  follow-up that needs the resolver wired into the Web Shell prompt path.
- [Critical] reloadServer now isolates the resource refetch in its own
  try/catch so a failed resource load can't report a successful
  reconnect/enable as failed.
- [Critical] resolveServerMcpResources/Prompts skip a throwing session in
  the pool-mode fallback so one degraded session can't blank the base
  /workspace/mcp status (which now carries the counts).
- [Suggestion] Resource section renders an "unavailable" state when a
  server advertises a count but the drill-down list is empty.
- [Suggestion] Add SDK client URL-encoding test (workspaceMcpResources),
  route-table matching test, and an unconfigured-server error-branch test.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: APPROVE (downgraded to comment — CI still running)

LGTM! ✅ Clean parity port of the TUI MCP resource browser to the Web Shell. The plumbing is consistent at every layer (daemon handler → ACP route → SDK client → webui action → React component), tests cover the new paths including graceful fallback for older daemons, and the follow-up commit (2abb167) addresses all previously raised inline comments.

Downgrade reason: 30 CI checks are still pending. Will re-approve once CI lands green.

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

yiliang114
yiliang114 previously approved these changes Jun 26, 2026

@yiliang114 yiliang114 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed current head 2abb167. The earlier critical points are resolved, the resources path now stays metadata-only in Web Shell, the fallback/route/client tests are in place, and current checks show no failing jobs. LGTM.

@wenshao

wenshao commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /resolve

Main refactored workspace status routes out of server.ts into
registerWorkspaceStatusRoutes(). PR had added a new
GET /workspace/mcp/:server/resources route inline. Resolution:
take main's refactoring and port the resources route into the
extracted workspace-status.ts module.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

Qwen Code resolved the merge conflicts and pushed the branch update.

Conflict Resolution Summary — PR #5879

Conflict

File: packages/cli/src/serve/server.ts (single conflict, lines 781–871)

What happened

  • origin/main (PR refactor(cli): Split serve server routes #5809refactor(cli): Split serve server routes) extracted
    the inline workspace-status routes (/workspace/mcp, /workspace/mcp/:server/tools,
    /workspace/skills, /workspace/tools, /workspace/providers) out of server.ts
    into a new registerWorkspaceStatusRoutes() function in
    packages/cli/src/serve/routes/workspace-status.ts.

  • PR feat(web-shell): browse MCP server resources in the /mcp dialog #5879 HEAD added a new route GET /workspace/mcp/:server/resources inline
    in server.ts, between the existing /workspace/mcp/:server/tools and
    /workspace/skills routes. This route calls bridge.getWorkspaceMcpResourcesStatus(serverName).

Resolution

  1. packages/cli/src/serve/server.ts — Kept origin/main's side: the inline
    routes are replaced by the single registerWorkspaceStatusRoutes(app, { ... })
    call. This preserves the main branch's refactoring.

  2. packages/cli/src/serve/routes/workspace-status.ts — Added the PR's new
    GET /workspace/mcp/:server/resources route to the extracted module, placed
    immediately after the existing /workspace/mcp/:server/tools route. Same
    validation pattern (server name required, length check via
    MAX_SERVER_NAME_LENGTH), same error handling (sendBridgeError), calling
    bridge.getWorkspaceMcpResourcesStatus(serverName).

Files modified

  • packages/cli/src/serve/server.ts — conflict markers removed, origin/main side kept
  • packages/cli/src/serve/routes/workspace-status.ts — resources route added

Files NOT modified

No other files were touched. The merge brought in many new files from main
(cua-driver package, serve server split modules, etc.) but those are part of
the merge itself, not conflict-resolution edits.

@yiliang114 yiliang114 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed current head ff62e0c after the conflict-resolution update. The new MCP resources route is correctly ported into registerWorkspaceStatusRoutes after the server route split, server.ts keeps the route registration clean, and there are no unresolved review threads. Current checks show no failures at review time; only the review automation is still pending. LGTM.

@yiliang114

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: COMMENT — 4 suggestions, no blockers.

The plumbing is consistent at every layer (daemon → ACP route → SDK → webui → McpDialog) and the test suite covers the happy paths well. A few targeted suggestions: two untested code paths (session fallback loop and catch-all error handler), one silent catch that should log, and a negative invariant in the disabled-server test that should be asserted explicitly.

if (sessionResources.length > 0) {
return sessionResources;
}
} catch {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The session fallback in resolveServerMcpResources swallows errors with a bare catch {} — no logging. The file uses debugLogger 58 times elsewhere for exactly this kind of silent-degradation scenario. The adjacent resolveServerMcpPrompts has the same pattern (line 3616).

Consider:

} catch {
  debugLogger.debug('[acpAgent] session fallback: no resources from any session for %s', serverName);
}

Without a log, a degraded session that can't return its config is invisible to operators debugging why a server shows resourceCount: 0 in the status but has resources in the registry.

if (resources.length > 0) {
return resources;
}
for (const session of this.getActiveSessions()) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The pool-mode session fallback loop (~30 lines) is untested. The new test mocks the workspace registry to return resources directly for docs, so this branch (iterating over active sessions when the registry is empty) is never entered.

A test like: mock pool.activeSessions() to return one session whose getConfig() has the matching server, assert resources are returned from the session path — would cover the fallback. The analogous tools builder (resolveServerMcpTools) presumably has the same gap, so both could be addressed together.

@@ -1611,6 +1642,8 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
mcpStatus: 'connected',
transport: 'http',
disabled: false,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] resourceCount: 0 and promptCount: 0 are correctly added to the remote (non-disabled) server entry. However, the disabled server entry below intentionally omits these fields — the implementation only adds counts when !disabled.

The test currently only asserts presence via toMatchObject but doesn't verify the negative invariant. Consider an explicit assertion:

const disabledEntry = mcp!.servers.find(s => s.name === 'disabled-server');
expect(disabledEntry).toBeDefined();
expect(disabledEntry!.resourceCount).toBeUndefined();
expect(disabledEntry!.promptCount).toBeUndefined();

This ensures a future refactor that accidentally adds counts to disabled servers gets caught.

acpChannelLive: true,
resources,
};
} catch (error) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The outer try { ... } catch at the bottom of buildWorkspaceMcpResourcesStatus handles unexpected errors (returning errors: [{ kind: 'mcp_resources', status: 'error' }]), but the test only exercises the happy path and the "not configured" early return.

A test that forces an error — e.g., stubbing resolveServerMcpResources to throw — and asserts the error envelope would cover this path. The earlier suggestion about the unconfigured-server error branch (line 5121) was already addressed in 2abb167; this is the remaining untested branch.

@wenshao
wenshao added this pull request to the merge queue Jun 26, 2026
Merged via the queue into QwenLM:main with commit ef0c39c Jun 26, 2026
54 checks passed
// isolated in its own try/catch so a failed resource refresh never
// turns a successful reconnect/enable into a reported failure.
if (nextServer.resourceCount) {
try {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The try/catch around mcp.loadResources(nextServer.name) is dead code. loadMcpResources in actions.ts catches all errors internally and returns a fallback ServeWorkspaceMcpResourcesStatus — it never rejects. The catch block ("Leave the prior resource list in place") can never execute; a failed fetch stores the fallback object (empty resources + error cell) into state instead, contrary to what the comment promises.

Either remove the try/catch since it's dead code, or change loadMcpResources in actions.ts to re-throw so the caller's catch can actually retain the prior list as the comment describes.

return;
}

case `${QWEN_METHOD_NS}workspace/mcp/resources`: {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The ACP dispatch for workspace/mcp/resources validates that serverName is non-empty but does not enforce MAX_NAME_LENGTH. The HTTP route in workspace-status.ts correctly checks serverName.length > MAX_SERVER_NAME_LENGTH, and the servers/add / servers/remove dispatch cases in this same file enforce name.length > MAX_NAME_LENGTH. The new resources dispatch is inconsistent with both.

An ACP/JSON-RPC client can send an arbitrarily long serverName that flows through to buildWorkspaceMcpResourcesStatus, which reflects it in the response body and error messages — enabling response amplification.

case `${QWEN_METHOD_NS}workspace/mcp/resources`: {
  const serverName = String(params['serverName'] ?? '');
  if (!serverName || serverName.length > MAX_NAME_LENGTH) {
    // ...
  }

Note: the existing workspace/mcp/tools dispatch has the same gap — not a reason to skip it here, but a candidate for a follow-up cleanup.

resource.title || resource.name || '';
return (
<div
key={resource.uri}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Nice to have] React list key uses resource.uri alone, but the expand-state tracking above uses the NUL-separated resourceKey(server.name, resource.uri). If an MCP server ever returns duplicate URIs (protocol violation, but defensive coding is cheap), React would emit a key-collision warning.

<div key={resourceKey(server.name, resource.uri)} className={styles.tool}>

// turns a successful reconnect/enable into a reported failure.
if (nextServer.resourceCount) {
try {
const nextResources = await mcp.loadResources(nextServer.name);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] mcp.loadTools() (line 381) and mcp.loadResources() (this line) execute sequentially — the resource fetch only begins after the tools fetch resolves. These are independent network round-trips with no data dependency. The initial-load path in App.tsx correctly parallelizes both fetches with Promise.all, so this is an inconsistency.

Reload latency is T_tools + T_resources instead of max(T_tools, T_resources). Consider fetching both in parallel with Promise.all and then updating state in the same synchronous block.

// Disabled servers are not discovered, so leave their counts
// absent — mirrors the TUI ServerDetailStep gating.
if (!disabled) {
out.resourceCount = this.resolveServerMcpResources(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] resourceCount and promptCount assignments are not wrapped in try/catch. The resolveServerMcpResources method has defensive optional chaining for getResourceRegistry?.(), but getResourcesByServer(serverName) itself (a Map iteration + sort) could throw on corrupted registry data. Since this runs inside servers.map(...) building the base status, one throw rejects the entire /workspace/mcp response — the MCP dialog goes completely blank.

The intent is clearly defensive (the comment says "a missing registry must degrade to no resources rather than throwing and collapsing the whole /mcp status"), but the call site does not match. Consider wrapping each assignment in its own try/catch so a bad registry degrades gracefully.

@ytahdn ytahdn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — clean end-to-end plumbing that mirrors the existing tools pattern at every layer. The layered test coverage (daemon handler → HTTP route → SDK client → webui provider) is thorough. A few non-blocking suggestions posted as inline comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants