@cocorograph/hub-agent 0.8.72 → 0.8.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/codex-mcp-control.mjs +38 -14
- package/src/codex-stream-bridge.mjs +86 -25
- package/src/main.mjs +21 -11
package/package.json
CHANGED
|
@@ -58,11 +58,11 @@ const SETTLE_MAX_MS = 25_000
|
|
|
58
58
|
* @param {{status?: string, error?: string|null}|undefined} startup
|
|
59
59
|
* @returns {{name: string, status: string, serverInfo?: object, error?: string, tools: {name: string}[]}}
|
|
60
60
|
*/
|
|
61
|
-
export function normalizeCodexMcpServer(entry, startup) {
|
|
61
|
+
export function normalizeCodexMcpServer(entry, startup, runtime) {
|
|
62
62
|
const name = entry?.name
|
|
63
63
|
const authStatus = entry?.authStatus
|
|
64
|
-
const startupState = startup?.status
|
|
65
|
-
const startupError = startup?.error || undefined
|
|
64
|
+
const startupState = runtime?.status || startup?.status
|
|
65
|
+
const startupError = (runtime ? runtime.error : startup?.error) || undefined
|
|
66
66
|
|
|
67
67
|
// tools は {toolName: {name, description, inputSchema...}} のマップで来る。
|
|
68
68
|
// inputSchema を載せると WS が 1MB 超になるため、名前だけに落とす。
|
|
@@ -74,7 +74,9 @@ export function normalizeCodexMcpServer(entry, startup) {
|
|
|
74
74
|
} else if (startupState === "starting") {
|
|
75
75
|
status = "pending"
|
|
76
76
|
} else if (startupState === "failed" || startupState === "cancelled") {
|
|
77
|
-
status = isAuthFailure(startupError, authStatus) ? "needs-auth" : "failed"
|
|
77
|
+
status = (runtime || startup)?.failureReason === "reauthenticationRequired" || isAuthFailure(startupError, authStatus) ? "needs-auth" : "failed"
|
|
78
|
+
} else if (startupState) {
|
|
79
|
+
status = "pending"
|
|
78
80
|
} else if (authStatus === "notLoggedIn") {
|
|
79
81
|
// 通知が来ていない (thread 起動前 / 取りこぼし) が、未ログインは確実に再認証が要る。
|
|
80
82
|
status = "needs-auth"
|
|
@@ -88,6 +90,7 @@ export function normalizeCodexMcpServer(entry, startup) {
|
|
|
88
90
|
return {
|
|
89
91
|
name,
|
|
90
92
|
status,
|
|
93
|
+
canAuthenticate: authStatus === "oAuth" || authStatus === "notLoggedIn",
|
|
91
94
|
...(entry?.serverInfo ? { serverInfo: entry.serverInfo } : {}),
|
|
92
95
|
...(startupError ? { error: truncateError(startupError) } : {}),
|
|
93
96
|
tools: toolNames.map((n) => ({ name: n })),
|
|
@@ -95,22 +98,47 @@ export function normalizeCodexMcpServer(entry, startup) {
|
|
|
95
98
|
}
|
|
96
99
|
|
|
97
100
|
/** 起動失敗が「認証が要る」由来か。OAuth フロー (再認証ボタン) に倒してよいかの判定。 */
|
|
98
|
-
function isAuthFailure(error, authStatus) {
|
|
101
|
+
export function isAuthFailure(error, authStatus) {
|
|
99
102
|
if (authStatus === "notLoggedIn") return true
|
|
100
103
|
if (!error) return false
|
|
101
104
|
// 実測のエラー文言:
|
|
102
105
|
// "MCP client for `hub` failed to start: ... Auth error: OAuth authorization required,
|
|
103
106
|
// when send initialize request"
|
|
104
|
-
return /oauth|
|
|
107
|
+
return /oauth|auth(?:entication|orization)? required|unauthorized|401/i.test(error)
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
/** エラー文言は UI の 1 行に収まらないため要約する (全文は agent のログに残る)。 */
|
|
108
111
|
function truncateError(error) {
|
|
109
112
|
const s = String(error)
|
|
110
|
-
if (/oauth|
|
|
113
|
+
if (/oauth|auth(?:entication|orization)? required/i.test(s)) return "OAuth 認証が必要です"
|
|
111
114
|
return s.length > 160 ? `${s.slice(0, 157)}…` : s
|
|
112
115
|
}
|
|
113
116
|
|
|
117
|
+
/** Both control and conversation clients use the same bounded, validated inventory pagination. */
|
|
118
|
+
export async function listCodexMcpServers(client, { threadId, startup = new Map(), runtime = new Map() } = {}) {
|
|
119
|
+
const servers = []
|
|
120
|
+
const seenCursors = new Set()
|
|
121
|
+
let cursor
|
|
122
|
+
do {
|
|
123
|
+
const res = await client.request("mcpServerStatus/list", {
|
|
124
|
+
detail: "toolsAndAuthOnly",
|
|
125
|
+
...(threadId ? { threadId } : {}),
|
|
126
|
+
...(cursor ? { cursor } : {}),
|
|
127
|
+
})
|
|
128
|
+
if (!Array.isArray(res?.data) || res.data.some((entry) => typeof entry?.name !== "string" || !entry.name)) {
|
|
129
|
+
throw new Error("codex_mcp_status_invalid")
|
|
130
|
+
}
|
|
131
|
+
servers.push(...res.data.map((entry) => normalizeCodexMcpServer(entry, startup.get(entry.name), runtime.get(entry.name))))
|
|
132
|
+
cursor = res.nextCursor
|
|
133
|
+
if (cursor != null && (typeof cursor !== "string" || !cursor || seenCursors.has(cursor))) {
|
|
134
|
+
throw new Error("codex_mcp_status_cursor_invalid")
|
|
135
|
+
}
|
|
136
|
+
if (cursor) seenCursors.add(cursor)
|
|
137
|
+
if (seenCursors.size > 100) throw new Error("codex_mcp_status_page_limit")
|
|
138
|
+
} while (cursor)
|
|
139
|
+
return servers
|
|
140
|
+
}
|
|
141
|
+
|
|
114
142
|
export class CodexMcpControl {
|
|
115
143
|
/**
|
|
116
144
|
* @param {{logger?: object, codexCmd?: string, cwd?: string, idleMs?: number,
|
|
@@ -185,7 +213,7 @@ export class CodexMcpControl {
|
|
|
185
213
|
if (msg?.method === "mcpServer/startupStatus/updated") {
|
|
186
214
|
const p = msg.params || {}
|
|
187
215
|
if (p.name) {
|
|
188
|
-
this.startup.set(p.name, { status: p.status, error: p.error ?? null })
|
|
216
|
+
this.startup.set(p.name, { status: p.status, error: p.error ?? null, failureReason: p.failureReason })
|
|
189
217
|
}
|
|
190
218
|
return
|
|
191
219
|
}
|
|
@@ -243,13 +271,9 @@ export class CodexMcpControl {
|
|
|
243
271
|
const client = await this._ensure()
|
|
244
272
|
await this._waitForStartupSettle()
|
|
245
273
|
|
|
246
|
-
const
|
|
247
|
-
// full だと resources / resourceTemplates まで載って更に膨らむ。
|
|
248
|
-
detail: "toolsAndAuthOnly",
|
|
249
|
-
})
|
|
250
|
-
const rows = res?.data ?? []
|
|
274
|
+
const servers = await listCodexMcpServers(client, { startup: this.startup })
|
|
251
275
|
this._touch()
|
|
252
|
-
return
|
|
276
|
+
return servers
|
|
253
277
|
}
|
|
254
278
|
|
|
255
279
|
/**
|
|
@@ -48,6 +48,7 @@ import { recordInjectSourcePersistent } from "./inject-source-store.mjs"
|
|
|
48
48
|
|
|
49
49
|
import { AsyncTtlCache } from "./async-ttl-cache.mjs"
|
|
50
50
|
import { CodexAppServerClient } from "./codex-appserver-client.mjs"
|
|
51
|
+
import { listCodexMcpServers } from "./codex-mcp-control.mjs"
|
|
51
52
|
import { addCodexSessionTitles, operateCodexSession, validateCodexSessionOperation } from "./codex-session-operations.mjs"
|
|
52
53
|
import { defaultCodexDir } from "./codex-profiles.mjs"
|
|
53
54
|
import {
|
|
@@ -2104,6 +2105,86 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
2104
2105
|
return Array.from(new Set(this.sessions.values())).filter((s) => !s.dead)
|
|
2105
2106
|
}
|
|
2106
2107
|
|
|
2108
|
+
_mcpSession(stream_id) {
|
|
2109
|
+
if (typeof stream_id !== "string" || !stream_id) throw new Error("codex_mcp_stream_required")
|
|
2110
|
+
const session = this.sessions.get(stream_id)
|
|
2111
|
+
if (!session || session.dead) throw new Error("codex_mcp_session_missing")
|
|
2112
|
+
if (!session.threadId || !session.client || session.client.closed) throw new Error("codex_mcp_session_unavailable")
|
|
2113
|
+
return session
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
async mcpStatus({ stream_id } = {}) {
|
|
2117
|
+
const session = this._mcpSession(stream_id)
|
|
2118
|
+
const servers = await listCodexMcpServers(session.client, {
|
|
2119
|
+
threadId: session.threadId,
|
|
2120
|
+
startup: session.mcpStartupByThread?.get(session.threadId),
|
|
2121
|
+
runtime: session.mcpRuntimeByThread?.get(session.threadId),
|
|
2122
|
+
})
|
|
2123
|
+
// A detach/reattach during the RPC must not return another conversation's state.
|
|
2124
|
+
if (this._mcpSession(stream_id) !== session) throw new Error("codex_mcp_session_changed")
|
|
2125
|
+
return servers
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
async mcpReconnect(serverName, { stream_id } = {}) {
|
|
2129
|
+
const session = this._mcpSession(stream_id)
|
|
2130
|
+
await session.client.request("config/mcpServer/reload", null)
|
|
2131
|
+
if (this._mcpSession(stream_id) !== session) throw new Error("codex_mcp_session_changed")
|
|
2132
|
+
// The RPC queues a refresh of loaded threads; acceptance is not proof of recovery.
|
|
2133
|
+
return { targeted: 1, refreshQueued: true }
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
async mcpAuthenticate(serverName, { stream_id } = {}) {
|
|
2137
|
+
if (typeof serverName !== "string" || !serverName.trim()) throw new Error("codex_mcp_server_required")
|
|
2138
|
+
const session = this._mcpSession(stream_id)
|
|
2139
|
+
const result = await session.client.request("mcpServer/oauth/login", { name: serverName, threadId: session.threadId })
|
|
2140
|
+
if (this._mcpSession(stream_id) !== session) throw new Error("codex_mcp_session_changed")
|
|
2141
|
+
if (typeof result?.authorizationUrl !== "string" || !result.authorizationUrl) throw new Error("codex_mcp_oauth_response_invalid")
|
|
2142
|
+
return { authorizationUrl: result.authorizationUrl, callbackExpected: false, requiresUserAction: true }
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
_observeMcpNotification(session, msg) {
|
|
2146
|
+
const p = msg?.params
|
|
2147
|
+
const threadId = p?.threadId
|
|
2148
|
+
// Startup may arrive before thread/start resolves. Key by its explicit thread id.
|
|
2149
|
+
// OAuth completions without a thread id cannot establish which conversation recovered.
|
|
2150
|
+
if (typeof threadId !== "string" || !threadId) return
|
|
2151
|
+
if (msg.method === "mcpServer/oauthLogin/completed" && threadId === session.threadId && p.success === true && typeof p.name === "string" && p.name) {
|
|
2152
|
+
session.mcpRuntimeByThread ||= new Map()
|
|
2153
|
+
if (!session.mcpRuntimeByThread.has(threadId)) session.mcpRuntimeByThread.set(threadId, new Map())
|
|
2154
|
+
session.mcpRuntimeByThread.get(threadId).set(p.name, { status: "starting", error: null })
|
|
2155
|
+
return
|
|
2156
|
+
}
|
|
2157
|
+
if (msg.method === "mcpServer/startupStatus/updated" && typeof p.name === "string" && p.name) {
|
|
2158
|
+
session.mcpStartupByThread ||= new Map()
|
|
2159
|
+
if (!session.mcpStartupByThread.has(threadId)) session.mcpStartupByThread.set(threadId, new Map())
|
|
2160
|
+
session.mcpStartupByThread.get(threadId).set(p.name, { status: p.status, error: p.error ?? null, failureReason: p.failureReason })
|
|
2161
|
+
const runtime = session.mcpRuntimeByThread?.get(threadId)
|
|
2162
|
+
if ((p.status === "failed" || p.status === "cancelled") && runtime?.get(p.name)?.status === "starting") {
|
|
2163
|
+
runtime.delete(p.name)
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
const item = p.item
|
|
2167
|
+
if (msg.method !== "item/completed" || item?.type !== "mcpToolCall" || typeof item.server !== "string" || !item.server) return
|
|
2168
|
+
session.mcpRuntimeByThread ||= new Map()
|
|
2169
|
+
if (!session.mcpRuntimeByThread.has(threadId)) session.mcpRuntimeByThread.set(threadId, new Map())
|
|
2170
|
+
const runtime = session.mcpRuntimeByThread.get(threadId)
|
|
2171
|
+
const errorText = typeof item.error === "string" ? item.error : item.error?.message
|
|
2172
|
+
const resultText = Array.isArray(item.result?.content)
|
|
2173
|
+
? item.result.content.filter((part) => part?.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n") : ""
|
|
2174
|
+
const failed = item.status === "failed" || item.error || item.result?.isError === true
|
|
2175
|
+
// Tool failures include ordinary business errors; only explicit auth requirements affect
|
|
2176
|
+
// server health. Mentioning OAuth in a response is not itself an authentication failure.
|
|
2177
|
+
const requiresAuthentication = (text) => typeof text === "string" && /\bauth(?:entication|orization)?\s+required\b/i.test(text)
|
|
2178
|
+
if (failed && (requiresAuthentication(errorText) || requiresAuthentication(resultText))) {
|
|
2179
|
+
runtime.set(item.server, { status: "failed", error: "Authentication required" })
|
|
2180
|
+
} else if (!failed && item.status === "completed" && Array.isArray(item.result?.content)) {
|
|
2181
|
+
runtime.delete(item.server)
|
|
2182
|
+
session.mcpStartupByThread ||= new Map()
|
|
2183
|
+
if (!session.mcpStartupByThread.has(threadId)) session.mcpStartupByThread.set(threadId, new Map())
|
|
2184
|
+
session.mcpStartupByThread.get(threadId).set(item.server, { status: "ready", error: null })
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2107
2188
|
_recordAcceptedInput(session, inputId) {
|
|
2108
2189
|
if (!session || typeof inputId !== "string" || !inputId) return
|
|
2109
2190
|
const cancelledIds = session.cancelledInputIds || (session.cancelledInputIds = [])
|
|
@@ -3211,6 +3292,7 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
3211
3292
|
|
|
3212
3293
|
_onNotification(session, msg) {
|
|
3213
3294
|
const { stream_id } = session
|
|
3295
|
+
this._observeMcpNotification(session, msg)
|
|
3214
3296
|
// app-server からの通知 = ターンが進行している証跡。これで touch することで、
|
|
3215
3297
|
// 正常に走っているターンは reaper に回収されず、通知が途絶えたスタックターンだけが
|
|
3216
3298
|
// 放棄セッションとして回収される。
|
|
@@ -4062,11 +4144,10 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
4062
4144
|
* 誤配送は取り返しがつかないため、両者の判定を意図的に分けてある。
|
|
4063
4145
|
*
|
|
4064
4146
|
* ⚠️ context%(ドーナツ)は thread 固有の値なので「どのセッションから読んでも同じ」という
|
|
4065
|
-
* 旧 rate-limit 前提(先着 1 件で十分)は成り立たない。
|
|
4066
|
-
*
|
|
4067
|
-
*
|
|
4068
|
-
*
|
|
4069
|
-
* のを防ぐ)。
|
|
4147
|
+
* 旧 rate-limit 前提(先着 1 件で十分)は成り立たない。per-cwd の表示系が同じ thread を
|
|
4148
|
+
* 選ぶよう、この一意規則へ寄せる。「最新」は閲覧 keepalive で動く lastActivityMs ではなく
|
|
4149
|
+
* 会話ターン境界時刻 lastTurnAtMs を基準にする(見ているだけの古い会話が本命を追い越すのを
|
|
4150
|
+
* 防ぐ)。
|
|
4070
4151
|
*
|
|
4071
4152
|
* @param {string} cwd
|
|
4072
4153
|
* @returns {object|null} 権威セッション(無ければ null)
|
|
@@ -4087,26 +4168,6 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
4087
4168
|
return best
|
|
4088
4169
|
}
|
|
4089
4170
|
|
|
4090
|
-
/**
|
|
4091
|
-
* 指定 cwd に一致するライブ (attach 済み) codex セッションの threadId を返す。
|
|
4092
|
-
* Claude 版 `liveSessionIdForCwd` の対。
|
|
4093
|
-
*
|
|
4094
|
-
* ⚠️ 2026-08-02 時点で **本番経路からの呼び出し元は無い** (テストのみ)。旧利用元だった
|
|
4095
|
-
* terminal mirror は撤去済み。「main.mjs の state loop から使う」という記述も以前から
|
|
4096
|
-
* 実態と食い違っていた (startStateLoop に codexBridge は渡るが、usage.mjs 側で参照して
|
|
4097
|
-
* いない)。撤去可否は別途調査してから判断すること。
|
|
4098
|
-
*
|
|
4099
|
-
* ⚠️ T06407 増分2: 旧実装は「先に見つかったもの」を返していたが、これは 5h/7d(アカウント
|
|
4100
|
-
* 全体で代表 1 件で十分)前提だった。context% は thread 固有なので liveSessionForCwd の
|
|
4101
|
-
* 権威規則(最終活性が最新)で解決し、usage.response の per-cwd 集計と収束させる。
|
|
4102
|
-
*
|
|
4103
|
-
* @param {string} cwd
|
|
4104
|
-
* @returns {string|null}
|
|
4105
|
-
*/
|
|
4106
|
-
threadIdForCwd(cwd) {
|
|
4107
|
-
return this.liveSessionForCwd(cwd)?.threadId ?? null
|
|
4108
|
-
}
|
|
4109
|
-
|
|
4110
4171
|
/**
|
|
4111
4172
|
* 指定 cwd でライブなセッションへメッセージを直接注入する (sibling-inject 用、
|
|
4112
4173
|
* 2026-07-04)。Claude 版 `sendToCwd` の対。
|
package/src/main.mjs
CHANGED
|
@@ -1352,9 +1352,7 @@ export async function startDaemon({ version, claudeSdk } = {}) {
|
|
|
1352
1352
|
logger,
|
|
1353
1353
|
intervalMs: 5_000,
|
|
1354
1354
|
claudeBridge,
|
|
1355
|
-
//
|
|
1356
|
-
// rollout ベースで解決するために渡す (main.mjs startStateLoop 内の codexBridge.threadIdForCwd
|
|
1357
|
-
// 参照)。
|
|
1355
|
+
// 背景 Codex チャットセッションの状態を state loop へ渡す。
|
|
1358
1356
|
codexBridge,
|
|
1359
1357
|
readinessTracker,
|
|
1360
1358
|
// tmux session_name → bind 中の session_id。turn_active を「そのペインが実際に走らせている
|
|
@@ -3335,6 +3333,9 @@ export function handleClaudeChatInput(msg, ctx) {
|
|
|
3335
3333
|
/**
|
|
3336
3334
|
* `tmux.exec` を処理し、`tmux.result` を返す。
|
|
3337
3335
|
*
|
|
3336
|
+
* 公式 Hub の送信入口は撤去済みだが、公開 0.x package の root handler / deep import、
|
|
3337
|
+
* plugin hook、旧 Hub との互換入口として handler・dispatch・execTmux を維持する。
|
|
3338
|
+
*
|
|
3338
3339
|
* plugin の `interceptTmuxExec` hook が `{ command, args, env }` を返したときは、その
|
|
3339
3340
|
* command を代わりに spawn する (例: remote-ops プラグインで ssh 越しに実行)。hook が
|
|
3340
3341
|
* 何も返さなければ既定のローカル `execTmux` へ落ちる。どちらの経路でも
|
|
@@ -4500,7 +4501,7 @@ export async function dispatch(msg, ctx) {
|
|
|
4500
4501
|
case "codex.mcp.status":
|
|
4501
4502
|
case "codex.mcp.reconnect":
|
|
4502
4503
|
case "codex.mcp.authenticate": {
|
|
4503
|
-
//
|
|
4504
|
+
// stream_id selects the live conversation client. Legacy callers retain control scope.
|
|
4504
4505
|
// claude.mcp.* と同じ request_id 相関・同じ応答形 (servers[] は Cockpit の
|
|
4505
4506
|
// McpServerStatus に正規化済み) にすることで、フロントの Popover / 再認証 UI を
|
|
4506
4507
|
// そのまま再利用する。
|
|
@@ -4510,11 +4511,15 @@ export async function dispatch(msg, ctx) {
|
|
|
4510
4511
|
// (authenticate の応答で callbackExpected: false を返す)
|
|
4511
4512
|
// - clearauth: 対応する RPC が無い (CLI の `codex mcp logout` のみ)
|
|
4512
4513
|
const request_id = msg.request_id
|
|
4513
|
-
|
|
4514
|
+
const sessionScope = Object.hasOwn(msg, "stream_id")
|
|
4515
|
+
const scope = sessionScope ? "session" : "control"
|
|
4516
|
+
const correlation = { ...(sessionScope ? { stream_id: msg.stream_id } : {}), scope }
|
|
4517
|
+
if ((sessionScope ? !ctx.codexBridge : !ctx.codexMcpControl) || !codexAvailableForDispatch(ctx)) {
|
|
4514
4518
|
ctx.client.send({
|
|
4515
4519
|
type: "codex.mcp.response",
|
|
4516
4520
|
request_id,
|
|
4517
4521
|
action: msg.type,
|
|
4522
|
+
...correlation,
|
|
4518
4523
|
error: "codex_cli_unavailable",
|
|
4519
4524
|
})
|
|
4520
4525
|
return
|
|
@@ -4524,19 +4529,22 @@ export async function dispatch(msg, ctx) {
|
|
|
4524
4529
|
switch (msg.type) {
|
|
4525
4530
|
case "codex.mcp.status":
|
|
4526
4531
|
result = {
|
|
4527
|
-
servers:
|
|
4528
|
-
|
|
4529
|
-
|
|
4532
|
+
servers: sessionScope
|
|
4533
|
+
? await ctx.codexBridge.mcpStatus({ stream_id: msg.stream_id })
|
|
4534
|
+
: await ctx.codexMcpControl.status({ reload: msg.reload === true }),
|
|
4530
4535
|
}
|
|
4531
4536
|
break
|
|
4532
4537
|
case "codex.mcp.reconnect": {
|
|
4533
|
-
|
|
4534
|
-
|
|
4538
|
+
result = sessionScope
|
|
4539
|
+
? await ctx.codexBridge.mcpReconnect(msg.server_name, { stream_id: msg.stream_id })
|
|
4540
|
+
: await ctx.codexMcpControl.reconnect(msg.server_name)
|
|
4535
4541
|
break
|
|
4536
4542
|
}
|
|
4537
4543
|
case "codex.mcp.authenticate":
|
|
4538
4544
|
result = {
|
|
4539
|
-
auth:
|
|
4545
|
+
auth: sessionScope
|
|
4546
|
+
? await ctx.codexBridge.mcpAuthenticate(msg.server_name, { stream_id: msg.stream_id })
|
|
4547
|
+
: await ctx.codexMcpControl.authenticate(msg.server_name),
|
|
4540
4548
|
}
|
|
4541
4549
|
break
|
|
4542
4550
|
}
|
|
@@ -4545,6 +4553,7 @@ export async function dispatch(msg, ctx) {
|
|
|
4545
4553
|
request_id,
|
|
4546
4554
|
action: msg.type,
|
|
4547
4555
|
server_name: msg.server_name,
|
|
4556
|
+
...correlation,
|
|
4548
4557
|
...result,
|
|
4549
4558
|
})
|
|
4550
4559
|
} catch (err) {
|
|
@@ -4557,6 +4566,7 @@ export async function dispatch(msg, ctx) {
|
|
|
4557
4566
|
request_id,
|
|
4558
4567
|
action: msg.type,
|
|
4559
4568
|
server_name: msg.server_name,
|
|
4569
|
+
...correlation,
|
|
4560
4570
|
error: err?.message || "mcp_control_failed",
|
|
4561
4571
|
})
|
|
4562
4572
|
}
|