Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions packages/acp-bridge/src/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3745,6 +3745,28 @@ export function createAcpSessionBridge(opts: BridgeOptions): AcpSessionBridge {
);
},

async getWorkspaceMcpResourcesStatus(serverName) {
return requestWorkspaceStatus(
SERVE_STATUS_EXT_METHODS.workspaceMcpResources,
() => ({
v: STATUS_SCHEMA_VERSION,
workspaceCwd: boundWorkspace,
serverName,
initialized: false,
acpChannelLive: false,
resources: [],
errors: [
{
kind: 'mcp_resources',
status: 'not_started' as const,
hint: 'spawn a session to populate',
},
],
}),
{ serverName },
);
},

async getWorkspaceToolsStatus() {
return requestWorkspaceStatus(
SERVE_STATUS_EXT_METHODS.workspaceTools,
Expand Down
11 changes: 11 additions & 0 deletions packages/acp-bridge/src/bridgeTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type {
ServeWorkspaceExtensionsStatus,
ServeWorkspaceHooksStatus,
ServeWorkspaceMcpToolsStatus,
ServeWorkspaceMcpResourcesStatus,
ServeWorkspaceToolsStatus,
ServeSessionContextUsageStatus,
ServeSessionStatsStatus,
Expand Down Expand Up @@ -457,6 +458,16 @@ export interface AcpSessionBridge {
serverName: string,
): Promise<ServeWorkspaceMcpToolsStatus>;

/**
* Read discovered MCP resources (`resources/list`) for one server from
* the live ACP registry. Drill-down companion to
* `getWorkspaceMcpToolsStatus`; the per-server `resourceCount` rides
* the base `/workspace/mcp` status.
*/
getWorkspaceMcpResourcesStatus(
serverName: string,
): Promise<ServeWorkspaceMcpResourcesStatus>;

/**
* Read the live built-in tool registry for the bound workspace.
* (New in upstream — kept in bridge pending workspace service migration.)
Expand Down
51 changes: 51 additions & 0 deletions packages/acp-bridge/src/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export class MissingCliEntryError extends Error {
export const SERVE_STATUS_EXT_METHODS = {
workspaceMcp: 'qwen/status/workspace/mcp',
workspaceMcpTools: 'qwen/status/workspace/mcp/tools',
workspaceMcpResources: 'qwen/status/workspace/mcp/resources',
workspaceSkills: 'qwen/status/workspace/skills',
workspaceTools: 'qwen/status/workspace/tools',
workspaceProviders: 'qwen/status/workspace/providers',
Expand Down Expand Up @@ -196,6 +197,24 @@ export interface ServeWorkspaceMcpServerStatus extends ServeStatusCell {
};
description?: string;
extensionName?: string;
/**
* Count of MCP resources (`resources/list`) this server advertises,
* from the workspace `ResourceRegistry`. Rides the existing status
* payload so dashboards can show a "Resources: N" line and gate a
* resource-browser affordance without a separate fetch. Absent on
* older daemons; present (including `0`) on newer daemons for
* non-disabled servers. The full list is fetched lazily via
* `qwen/status/workspace/mcp/resources`.
*/
resourceCount?: number;
/**
* Count of MCP prompts (`prompts/list`) this server advertises, from
* the workspace `PromptRegistry`. Inline-only (there is no prompt
* drill-down endpoint — prompts surface as slash commands), so this
* count is the sole signal a dashboard has. Absent on older daemons;
* present (including `0`) on newer daemons for non-disabled servers.
*/
promptCount?: number;
/**
* Why this server is not live, when known. Distinguishes
* operator-disabled (`disabled: true` from `disabledMcpServers`
Expand Down Expand Up @@ -324,6 +343,38 @@ export interface ServeWorkspaceMcpToolsStatus {
errors?: ServeStatusCell[];
}

/**
* One resource advertised by an MCP server (`resources/list`). Mirrors
* the `MCPResourceDisplayInfo` the TUI `/mcp` dialog renders: metadata
* only (no content). The content is read on demand in-chat via the
* `@<serverName>:<uri>` reference, which the frontend reconstructs from
* `serverName` (the parent `ServeWorkspaceMcpResourcesStatus`) + `uri`.
*/
export interface ServeWorkspaceMcpResourceStatus {
uri: string;
name?: string;
title?: string;
description?: string;
mimeType?: string;
size?: number;
}

/**
* Drill-down payload for `GET /workspace/mcp/:server/resources`. Mirrors
* `ServeWorkspaceMcpToolsStatus` — resources are a per-server drill-down
* exactly like tools, kept off the base `/workspace/mcp` status so that
* frequently-polled payload stays lean.
*/
export interface ServeWorkspaceMcpResourcesStatus {
v: typeof STATUS_SCHEMA_VERSION;
workspaceCwd: string;
serverName: string;
initialized: boolean;
acpChannelLive: boolean;
resources: ServeWorkspaceMcpResourceStatus[];
errors?: ServeStatusCell[];
}

export type ServeSkillLevel = 'project' | 'user' | 'extension' | 'bundled';

export interface ServeWorkspaceSkillStatus extends ServeStatusCell {
Expand Down
70 changes: 70 additions & 0 deletions packages/cli/src/acp-integration/acpAgent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1560,6 +1560,27 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
]),
getActiveRuntimeModelSnapshot: vi.fn().mockReturnValue(undefined),
getModel: vi.fn().mockReturnValue('qwen-plus'),
getResourceRegistry: vi.fn().mockReturnValue({
getResourcesByServer: (name: string) =>
name === 'docs'
? [
{
uri: 'file:///docs/intro.md',
name: 'Intro',
title: 'Introduction',
description: 'Getting started',
mimeType: 'text/markdown',
size: 1024,
serverName: 'docs',
},
{ uri: 'file:///docs/api.md', serverName: 'docs' },
]
: [],
}),
getPromptRegistry: vi.fn().mockReturnValue({
getPromptsByServer: (name: string) =>
name === 'docs' ? [{ name: 'summarize', serverName: 'docs' }] : [],
}),
} as unknown as Config;

const agentPromise = runAcpAgent(
Expand All @@ -1579,6 +1600,14 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
SERVE_STATUS_EXT_METHODS.workspaceMcp,
{},
);
const mcpResources = await agent.extMethod(
SERVE_STATUS_EXT_METHODS.workspaceMcpResources,
{ serverName: 'docs' },
);
const mcpResourcesMissing = await agent.extMethod(
SERVE_STATUS_EXT_METHODS.workspaceMcpResources,
{ serverName: 'not-configured' },
);
const skills = await agent.extMethod(
SERVE_STATUS_EXT_METHODS.workspaceSkills,
{},
Expand All @@ -1603,6 +1632,8 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
disabled: false,
description: 'Docs server',
extensionName: 'docs-ext',
resourceCount: 2,
promptCount: 1,
},
{
kind: 'mcp_server',
Expand All @@ -1611,6 +1642,8 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
mcpStatus: 'connected',
transport: 'http',
disabled: false,
resourceCount: 0,
promptCount: 0,
},
{
kind: 'mcp_server',
Expand All @@ -1634,6 +1667,43 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
expect(JSON.stringify(mcp)).not.toContain('Authorization');
expect(JSON.stringify(mcp)).not.toContain('bad-ext');

// The resources drill-down returns metadata-only entries (serverName is
// implied by the request, not echoed into each item).
expect(mcpResources).toMatchObject({
v: 1,
workspaceCwd: '/work/status',
serverName: 'docs',
initialized: true,
acpChannelLive: true,
resources: [
{
uri: 'file:///docs/intro.md',
name: 'Intro',
title: 'Introduction',
description: 'Getting started',
mimeType: 'text/markdown',
size: 1024,
},
{ uri: 'file:///docs/api.md' },
],
});
expect(
(mcpResources as { resources: Array<Record<string, unknown>> })
.resources[1],
).not.toHaveProperty('serverName');

// An unconfigured server name returns an error cell and does NOT fall
// back to scanning other servers/sessions.
expect(mcpResourcesMissing).toMatchObject({
v: 1,
workspaceCwd: '/work/status',
serverName: 'not-configured',
initialized: true,
acpChannelLive: true,
resources: [],
errors: [{ kind: 'mcp_resources', status: 'error' }],
});

expect(skills).toMatchObject({
v: 1,
workspaceCwd: '/work/status',
Expand Down
Loading
Loading