Merge pull request #7202 from deepseek-harness/worktree/sync-dsh-0.2.1-acp-stdout-tooltip-to-master
release(dsh): sync 0.2.1 ACP stdout and view-tab tooltip fixes back to master
This commit is contained in:
commit
10551d0e4e
14 files changed
+135
-21
No files matched your search
@@ -1,4 +1,4 @@
|
||||
/** Account catalog diagnostics preserve the shipped ACP profile's JSON-RPC stream. */
|
||||
/** Account catalog and MCP resource-listing diagnostics keep the shipped ACP profile's stdout JSON-RPC only. */
|
||||
import { once } from 'node:events'
|
||||
import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'
|
||||
import { createServer } from 'node:http'
|
||||
@@ -6,10 +6,22 @@ import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk'
|
||||
import { startMockLlmServer } from '@deepseek-ai/dsh-llm-mock-server'
|
||||
import { launchAcpTestAgent, type AgentUnderTest, type LaunchedAcpTestAgent } from '@deepseek-ai/dsh-session-snapshot'
|
||||
import { expect, it } from 'vitest'
|
||||
import { cleanupAcpExampleTest } from './cleanup.ts'
|
||||
|
||||
const repoRoot = fileURLToPath(new URL('../../../../../../', import.meta.url))
|
||||
const binScript = join(repoRoot, 'apps/cli/src/bin.ts')
|
||||
const tsconfigPath = join(repoRoot, 'tsconfig.json')
|
||||
|
||||
/** Parse every stdout line as a JSON-RPC frame; a diagnostic line fails the parse with its leading text. */
|
||||
function jsonRpcFrames(launched: LaunchedAcpTestAgent): Record<string, unknown>[] {
|
||||
const frames = launched.rawStdout().trim().split('\n').map(line => JSON.parse(line) as Record<string, unknown>)
|
||||
for (const frame of frames) expect(frame).toMatchObject({ jsonrpc: '2.0' })
|
||||
return frames
|
||||
}
|
||||
|
||||
it('keeps account model-listing diagnostics on stderr during API-key session creation', async () => {
|
||||
const cwd = await mkdtemp(join(tmpdir(), 'dsh-acp-stdout-'))
|
||||
const token = 'acp-catalog-fixture-token'
|
||||
@@ -100,3 +112,48 @@ it('keeps account model-listing diagnostics on stderr during API-key session cre
|
||||
if (failures.length > 0) throw new AggregateError(failures, 'ACP catalog test cleanup failed')
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps MCP resource-listing diagnostics off stdout for a tools-only client server', async () => {
|
||||
const cwd = await mkdtemp(join(tmpdir(), 'dsh-acp-stdout-'))
|
||||
const llm = await startMockLlmServer({
|
||||
sequence: ['tool_call_success', 'success'],
|
||||
toolName: 'list_mcp_resources',
|
||||
toolArguments: JSON.stringify({ server: 'fixture' }),
|
||||
})
|
||||
let launched: LaunchedAcpTestAgent | undefined
|
||||
try {
|
||||
const configPath = join(cwd, 'mcp.patch.yml')
|
||||
await writeFile(configPath, JSON.stringify([
|
||||
{ id: 'llm-deepseek', config: { baseURL: llm.baseURL, models: [{ id: 'fixture-api-key-model' }] } },
|
||||
{ id: 'acp', config: { provider: 'deepseek-official', model: 'fixture-api-key-model' } },
|
||||
{ id: 'session-persistence-jsonl', config: { root: join(cwd, 'sessions'), compression: 'none' } },
|
||||
]) + '\n')
|
||||
launched = launchAcpTestAgent({
|
||||
agent: { binScript, configPath, profile: 'acp', tsconfigPath },
|
||||
cwd,
|
||||
env: { DEEPSEEK_API_KEY: 'acp-fixture-api-key', DEEPSEEK_BASE_URL: llm.baseURL, DSH_TELEMETRY_DISABLED: '1' },
|
||||
})
|
||||
await launched.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
|
||||
// The fixture server advertises only the tools capability.
|
||||
const mcpServer = join(repoRoot, 'packages/mcp/mcp-client/tests/fixture-server.ts')
|
||||
const created = await launched.client.newSession({
|
||||
cwd, mcpServers: [{ name: 'fixture', command: process.execPath, args: [mcpServer], env: [] }],
|
||||
})
|
||||
await expect(launched.client.prompt({
|
||||
sessionId: created.sessionId,
|
||||
prompt: [{ type: 'text', text: 'list the fixture resources' }],
|
||||
})).resolves.toEqual({ stopReason: 'end_turn' })
|
||||
expect(launched.updates).toContainEqual(expect.objectContaining({ sessionUpdate: 'tool_call_update', status: 'completed' }))
|
||||
await launched.client.closeSession({ sessionId: created.sessionId })
|
||||
await launched.close()
|
||||
|
||||
expect(launched.child.exitCode).toBe(0)
|
||||
expect(llm.requests.map(request => request.behavior)).toEqual(['tool_call_success', 'success'])
|
||||
expect(jsonRpcFrames(launched).length).toBeGreaterThanOrEqual(5)
|
||||
} finally {
|
||||
const cleanup = await Promise.allSettled([cleanupAcpExampleTest(launched, cwd), llm.close()])
|
||||
const failures: unknown[] = []
|
||||
for (const result of cleanup) if (result.status === 'rejected') failures.push(result.reason)
|
||||
if (failures.length > 0) throw new AggregateError(failures, 'ACP MCP test cleanup failed')
|
||||
}
|
||||
})
|
||||
@@ -21,8 +21,8 @@
|
||||
en: 7871f9fe6bcc4459
|
||||
zh: 0d51565c600b3958
|
||||
/deepseek-ai-dsh-client-ui-conversation/shell-and-standard-props:
|
||||
en: 63ad16e66a02a67f
|
||||
zh: d035a62bacbc1bdf
|
||||
en: 5a9264158ebf0033
|
||||
zh: b74e5807ed883ef6
|
||||
/deepseek-ai-dsh-client-ui-conversation/temporary-composer-entries:
|
||||
en: bb1244310f5c0382
|
||||
zh: 824b84077432b559
|
||||
|
||||
@@ -82,7 +82,7 @@ The context-occupancy button shows a ring and percentage below the input card, a
|
||||
|
||||
With Coding Tools off, the shell hides only Trajectory; other plugin-contributed Views remain available. The View tab bar is hidden when fewer than two Views are available. Turning Coding Tools off while Trajectory is active returns to Chat; stored View preferences and recorded Session data remain intact. Turning it on makes Trajectory available again. View owners receive the available roster so their navigation actions can follow the same visibility.
|
||||
|
||||
While the root-scoped `conversation.session.header.views.actions` list has registrations, a **More conversation views** button with a tooltip follows the View tabs in the tab row. It opens a menu whose rows are that list's registrations. Each receives `entries` (Views outside the header in registration order with `id`, `label`, `pinned`, and `active`; Chat is never listed), `onOpen(id)`, which selects the View in this Session, `onPinnedChange(id, pinned)`, and `onDismiss()`, which closes the menu. The header keeps an in-memory list of unpinned View ids shared by every Session; the tab bar omits unpinned Views and places registered Chat first so it always remains outside More. Plugin reload resets pin choices, and legacy localStorage values are ignored. The tab bar's visibility counts available Views, not pinned ones, so the bar and More stay while every other View is unpinned. Unpinning the active View hides its tab immediately without changing the selected content. More lists only Views outside the header and identifies the current View through `active`. The header reads its available row width, reserves 48px for More and padding, and estimates 120px per tab. Remaining Views keep registration order in More. A single ResizeObserver watches the tab row so window resizing and sidebar expansion update the split; active selection does not change the order. The observer disconnects when the row is removed. If ResizeObserver is unavailable, the initial width calculation still applies without subsequent observation. The More button highlights and names the active View in its tooltip when that View is folded. Individual tabs are not measured. At least one preferred tab remains outside, and long labels truncate with a full-name tooltip. Unregistered View ids remain in the in-memory pin list until plugin reload.
|
||||
While the root-scoped `conversation.session.header.views.actions` list has registrations, a **More conversation views** button with a tooltip follows the View tabs in the tab row. It opens a menu whose rows are that list's registrations. Each receives `entries` (Views outside the header in registration order with `id`, `label`, `pinned`, and `active`; Chat is never listed), `onOpen(id)`, which selects the View in this Session, `onPinnedChange(id, pinned)`, and `onDismiss()`, which closes the menu. The header keeps an in-memory list of unpinned View ids shared by every Session; the tab bar omits unpinned Views and places registered Chat first so it always remains outside More. Plugin reload resets pin choices, and legacy localStorage values are ignored. The tab bar's visibility counts available Views, not pinned ones, so the bar and More stay while every other View is unpinned. Unpinning the active View hides its tab immediately without changing the selected content. More lists only Views outside the header and identifies the current View through `active`. The header reads its available row width, reserves 48px for More and padding, and estimates 120px per tab. Remaining Views keep registration order in More. A single ResizeObserver watches the tab row so window resizing and sidebar expansion update the split; active selection does not change the order. The observer disconnects when the row is removed. If ResizeObserver is unavailable, the initial width calculation still applies without subsequent observation. The More button highlights and names the active View in its tooltip when that View is folded. Each tab label estimates its text width from the glyphs; its full-name tooltip appears only when that estimate exceeds the 88px cell. At least one preferred tab remains outside. Unregistered View ids remain in the in-memory pin list until plugin reload.
|
||||
|
||||
The composer registers the File command action and owns its label, availability, and native file-dialog callback. Menu availability and invocation both consult the mounted composer's current attachment-intake policy. Unmounting or locking the composer disables that action; disposing the plugin removes its registration. The callback binding stays inside the input module.
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ composer 在挂载、切换 Session 和解除锁定时使用共享的[文字输
|
||||
|
||||
关闭代码工作工具时,外壳仅隐藏 Trajectory;其他插件贡献的 View 仍然可用。可用 View 少于两个时隐藏 View 标签栏。在 Trajectory 激活时关闭代码工作工具会返回 Chat;已保存的 View 偏好和 Session 记录保持不变。开启后,Trajectory 恢复可用。View 所有者接收可用列表,使导航操作跟随相同的可见性。
|
||||
|
||||
root 作用域的 `conversation.session.header.views.actions` list 有注册项时,标签行中的 View 标签之后显示带提示框的**更多会话视图**按钮。它打开的菜单以该 list 的注册项为菜单行。每个注册项接收 `entries`(按注册顺序的头部之外的 View,含 `id`、`label`、`pinned` 与 `active`;不含 Chat)、在本 Session 选择 View 的 `onOpen(id)`、`onPinnedChange(id, pinned)`,以及关闭菜单的 `onDismiss()`。头部在内存中保存所有 Session 共用的已收起 View id 列表;标签栏不显示已收起的 View,并将已注册的 Chat 放在首位,确保它始终留在更多菜单外。插件重新加载时 Pin 选择重置,不读取旧 localStorage 值。标签栏是否显示按可用 View 计算,不按已固定的 View 计算,因此其他 View 全部收起时,标签栏和“更多”仍然显示。收起当前激活的 View 会立即隐藏其标签,但不会切走其内容。更多菜单仅包含头部之外的 View,通过 `active` 标识当前 View。头部读取当前行可用宽度,为更多按钮与内边距预留 48px,每个标签估算 120px,其余 View 按注册顺序进入更多。单个 ResizeObserver 观察标签行,使窗口缩放和侧栏展开能够更新分组;当前选中项变化不改变顺序。标签行移除时断开观察。如果环境没有 ResizeObserver,仍按初始宽度计算,但不继续观察。当前 View 收起时,更多按钮高亮并在提示框中显示其名称。不逐项测量标签。至少保留一个优先展示的外部标签,长标题截断并通过提示框提供全名。未注册 View 的 id 保留在内存 Pin 列表中,直到插件重新加载。
|
||||
root 作用域的 `conversation.session.header.views.actions` list 有注册项时,标签行中的 View 标签之后显示带提示框的**更多会话视图**按钮。它打开的菜单以该 list 的注册项为菜单行。每个注册项接收 `entries`(按注册顺序的头部之外的 View,含 `id`、`label`、`pinned` 与 `active`;不含 Chat)、在本 Session 选择 View 的 `onOpen(id)`、`onPinnedChange(id, pinned)`,以及关闭菜单的 `onDismiss()`。头部在内存中保存所有 Session 共用的已收起 View id 列表;标签栏不显示已收起的 View,并将已注册的 Chat 放在首位,确保它始终留在更多菜单外。插件重新加载时 Pin 选择重置,不读取旧 localStorage 值。标签栏是否显示按可用 View 计算,不按已固定的 View 计算,因此其他 View 全部收起时,标签栏和“更多”仍然显示。收起当前激活的 View 会立即隐藏其标签,但不会切走其内容。更多菜单仅包含头部之外的 View,通过 `active` 标识当前 View。头部读取当前行可用宽度,为更多按钮与内边距预留 48px,每个标签估算 120px,其余 View 按注册顺序进入更多。单个 ResizeObserver 观察标签行,使窗口缩放和侧栏展开能够更新分组;当前选中项变化不改变顺序。标签行移除时断开观察。如果环境没有 ResizeObserver,仍按初始宽度计算,但不继续观察。当前 View 收起时,更多按钮高亮并在提示框中显示其名称。每个标签按字符宽度估算文本宽度;只有估算结果超过 88px 单元格时,才通过提示框提供全名。至少保留一个优先展示的外部标签。未注册 View 的 id 保留在内存 Pin 列表中,直到插件重新加载。
|
||||
|
||||
输入框注册「文件」命令动作,负责其标题、可用性和原生文件选择器回调。菜单可用性与实际调用都读取已挂载输入框当前的附件接收策略。输入框卸载或锁定后该动作不可用,插件 dispose(资源释放)时移除注册。回调绑定留在输入模块内部。
|
||||
|
||||
|
||||
@@ -43,6 +43,29 @@ function ViewsMoreMenu({ label, current, entries, onOpen, onPinnedChange, render
|
||||
)
|
||||
}
|
||||
|
||||
/** Approximate 13px text advance: full-width glyphs take one em, other glyphs about half. */
|
||||
const WIDE_GLYPH = /[\u1100-\u115F\u2E80-\uA4CF\uAC00-\uD7A3\uF900-\uFAFF\uFE30-\uFE4F\uFF00-\uFF60\uFFE0-\uFFE6]/u
|
||||
const WIDE_GLYPH_WIDTH = 13
|
||||
const NARROW_GLYPH_WIDTH = 7
|
||||
/** Mirrors the `.tabLabel` max-width in `ConversationRoot.module.css`. */
|
||||
const TAB_LABEL_MAX_WIDTH = 88
|
||||
|
||||
/** Estimate whether a View tab label exceeds its 88px cell. */
|
||||
function labelNeedsTooltip(label: string): boolean {
|
||||
let width = 0
|
||||
for (const glyph of label) width += WIDE_GLYPH.test(glyph) ? WIDE_GLYPH_WIDTH : NARROW_GLYPH_WIDTH
|
||||
return width > TAB_LABEL_MAX_WIDTH
|
||||
}
|
||||
|
||||
/** One View tab label; its full-name tooltip is offered only when the text is expected to overflow. */
|
||||
function ViewTabLabel({ label }: { readonly label: string }) {
|
||||
return (
|
||||
<Tooltip label={label} portal disabled={!labelNeedsTooltip(label)}>
|
||||
<span className={css.tabLabel}>{label}</span>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
interface Breadcrumb {
|
||||
readonly id: SessionId
|
||||
readonly displayTitle: string
|
||||
@@ -205,7 +228,7 @@ export function ConversationSessionHeader({
|
||||
className={clsx(css.tab, viewTab.id === active?.id && css.tabActive)}
|
||||
onClick={() => { selectView(viewTab.id) }}
|
||||
>
|
||||
<Tooltip label={viewTab.label} portal><span className={css.tabLabel}>{viewTab.label}</span></Tooltip>
|
||||
<ViewTabLabel label={viewTab.label} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { GlobalStandardProps, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createContext, useContext, type ReactNode } from 'react'
|
||||
import { act, cleanup, fireEvent, render, within } from '@testing-library/react'
|
||||
import { act, cleanup, fireEvent, render, screen, within } from '@testing-library/react'
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type { SessionListState, SessionSnapshot } from '@deepseek-ai/dsh-api-session-controller/client'
|
||||
import type { WorkspaceSnapshot, WorkspaceView } from '@deepseek-ai/dsh-api-workspace-controller/client'
|
||||
@@ -735,6 +735,29 @@ describe('ConversationRoot resident composer', () => {
|
||||
expect(b.view.queryByRole('menu')).toBeNull()
|
||||
})
|
||||
|
||||
it('offers a View tab tooltip only when the label is expected to overflow', () => {
|
||||
const b = mount(sessionSnapshotOf(), undefined, undefined, {
|
||||
viewTabs: [
|
||||
{ id: 'chat', label: 'Chat' },
|
||||
{ id: 'short', label: '轨迹' },
|
||||
{ id: 'long-cjk', label: '很长的视图名称' },
|
||||
{ id: 'long-latin', label: 'A very long view name' },
|
||||
],
|
||||
headerOnly: true,
|
||||
})
|
||||
const tooltipFor = (label: string): string | null => {
|
||||
const span = within(b.view.getByRole('tab', { name: label })).getByText(label)
|
||||
fireEvent.pointerEnter(span)
|
||||
const tooltip = screen.queryByRole('tooltip', { hidden: true })?.textContent ?? null
|
||||
fireEvent.pointerLeave(span)
|
||||
return tooltip
|
||||
}
|
||||
expect(tooltipFor('Chat')).toBeNull()
|
||||
expect(tooltipFor('轨迹')).toBeNull()
|
||||
expect(tooltipFor('很长的视图名称')).toBe('很长的视图名称')
|
||||
expect(tooltipFor('A very long view name')).toBe('A very long view name')
|
||||
})
|
||||
|
||||
it('reuses the width budget on View roster and selection changes without a window resize listener', () => {
|
||||
let width = 480
|
||||
const readWidth = vi.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockImplementation(() => width)
|
||||
|
||||
@@ -42,8 +42,8 @@
|
||||
en: ae9029b48b2a2e4e
|
||||
zh: 048f08361b799667
|
||||
/deepseek-ai-dsh-mcp-client/understand-the-implementation/tool-execution-internals:
|
||||
en: b4333df1bd3f027c
|
||||
zh: 6bb4be691452c27a
|
||||
en: bb161208cc002746
|
||||
zh: 3ab0588ca71effe5
|
||||
/deepseek-ai-dsh-mcp-client/understand-the-implementation/environment-scrubbing-stdio:
|
||||
en: fef482a6f8cc789f
|
||||
zh: 08f4c594ec83b4a1
|
||||
|
||||
@@ -130,7 +130,7 @@ The SDK receives tool-list changes through legacy notifications or a modern subs
|
||||
|
||||
### Tool execution internals
|
||||
|
||||
A tool call uses the SDK with the raw name, complete tool definition, JSON arguments, abort signal, and configured timeout. The SDK owns protocol validation, advertised output-schema validation, and modern request headers. Canonical success is `{ content: JsonValue[], structuredContent? }`, preserving valid MCP JSON blocks for programmatic and PTC mode callers. An MCP `isError` result throws before image persistence. The bridge validates each image batch before saving it; a refusal projects every image as diagnostic text.
|
||||
A tool call uses the SDK with the raw name, complete tool definition, JSON arguments, abort signal, and configured timeout. The SDK owns protocol validation, advertised output-schema validation, and modern request headers. Canonical success is `{ content: JsonValue[], structuredContent? }`, preserving valid MCP JSON blocks for programmatic and PTC mode callers. An MCP `isError` result throws before image persistence. The bridge validates each image batch before saving it; a refusal projects every image as diagnostic text. For a server that does not advertise `resources`, resource and template lists return empty pages without calling the SDK, whose fallback writes a diagnostic to stdout, where the ACP profile carries JSON-RPC.
|
||||
|
||||
### Environment scrubbing (stdio)
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ SDK 通过旧版通知或现代协议订阅接收工具列表变化。监督器
|
||||
|
||||
### 工具执行内部细节
|
||||
|
||||
工具调用向 SDK 提供原始名称、完整工具定义、JSON 参数、取消信号及配置的超时。SDK 负责协议校验、已声明输出 schema 的校验和现代协议请求 header。成功结果规范值为 `{ content: JsonValue[], structuredContent? }`,为编程调用方及 PTC 模式保留有效的 MCP JSON 块。MCP `isError` 结果会在图片持久化前抛出。桥接器在保存前校验整批图片;拒绝时将每张图片投影为诊断文本。
|
||||
工具调用向 SDK 提供原始名称、完整工具定义、JSON 参数、取消信号及配置的超时。SDK 负责协议校验、已声明输出 schema 的校验和现代协议请求 header。成功结果规范值为 `{ content: JsonValue[], structuredContent? }`,为编程调用方及 PTC 模式保留有效的 MCP JSON 块。MCP `isError` 结果会在图片持久化前抛出。桥接器在保存前校验整批图片;拒绝时将每张图片投影为诊断文本。对未声明 `resources` 能力的服务器,资源列表与模板列表直接返回空页而不调用 SDK,因为 SDK 的兜底逻辑会把诊断写到 stdout,而 ACP 配置档在 stdout 上传输 JSON-RPC。
|
||||
|
||||
### 环境清洗(stdio)
|
||||
|
||||
|
||||
@@ -368,15 +368,22 @@ export function startConnection(ctx: Context, config: Config, policy: ResolvedRe
|
||||
const generation = client
|
||||
if (!generation || connectedAt === undefined) throw new Error(`${label}: server is disconnected`)
|
||||
const options = { signal: exec.signal, timeout: config.toolCallTimeoutMs }
|
||||
// The SDK answers an unadvertised capability with the same empty page, but
|
||||
// writes a console.debug line to stdout, which the ACP profile reserves for JSON-RPC.
|
||||
const listable = generation.getServerCapabilities()?.resources !== undefined
|
||||
switch (request.method) {
|
||||
case 'resources/list':
|
||||
return await generation.listResources(
|
||||
request.cursor === undefined ? undefined : { cursor: request.cursor }, options,
|
||||
) as JsonValue
|
||||
return listable
|
||||
? await generation.listResources(
|
||||
request.cursor === undefined ? undefined : { cursor: request.cursor }, options,
|
||||
) as JsonValue
|
||||
: { resources: [] }
|
||||
case 'resources/templates/list':
|
||||
return await generation.listResourceTemplates(
|
||||
request.cursor === undefined ? undefined : { cursor: request.cursor }, options,
|
||||
) as JsonValue
|
||||
return listable
|
||||
? await generation.listResourceTemplates(
|
||||
request.cursor === undefined ? undefined : { cursor: request.cursor }, options,
|
||||
) as JsonValue
|
||||
: { resourceTemplates: [] }
|
||||
case 'resources/read':
|
||||
return await generation.readResource({ uri: request.uri }, options) as JsonValue
|
||||
/* v8 ignore next 2 -- resource requests are the closed, typed tool operation union */
|
||||
|
||||
@@ -65,6 +65,8 @@ describe('modern MCP connections', () => {
|
||||
expect(names.toSorted()).toEqual([
|
||||
'list_mcp_resource_templates', 'list_mcp_resources', 'mcp__fixture__ping', 'read_mcp_resource',
|
||||
])
|
||||
const debug = vi.spyOn(console, 'debug').mockImplementation(() => {})
|
||||
onTestFinished(() => { debug.mockRestore() })
|
||||
for (const [name, expected] of [
|
||||
['list_mcp_resources', { resources: [] }],
|
||||
['list_mcp_resource_templates', { resourceTemplates: [] }],
|
||||
@@ -75,6 +77,8 @@ describe('modern MCP connections', () => {
|
||||
})
|
||||
expect(result).toMatchObject({ isError: false, value: expected })
|
||||
}
|
||||
// console.debug writes stdout, which the ACP profile reserves for JSON-RPC.
|
||||
expect(debug).not.toHaveBeenCalled()
|
||||
const read = await executeToolFixture(ctx, {
|
||||
name: 'read_mcp_resource', arguments: { server: 'fixture', uri: 'memo://readme' },
|
||||
callId: ToolCallId('unsupported-resource-read'), signal: new AbortController().signal,
|
||||
|
||||
@@ -57,8 +57,8 @@
|
||||
en: ea0dfc069bf175c0
|
||||
zh: 3e1465e56ff79764
|
||||
/deepseek-ai-dsh-mcp-resources/known-limitations-and-deferred-work:
|
||||
en: 58d13dc99946c11d
|
||||
zh: 4099c46a1ca60512
|
||||
en: 73f1775fd13a7e98
|
||||
zh: 8501425c7dd12aee
|
||||
/deepseek-ai-dsh-mcp-resources/known-limitations-and-deferred-work/dev-note:
|
||||
en: e67f633dc6fe7773
|
||||
zh: 3bb0c20c93f16f98
|
||||
@@ -117,7 +117,7 @@ Each result appends to history without rewriting earlier results. Later reads ca
|
||||
|
||||
Resource access is explicit and on demand.
|
||||
|
||||
- A configured server without the MCP `resources` capability still appears in the server-name prompt and keeps shared resource tools available. The SDK returns empty resource and template lists; unsupported reads fail.
|
||||
- A configured server without the MCP `resources` capability still appears in the server-name prompt and keeps shared resource tools available. The MCP client returns empty resource and template lists without contacting the server; unsupported reads fail.
|
||||
- `tools.restrict()` checks names supplied by global or ancestor scopes when the filter is registered. Naming a resource tool absent from those scopes fails as an unknown tool. Resource tools registered in the caller's own scope are outside allow/deny masks.
|
||||
- Resource subscriptions and update notifications are unsupported; call the list or read tools again to obtain current content.
|
||||
- Binary resources are not projected as native images or audio. Programmatic callers retain their canonical base64 values.
|
||||
|
||||
@@ -117,7 +117,7 @@ kind: "package-reference"
|
||||
|
||||
资源访问由显式调用按需发起。
|
||||
|
||||
- 不具备 MCP `resources` 能力的已配置服务器仍会出现在服务器名称提示词中,并保持共享资源工具可用。SDK 返回空的资源列表与模板列表;不受支持的读取会失败。
|
||||
- 不具备 MCP `resources` 能力的已配置服务器仍会出现在服务器名称提示词中,并保持共享资源工具可用。MCP 客户端直接返回空的资源列表与模板列表,不联系服务器;不受支持的读取会失败。
|
||||
- `tools.restrict()` 在注册过滤条件时检查全局或祖先作用域提供的名称。引用这些作用域中不存在的资源工具名称会报未知工具错误。注册在调用方自身作用域中的资源工具不受 allow/deny 掩码过滤。
|
||||
- 不支持资源订阅与更新通知;再次调用列表或读取工具以获取当前内容。
|
||||
- 二进制资源不会投影为原生图片或音频。程序化调用方保留其规范 base64 值。
|
||||
|
||||
Reference in new issue
Block a user