draftgo-cli 4.0.23 → 4.0.24
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 +1 -1
- package/bin/draftgo.js +8 -8
- package/package.json +72 -72
- package/resources/custom-service-sdk/auth_test.go +1 -1
- package/resources/custom-service-sdk/manifest.json +11 -11
- package/resources/custom-service-sdk/platform.go +10 -3
- package/resources/custom-service-sdk/resources.go +1 -0
- package/resources/custom-service-sdk/resources_scope_test.go +10 -5
- package/resources/custom-service-sdk/sdk.go +4 -3
- package/resources/skill/SKILL.md +1 -1
- package/resources/skill/manifest.json +1 -1
- package/resources/skill/references/aihub.md +74 -74
- package/resources/skill/references/app-api.md +78 -78
- package/resources/skill/references/architecture.md +40 -40
- package/resources/skill/references/checkout.md +105 -105
- package/resources/skill/references/custom-services.md +4 -4
- package/resources/skill/references/data.md +168 -168
- package/resources/skill/references/methods.md +3 -0
- package/resources/skill/references/modules.md +47 -47
- package/resources/skill/references/runtime.md +95 -96
- package/resources/skill/story/SKILL.md +264 -264
- package/src/commands/help.js +72 -72
- package/src/commands/listTargets.js +12 -12
- package/src/commands/status.js +2 -2
- package/src/commands/uninstall.js +45 -45
- package/src/commands/update.js +20 -20
- package/src/customServices.js +5 -4
- package/src/detect.js +14 -14
- package/src/fsx.js +67 -67
- package/src/index.js +25 -25
- package/src/localRuntime/detect.js +76 -76
- package/src/localRuntime/mysqlClient.js +138 -138
- package/src/logger.js +37 -37
- package/src/mcp/client.js +586 -595
- package/src/mcp/hosts.js +520 -520
- package/src/mcp/protocol.js +167 -167
- package/src/prompt.js +94 -94
- package/src/updateCheck.js +16 -16
package/src/mcp/client.js
CHANGED
|
@@ -1,602 +1,593 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const pkg = require('../../package.json');
|
|
4
|
-
const {
|
|
5
|
-
DEFAULT_PROTOCOL_VERSION,
|
|
6
|
-
McpHttpError,
|
|
7
|
-
postJsonRpc,
|
|
8
|
-
redactText,
|
|
9
|
-
} = require('./protocol');
|
|
10
|
-
const { allWithAbort } = require('./parallel');
|
|
11
|
-
|
|
12
|
-
const SAFE_TEST_TOOLS = [
|
|
13
|
-
'draftgo_project_overview',
|
|
14
|
-
'draftgo_resource_list',
|
|
15
|
-
'draftgo_api_search',
|
|
16
|
-
'draftgo_api_describe',
|
|
17
|
-
'draftgo_api_call',
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
const REQUIRED_DRAFTGO_TOOLS = [
|
|
21
|
-
'draftgo_project_overview',
|
|
22
|
-
'draftgo_resource_list',
|
|
23
|
-
'draftgo_resource_search',
|
|
24
|
-
'draftgo_resource_get_metadata',
|
|
25
|
-
'draftgo_resource_read_fragment',
|
|
26
|
-
'draftgo_api_search',
|
|
27
|
-
'draftgo_api_describe',
|
|
28
|
-
'draftgo_api_call',
|
|
29
|
-
];
|
|
30
|
-
|
|
31
|
-
function diagnosticArguments(canonicalName) {
|
|
32
|
-
if (canonicalName === 'draftgo_resource_list') {
|
|
33
|
-
return { resource_type: 'pages', limit: 1 };
|
|
34
|
-
}
|
|
35
|
-
if (canonicalName === 'draftgo_api_search') return { query: 'project', limit: 1 };
|
|
36
|
-
return {};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function diagnosticData(result) {
|
|
40
|
-
let value = result;
|
|
41
|
-
if (value && value.structuredContent) value = value.structuredContent;
|
|
42
|
-
else if (value && value.structured_content) value = value.structured_content;
|
|
43
|
-
if (value && value.data) value = value.data;
|
|
44
|
-
if (value && Object.prototype.hasOwnProperty.call(value, 'value')) value = value.value;
|
|
45
|
-
return value;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function findDBMetaListOperation(result) {
|
|
49
|
-
const value = diagnosticData(result);
|
|
50
|
-
const items = value && Array.isArray(value.items) ? value.items : [];
|
|
51
|
-
return items.find((operation) => operation
|
|
52
|
-
&& String(operation.method || '').toUpperCase() === 'GET'
|
|
53
|
-
&& String(operation.resource_type || '').toLowerCase() === 'db_meta'
|
|
54
|
-
&& String(operation.path || operation.path_template || '') === '/api/db-meta'
|
|
55
|
-
&& operation.destructive !== true) || null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
class McpRpcError extends Error {
|
|
59
|
-
constructor(message, { code = -32000, data, id = null } = {}) {
|
|
60
|
-
super(message);
|
|
61
|
-
this.name = 'McpRpcError';
|
|
62
|
-
this.code = code;
|
|
63
|
-
this.data = data;
|
|
64
|
-
this.id = id;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
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
|
-
|
|
106
|
-
function diagnosticNextCursor(result) {
|
|
107
|
-
const value = diagnosticData(result);
|
|
108
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
109
|
-
const hasMore = value.has_more === true || value.hasMore === true;
|
|
110
|
-
|
|
111
|
-
let cursor;
|
|
112
|
-
let present = false;
|
|
113
|
-
if (Object.prototype.hasOwnProperty.call(value, 'next_cursor')) {
|
|
114
|
-
cursor = value.next_cursor;
|
|
115
|
-
present = true;
|
|
116
|
-
} else if (Object.prototype.hasOwnProperty.call(value, 'nextCursor')) {
|
|
117
|
-
cursor = value.nextCursor;
|
|
118
|
-
present = true;
|
|
119
|
-
} else if (hasMore) {
|
|
120
|
-
if (!Object.prototype.hasOwnProperty.call(value, 'cursor')) {
|
|
121
|
-
throw new McpRpcError('DraftGo api_search reported more pages without a cursor.');
|
|
122
|
-
}
|
|
123
|
-
cursor = value.cursor;
|
|
124
|
-
present = true;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (!present) return null;
|
|
128
|
-
if (cursor == null || cursor === '') {
|
|
129
|
-
if (hasMore) {
|
|
130
|
-
throw new McpRpcError('DraftGo api_search reported more pages with an empty cursor.');
|
|
131
|
-
}
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
if (typeof cursor !== 'string' || !cursor.trim()) {
|
|
135
|
-
throw new McpRpcError('DraftGo api_search returned an invalid cursor.');
|
|
136
|
-
}
|
|
137
|
-
return cursor;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function redactValue(value, secrets = [], seen = new WeakMap()) {
|
|
141
|
-
if (typeof value === 'string') return redactText(value, secrets);
|
|
142
|
-
if (!value || typeof value !== 'object') return value;
|
|
143
|
-
if (seen.has(value)) return seen.get(value);
|
|
144
|
-
if (Array.isArray(value)) {
|
|
145
|
-
const result = [];
|
|
146
|
-
seen.set(value, result);
|
|
147
|
-
for (const item of value) result.push(redactValue(item, secrets, seen));
|
|
148
|
-
return result;
|
|
149
|
-
}
|
|
150
|
-
const result = {};
|
|
151
|
-
seen.set(value, result);
|
|
152
|
-
for (const [key, item] of Object.entries(value)) {
|
|
153
|
-
result[key] = redactValue(item, secrets, seen);
|
|
154
|
-
}
|
|
155
|
-
return result;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function isObject(value) {
|
|
159
|
-
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function sameId(left, right) {
|
|
163
|
-
return left === right;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function findInitializeRequest(message) {
|
|
167
|
-
const messages = Array.isArray(message) ? message : [message];
|
|
168
|
-
return messages.find((item) => isObject(item) && item.method === 'initialize' && item.id != null);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function findResponse(messages, id) {
|
|
172
|
-
return messages.find((message) => isObject(message)
|
|
173
|
-
&& Object.prototype.hasOwnProperty.call(message, 'id')
|
|
174
|
-
&& sameId(message.id, id)
|
|
175
|
-
&& !message.method);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function toolMatches(name, candidate) {
|
|
179
|
-
return name === candidate || name.endsWith(`.${candidate}`)
|
|
180
|
-
|| name.endsWith(`/${candidate}`) || name.endsWith(`:${candidate}`);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
class DraftGoMcpClient {
|
|
184
|
-
constructor(config, options = {}) {
|
|
185
|
-
if (!config || typeof config !== 'object') throw new TypeError('DraftGo MCP config is required.');
|
|
186
|
-
const token = String(config.token || config.sat || '').trim();
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const pkg = require('../../package.json');
|
|
4
|
+
const {
|
|
5
|
+
DEFAULT_PROTOCOL_VERSION,
|
|
6
|
+
McpHttpError,
|
|
7
|
+
postJsonRpc,
|
|
8
|
+
redactText,
|
|
9
|
+
} = require('./protocol');
|
|
10
|
+
const { allWithAbort } = require('./parallel');
|
|
11
|
+
|
|
12
|
+
const SAFE_TEST_TOOLS = [
|
|
13
|
+
'draftgo_project_overview',
|
|
14
|
+
'draftgo_resource_list',
|
|
15
|
+
'draftgo_api_search',
|
|
16
|
+
'draftgo_api_describe',
|
|
17
|
+
'draftgo_api_call',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const REQUIRED_DRAFTGO_TOOLS = [
|
|
21
|
+
'draftgo_project_overview',
|
|
22
|
+
'draftgo_resource_list',
|
|
23
|
+
'draftgo_resource_search',
|
|
24
|
+
'draftgo_resource_get_metadata',
|
|
25
|
+
'draftgo_resource_read_fragment',
|
|
26
|
+
'draftgo_api_search',
|
|
27
|
+
'draftgo_api_describe',
|
|
28
|
+
'draftgo_api_call',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function diagnosticArguments(canonicalName) {
|
|
32
|
+
if (canonicalName === 'draftgo_resource_list') {
|
|
33
|
+
return { resource_type: 'pages', limit: 1 };
|
|
34
|
+
}
|
|
35
|
+
if (canonicalName === 'draftgo_api_search') return { query: 'project', limit: 1 };
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function diagnosticData(result) {
|
|
40
|
+
let value = result;
|
|
41
|
+
if (value && value.structuredContent) value = value.structuredContent;
|
|
42
|
+
else if (value && value.structured_content) value = value.structured_content;
|
|
43
|
+
if (value && value.data) value = value.data;
|
|
44
|
+
if (value && Object.prototype.hasOwnProperty.call(value, 'value')) value = value.value;
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function findDBMetaListOperation(result) {
|
|
49
|
+
const value = diagnosticData(result);
|
|
50
|
+
const items = value && Array.isArray(value.items) ? value.items : [];
|
|
51
|
+
return items.find((operation) => operation
|
|
52
|
+
&& String(operation.method || '').toUpperCase() === 'GET'
|
|
53
|
+
&& String(operation.resource_type || '').toLowerCase() === 'db_meta'
|
|
54
|
+
&& String(operation.path || operation.path_template || '') === '/api/db-meta'
|
|
55
|
+
&& operation.destructive !== true) || null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
class McpRpcError extends Error {
|
|
59
|
+
constructor(message, { code = -32000, data, id = null } = {}) {
|
|
60
|
+
super(message);
|
|
61
|
+
this.name = 'McpRpcError';
|
|
62
|
+
this.code = code;
|
|
63
|
+
this.data = data;
|
|
64
|
+
this.id = id;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
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
|
+
|
|
106
|
+
function diagnosticNextCursor(result) {
|
|
107
|
+
const value = diagnosticData(result);
|
|
108
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
109
|
+
const hasMore = value.has_more === true || value.hasMore === true;
|
|
110
|
+
|
|
111
|
+
let cursor;
|
|
112
|
+
let present = false;
|
|
113
|
+
if (Object.prototype.hasOwnProperty.call(value, 'next_cursor')) {
|
|
114
|
+
cursor = value.next_cursor;
|
|
115
|
+
present = true;
|
|
116
|
+
} else if (Object.prototype.hasOwnProperty.call(value, 'nextCursor')) {
|
|
117
|
+
cursor = value.nextCursor;
|
|
118
|
+
present = true;
|
|
119
|
+
} else if (hasMore) {
|
|
120
|
+
if (!Object.prototype.hasOwnProperty.call(value, 'cursor')) {
|
|
121
|
+
throw new McpRpcError('DraftGo api_search reported more pages without a cursor.');
|
|
122
|
+
}
|
|
123
|
+
cursor = value.cursor;
|
|
124
|
+
present = true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!present) return null;
|
|
128
|
+
if (cursor == null || cursor === '') {
|
|
129
|
+
if (hasMore) {
|
|
130
|
+
throw new McpRpcError('DraftGo api_search reported more pages with an empty cursor.');
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
if (typeof cursor !== 'string' || !cursor.trim()) {
|
|
135
|
+
throw new McpRpcError('DraftGo api_search returned an invalid cursor.');
|
|
136
|
+
}
|
|
137
|
+
return cursor;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function redactValue(value, secrets = [], seen = new WeakMap()) {
|
|
141
|
+
if (typeof value === 'string') return redactText(value, secrets);
|
|
142
|
+
if (!value || typeof value !== 'object') return value;
|
|
143
|
+
if (seen.has(value)) return seen.get(value);
|
|
144
|
+
if (Array.isArray(value)) {
|
|
145
|
+
const result = [];
|
|
146
|
+
seen.set(value, result);
|
|
147
|
+
for (const item of value) result.push(redactValue(item, secrets, seen));
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
const result = {};
|
|
151
|
+
seen.set(value, result);
|
|
152
|
+
for (const [key, item] of Object.entries(value)) {
|
|
153
|
+
result[key] = redactValue(item, secrets, seen);
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isObject(value) {
|
|
159
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function sameId(left, right) {
|
|
163
|
+
return left === right;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function findInitializeRequest(message) {
|
|
167
|
+
const messages = Array.isArray(message) ? message : [message];
|
|
168
|
+
return messages.find((item) => isObject(item) && item.method === 'initialize' && item.id != null);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function findResponse(messages, id) {
|
|
172
|
+
return messages.find((message) => isObject(message)
|
|
173
|
+
&& Object.prototype.hasOwnProperty.call(message, 'id')
|
|
174
|
+
&& sameId(message.id, id)
|
|
175
|
+
&& !message.method);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function toolMatches(name, candidate) {
|
|
179
|
+
return name === candidate || name.endsWith(`.${candidate}`)
|
|
180
|
+
|| name.endsWith(`/${candidate}`) || name.endsWith(`:${candidate}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
class DraftGoMcpClient {
|
|
184
|
+
constructor(config, options = {}) {
|
|
185
|
+
if (!config || typeof config !== 'object') throw new TypeError('DraftGo MCP config is required.');
|
|
186
|
+
const token = String(config.token || config.sat || '').trim();
|
|
187
187
|
if (!token) throw new Error('DraftGo MCP config is missing API Key.');
|
|
188
|
-
if (!config.server && !config.mcp_url) throw new Error('DraftGo MCP config is missing server.');
|
|
189
|
-
|
|
190
|
-
this.config = { ...config, token };
|
|
191
|
-
this.secrets = [token];
|
|
192
|
-
this.sessionId = null;
|
|
193
|
-
this.protocolVersion = options.protocolVersion || DEFAULT_PROTOCOL_VERSION;
|
|
194
|
-
this.nextId = 1;
|
|
195
|
-
this.sessionGeneration = 0;
|
|
196
|
-
this.sessionRecovery = null;
|
|
197
|
-
this.sessionRecoveryCount = 0;
|
|
198
|
-
this.onMessage = typeof options.onMessage === 'function' ? options.onMessage : null;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
async forward(message, options = {}) {
|
|
202
|
-
if (!isObject(message) && !Array.isArray(message)) {
|
|
203
|
-
throw new TypeError('MCP message must be a JSON object or batch.');
|
|
204
|
-
}
|
|
205
|
-
|
|
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;
|
|
212
|
-
const requestedVersion = initialize
|
|
213
|
-
&& initialize.params
|
|
214
|
-
&& initialize.params.protocolVersion;
|
|
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;
|
|
224
|
-
|
|
225
|
-
try {
|
|
226
|
-
await postJsonRpc(this.config, message, {
|
|
227
|
-
signal: options.signal,
|
|
228
|
-
timeoutMs: options.timeoutMs,
|
|
229
|
-
sessionId: attemptedSessionId,
|
|
230
|
-
protocolVersion: requestedVersion || this.protocolVersion,
|
|
231
|
-
onSession: (sessionId) => {
|
|
232
|
-
if (initialize || (this.sessionGeneration === attemptedGeneration
|
|
233
|
-
&& this.sessionId === attemptedSessionId)) {
|
|
234
|
-
this.sessionId = sessionId;
|
|
235
|
-
}
|
|
236
|
-
},
|
|
237
|
-
onMessage: (remoteMessage) => {
|
|
238
|
-
const safe = redactValue(remoteMessage, this.secrets);
|
|
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;
|
|
246
|
-
if (this.onMessage) this.onMessage(safe);
|
|
247
|
-
if (typeof options.onMessage === 'function') options.onMessage(safe);
|
|
248
|
-
},
|
|
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
|
-
}
|
|
259
|
-
} catch (error) {
|
|
260
|
-
if (error && error.rpc) error.rpc = redactValue(error.rpc, this.secrets);
|
|
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
|
-
}
|
|
266
|
-
throw error;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (initialize) {
|
|
270
|
-
const response = findResponse(delivered, initialize.id);
|
|
271
|
-
const negotiated = response && response.result && response.result.protocolVersion;
|
|
272
|
-
if (negotiated) this.protocolVersion = negotiated;
|
|
273
|
-
}
|
|
274
|
-
return delivered;
|
|
275
|
-
}
|
|
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
|
-
|
|
325
|
-
async request(method, params, options = {}) {
|
|
326
|
-
const id = options.id == null ? this.nextId++ : options.id;
|
|
327
|
-
const message = { jsonrpc: '2.0', id, method };
|
|
328
|
-
if (params !== undefined) message.params = params;
|
|
329
|
-
const messages = await this.forward(message, options);
|
|
330
|
-
const response = findResponse(messages, id);
|
|
331
|
-
if (!response) {
|
|
332
|
-
throw new McpRpcError(`DraftGo MCP returned no response for ${method}.`, { id });
|
|
333
|
-
}
|
|
334
|
-
if (response.error) {
|
|
335
|
-
throw new McpRpcError(
|
|
336
|
-
redactText(response.error.message || `MCP error ${response.error.code}`, this.secrets),
|
|
337
|
-
{
|
|
338
|
-
code: response.error.code,
|
|
339
|
-
data: redactValue(response.error.data, this.secrets),
|
|
340
|
-
id,
|
|
341
|
-
},
|
|
342
|
-
);
|
|
343
|
-
}
|
|
344
|
-
return response.result;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
async notify(method, params, options = {}) {
|
|
348
|
-
const message = { jsonrpc: '2.0', method };
|
|
349
|
-
if (params !== undefined) message.params = params;
|
|
350
|
-
await this.forward(message, options);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
async initialize(options = {}) {
|
|
354
|
-
if (typeof options.onStage === 'function') options.onStage('initialize', 'started');
|
|
355
|
-
let result;
|
|
356
|
-
try {
|
|
357
|
-
result = await this.request('initialize', {
|
|
358
|
-
protocolVersion: options.protocolVersion || this.protocolVersion,
|
|
359
|
-
capabilities: options.capabilities || {},
|
|
360
|
-
clientInfo: options.clientInfo || {
|
|
361
|
-
name: 'draftgo-cli',
|
|
362
|
-
version: pkg.version,
|
|
363
|
-
},
|
|
364
|
-
}, options);
|
|
365
|
-
} catch (error) {
|
|
366
|
-
if (typeof options.onStage === 'function') options.onStage('initialize', 'failed');
|
|
367
|
-
throw error;
|
|
368
|
-
}
|
|
369
|
-
if (typeof options.onStage === 'function') options.onStage('initialize', 'succeeded', result);
|
|
370
|
-
if (result && result.protocolVersion) this.protocolVersion = result.protocolVersion;
|
|
371
|
-
try {
|
|
372
|
-
await this.notify('notifications/initialized', undefined, options);
|
|
373
|
-
} catch (error) {
|
|
374
|
-
if (typeof options.onStage === 'function') options.onStage('initialized', 'failed');
|
|
375
|
-
throw error;
|
|
376
|
-
}
|
|
377
|
-
if (typeof options.onStage === 'function') options.onStage('initialized', 'succeeded');
|
|
378
|
-
this.sessionGeneration += 1;
|
|
379
|
-
return result;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
async toolsList(cursor, options = {}) {
|
|
383
|
-
const params = cursor ? { cursor } : {};
|
|
384
|
-
return this.request('tools/list', params, options);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
async listAllTools(options = {}) {
|
|
388
|
-
const tools = [];
|
|
389
|
-
const seenCursors = new Set();
|
|
390
|
-
let cursor;
|
|
391
|
-
const maxPages = Number(options.maxPages || 100);
|
|
392
|
-
for (let page = 0; page < maxPages; page += 1) {
|
|
393
|
-
const result = await this.toolsList(cursor, options);
|
|
394
|
-
if (result && Array.isArray(result.tools)) tools.push(...result.tools);
|
|
395
|
-
const next = result && result.nextCursor;
|
|
396
|
-
if (!next) return tools;
|
|
397
|
-
if (seenCursors.has(next)) throw new McpRpcError('DraftGo MCP repeated a tools/list cursor.');
|
|
398
|
-
seenCursors.add(next);
|
|
399
|
-
cursor = next;
|
|
400
|
-
}
|
|
401
|
-
throw new McpRpcError(`DraftGo MCP tools/list exceeded ${maxPages} pages.`);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
async toolsCall(name, args = {}, options = {}) {
|
|
405
|
-
const result = await this.request('tools/call', { name, arguments: args }, options);
|
|
406
|
-
if (result && result.isError === true) {
|
|
407
|
-
const structured = result.structuredContent || result.structured_content || {};
|
|
408
|
-
const text = Array.isArray(result.content)
|
|
409
|
-
? result.content.find((part) => part && part.type === 'text' && typeof part.text === 'string')
|
|
410
|
-
: null;
|
|
411
|
-
const message = structured.message || structured.error && structured.error.message
|
|
412
|
-
|| text && text.text || `DraftGo tool failed: ${name}`;
|
|
413
|
-
throw new McpRpcError(redactText(String(message).slice(0, 4096), this.secrets), {
|
|
414
|
-
code: structured.code || structured.error && structured.error.code || -32000,
|
|
415
|
-
data: redactValue(structured, this.secrets),
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
return result;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
async testConnection(options = {}) {
|
|
422
|
-
const initialized = await this.initialize(options);
|
|
423
|
-
if (typeof options.onStage === 'function') options.onStage('tools/list', 'started');
|
|
424
|
-
let tools;
|
|
425
|
-
try {
|
|
426
|
-
tools = await this.listAllTools(options);
|
|
427
|
-
} catch (error) {
|
|
428
|
-
if (typeof options.onStage === 'function') options.onStage('tools/list', 'failed');
|
|
429
|
-
throw error;
|
|
430
|
-
}
|
|
431
|
-
if (typeof options.onStage === 'function') options.onStage('tools/list', 'succeeded', { count: tools.length });
|
|
432
|
-
const requiredTools = options.requiredTools || REQUIRED_DRAFTGO_TOOLS;
|
|
433
|
-
const missing = requiredTools.filter((expected) => !tools.some((tool) =>
|
|
434
|
-
tool && typeof tool.name === 'string' && toolMatches(tool.name, expected)));
|
|
435
|
-
if (missing.length) {
|
|
436
|
-
throw new McpRpcError(`DraftGo MCP is missing required tools: ${missing.join(', ')}.`, { code: -32601 });
|
|
437
|
-
}
|
|
438
|
-
const safeTools = options.safeTools || SAFE_TEST_TOOLS;
|
|
439
|
-
const hasDiagnosticOverrides = Boolean(options.requiredTools || options.safeTools);
|
|
440
|
-
const firstSafeTool = tools.find((tool) => tool && typeof tool.name === 'string'
|
|
441
|
-
&& safeTools.some((canonical) => toolMatches(tool.name, canonical)));
|
|
442
|
-
if (!firstSafeTool) {
|
|
443
|
-
throw new McpRpcError(
|
|
444
|
-
`DraftGo MCP exposes no safe diagnostic tool (${safeTools.join(', ')}).`,
|
|
445
|
-
{ code: -32601 },
|
|
446
|
-
);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
const selected = safeTools.map((canonical) => ({
|
|
450
|
-
canonical,
|
|
451
|
-
tool: tools.find((candidate) => candidate && typeof candidate.name === 'string'
|
|
452
|
-
&& toolMatches(candidate.name, canonical)),
|
|
453
|
-
})).filter((entry) => entry.tool);
|
|
454
|
-
const byCanonical = Object.fromEntries(selected.map((entry) => [entry.canonical, entry.tool]));
|
|
455
|
-
const overrideCanonical = safeTools.find((canonical) => toolMatches(firstSafeTool.name, canonical));
|
|
188
|
+
if (!config.server && !config.mcp_url) throw new Error('DraftGo MCP config is missing server.');
|
|
189
|
+
|
|
190
|
+
this.config = { ...config, token };
|
|
191
|
+
this.secrets = [token];
|
|
192
|
+
this.sessionId = null;
|
|
193
|
+
this.protocolVersion = options.protocolVersion || DEFAULT_PROTOCOL_VERSION;
|
|
194
|
+
this.nextId = 1;
|
|
195
|
+
this.sessionGeneration = 0;
|
|
196
|
+
this.sessionRecovery = null;
|
|
197
|
+
this.sessionRecoveryCount = 0;
|
|
198
|
+
this.onMessage = typeof options.onMessage === 'function' ? options.onMessage : null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async forward(message, options = {}) {
|
|
202
|
+
if (!isObject(message) && !Array.isArray(message)) {
|
|
203
|
+
throw new TypeError('MCP message must be a JSON object or batch.');
|
|
204
|
+
}
|
|
205
|
+
|
|
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;
|
|
212
|
+
const requestedVersion = initialize
|
|
213
|
+
&& initialize.params
|
|
214
|
+
&& initialize.params.protocolVersion;
|
|
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;
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
await postJsonRpc(this.config, message, {
|
|
227
|
+
signal: options.signal,
|
|
228
|
+
timeoutMs: options.timeoutMs,
|
|
229
|
+
sessionId: attemptedSessionId,
|
|
230
|
+
protocolVersion: requestedVersion || this.protocolVersion,
|
|
231
|
+
onSession: (sessionId) => {
|
|
232
|
+
if (initialize || (this.sessionGeneration === attemptedGeneration
|
|
233
|
+
&& this.sessionId === attemptedSessionId)) {
|
|
234
|
+
this.sessionId = sessionId;
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
onMessage: (remoteMessage) => {
|
|
238
|
+
const safe = redactValue(remoteMessage, this.secrets);
|
|
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;
|
|
246
|
+
if (this.onMessage) this.onMessage(safe);
|
|
247
|
+
if (typeof options.onMessage === 'function') options.onMessage(safe);
|
|
248
|
+
},
|
|
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
|
+
}
|
|
259
|
+
} catch (error) {
|
|
260
|
+
if (error && error.rpc) error.rpc = redactValue(error.rpc, this.secrets);
|
|
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
|
+
}
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (initialize) {
|
|
270
|
+
const response = findResponse(delivered, initialize.id);
|
|
271
|
+
const negotiated = response && response.result && response.result.protocolVersion;
|
|
272
|
+
if (negotiated) this.protocolVersion = negotiated;
|
|
273
|
+
}
|
|
274
|
+
return delivered;
|
|
275
|
+
}
|
|
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
|
+
|
|
325
|
+
async request(method, params, options = {}) {
|
|
326
|
+
const id = options.id == null ? this.nextId++ : options.id;
|
|
327
|
+
const message = { jsonrpc: '2.0', id, method };
|
|
328
|
+
if (params !== undefined) message.params = params;
|
|
329
|
+
const messages = await this.forward(message, options);
|
|
330
|
+
const response = findResponse(messages, id);
|
|
331
|
+
if (!response) {
|
|
332
|
+
throw new McpRpcError(`DraftGo MCP returned no response for ${method}.`, { id });
|
|
333
|
+
}
|
|
334
|
+
if (response.error) {
|
|
335
|
+
throw new McpRpcError(
|
|
336
|
+
redactText(response.error.message || `MCP error ${response.error.code}`, this.secrets),
|
|
337
|
+
{
|
|
338
|
+
code: response.error.code,
|
|
339
|
+
data: redactValue(response.error.data, this.secrets),
|
|
340
|
+
id,
|
|
341
|
+
},
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
return response.result;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async notify(method, params, options = {}) {
|
|
348
|
+
const message = { jsonrpc: '2.0', method };
|
|
349
|
+
if (params !== undefined) message.params = params;
|
|
350
|
+
await this.forward(message, options);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async initialize(options = {}) {
|
|
354
|
+
if (typeof options.onStage === 'function') options.onStage('initialize', 'started');
|
|
355
|
+
let result;
|
|
356
|
+
try {
|
|
357
|
+
result = await this.request('initialize', {
|
|
358
|
+
protocolVersion: options.protocolVersion || this.protocolVersion,
|
|
359
|
+
capabilities: options.capabilities || {},
|
|
360
|
+
clientInfo: options.clientInfo || {
|
|
361
|
+
name: 'draftgo-cli',
|
|
362
|
+
version: pkg.version,
|
|
363
|
+
},
|
|
364
|
+
}, options);
|
|
365
|
+
} catch (error) {
|
|
366
|
+
if (typeof options.onStage === 'function') options.onStage('initialize', 'failed');
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
369
|
+
if (typeof options.onStage === 'function') options.onStage('initialize', 'succeeded', result);
|
|
370
|
+
if (result && result.protocolVersion) this.protocolVersion = result.protocolVersion;
|
|
371
|
+
try {
|
|
372
|
+
await this.notify('notifications/initialized', undefined, options);
|
|
373
|
+
} catch (error) {
|
|
374
|
+
if (typeof options.onStage === 'function') options.onStage('initialized', 'failed');
|
|
375
|
+
throw error;
|
|
376
|
+
}
|
|
377
|
+
if (typeof options.onStage === 'function') options.onStage('initialized', 'succeeded');
|
|
378
|
+
this.sessionGeneration += 1;
|
|
379
|
+
return result;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async toolsList(cursor, options = {}) {
|
|
383
|
+
const params = cursor ? { cursor } : {};
|
|
384
|
+
return this.request('tools/list', params, options);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async listAllTools(options = {}) {
|
|
388
|
+
const tools = [];
|
|
389
|
+
const seenCursors = new Set();
|
|
390
|
+
let cursor;
|
|
391
|
+
const maxPages = Number(options.maxPages || 100);
|
|
392
|
+
for (let page = 0; page < maxPages; page += 1) {
|
|
393
|
+
const result = await this.toolsList(cursor, options);
|
|
394
|
+
if (result && Array.isArray(result.tools)) tools.push(...result.tools);
|
|
395
|
+
const next = result && result.nextCursor;
|
|
396
|
+
if (!next) return tools;
|
|
397
|
+
if (seenCursors.has(next)) throw new McpRpcError('DraftGo MCP repeated a tools/list cursor.');
|
|
398
|
+
seenCursors.add(next);
|
|
399
|
+
cursor = next;
|
|
400
|
+
}
|
|
401
|
+
throw new McpRpcError(`DraftGo MCP tools/list exceeded ${maxPages} pages.`);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async toolsCall(name, args = {}, options = {}) {
|
|
405
|
+
const result = await this.request('tools/call', { name, arguments: args }, options);
|
|
406
|
+
if (result && result.isError === true) {
|
|
407
|
+
const structured = result.structuredContent || result.structured_content || {};
|
|
408
|
+
const text = Array.isArray(result.content)
|
|
409
|
+
? result.content.find((part) => part && part.type === 'text' && typeof part.text === 'string')
|
|
410
|
+
: null;
|
|
411
|
+
const message = structured.message || structured.error && structured.error.message
|
|
412
|
+
|| text && text.text || `DraftGo tool failed: ${name}`;
|
|
413
|
+
throw new McpRpcError(redactText(String(message).slice(0, 4096), this.secrets), {
|
|
414
|
+
code: structured.code || structured.error && structured.error.code || -32000,
|
|
415
|
+
data: redactValue(structured, this.secrets),
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
return result;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async testConnection(options = {}) {
|
|
422
|
+
const initialized = await this.initialize(options);
|
|
423
|
+
if (typeof options.onStage === 'function') options.onStage('tools/list', 'started');
|
|
424
|
+
let tools;
|
|
425
|
+
try {
|
|
426
|
+
tools = await this.listAllTools(options);
|
|
427
|
+
} catch (error) {
|
|
428
|
+
if (typeof options.onStage === 'function') options.onStage('tools/list', 'failed');
|
|
429
|
+
throw error;
|
|
430
|
+
}
|
|
431
|
+
if (typeof options.onStage === 'function') options.onStage('tools/list', 'succeeded', { count: tools.length });
|
|
432
|
+
const requiredTools = options.requiredTools || REQUIRED_DRAFTGO_TOOLS;
|
|
433
|
+
const missing = requiredTools.filter((expected) => !tools.some((tool) =>
|
|
434
|
+
tool && typeof tool.name === 'string' && toolMatches(tool.name, expected)));
|
|
435
|
+
if (missing.length) {
|
|
436
|
+
throw new McpRpcError(`DraftGo MCP is missing required tools: ${missing.join(', ')}.`, { code: -32601 });
|
|
437
|
+
}
|
|
438
|
+
const safeTools = options.safeTools || SAFE_TEST_TOOLS;
|
|
439
|
+
const hasDiagnosticOverrides = Boolean(options.requiredTools || options.safeTools);
|
|
440
|
+
const firstSafeTool = tools.find((tool) => tool && typeof tool.name === 'string'
|
|
441
|
+
&& safeTools.some((canonical) => toolMatches(tool.name, canonical)));
|
|
442
|
+
if (!firstSafeTool) {
|
|
443
|
+
throw new McpRpcError(
|
|
444
|
+
`DraftGo MCP exposes no safe diagnostic tool (${safeTools.join(', ')}).`,
|
|
445
|
+
{ code: -32601 },
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const selected = safeTools.map((canonical) => ({
|
|
450
|
+
canonical,
|
|
451
|
+
tool: tools.find((candidate) => candidate && typeof candidate.name === 'string'
|
|
452
|
+
&& toolMatches(candidate.name, canonical)),
|
|
453
|
+
})).filter((entry) => entry.tool);
|
|
454
|
+
const byCanonical = Object.fromEntries(selected.map((entry) => [entry.canonical, entry.tool]));
|
|
455
|
+
const overrideCanonical = safeTools.find((canonical) => toolMatches(firstSafeTool.name, canonical));
|
|
456
456
|
let defaultPlan = hasDiagnosticOverrides ? [{
|
|
457
|
-
label: overrideCanonical,
|
|
458
|
-
canonical: overrideCanonical,
|
|
459
|
-
tool: firstSafeTool,
|
|
460
|
-
args: diagnosticArguments(overrideCanonical),
|
|
461
|
-
}] : [
|
|
462
|
-
{ label: 'project', canonical: 'draftgo_project_overview', tool: byCanonical.draftgo_project_overview, args: {} },
|
|
463
|
-
...['pages', 'navigations', 'docs/articles'].map((resourceType) => ({
|
|
464
|
-
label: `resource:${resourceType}`,
|
|
465
|
-
canonical: 'draftgo_resource_list',
|
|
466
|
-
tool: byCanonical.draftgo_resource_list,
|
|
467
|
-
args: { resource_type: resourceType, limit: 1 },
|
|
468
|
-
})),
|
|
469
|
-
{
|
|
470
|
-
label: 'api:db_meta',
|
|
471
|
-
canonical: 'draftgo_api_search',
|
|
472
|
-
tool: byCanonical.draftgo_api_search,
|
|
473
|
-
args: { resource_type: 'db_meta', limit: 100 },
|
|
457
|
+
label: overrideCanonical,
|
|
458
|
+
canonical: overrideCanonical,
|
|
459
|
+
tool: firstSafeTool,
|
|
460
|
+
args: diagnosticArguments(overrideCanonical),
|
|
461
|
+
}] : [
|
|
462
|
+
{ label: 'project', canonical: 'draftgo_project_overview', tool: byCanonical.draftgo_project_overview, args: {} },
|
|
463
|
+
...['pages', 'navigations', 'docs/articles'].map((resourceType) => ({
|
|
464
|
+
label: `resource:${resourceType}`,
|
|
465
|
+
canonical: 'draftgo_resource_list',
|
|
466
|
+
tool: byCanonical.draftgo_resource_list,
|
|
467
|
+
args: { resource_type: resourceType, limit: 1 },
|
|
468
|
+
})),
|
|
469
|
+
{
|
|
470
|
+
label: 'api:db_meta',
|
|
471
|
+
canonical: 'draftgo_api_search',
|
|
472
|
+
tool: byCanonical.draftgo_api_search,
|
|
473
|
+
args: { resource_type: 'db_meta', limit: 100 },
|
|
474
474
|
},
|
|
475
475
|
];
|
|
476
|
-
const spaceScope = options.scopeType === 'space' && Number(options.spaceId) > 0
|
|
477
|
-
? { scope_type: 'space', space_id: Number(options.spaceId) }
|
|
478
|
-
: {};
|
|
479
|
-
if (!hasDiagnosticOverrides && !spaceScope.scope_type) {
|
|
480
|
-
defaultPlan = defaultPlan.filter(entry => entry.label !== 'resource:docs/articles');
|
|
481
|
-
}
|
|
482
|
-
for (const entry of defaultPlan) {
|
|
483
|
-
if (entry.label === 'resource:docs/articles') entry.args = { ...entry.args, ...spaceScope };
|
|
484
|
-
}
|
|
485
476
|
const runPlan = (plan, startIndex = 0) => allWithAbort(plan.map((entry, offset) =>
|
|
486
|
-
async (queryOptions) => {
|
|
487
|
-
if (typeof options.onStage === 'function') options.onStage(`tools/call:${entry.label}`, 'started');
|
|
488
|
-
let result;
|
|
489
|
-
try {
|
|
490
|
-
result = await this.toolsCall(
|
|
491
|
-
entry.tool.name,
|
|
492
|
-
options.toolArguments && startIndex + offset === 0
|
|
493
|
-
? options.toolArguments
|
|
494
|
-
: entry.args,
|
|
495
|
-
queryOptions,
|
|
496
|
-
);
|
|
497
|
-
} catch (error) {
|
|
498
|
-
if (typeof options.onStage === 'function') options.onStage(`tools/call:${entry.label}`, 'failed');
|
|
499
|
-
throw error;
|
|
500
|
-
}
|
|
501
|
-
if (typeof options.onStage === 'function') options.onStage(`tools/call:${entry.label}`, 'succeeded');
|
|
502
|
-
return ({
|
|
503
|
-
label: entry.label,
|
|
504
|
-
canonical: entry.canonical,
|
|
505
|
-
name: entry.tool.name,
|
|
506
|
-
arguments: options.toolArguments && startIndex + offset === 0
|
|
507
|
-
? options.toolArguments
|
|
508
|
-
: entry.args,
|
|
509
|
-
result,
|
|
510
|
-
});
|
|
511
|
-
}), options);
|
|
512
|
-
const tested = await runPlan(defaultPlan);
|
|
513
|
-
if (!hasDiagnosticOverrides) {
|
|
514
|
-
let discovery = tested.find((entry) => entry.label === 'api:db_meta');
|
|
515
|
-
let operation = discovery && findDBMetaListOperation(discovery.result);
|
|
516
|
-
const seenCursors = new Set();
|
|
517
|
-
const maxPages = Number(options.maxPages || 100);
|
|
518
|
-
let page = 1;
|
|
519
|
-
while (!operation) {
|
|
520
|
-
const cursor = diagnosticNextCursor(discovery && discovery.result);
|
|
521
|
-
if (cursor == null) break;
|
|
522
|
-
if (seenCursors.has(cursor)) {
|
|
523
|
-
throw new McpRpcError('DraftGo api_search repeated a db_meta cursor.');
|
|
524
|
-
}
|
|
525
|
-
seenCursors.add(cursor);
|
|
526
|
-
if (page >= maxPages) {
|
|
527
|
-
throw new McpRpcError(`DraftGo api_search exceeded ${maxPages} db_meta pages.`);
|
|
528
|
-
}
|
|
529
|
-
page += 1;
|
|
530
|
-
[discovery] = await runPlan([{
|
|
531
|
-
label: `api:db_meta:page-${page}`,
|
|
532
|
-
canonical: 'draftgo_api_search',
|
|
533
|
-
tool: byCanonical.draftgo_api_search,
|
|
477
|
+
async (queryOptions) => {
|
|
478
|
+
if (typeof options.onStage === 'function') options.onStage(`tools/call:${entry.label}`, 'started');
|
|
479
|
+
let result;
|
|
480
|
+
try {
|
|
481
|
+
result = await this.toolsCall(
|
|
482
|
+
entry.tool.name,
|
|
483
|
+
options.toolArguments && startIndex + offset === 0
|
|
484
|
+
? options.toolArguments
|
|
485
|
+
: entry.args,
|
|
486
|
+
queryOptions,
|
|
487
|
+
);
|
|
488
|
+
} catch (error) {
|
|
489
|
+
if (typeof options.onStage === 'function') options.onStage(`tools/call:${entry.label}`, 'failed');
|
|
490
|
+
throw error;
|
|
491
|
+
}
|
|
492
|
+
if (typeof options.onStage === 'function') options.onStage(`tools/call:${entry.label}`, 'succeeded');
|
|
493
|
+
return ({
|
|
494
|
+
label: entry.label,
|
|
495
|
+
canonical: entry.canonical,
|
|
496
|
+
name: entry.tool.name,
|
|
497
|
+
arguments: options.toolArguments && startIndex + offset === 0
|
|
498
|
+
? options.toolArguments
|
|
499
|
+
: entry.args,
|
|
500
|
+
result,
|
|
501
|
+
});
|
|
502
|
+
}), options);
|
|
503
|
+
const tested = await runPlan(defaultPlan);
|
|
504
|
+
if (!hasDiagnosticOverrides) {
|
|
505
|
+
let discovery = tested.find((entry) => entry.label === 'api:db_meta');
|
|
506
|
+
let operation = discovery && findDBMetaListOperation(discovery.result);
|
|
507
|
+
const seenCursors = new Set();
|
|
508
|
+
const maxPages = Number(options.maxPages || 100);
|
|
509
|
+
let page = 1;
|
|
510
|
+
while (!operation) {
|
|
511
|
+
const cursor = diagnosticNextCursor(discovery && discovery.result);
|
|
512
|
+
if (cursor == null) break;
|
|
513
|
+
if (seenCursors.has(cursor)) {
|
|
514
|
+
throw new McpRpcError('DraftGo api_search repeated a db_meta cursor.');
|
|
515
|
+
}
|
|
516
|
+
seenCursors.add(cursor);
|
|
517
|
+
if (page >= maxPages) {
|
|
518
|
+
throw new McpRpcError(`DraftGo api_search exceeded ${maxPages} db_meta pages.`);
|
|
519
|
+
}
|
|
520
|
+
page += 1;
|
|
521
|
+
[discovery] = await runPlan([{
|
|
522
|
+
label: `api:db_meta:page-${page}`,
|
|
523
|
+
canonical: 'draftgo_api_search',
|
|
524
|
+
tool: byCanonical.draftgo_api_search,
|
|
534
525
|
args: { resource_type: 'db_meta', limit: 100, cursor },
|
|
535
|
-
}], tested.length);
|
|
536
|
-
tested.push(discovery);
|
|
537
|
-
operation = findDBMetaListOperation(discovery.result);
|
|
538
|
-
}
|
|
539
|
-
if (!operation || !operation.operation_id) {
|
|
540
|
-
throw new McpRpcError('DraftGo MCP exposes no read-only GET /api/db-meta operation.', {
|
|
541
|
-
code: -32601,
|
|
542
|
-
});
|
|
543
|
-
}
|
|
544
|
-
const operationID = String(operation.operation_id);
|
|
545
|
-
tested.push(...await runPlan([
|
|
546
|
-
{
|
|
547
|
-
label: 'api:describe-db_meta',
|
|
548
|
-
canonical: 'draftgo_api_describe',
|
|
549
|
-
tool: byCanonical.draftgo_api_describe,
|
|
550
|
-
args: { operation_id: operationID },
|
|
551
|
-
},
|
|
552
|
-
{
|
|
553
|
-
label: 'api:call-db_meta',
|
|
554
|
-
canonical: 'draftgo_api_call',
|
|
555
|
-
tool: byCanonical.draftgo_api_call,
|
|
526
|
+
}], tested.length);
|
|
527
|
+
tested.push(discovery);
|
|
528
|
+
operation = findDBMetaListOperation(discovery.result);
|
|
529
|
+
}
|
|
530
|
+
if (!operation || !operation.operation_id) {
|
|
531
|
+
throw new McpRpcError('DraftGo MCP exposes no read-only GET /api/db-meta operation.', {
|
|
532
|
+
code: -32601,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
const operationID = String(operation.operation_id);
|
|
536
|
+
tested.push(...await runPlan([
|
|
537
|
+
{
|
|
538
|
+
label: 'api:describe-db_meta',
|
|
539
|
+
canonical: 'draftgo_api_describe',
|
|
540
|
+
tool: byCanonical.draftgo_api_describe,
|
|
541
|
+
args: { operation_id: operationID },
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
label: 'api:call-db_meta',
|
|
545
|
+
canonical: 'draftgo_api_call',
|
|
546
|
+
tool: byCanonical.draftgo_api_call,
|
|
556
547
|
args: { operation_id: operationID, query: { page: 1, page_size: 1 } },
|
|
557
|
-
},
|
|
558
|
-
], tested.length));
|
|
559
|
-
}
|
|
560
|
-
return {
|
|
561
|
-
initialized,
|
|
562
|
-
protocolVersion: this.protocolVersion,
|
|
563
|
-
sessionId: this.sessionId,
|
|
564
|
-
sessionRecoveries: this.sessionRecoveryCount,
|
|
565
|
-
tools,
|
|
566
|
-
testedTool: tested[0].name,
|
|
567
|
-
toolResult: tested[0].result,
|
|
568
|
-
testedTools: [...new Set(tested.map((entry) => entry.name))],
|
|
569
|
-
testedCalls: tested.map((entry) => entry.label),
|
|
570
|
-
diagnosticResults: tested,
|
|
571
|
-
};
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
async function testConnection(config, options = {}) {
|
|
576
|
-
const client = new DraftGoMcpClient(config, options);
|
|
577
|
-
return client.testConnection(options);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
async function callTool(config, name, args = {}, options = {}) {
|
|
581
|
-
const client = options.client || new DraftGoMcpClient(config, options);
|
|
582
|
-
if (!options.initialized) await client.initialize(options);
|
|
583
|
-
return client.toolsCall(name, args, options);
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
module.exports = {
|
|
587
|
-
DraftGoMcpClient,
|
|
588
|
-
McpClient: DraftGoMcpClient,
|
|
589
|
-
McpRpcError,
|
|
590
|
-
McpHttpError,
|
|
591
|
-
SAFE_TEST_TOOLS,
|
|
592
|
-
REQUIRED_DRAFTGO_TOOLS,
|
|
593
|
-
diagnosticArguments,
|
|
594
|
-
diagnosticData,
|
|
595
|
-
diagnosticNextCursor,
|
|
596
|
-
findDBMetaListOperation,
|
|
597
|
-
isSessionInvalidError,
|
|
598
|
-
redactValue,
|
|
599
|
-
testConnection,
|
|
600
|
-
callTool,
|
|
601
|
-
toolMatches,
|
|
602
|
-
};
|
|
548
|
+
},
|
|
549
|
+
], tested.length));
|
|
550
|
+
}
|
|
551
|
+
return {
|
|
552
|
+
initialized,
|
|
553
|
+
protocolVersion: this.protocolVersion,
|
|
554
|
+
sessionId: this.sessionId,
|
|
555
|
+
sessionRecoveries: this.sessionRecoveryCount,
|
|
556
|
+
tools,
|
|
557
|
+
testedTool: tested[0].name,
|
|
558
|
+
toolResult: tested[0].result,
|
|
559
|
+
testedTools: [...new Set(tested.map((entry) => entry.name))],
|
|
560
|
+
testedCalls: tested.map((entry) => entry.label),
|
|
561
|
+
diagnosticResults: tested,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
async function testConnection(config, options = {}) {
|
|
567
|
+
const client = new DraftGoMcpClient(config, options);
|
|
568
|
+
return client.testConnection(options);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
async function callTool(config, name, args = {}, options = {}) {
|
|
572
|
+
const client = options.client || new DraftGoMcpClient(config, options);
|
|
573
|
+
if (!options.initialized) await client.initialize(options);
|
|
574
|
+
return client.toolsCall(name, args, options);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
module.exports = {
|
|
578
|
+
DraftGoMcpClient,
|
|
579
|
+
McpClient: DraftGoMcpClient,
|
|
580
|
+
McpRpcError,
|
|
581
|
+
McpHttpError,
|
|
582
|
+
SAFE_TEST_TOOLS,
|
|
583
|
+
REQUIRED_DRAFTGO_TOOLS,
|
|
584
|
+
diagnosticArguments,
|
|
585
|
+
diagnosticData,
|
|
586
|
+
diagnosticNextCursor,
|
|
587
|
+
findDBMetaListOperation,
|
|
588
|
+
isSessionInvalidError,
|
|
589
|
+
redactValue,
|
|
590
|
+
testConnection,
|
|
591
|
+
callTool,
|
|
592
|
+
toolMatches,
|
|
593
|
+
};
|