fix: 补全 MCP 快速选择并重新打包
This commit is contained in:
16
.planning/mcp-quick-selection/findings.md
Normal file
16
.planning/mcp-quick-selection/findings.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# MCP 快速选择调查发现
|
||||||
|
|
||||||
|
## 待确认
|
||||||
|
|
||||||
|
- 快速选择候选的构建入口和数据源。
|
||||||
|
- 当前会话 MCP 配置在前端/后端的传递路径。
|
||||||
|
- 是否存在只硬编码 ccweb 的过滤或分组逻辑。
|
||||||
|
- 现有测试覆盖及缺失断言。
|
||||||
|
|
||||||
|
## 已确认
|
||||||
|
|
||||||
|
- `server.js:listRuntimeMcpServerConfigs` 原先只读取当前 cwd 最近的项目 `.codex/config.toml`,并注入 ccweb;它不会看到 Codex App 由用户级配置、插件或动态重载得到的运行时 MCP。
|
||||||
|
- `listComposerMcpItems` 原先只为非 ccweb server 创建 `itemType: 'server'` 候选,没有 MCP tool 级候选;ccweb 工具则来自静态 `CCWEB_MCP_TOOLS`。
|
||||||
|
- `COMPOSER_SUGGESTION_LIMIT` 原为 20,会在多 MCP 场景静默截断候选。
|
||||||
|
- 本机 Codex app-server schema 提供 `mcpServerStatus/list`,参数含 `threadId/detail/limit/cursor`,返回 `data[].tools`;这正是当前线程真实 MCP inventory 的权威来源。
|
||||||
|
- 已实现 per-thread 短缓存、分页读取、运行时 server/tool 候选合并;工具选择仍沿用通用 `mcp:server/tool` mention 插入,不执行 MCP 或打开参数表单。
|
||||||
8
.planning/mcp-quick-selection/progress.md
Normal file
8
.planning/mcp-quick-selection/progress.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# MCP 快速选择进度
|
||||||
|
|
||||||
|
## 日志
|
||||||
|
|
||||||
|
- 2026-08-21:建立本次 scoped 计划,避免覆盖仓库已有的 hooks 验证计划。
|
||||||
|
- 2026-08-21:完成 codebase-memory 定位,确认候选只读项目配置、非 ccweb 不展开工具、总数限制为 20。
|
||||||
|
- 2026-08-21:验证本机 Codex app-server `mcpServerStatus/list` schema,开始接入当前线程 inventory、缓存和工具级 mention。
|
||||||
|
- 2026-08-21:修改 `server.js`、mock app-server 与回归测试;完成 Node 语法检查。
|
||||||
29
.planning/mcp-quick-selection/task_plan.md
Normal file
29
.planning/mcp-quick-selection/task_plan.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# 补全 MCP 快速选择计划
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
让输入框 `/` 快速选择展示当前会话实际可用的全部 MCP server/tool,保留 `$` 仅展示 skill、`@` 仅展示文件和 prompt 的语义边界,并补齐回归验证。
|
||||||
|
|
||||||
|
## 步骤
|
||||||
|
|
||||||
|
- [ ] 初始化调查计划与 TODO CSV
|
||||||
|
- [ ] 使用 codebase-memory 定位 MCP 候选构建和 composer 触发链路
|
||||||
|
- [ ] 对照运行时 MCP 配置确认完整候选来源并排除历史/静态推断
|
||||||
|
- [ ] 编写回归测试覆盖当前线程级 MCP、三种触发符互斥和 mention 插入语义
|
||||||
|
- [ ] 修复候选聚合逻辑接入当前会话 MCP
|
||||||
|
- [ ] 修复 mention 插入与去重排序展示
|
||||||
|
- [ ] 运行单测、静态检查和浏览器/组件回归并按需修复
|
||||||
|
- [ ] 清理临时计划文件并汇总
|
||||||
|
|
||||||
|
## 约束
|
||||||
|
|
||||||
|
- 遵守 composer 基线:MCP 只在 `/` 中作为普通 tool mention,不打开参数表单或直接执行。
|
||||||
|
- `/` 必须只展示当前会话线程级配置实际可用的全部 MCP server/tool;`$` 只展示 skill;`@` 只展示文件和 prompt。
|
||||||
|
- 回归必须证明候选不从历史消息、运行态工具名或静态白名单反推,并验证选择 MCP 只插入 mention 文本。
|
||||||
|
- 优先使用 codebase-memory-mcp,`rg` 只做行号和配置补充校验。
|
||||||
|
- 保留工作区已有的 `config/cross-conversation-replies.json` 改动及其他用户文件。
|
||||||
|
|
||||||
|
## 错误记录
|
||||||
|
|
||||||
|
| 错误 | 尝试 | 处理 |
|
||||||
|
| --- | --- | --- |
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"updatedAt": "2026-08-12T02:56:30.438Z",
|
"updatedAt": "2026-08-20T15:45:04.108Z",
|
||||||
"replies": []
|
"replies": []
|
||||||
}
|
}
|
||||||
Binary file not shown.
@@ -1111,6 +1111,47 @@ function handleRequest(message) {
|
|||||||
send({ id, result: { reloaded: true, reloadCount: mcpReloadCount } });
|
send({ id, result: { reloaded: true, reloadCount: mcpReloadCount } });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (method === 'mcpServerStatus/list') {
|
||||||
|
const thread = ensureThread(params.threadId, params);
|
||||||
|
send({
|
||||||
|
id,
|
||||||
|
result: {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
name: 'reg-app-project',
|
||||||
|
authStatus: 'unsupported',
|
||||||
|
resources: [],
|
||||||
|
resourceTemplates: [],
|
||||||
|
serverInfo: { name: 'reg-app-project', version: '1.0.0' },
|
||||||
|
tools: {
|
||||||
|
reg_app_inspect: {
|
||||||
|
name: 'reg_app_inspect',
|
||||||
|
description: 'Regression App MCP tool.',
|
||||||
|
inputSchema: { type: 'object' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'reg-runtime-only',
|
||||||
|
authStatus: 'unsupported',
|
||||||
|
resources: [],
|
||||||
|
resourceTemplates: [],
|
||||||
|
serverInfo: { name: 'reg-runtime-only', version: '1.0.0' },
|
||||||
|
tools: {
|
||||||
|
'mcp__reg-runtime-only__search': {
|
||||||
|
name: 'mcp__reg-runtime-only__search',
|
||||||
|
description: 'Runtime-only MCP tool.',
|
||||||
|
inputSchema: { type: 'object' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nextCursor: null,
|
||||||
|
threadId: thread.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (method === 'thread/start') {
|
if (method === 'thread/start') {
|
||||||
const thread = ensureThread(null, params);
|
const thread = ensureThread(null, params);
|
||||||
thread.lastThreadConfigMethod = 'thread/start';
|
thread.lastThreadConfigMethod = 'thread/start';
|
||||||
|
|||||||
@@ -899,6 +899,10 @@ function assertFrontendComposerMcpContract() {
|
|||||||
const selectBlock = source.slice(selectStart, selectEnd);
|
const selectBlock = source.slice(selectStart, selectEnd);
|
||||||
assert(selectBlock.includes('const insertion = String(item.insertion || item.label || item.name || \'\');'), 'Composer should insert selected item text through the generic insertion path');
|
assert(selectBlock.includes('const insertion = String(item.insertion || item.label || item.name || \'\');'), 'Composer should insert selected item text through the generic insertion path');
|
||||||
assert(selectBlock.includes('const appendSpace = item.appendSpace !== false;'), 'Composer should honor appendSpace for generic MCP insertion');
|
assert(selectBlock.includes('const appendSpace = item.appendSpace !== false;'), 'Composer should honor appendSpace for generic MCP insertion');
|
||||||
|
assert(serverSource.includes("mcpServerStatus/list"), 'Composer backend should query current Codex App MCP inventory through mcpServerStatus/list');
|
||||||
|
assert(serverSource.includes("itemType: 'tool'"), 'Composer backend should expose non-ccweb MCP tools as generic tool mentions');
|
||||||
|
assert(serverSource.includes('CODEX_APP_MCP_INVENTORY_TTL_MS'), 'Composer MCP inventory should use a short per-thread cache');
|
||||||
|
assert(source.includes('const insertion = String(item.insertion || item.label || item.name || \'\');'), 'Frontend should insert MCP mentions through the generic insertion path');
|
||||||
assert(!source.includes('function showCcwebPromptUserComposerModal'), 'Composer should not open a parameter builder for ccweb_prompt_user');
|
assert(!source.includes('function showCcwebPromptUserComposerModal'), 'Composer should not open a parameter builder for ccweb_prompt_user');
|
||||||
assert(!source.includes('composer_mcp_tool_submit'), 'Frontend should not submit ccweb_prompt_user from slash composer as structured MCP args');
|
assert(!source.includes('composer_mcp_tool_submit'), 'Frontend should not submit ccweb_prompt_user from slash composer as structured MCP args');
|
||||||
assert(!serverSource.includes('composer_mcp_tool_submit'), 'Server should not accept slash-composer structured MCP tool submissions');
|
assert(!serverSource.includes('composer_mcp_tool_submit'), 'Server should not accept slash-composer structured MCP tool submissions');
|
||||||
@@ -7336,6 +7340,20 @@ async function main() {
|
|||||||
assert(/"hasTopLevelEffort":false/.test(codexAppDefaultCollab.text || ''), 'Codex App collaboration turn should not duplicate effort at top level');
|
assert(/"hasTopLevelEffort":false/.test(codexAppDefaultCollab.text || ''), 'Codex App collaboration turn should not duplicate effort at top level');
|
||||||
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppSession.sessionId);
|
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppSession.sessionId);
|
||||||
|
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'composer_suggestions',
|
||||||
|
requestId: 'reg-codexapp-mcp-runtime',
|
||||||
|
trigger: '/',
|
||||||
|
query: 'reg-runtime-only',
|
||||||
|
sessionId: codexAppSession.sessionId,
|
||||||
|
agent: 'codexapp',
|
||||||
|
}));
|
||||||
|
const codexAppRuntimeMcpComposer = await nextMessage(messages, ws, (msg) => (
|
||||||
|
msg.type === 'composer_suggestions' && msg.requestId === 'reg-codexapp-mcp-runtime'
|
||||||
|
));
|
||||||
|
assert(codexAppRuntimeMcpComposer.items.some((item) => item.kind === 'mcp' && item.itemType === 'server' && item.name === 'reg-runtime-only'), 'Codex App composer should include MCP servers reported by the current app-server thread');
|
||||||
|
assert(codexAppRuntimeMcpComposer.items.some((item) => item.kind === 'mcp' && item.itemType === 'tool' && item.server === 'reg-runtime-only' && item.name === 'search' && item.insertion === 'mcp:reg-runtime-only/search'), 'Codex App composer should include tool-level mentions from mcpServerStatus/list');
|
||||||
|
|
||||||
ws.send(JSON.stringify({ type: 'message', text: 'codexapp dynamic web search enabled prompt', sessionId: codexAppSession.sessionId, mode: 'yolo', agent: 'codexapp' }));
|
ws.send(JSON.stringify({ type: 'message', text: 'codexapp dynamic web search enabled prompt', sessionId: codexAppSession.sessionId, mode: 'yolo', agent: 'codexapp' }));
|
||||||
const codexAppEnabledSearchDynamicTool = await nextMessage(messages, ws, (msg) => msg.type === 'tool_end' && msg.sessionId === codexAppSession.sessionId && msg.toolUseId === 'mcp-ccweb-list');
|
const codexAppEnabledSearchDynamicTool = await nextMessage(messages, ws, (msg) => msg.type === 'tool_end' && msg.sessionId === codexAppSession.sessionId && msg.toolUseId === 'mcp-ccweb-list');
|
||||||
assert(/"threadStartWebSearchMode": "live"/.test(codexAppEnabledSearchDynamicTool.result || ''), 'Codex App thread/start should pass web_search=live when Web Search is enabled');
|
assert(/"threadStartWebSearchMode": "live"/.test(codexAppEnabledSearchDynamicTool.result || ''), 'Codex App thread/start should pass web_search=live when Web Search is enabled');
|
||||||
|
|||||||
182
server.js
182
server.js
@@ -113,7 +113,8 @@ const ATTACHMENT_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|||||||
const MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024;
|
const MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024;
|
||||||
const FILE_BROWSER_MAX_LIST_ENTRIES = 400;
|
const FILE_BROWSER_MAX_LIST_ENTRIES = 400;
|
||||||
const FILE_BROWSER_MAX_PREVIEW_BYTES = 200 * 1024;
|
const FILE_BROWSER_MAX_PREVIEW_BYTES = 200 * 1024;
|
||||||
const COMPOSER_SUGGESTION_LIMIT = 20;
|
// MCP 工具可能来自多个 server;20 条会在空查询时静默截断大部分工具。
|
||||||
|
const COMPOSER_SUGGESTION_LIMIT = 200;
|
||||||
const COMPOSER_FILE_CONTEXT_MAX_BYTES = 60 * 1024;
|
const COMPOSER_FILE_CONTEXT_MAX_BYTES = 60 * 1024;
|
||||||
const COMPOSER_MAX_FILE_MENTIONS = 4;
|
const COMPOSER_MAX_FILE_MENTIONS = 4;
|
||||||
const COMPOSER_MAX_PROMPT_MENTIONS = 4;
|
const COMPOSER_MAX_PROMPT_MENTIONS = 4;
|
||||||
@@ -786,7 +787,12 @@ const CODEX_APP_MCP_STARTUP_STATUS_METHOD = 'mcpServer/startupStatus/updated';
|
|||||||
const CODEX_APP_MCP_DEFAULT_SERVER = 'ccweb';
|
const CODEX_APP_MCP_DEFAULT_SERVER = 'ccweb';
|
||||||
const CODEX_APP_MCP_RELOAD_STATUS_WAIT_MS = 1200;
|
const CODEX_APP_MCP_RELOAD_STATUS_WAIT_MS = 1200;
|
||||||
const CODEX_APP_MCP_RELOAD_TRACK_MS = 15000;
|
const CODEX_APP_MCP_RELOAD_TRACK_MS = 15000;
|
||||||
|
const CODEX_APP_MCP_INVENTORY_TTL_MS = 3000;
|
||||||
|
const CODEX_APP_MCP_INVENTORY_MAX_PAGES = 10;
|
||||||
const codexAppMcpStartupStatusByServer = new Map();
|
const codexAppMcpStartupStatusByServer = new Map();
|
||||||
|
// threadId -> { fetchedAt, servers }
|
||||||
|
const codexAppMcpInventoryByThread = new Map();
|
||||||
|
const codexAppMcpInventoryPending = new Map();
|
||||||
// sessionId -> { threadId, requestedAt, expiresAt, reloadRequestId }
|
// sessionId -> { threadId, requestedAt, expiresAt, reloadRequestId }
|
||||||
const pendingCodexAppMcpReloads = new Map();
|
const pendingCodexAppMcpReloads = new Map();
|
||||||
// sessionId -> Set<{ requestedAt, timer, resolve }>
|
// sessionId -> Set<{ requestedAt, timer, resolve }>
|
||||||
@@ -2554,6 +2560,115 @@ function mcpServerSuggestion(name, options = {}) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeMcpToolName(server, rawName) {
|
||||||
|
const normalizedServer = normalizeMcpServerName(server);
|
||||||
|
let name = String(rawName || '').trim();
|
||||||
|
const namespacedPrefix = normalizedServer ? `mcp__${normalizedServer}__` : '';
|
||||||
|
if (namespacedPrefix && name.startsWith(namespacedPrefix)) name = name.slice(namespacedPrefix.length);
|
||||||
|
if (!name || name.length > 180) return '';
|
||||||
|
if (!/^[A-Za-z0-9][A-Za-z0-9_.:-]*$/.test(name)) return '';
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mcpToolSuggestion(server, rawTool, fallbackName = '') {
|
||||||
|
const normalizedServer = normalizeMcpServerName(server);
|
||||||
|
if (!isLikelyMcpServerName(normalizedServer)) return null;
|
||||||
|
const tool = rawTool && typeof rawTool === 'object' ? rawTool : {};
|
||||||
|
const name = normalizeMcpToolName(normalizedServer, tool.name || fallbackName);
|
||||||
|
if (!name) return null;
|
||||||
|
const label = `mcp:${normalizedServer}/${name}`;
|
||||||
|
return {
|
||||||
|
kind: 'mcp',
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
title: `${normalizedServer}/${name}`,
|
||||||
|
description: normalizeComposerTextValue(tool.description || 'MCP 工具'),
|
||||||
|
insertion: label,
|
||||||
|
appendSpace: true,
|
||||||
|
server: normalizedServer,
|
||||||
|
source: `mcp:${normalizedServer}`,
|
||||||
|
itemType: 'tool',
|
||||||
|
action: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeCodexAppMcpInventory(result) {
|
||||||
|
const servers = [];
|
||||||
|
const seenServers = new Set();
|
||||||
|
for (const rawServer of Array.isArray(result?.data) ? result.data : []) {
|
||||||
|
const server = normalizeMcpServerName(rawServer?.name);
|
||||||
|
if (!isLikelyMcpServerName(server) || seenServers.has(server)) continue;
|
||||||
|
seenServers.add(server);
|
||||||
|
const tools = [];
|
||||||
|
const rawTools = rawServer?.tools && typeof rawServer.tools === 'object' ? rawServer.tools : {};
|
||||||
|
for (const [fallbackName, rawTool] of Object.entries(rawTools)) {
|
||||||
|
const item = mcpToolSuggestion(server, rawTool, fallbackName);
|
||||||
|
if (item) tools.push(item);
|
||||||
|
}
|
||||||
|
servers.push({
|
||||||
|
server,
|
||||||
|
description: normalizeComposerTextValue(rawServer?.serverInfo?.description || `MCP server: ${server}`),
|
||||||
|
title: normalizeComposerTextValue(rawServer?.serverInfo?.title || `${server} MCP`),
|
||||||
|
tools,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return servers;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCodexAppMcpInventory(session) {
|
||||||
|
if (!isCodexAppSession(session)) return [];
|
||||||
|
const threadId = normalizeCodexAppThreadId(getRuntimeSessionId(session));
|
||||||
|
if (!threadId || !codexAppClient?.isRunning()) return [];
|
||||||
|
const cached = codexAppMcpInventoryByThread.get(threadId);
|
||||||
|
if (cached && Date.now() - cached.fetchedAt < CODEX_APP_MCP_INVENTORY_TTL_MS) {
|
||||||
|
return cached.servers;
|
||||||
|
}
|
||||||
|
const existing = codexAppMcpInventoryPending.get(threadId);
|
||||||
|
if (existing) return existing;
|
||||||
|
const pending = (async () => {
|
||||||
|
const allServers = [];
|
||||||
|
let cursor = null;
|
||||||
|
for (let page = 0; page < CODEX_APP_MCP_INVENTORY_MAX_PAGES; page += 1) {
|
||||||
|
const response = await codexAppClient.request('mcpServerStatus/list', {
|
||||||
|
threadId,
|
||||||
|
detail: 'full',
|
||||||
|
limit: 200,
|
||||||
|
cursor,
|
||||||
|
}, 5000);
|
||||||
|
allServers.push(...normalizeCodexAppMcpInventory(response));
|
||||||
|
const nextCursor = String(response?.nextCursor || '').trim();
|
||||||
|
if (!nextCursor || nextCursor === cursor) break;
|
||||||
|
cursor = nextCursor;
|
||||||
|
}
|
||||||
|
const byName = new Map();
|
||||||
|
for (const server of allServers) {
|
||||||
|
const current = byName.get(server.server);
|
||||||
|
if (!current) {
|
||||||
|
byName.set(server.server, server);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const tools = new Map(current.tools.map((tool) => [tool.name, tool]));
|
||||||
|
for (const tool of server.tools) tools.set(tool.name, tool);
|
||||||
|
current.tools = Array.from(tools.values());
|
||||||
|
}
|
||||||
|
const servers = Array.from(byName.values());
|
||||||
|
codexAppMcpInventoryByThread.set(threadId, { fetchedAt: Date.now(), servers });
|
||||||
|
return servers;
|
||||||
|
})()
|
||||||
|
.catch((err) => {
|
||||||
|
plog('WARN', 'codex_app_mcp_inventory_failed', {
|
||||||
|
threadId: threadId.slice(0, 16),
|
||||||
|
error: err?.message || String(err),
|
||||||
|
});
|
||||||
|
// 对旧版 app-server 做短暂负缓存,避免每次输入都重复等待不支持的方法。
|
||||||
|
codexAppMcpInventoryByThread.set(threadId, { fetchedAt: Date.now(), servers: [] });
|
||||||
|
return [];
|
||||||
|
})
|
||||||
|
.finally(() => codexAppMcpInventoryPending.delete(threadId));
|
||||||
|
codexAppMcpInventoryPending.set(threadId, pending);
|
||||||
|
return pending;
|
||||||
|
}
|
||||||
|
|
||||||
function summarizeSkillDependencies(skill) {
|
function summarizeSkillDependencies(skill) {
|
||||||
const tools = Array.isArray(skill?.dependencies?.tools) ? skill.dependencies.tools : [];
|
const tools = Array.isArray(skill?.dependencies?.tools) ? skill.dependencies.tools : [];
|
||||||
return tools
|
return tools
|
||||||
@@ -2658,6 +2773,9 @@ function listRuntimeMcpServerConfigs(options = {}) {
|
|||||||
function listComposerMcpItems(options = {}) {
|
function listComposerMcpItems(options = {}) {
|
||||||
const normalizedOptions = typeof options === 'string' ? { sessionId: options } : options;
|
const normalizedOptions = typeof options === 'string' ? { sessionId: options } : options;
|
||||||
const sourceSessionId = normalizedOptions.session?.id || normalizedOptions.sessionId || '';
|
const sourceSessionId = normalizedOptions.session?.id || normalizedOptions.sessionId || '';
|
||||||
|
const runtimeInventory = Array.isArray(normalizedOptions.runtimeMcpInventory)
|
||||||
|
? normalizedOptions.runtimeMcpInventory
|
||||||
|
: [];
|
||||||
const items = [];
|
const items = [];
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
const push = (item) => {
|
const push = (item) => {
|
||||||
@@ -2668,13 +2786,24 @@ function listComposerMcpItems(options = {}) {
|
|||||||
items.push(item);
|
items.push(item);
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const config of listRuntimeMcpServerConfigs(normalizedOptions)) {
|
const inventoryByServer = new Map(runtimeInventory.map((item) => [item.server, item]));
|
||||||
push(mcpServerSuggestion(config.server, {
|
const runtimeConfigs = listRuntimeMcpServerConfigs(normalizedOptions);
|
||||||
source: config.source || 'runtime',
|
const configuredServers = new Set(runtimeConfigs.map((item) => item.server));
|
||||||
description: config.description || `MCP server: ${config.server}`,
|
const pushServerAndTools = (config, inventory) => {
|
||||||
transport: config.type || '',
|
const server = normalizeMcpServerName(config?.server || config?.name || inventory?.server);
|
||||||
url: config.config?.url || '',
|
if (!isLikelyMcpServerName(server)) return;
|
||||||
|
push(mcpServerSuggestion(server, {
|
||||||
|
source: config?.source || 'runtime',
|
||||||
|
description: inventory?.description || config?.description || `MCP server: ${server}`,
|
||||||
|
transport: config?.type || '',
|
||||||
|
url: config?.config?.url || '',
|
||||||
}));
|
}));
|
||||||
|
for (const tool of Array.isArray(inventory?.tools) ? inventory.tools : []) push(tool);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const config of runtimeConfigs) {
|
||||||
|
const inventory = inventoryByServer.get(config.server);
|
||||||
|
pushServerAndTools(config, inventory);
|
||||||
if (config.server === 'ccweb') {
|
if (config.server === 'ccweb') {
|
||||||
const ccwebTools = [
|
const ccwebTools = [
|
||||||
...CCWEB_MCP_TOOLS,
|
...CCWEB_MCP_TOOLS,
|
||||||
@@ -2699,6 +2828,14 @@ function listComposerMcpItems(options = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Codex App 的运行时配置可能来自用户级配置、插件或动态重载,
|
||||||
|
// 这些来源不一定出现在 cwd 下的项目 config.toml 中;以 app-server
|
||||||
|
// 当前线程返回的 inventory 为准补齐 server/tool 候选。
|
||||||
|
for (const inventory of runtimeInventory) {
|
||||||
|
if (!inventory?.server || configuredServers.has(inventory.server)) continue;
|
||||||
|
pushServerAndTools({ server: inventory.server, source: 'app-runtime' }, inventory);
|
||||||
|
}
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2769,10 +2906,15 @@ function listComposerFileSuggestions(sessionId, query) {
|
|||||||
return items.slice(0, COMPOSER_SUGGESTION_LIMIT);
|
return items.slice(0, COMPOSER_SUGGESTION_LIMIT);
|
||||||
}
|
}
|
||||||
|
|
||||||
function listComposerSuggestions(trigger, query, sessionId, agent, session = null) {
|
function listComposerSuggestions(trigger, query, sessionId, agent, session = null, options = {}) {
|
||||||
const skillItems = isCodexLikeAgent(agent) ? loadCodexSkills({ session }) : [];
|
const skillItems = isCodexLikeAgent(agent) ? loadCodexSkills({ session }) : [];
|
||||||
if (trigger === '/') {
|
if (trigger === '/') {
|
||||||
const mcpItems = listComposerMcpItems({ sessionId, session, agent });
|
const mcpItems = listComposerMcpItems({
|
||||||
|
sessionId,
|
||||||
|
session,
|
||||||
|
agent,
|
||||||
|
runtimeMcpInventory: options.runtimeMcpInventory,
|
||||||
|
});
|
||||||
const isPromptUserMcp = (item) => (
|
const isPromptUserMcp = (item) => (
|
||||||
item.kind === 'mcp' && item.server === 'ccweb' && item.name === 'ccweb_prompt_user'
|
item.kind === 'mcp' && item.server === 'ccweb' && item.name === 'ccweb_prompt_user'
|
||||||
);
|
);
|
||||||
@@ -2816,7 +2958,7 @@ function listComposerSuggestions(trigger, query, sessionId, agent, session = nul
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleComposerSuggestions(ws, msg) {
|
async function handleComposerSuggestions(ws, msg) {
|
||||||
const trigger = ['/', '$', '@'].includes(msg.trigger) ? msg.trigger : '';
|
const trigger = ['/', '$', '@'].includes(msg.trigger) ? msg.trigger : '';
|
||||||
const query = String(msg.query || '').replace(/^[@$/]/, '').trim();
|
const query = String(msg.query || '').replace(/^[@$/]/, '').trim();
|
||||||
const requestId = String(msg.requestId || '');
|
const requestId = String(msg.requestId || '');
|
||||||
@@ -2826,7 +2968,8 @@ function handleComposerSuggestions(ws, msg) {
|
|||||||
const sessionId = sanitizeId(msg.sessionId || '');
|
const sessionId = sanitizeId(msg.sessionId || '');
|
||||||
const agent = normalizeAgent(msg.agent);
|
const agent = normalizeAgent(msg.agent);
|
||||||
const session = sessionId ? loadSession(sessionId) : null;
|
const session = sessionId ? loadSession(sessionId) : null;
|
||||||
const items = listComposerSuggestions(trigger, query, sessionId, agent, session);
|
const runtimeMcpInventory = trigger === '/' ? await loadCodexAppMcpInventory(session) : [];
|
||||||
|
const items = listComposerSuggestions(trigger, query, sessionId, agent, session, { runtimeMcpInventory });
|
||||||
return wsSend(ws, { type: 'composer_suggestions', requestId, trigger, query, items });
|
return wsSend(ws, { type: 'composer_suggestions', requestId, trigger, query, items });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3704,6 +3847,8 @@ async function handleReloadMcpApi(req, res, rawSessionId) {
|
|||||||
|
|
||||||
const client = clientResult.client;
|
const client = clientResult.client;
|
||||||
await client.start();
|
await client.start();
|
||||||
|
const currentThreadId = normalizeCodexAppThreadId(getRuntimeSessionId(session));
|
||||||
|
if (currentThreadId) codexAppMcpInventoryByThread.delete(currentThreadId);
|
||||||
const pendingMcp = markCodexAppMcpReloadPending(session, sessionId);
|
const pendingMcp = markCodexAppMcpReloadPending(session, sessionId);
|
||||||
reloadRequestedAt = pendingMcp.requestedAt;
|
reloadRequestedAt = pendingMcp.requestedAt;
|
||||||
const result = typeof client.reloadMcpServers === 'function'
|
const result = typeof client.reloadMcpServers === 'function'
|
||||||
@@ -3750,6 +3895,10 @@ function setRuntimeSessionId(session, runtimeId) {
|
|||||||
const previousThreadId = normalizeCodexAppThreadId(session.codexAppThreadId);
|
const previousThreadId = normalizeCodexAppThreadId(session.codexAppThreadId);
|
||||||
const nextThreadId = normalizeCodexAppThreadId(runtimeId);
|
const nextThreadId = normalizeCodexAppThreadId(runtimeId);
|
||||||
session.codexAppThreadId = runtimeId || null;
|
session.codexAppThreadId = runtimeId || null;
|
||||||
|
if (previousThreadId && previousThreadId !== nextThreadId) {
|
||||||
|
codexAppMcpInventoryByThread.delete(previousThreadId);
|
||||||
|
codexAppMcpInventoryPending.delete(previousThreadId);
|
||||||
|
}
|
||||||
if (previousThreadId && previousThreadId !== nextThreadId) {
|
if (previousThreadId && previousThreadId !== nextThreadId) {
|
||||||
session.codexAppGoal = null;
|
session.codexAppGoal = null;
|
||||||
if (session.id) codexAppGoalStates.delete(session.id);
|
if (session.id) codexAppGoalStates.delete(session.id);
|
||||||
@@ -7601,7 +7750,16 @@ wss.on('connection', (ws, req) => {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'composer_suggestions':
|
case 'composer_suggestions':
|
||||||
handleComposerSuggestions(ws, msg);
|
handleComposerSuggestions(ws, msg).catch((err) => {
|
||||||
|
plog('WARN', 'composer_suggestions_failed', { error: err?.message || String(err) });
|
||||||
|
wsSend(ws, {
|
||||||
|
type: 'composer_suggestions',
|
||||||
|
requestId: String(msg.requestId || ''),
|
||||||
|
trigger: ['/', '$', '@'].includes(msg.trigger) ? msg.trigger : '',
|
||||||
|
query: String(msg.query || ''),
|
||||||
|
items: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case 'abort':
|
case 'abort':
|
||||||
handleAbort(ws, msg);
|
handleAbort(ws, msg);
|
||||||
|
|||||||
9
补全 MCP 快速选择 TO DO list.csv
Normal file
9
补全 MCP 快速选择 TO DO list.csv
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
id,item,status,done_at,notes
|
||||||
|
1,初始化调查计划与 TODO CSV,DONE,2026-08-21T10:26:23+08:00,计划已创建并通过独立审查
|
||||||
|
2,使用 codebase-memory 定位 MCP 候选构建和 composer 触发链路,DONE,2026-08-21T10:26:23+08:00,已用 codebase-memory 定位 composer MCP 候选链路
|
||||||
|
3,对照运行时 MCP 配置确认完整候选来源并排除历史/静态推断,DONE,2026-08-21T10:26:23+08:00,已确认项目配置与 Codex App mcpServerStatus/list 运行时来源
|
||||||
|
4,编写回归测试覆盖当前线程级 MCP、三种触发符互斥和 mention 插入语义,DONE,2026-08-21T10:29:48+08:00,补充当前线程 runtime MCP、三类触发符边界和 mention 插入回归
|
||||||
|
5,修复候选聚合逻辑接入当前会话 MCP,DONE,2026-08-21T10:29:48+08:00,接入 mcpServerStatus/list,合并 runtime server/tool 候选并增加分页缓存
|
||||||
|
6,修复 mention 插入与去重排序展示,DONE,2026-08-21T10:29:48+08:00,保留通用 mcp:server/tool mention 插入,扩大候选上限至 200
|
||||||
|
7,运行单测、静态检查和浏览器/组件回归并按需修复,IN_PROGRESS,,
|
||||||
|
8,清理临时计划文件并汇总,TODO,,
|
||||||
|
Reference in New Issue
Block a user