Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions docs/reference/frontend-mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ Each exposed tool receives a stable model-visible name:
- Streamable HTTP is the initial transport.
- Discovery and connection have a bounded timeout (8 seconds by default).
- Remote servers require HTTPS. Loopback HTTP is allowed only without headers.
- A server URL may be one exact environment reference such as `${MCP_URL}`.
- Header values may reference one exact environment variable with
`${VARIABLE}`. A missing variable is a configuration error.
- Every enabled tool must explicitly declare `readOnly`. A writable tool must
Expand Down
1 change: 1 addition & 0 deletions docs/reference/frontend-mcp.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ DOCUMENT_MCP_AUTHORIZATION=Bearer replace-me
- 首个版本使用 Streamable HTTP Transport。
- 工具发现和连接有超时边界,默认 8 秒。
- 远端服务必须使用 HTTPS;回环地址可以使用 HTTP,但不能携带 Header。
- Server URL 可以用 `${MCP_URL}` 精确引用一个环境变量。
- Header 值可以用 `${VARIABLE}` 精确引用一个环境变量;变量缺失即配置错误。
- 启用的工具必须明确声明 `readOnly`。可写工具还必须设置
`approval: "required"`,否则 Gateway 会在启动时拒绝该配置。
Expand Down
2 changes: 2 additions & 0 deletions examples/car/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ QWEN_AUDIO_REALTIME_VOICE=longanqian
COCKPIT_DOMAIN_HOST=127.0.0.1
COCKPIT_DOMAIN_PORT=3010
COCKPIT_DOMAIN_ORIGIN=http://127.0.0.1:3010
# Optional explicit foreground MCP endpoint; derived from COCKPIT_DOMAIN_ORIGIN by default.
# COCKPIT_FRONTEND_MCP_URL=http://127.0.0.1:3010/mcp/frontend
COCKPIT_AGENT_HOST=127.0.0.1
COCKPIT_AGENT_PORT=3020
COCKPIT_AGENT_CARD_URL=http://127.0.0.1:3020/.well-known/agent-card.json
Expand Down
2 changes: 1 addition & 1 deletion examples/car/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
- `react-app/` 是可替换的客户场景客户端,只依赖 GCP Client SDK;浏览器音频和 UI 保持场景本地。
- `gateway.mjs` 是场景装配点,不复制 Gateway、Realtime、Task、播报或历史实现。
- `agent/` 是轻量、可替换的 A2A 后台示例,不扩展成通用 Agent 框架。
- `domain/` 是单一业务状态源,通过 MCP 服务 Agent、通过 HTTP/SSE 服务 UI。
- `domain/` 是单一业务状态源,通过 HTTP/SSE 服务 UI;`tools/` 按领域组织场景工具并通过前台/后台 MCP 工具面暴露
- 不增加 `actions[]`、第二套 Realtime Server、第二套会话历史或 Gateway 可解析的座舱对象。

前端使用 React 19、Vite 8 和 JavaScript;组件用函数与 Hooks,样式保留在 `App.css`。提交前运行:
Expand Down
5 changes: 3 additions & 2 deletions examples/car/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ A preflight validates the Realtime configuration and all four ports before any c
- The UI talks to the Gateway through GCP and knows nothing about the Realtime provider or backend Agent.
- The primary cockpit stays voice-only. Transcripts appear only in the debug panel, and ASR displays final results only.
- Scenario-specific HTTP/SSE projects vehicle, route, media, weather, and order state, plus fine-grained scenario progress. The Gateway does not parse those objects.
- The foreground Agent owns realtime conversation and submits cockpit work through the fixed `spawn_thinking` bridge. The scenario customizes only the backend capability description in [`spawn-thinking-tool.mjs`](spawn-thinking-tool.mjs), while the tool name and argument contract stay fixed.
- The example backend attaches over A2A and invokes domain capabilities through MCP. It intentionally implements only a small intent router.
- The foreground Agent owns realtime conversation and can call the read-only weather tool directly through standard MCP. Other cockpit work goes through the fixed `spawn_thinking` bridge.
- The example backend attaches over A2A and uses a separate backend MCP surface for vehicle, navigation, music, and flash-buy operations. It intentionally implements only a small intent router.
- Scenario tools live in domain-oriented packages under [`tools/`](tools/README.md). One explicit registry adds groups or assigns them to the foreground or backend without changing Gateway protocols.
- Customers can replace the UI, backend Agent, or domain service without changing the framework core.

## Development and tests
Expand Down
5 changes: 3 additions & 2 deletions examples/car/README_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ npm run example:car
- UI 仅通过 GCP 与 Gateway 对话,不感知 Realtime Provider 或后台 Agent。
- 主座舱区域保持纯语音交互;文字转写只进入调试面板,并且 ASR 仅展示最终结果。
- UI 通过场景自己的 HTTP/SSE 通道展示车辆、路线、音乐、天气和订单状态,以及细粒度场景进度;Gateway 不解析这些对象。
- 前台 Agent 负责实时聊天,座舱任务通过固定的 `spawn_thinking` 桥梁提交给后台;场景只在 [`spawn-thinking-tool.mjs`](spawn-thinking-tool.mjs) 定义后台能力描述,不改工具名称和参数协议。
- 示例后台通过 A2A 接入 Gateway,并通过 MCP 调用领域能力;它只实现少量意图路由,不模拟完整行业 Agent。
- 前台 Agent 负责实时聊天,可通过标准 MCP 直接调用只读天气工具;其他座舱任务通过固定的 `spawn_thinking` 桥梁提交给后台。
- 示例后台通过 A2A 接入 Gateway,并通过独立的后台 MCP 工具面调用车控、导航、音乐和闪购;它只实现少量意图路由。
- 场景工具按领域收敛在 [`tools/`](tools/README.md),开发者通过显式注册表增加工具包或调整前后台归属,不需要修改 Gateway 协议。
- 客户可以替换整个 UI、后台 Agent 或领域服务,而不修改框架核心。

## 开发与测试
Expand Down
2 changes: 1 addition & 1 deletion examples/car/agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Defaults:

- A2A Agent: `http://127.0.0.1:3020`
- Agent Card: `http://127.0.0.1:3020/.well-known/agent-card.json`
- Cockpit MCP: `http://127.0.0.1:3010/mcp?cockpitId=default`
- Cockpit MCP: `http://127.0.0.1:3010/mcp/backend?cockpitId=default`

Environment variables:

Expand Down
2 changes: 1 addition & 1 deletion examples/car/agent/executor.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export class CockpitAgentExecutor {
const result = plan
? await this.tools.call(plan.name, plan.arguments, { signal: controller.signal })
: {
content: '这个轻量示例只演示车控、导航、音乐、天气和闪购能力。',
content: '这个轻量示例的后台 Agent 只演示车控、导航、音乐和闪购能力。',
data: {},
}
eventBus.publish(AgentEvent.artifactUpdate({
Expand Down
10 changes: 0 additions & 10 deletions examples/car/agent/intent-router.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,6 @@ function destination(text) {
return clean(match?.[1]).replace(/(?:怎么走|的路线|路线)$/u, '')
}

function city(text) {
const match = text.match(/([\p{Script=Han}]{2,12})(?:市|的)?(?:天气|气温)/u)
return clean(match?.[1]).replace(/市$/u, '') || '杭州'
}

function songQuery(text) {
const match = text.match(/(?:播放|放|听)(?:一首|一下)?([^,。,.!?!?\n]{1,30})/u)
return clean(match?.[1]).replace(/(?:这首歌|音乐|歌曲)$/u, '')
Expand Down Expand Up @@ -56,10 +51,6 @@ export function planCockpitTool(input) {
}
}

if (/(天气|气温)/u.test(text)) {
return { name: 'weather', arguments: { city: city(text) } }
}

if (/(停止导航|结束导航|取消导航)/u.test(text)) {
return { name: 'navigation_stop', arguments: {} }
}
Expand Down Expand Up @@ -129,6 +120,5 @@ export function describePlan(plan) {
if (plan.name.startsWith('vehicle_')) return '正在执行车辆操作'
if (plan.name.startsWith('navigation_')) return '正在处理导航请求'
if (plan.name.startsWith('music_')) return '正在处理音乐请求'
if (plan.name === 'weather') return '正在查询天气'
return '正在处理闪购请求'
}
2 changes: 1 addition & 1 deletion examples/car/agent/mcp-client.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export class CockpitMcpTools {
origin = 'http://127.0.0.1:3010',
cockpitId = 'default',
} = {}) {
this.url = new URL('/mcp', origin)
this.url = new URL('/mcp/backend', origin)
this.url.searchParams.set('cockpitId', cockpitId)
this.client = null
this.connecting = null
Expand Down
4 changes: 2 additions & 2 deletions examples/car/agent/server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ function agentCard(origin) {
skills: [{
id: 'cockpit_operations',
name: 'Cockpit operations',
description: 'Vehicle, navigation, music, weather and flash-buy example operations.',
description: 'Vehicle, navigation, music and flash-buy example operations.',
tags: ['cockpit', 'vehicle', 'navigation', 'music'],
examples: ['打开主驾车窗', '导航到西湖', '播放晴天', '杭州天气怎么样'],
examples: ['打开主驾车窗', '导航到西湖', '播放晴天'],
inputModes: ['text/plain'],
outputModes: ['text/plain', 'application/json'],
securityRequirements: [],
Expand Down
7 changes: 0 additions & 7 deletions examples/car/agent/test/integration.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,6 @@ test('runs core cockpit capabilities through A2A and MCP without UI actions', as
trafficSegments: [],
}
},
async weather(city) {
return { city, dayweather: '晴', daytemp: '26', nighttemp: '18' }
},
},
})
const domain = new CockpitDomainServer({ domain: cockpit, port: 0 })
Expand Down Expand Up @@ -65,9 +62,6 @@ test('runs core cockpit capabilities through A2A and MCP without UI actions', as
const music = await submit('播放晴天')
assert.match(music.content, /正在播放:晴天/u)

const weather = await submit('杭州天气怎么样')
assert.match(weather.content, /杭州,晴,26°/u)

const cart = await submit('帮我买杯奶茶')
assert.match(cart.content, /订单预览/u)
assert.match(cart.content, /确认是否下单/u)
Expand All @@ -82,6 +76,5 @@ test('runs core cockpit capabilities through A2A and MCP without UI actions', as
assert.equal(state.navigation.destination, '杭州西湖')
assert.equal(state.music.playing, true)
assert.equal(state.music.playlist[state.music.currentIndex].title, '晴天')
assert.equal(state.weather.dayweather, '晴')
assert.match(state.flashbuy.order.id, /^SG/u)
})
5 changes: 1 addition & 4 deletions examples/car/agent/test/intent-router.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,7 @@ test('routes representative cockpit requests to existing MCP tools', () => {
name: 'music_play',
arguments: { query: '晴天' },
})
assert.deepEqual(planCockpitTool('杭州天气怎么样'), {
name: 'weather',
arguments: { city: '杭州' },
})
assert.equal(planCockpitTool('杭州天气怎么样'), null)
})

test('keeps explicit order confirmation separate from product search', () => {
Expand Down
25 changes: 11 additions & 14 deletions examples/car/docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,13 @@
## 四个独立进程

```text
┌──────────────────────────┐ GCP 6.0 ┌──────────────────────────┐
│ cockpit-client │◄────────────────────►│ cockpit-gateway │
│ React UI + Browser Audio │ transcript/audio/ │ qwen-audio-agent Gateway │
│ │ Tasks/receipts │ + 前台 Realtime Agent │
└────────────┬─────────────┘ └────────────┬─────────────┘
│ HTTP/SSE │ BackendPort
│ 业务状态与面板操作 │ A2A
▼ ▼
┌──────────────────────────┐ MCP ┌──────────────────────────┐
│ cockpit-domain │◄─────────────────────│ cockpit-agent │
│ 单一领域状态与能力 │ │ 轻量示例后台 Agent │
└──────────────────────────┘ └──────────────────────────┘
cockpit-client ── GCP 6.0 ──► cockpit-gateway ── A2A ──► cockpit-agent
│ │ │
│ HTTP/SSE │ frontend MCP │ backend MCP
│ 业务状态 │ weather │ 车控/导航/音乐/闪购
▼ ▼ ▼
cockpit-domain
单一领域状态与工具执行
```

这里不存在“框架 WebUI”。`react-app` 是客户场景客户端的参考实现,它直接使用公开的 `qwen-audio-agent/gateway-client-sdk`,并自行负责浏览器麦克风、音频播放、页面布局和业务面板。
Expand All @@ -27,13 +22,15 @@

- `cockpit-domain` 是车辆、导航、音乐、天气和闪购状态的唯一来源。
- UI 通过 HTTP 获取快照、执行面板操作,通过 SSE 接收状态变化。
- 后台 Agent 通过 MCP 使用同一组领域能力。
- Gateway 的前台 Agent 通过 `/mcp/frontend` 直接使用只读天气工具。
- 后台 Agent 通过 `/mcp/backend` 使用车控、导航、音乐和闪购工具。
- 两个工具面由 `tools/registry.mjs` 显式组合,但共用同一份座舱状态。
- Gateway 不接收 `actions[]`,也不理解车辆、路线、媒体或订单结构。

因此后台任务还可以把详细状态发送给客户自己的座舱系统;Gateway 只接收适合继续对话和播报的 Task 进展与结果。

## 场景装配

`gateway.mjs` 是唯一的场景装配点:它通过公开入口创建 A2A Backend Adapter、Backend Agent Host 和 Gateway Application。场景人设集中在 `ASSISTANT.md`,没有引入策略注册表或座舱专用框架分支
`gateway.mjs` 是唯一的场景装配点:它通过公开入口创建 A2A Backend Adapter、Backend Agent Host 和 Gateway Application。场景人设集中在 `ASSISTANT.md`,前台 MCP 工具源由 `frontend-profile.json` 引用,没有引入座舱专用框架分支

四个进程的默认端口只用于本地示例,可通过 `.env.local` 覆盖。`COCKPIT_ID` 用于隔离不同座舱实例,UI 与 Agent 必须使用同一个值。
4 changes: 4 additions & 0 deletions examples/car/docs/replacing-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,7 @@
- 给 UI 的业务状态投影面(HTTP/SSE、消息总线或客户协议)。

不要把场景对象塞入 Gateway,也不要恢复 `actions[]` 作为隐式 UI 控制协议。

## 增加或调整工具

`tools/` 中每个目录是一个领域工具包:`manifest.json` 定义 MCP 工具,`execute.mjs` 实现场景逻辑。将工具包加入 `registry.mjs` 的 `FRONTEND_TOOL_GROUPS` 或 `BACKEND_TOOL_GROUPS` 即可决定调用面。这是代码层的明确修改点,不是新的动态插件框架。
4 changes: 2 additions & 2 deletions examples/car/docs/test-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
| 边界 | 覆盖内容 | 自动化入口 |
|---|---|---|
| cockpit-domain | 多座舱隔离、车控校验、音乐、导航阶段、闪购确认、状态事件 | `examples/car/domain/test` |
| MCP | 工具发现、参数传递、与 HTTP 共用单一状态 | `examples/car/domain/test/server.test.mjs` |
| cockpit-agent | 车控、导航、音乐、天气、闪购意图;未知请求不臆造能力 | `examples/car/agent/test` |
| MCP | 前/后台工具面隔离、工具发现、参数传递、与 HTTP 共用单一状态 | `examples/car/domain/test/server.test.mjs` |
| cockpit-agent | 车控、导航、音乐、闪购意图;天气和未知请求不进入后台 | `examples/car/agent/test` |
| A2A → MCP | 标准 Task 生命周期和真实领域状态变更 | `examples/car/agent/test/integration.test.mjs` |
| Gateway 装配 | 仅使用公开 API 注入 A2A Agent、健康和关闭 | `examples/car/test/gateway-composition.test.mjs` |
| 启动预检 | Realtime 配置、四进程端口、无效端口 | `examples/car/test/preflight.test.mjs` |
Expand Down
12 changes: 6 additions & 6 deletions examples/car/domain/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Cockpit domain service

This scenario-owned service is the single source of truth for the cockpit demo.
It exposes the same vehicle, navigation, music, weather, and flash-buy operations
through MCP and a small HTTP command endpoint. Cockpit panels consume snapshots
It exposes scenario operations through two scoped MCP surfaces and a small HTTP
command endpoint. Cockpit panels consume snapshots
and the SSE state stream directly; business state does not pass through the
qwen-audio-agent Gateway.

Expand All @@ -13,11 +13,11 @@ npm run example:car:domain

Endpoints:

- `POST /mcp` — stateless Streamable HTTP MCP.
- `POST /mcp/frontend` — foreground MCP surface; weather only.
- `POST /mcp/backend` — backend Agent MCP surface; vehicle, navigation, music, and flash-buy.
- `GET /api/cockpit/state?cockpitId=default` — current snapshot.
- `GET /api/cockpit/events?cockpitId=default` — snapshot plus state updates via SSE.
- `POST /api/cockpit/commands` — direct scenario UI operations using the same tool names.

The existing self-contained car server remains available during migration. The
GCP client and A2A agent issues will switch to this service before removing the
legacy action protocol.
Tool manifests and executors live under `../tools`; this service owns only their
shared state, external service dependencies, and protocol transport.
Loading