draftgo-cli 3.0.53 → 3.0.54
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/README.md +2 -0
- package/package.json +1 -1
- package/resources/skill/manifest.json +1 -1
- package/resources/skill/references/mcp.md +3 -1
- package/src/mcp/client.js +131 -2
package/README.md
CHANGED
|
@@ -101,6 +101,8 @@ draftgo mcp test # 验证 initialize、tools/list、项目/正文
|
|
|
101
101
|
draftgo mcp serve # 启动 stdio -> 远端 Streamable HTTP bridge
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
+
`mcp serve` 会保留远端 `Mcp-Session-Id`。服务重启或 session 过期后,它会自动重新 initialize,并只重试一次被明确拒绝为 session 无效的当前请求;普通工具失败、超时和 5xx 不会自动重试。`mcp test` 使用独立的新连接,只证明当前配置能够建立新 session,不能直接证明宿主此前持有的 session 仍有效。
|
|
105
|
+
|
|
104
106
|
`setup` 不传 target 时自动检测当前项目的宿主。可用 `--target codex,cursor` 传入多个目标。
|
|
105
107
|
|
|
106
108
|
当前 MCP 宿主支持:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "draftgo-cli",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.54",
|
|
4
4
|
"description": "Install and manage the DraftGo skill across AI coding agents (Claude Code, Codex, Cursor, Windsurf, Antigravity, Copilot, Gemini, Kiro).",
|
|
5
5
|
"bin": {
|
|
6
6
|
"draftgo": "bin/draftgo.js"
|
|
@@ -84,7 +84,9 @@ draftgo mcp serve
|
|
|
84
84
|
- `setup` 写入项目级宿主配置,只替换 `draftgo` MCP 条目并保留其他配置。
|
|
85
85
|
- `status` 检查配置是否存在、格式是否有效,以及是否误写了凭据或远端 URL。
|
|
86
86
|
- `test` 从 `.draftgo/config.json` 读取连接,验证 initialize、tools/list、三类正文 resource_list,以及 db_meta 的 api_search、api_describe 和只读 api_call。
|
|
87
|
-
- `serve` 启动 stdio bridge,把宿主请求代理到当前项目配置的远端 Streamable HTTP `/mcp
|
|
87
|
+
- `serve` 启动 stdio bridge,把宿主请求代理到当前项目配置的远端 Streamable HTTP `/mcp`。远端重启或 session 过期后,bridge 会清除旧 `Mcp-Session-Id`、重新 initialize,并对被明确拒绝为 session 无效的当前请求重试一次;普通超时、服务端错误和工具错误不会自动重试。
|
|
88
|
+
|
|
89
|
+
`draftgo mcp test` 每次使用独立的新连接,因此它证明当前 server、SAT 和协议可以建立新 session,不代表宿主已持有的旧 session 仍然有效。宿主工具报告 session 失效但 `mcp test` 成功时,bridge 应自动恢复;若恢复仍失败,再检查远端日志、代理头传递和服务可用性。
|
|
88
90
|
|
|
89
91
|
`draftgo connect` 保存并验证 server/SAT、探测 `/mcp` 和关键工具,并可提示宿主 setup;`--server` 表示基础地址,完整 MCP endpoint 使用 `--mcp-url` 显式传入。它不下载业务资源,
|
|
90
92
|
也不创建 pages、navigation、docs、db_meta 等本地镜像。底座仍在开发或暂不可达时,报告诊断结果即可,
|
package/src/mcp/client.js
CHANGED
|
@@ -65,6 +65,44 @@ class McpRpcError extends Error {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
const SESSION_INVALID_CODES = new Set([
|
|
69
|
+
-32002,
|
|
70
|
+
'SESSION_EXPIRED',
|
|
71
|
+
'SESSION_NOT_FOUND',
|
|
72
|
+
'MCP_SESSION_EXPIRED',
|
|
73
|
+
'MCP_SESSION_NOT_FOUND',
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
function isSessionInvalidError(error) {
|
|
77
|
+
if (!error) return false;
|
|
78
|
+
if (Number(error.status) === 404) return true;
|
|
79
|
+
const code = error.code != null
|
|
80
|
+
? error.code
|
|
81
|
+
: error.rpc && error.rpc.error && error.rpc.error.code;
|
|
82
|
+
if (SESSION_INVALID_CODES.has(code)) return true;
|
|
83
|
+
const message = String(error.message
|
|
84
|
+
|| error.rpc && error.rpc.error && error.rpc.error.message
|
|
85
|
+
|| '');
|
|
86
|
+
return /\b(?:mcp\s+)?session(?:\s+id)?\s+(?:was\s+)?(?:not\s+found|expired|invalid|unknown|lost)\b/i.test(message)
|
|
87
|
+
|| /\b(?:server|mcp)\s+(?:is\s+)?(?:not initialized|uninitialized)\b/i.test(message)
|
|
88
|
+
|| /\binitialize first\b/i.test(message);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function requestIdKeys(message) {
|
|
92
|
+
const messages = Array.isArray(message) ? message : [message];
|
|
93
|
+
return new Set(messages
|
|
94
|
+
.filter((item) => isObject(item) && typeof item.method === 'string' && item.id != null)
|
|
95
|
+
.map((item) => `${typeof item.id}:${JSON.stringify(item.id)}`));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function sessionInvalidResponse(message, expectedIds) {
|
|
99
|
+
const messages = Array.isArray(message) ? message : [message];
|
|
100
|
+
return messages.find((item) => isObject(item)
|
|
101
|
+
&& item.error
|
|
102
|
+
&& expectedIds.has(`${typeof item.id}:${JSON.stringify(item.id)}`)
|
|
103
|
+
&& isSessionInvalidError(item.error)) || null;
|
|
104
|
+
}
|
|
105
|
+
|
|
68
106
|
function diagnosticNextCursor(result) {
|
|
69
107
|
const value = diagnosticData(result);
|
|
70
108
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
@@ -154,6 +192,9 @@ class DraftGoMcpClient {
|
|
|
154
192
|
this.sessionId = null;
|
|
155
193
|
this.protocolVersion = options.protocolVersion || DEFAULT_PROTOCOL_VERSION;
|
|
156
194
|
this.nextId = 1;
|
|
195
|
+
this.sessionGeneration = 0;
|
|
196
|
+
this.sessionRecovery = null;
|
|
197
|
+
this.sessionRecoveryCount = 0;
|
|
157
198
|
this.onMessage = typeof options.onMessage === 'function' ? options.onMessage : null;
|
|
158
199
|
}
|
|
159
200
|
|
|
@@ -163,28 +204,65 @@ class DraftGoMcpClient {
|
|
|
163
204
|
}
|
|
164
205
|
|
|
165
206
|
const initialize = findInitializeRequest(message);
|
|
207
|
+
const internalRecovery = options._sessionRecoveryInternal === true;
|
|
208
|
+
if (!initialize && !internalRecovery && this.sessionRecovery) {
|
|
209
|
+
await this.sessionRecovery;
|
|
210
|
+
}
|
|
211
|
+
if (initialize) this.sessionId = null;
|
|
166
212
|
const requestedVersion = initialize
|
|
167
213
|
&& initialize.params
|
|
168
214
|
&& initialize.params.protocolVersion;
|
|
169
215
|
const delivered = [];
|
|
216
|
+
const expectedIds = requestIdKeys(message);
|
|
217
|
+
const attemptedSessionId = initialize ? null : this.sessionId;
|
|
218
|
+
const attemptedGeneration = this.sessionGeneration;
|
|
219
|
+
const canRecover = !initialize
|
|
220
|
+
&& !internalRecovery
|
|
221
|
+
&& options._sessionRecoveryAttempted !== true
|
|
222
|
+
&& attemptedSessionId != null;
|
|
223
|
+
let suppressedSessionError = null;
|
|
170
224
|
|
|
171
225
|
try {
|
|
172
226
|
await postJsonRpc(this.config, message, {
|
|
173
227
|
signal: options.signal,
|
|
174
228
|
timeoutMs: options.timeoutMs,
|
|
175
|
-
sessionId:
|
|
229
|
+
sessionId: attemptedSessionId,
|
|
176
230
|
protocolVersion: requestedVersion || this.protocolVersion,
|
|
177
|
-
onSession: (sessionId) => {
|
|
231
|
+
onSession: (sessionId) => {
|
|
232
|
+
if (initialize || (this.sessionGeneration === attemptedGeneration
|
|
233
|
+
&& this.sessionId === attemptedSessionId)) {
|
|
234
|
+
this.sessionId = sessionId;
|
|
235
|
+
}
|
|
236
|
+
},
|
|
178
237
|
onMessage: (remoteMessage) => {
|
|
179
238
|
const safe = redactValue(remoteMessage, this.secrets);
|
|
180
239
|
delivered.push(safe);
|
|
240
|
+
const invalid = canRecover && sessionInvalidResponse(safe, expectedIds);
|
|
241
|
+
if (invalid) {
|
|
242
|
+
suppressedSessionError = invalid;
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (options._suppressCallbacks === true) return;
|
|
181
246
|
if (this.onMessage) this.onMessage(safe);
|
|
182
247
|
if (typeof options.onMessage === 'function') options.onMessage(safe);
|
|
183
248
|
},
|
|
184
249
|
});
|
|
250
|
+
if (suppressedSessionError) {
|
|
251
|
+
const remoteError = suppressedSessionError.error;
|
|
252
|
+
const error = new McpRpcError(
|
|
253
|
+
remoteError.message || `MCP error ${remoteError.code}`,
|
|
254
|
+
{ code: remoteError.code, data: remoteError.data, id: suppressedSessionError.id },
|
|
255
|
+
);
|
|
256
|
+
error.rpc = suppressedSessionError;
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
185
259
|
} catch (error) {
|
|
186
260
|
if (error && error.rpc) error.rpc = redactValue(error.rpc, this.secrets);
|
|
187
261
|
if (error && error.message) error.message = redactText(error.message, this.secrets);
|
|
262
|
+
if (canRecover && isSessionInvalidError(error)) {
|
|
263
|
+
await this.recoverSession(attemptedSessionId, attemptedGeneration, options);
|
|
264
|
+
return this.forward(message, { ...options, _sessionRecoveryAttempted: true });
|
|
265
|
+
}
|
|
188
266
|
throw error;
|
|
189
267
|
}
|
|
190
268
|
|
|
@@ -196,6 +274,54 @@ class DraftGoMcpClient {
|
|
|
196
274
|
return delivered;
|
|
197
275
|
}
|
|
198
276
|
|
|
277
|
+
async recoverSession(failedSessionId, failedGeneration, options = {}) {
|
|
278
|
+
if (this.sessionGeneration !== failedGeneration) return;
|
|
279
|
+
if (this.sessionId != null && this.sessionId !== failedSessionId) return;
|
|
280
|
+
if (this.sessionRecovery) return this.sessionRecovery;
|
|
281
|
+
|
|
282
|
+
const recoveryNumber = this.sessionRecoveryCount + 1;
|
|
283
|
+
this.sessionId = null;
|
|
284
|
+
const recovery = (async () => {
|
|
285
|
+
if (typeof options.onSessionRecovery === 'function') {
|
|
286
|
+
options.onSessionRecovery('started', { attempt: recoveryNumber });
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
await this.initialize({
|
|
290
|
+
id: `draftgo-session-recovery-${recoveryNumber}`,
|
|
291
|
+
protocolVersion: this.protocolVersion,
|
|
292
|
+
timeoutMs: options.timeoutMs,
|
|
293
|
+
signal: options.signal,
|
|
294
|
+
_sessionRecoveryInternal: true,
|
|
295
|
+
_suppressCallbacks: true,
|
|
296
|
+
});
|
|
297
|
+
this.sessionRecoveryCount = recoveryNumber;
|
|
298
|
+
if (typeof options.onSessionRecovery === 'function') {
|
|
299
|
+
options.onSessionRecovery('succeeded', { attempt: recoveryNumber });
|
|
300
|
+
}
|
|
301
|
+
} catch (error) {
|
|
302
|
+
this.sessionId = null;
|
|
303
|
+
if (typeof options.onSessionRecovery === 'function') {
|
|
304
|
+
options.onSessionRecovery('failed', { attempt: recoveryNumber });
|
|
305
|
+
}
|
|
306
|
+
const message = redactText(error && error.message ? error.message : error, this.secrets);
|
|
307
|
+
const wrapped = new McpRpcError(`DraftGo MCP session recovery failed: ${message}`, {
|
|
308
|
+
code: error && error.code != null ? error.code : -32000,
|
|
309
|
+
data: error && error.data,
|
|
310
|
+
});
|
|
311
|
+
wrapped.status = Number(error && error.status) || 0;
|
|
312
|
+
wrapped.sessionRecovery = true;
|
|
313
|
+
wrapped.cause = error;
|
|
314
|
+
throw wrapped;
|
|
315
|
+
}
|
|
316
|
+
})();
|
|
317
|
+
this.sessionRecovery = recovery;
|
|
318
|
+
try {
|
|
319
|
+
await recovery;
|
|
320
|
+
} finally {
|
|
321
|
+
if (this.sessionRecovery === recovery) this.sessionRecovery = null;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
199
325
|
async request(method, params, options = {}) {
|
|
200
326
|
const id = options.id == null ? this.nextId++ : options.id;
|
|
201
327
|
const message = { jsonrpc: '2.0', id, method };
|
|
@@ -249,6 +375,7 @@ class DraftGoMcpClient {
|
|
|
249
375
|
throw error;
|
|
250
376
|
}
|
|
251
377
|
if (typeof options.onStage === 'function') options.onStage('initialized', 'succeeded');
|
|
378
|
+
this.sessionGeneration += 1;
|
|
252
379
|
return result;
|
|
253
380
|
}
|
|
254
381
|
|
|
@@ -425,6 +552,7 @@ class DraftGoMcpClient {
|
|
|
425
552
|
initialized,
|
|
426
553
|
protocolVersion: this.protocolVersion,
|
|
427
554
|
sessionId: this.sessionId,
|
|
555
|
+
sessionRecoveries: this.sessionRecoveryCount,
|
|
428
556
|
tools,
|
|
429
557
|
testedTool: tested[0].name,
|
|
430
558
|
toolResult: tested[0].result,
|
|
@@ -457,6 +585,7 @@ module.exports = {
|
|
|
457
585
|
diagnosticData,
|
|
458
586
|
diagnosticNextCursor,
|
|
459
587
|
findDBMetaListOperation,
|
|
588
|
+
isSessionInvalidError,
|
|
460
589
|
redactValue,
|
|
461
590
|
testConnection,
|
|
462
591
|
callTool,
|