draftgo-cli 3.0.41 → 3.0.43

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 CHANGED
@@ -48,6 +48,7 @@ DraftGo Skill 不能被 MCP 替代。根 `SKILL.md` 会在 Skill 触发时自动
48
48
  ```
49
49
 
50
50
  `context` 先验证并逐字提取当前 CLI 随包 Reference 的固定任务章节,再用同一 MCP 会话并行查询项目、资源、API 和 `db_meta` 数据结构;它不做摘要,也不限制后续 MCP 调用。输出中的 `reference_bundle` 会报告 CLI bundle 与项目已安装 Skill 的版本及同步状态,不同步时先运行 `draftgo update`。页面或导航任务使用 `frontend`,数据、自定义服务、AIHub、正文和项目清点分别可用 `data`、`custom-service`、`aihub`、`content`、`project`。`mcp test` 是连接诊断,不是每次资源查询的前置步骤。
51
+ 当前 context JSON 使用 `schema_version: "1.1"`;`live.resources` 和 `live.api` 以带来源的 `pages` 序列保留批量原始响应,`live.data_structures` 保留 db_meta 的 registry、describe 契约及完整分页读取结果。
51
52
 
52
53
  只回答无需实时状态的本地规则问题,或需要 context 路由之外的专项资料时,Agent 继续按根 Skill 表格手工补读。静态资源须先区分:平台内置目录由 `frontend.md` 说明,指定页面依赖经 MCP 定位、checkout 后本地搜索,服务器全量文件不能凭现有 Skill、MCP 摘要或 checkout 声称已枚举。
53
54
 
@@ -69,6 +70,8 @@ draftgo connect codex # 输入 server 和 SAT,验证 MCP,并配置 Co
69
70
  draftgo connect codex --server https://draftgo.example --token "$DRAFTGO_SAT"
70
71
  ```
71
72
 
73
+ `--server` 始终表示 DraftGo 基础地址。直接连接 MCP endpoint 时使用 `--mcp-url https://draftgo.example/mcp`;CLI 会显式保存 `mcp_url`,若未同时传 `--server`,则从标准 `/mcp` 后缀推导基础地址,避免猜测已有 base URL 的语义。
74
+
72
75
  `connect` 会先验证 SAT、MCP `initialize`、`tools/list` 和关键 `tools/call`,成功后写入项目私有配置 `.draftgo/config.json`,并尝试为指定或自动检测到的宿主执行 MCP setup。它不会 pull 页面、导航、文档或任何其他业务资源。
73
76
 
74
77
  远端暂不可用时,只有显式传入 `--allow-offline` 才会保存配置;恢复后运行 `draftgo mcp test`。使用 `--no-mcp-setup` 可跳过宿主配置。
@@ -89,7 +92,7 @@ SAT 只从 `.draftgo/config.json` 读取。宿主 MCP 配置、命令输出和
89
92
  ```bash
90
93
  draftgo mcp setup [target...] # 写入或合并项目级 MCP 配置
91
94
  draftgo mcp status [target...] # 检查配置是否存在且不含敏感连接参数
92
- draftgo mcp test # 验证 initialize、tools/list、关键 tools/call
95
+ draftgo mcp test # 验证 initialize、tools/list、项目/正文/API 三条调用路径
93
96
  draftgo mcp serve # 启动 stdio -> 远端 Streamable HTTP bridge
94
97
  ```
95
98
 
@@ -198,7 +201,7 @@ draftgo changelog add "完成文档管理系统及角色权限"
198
201
  | 命令 | 说明 |
199
202
  |---|---|
200
203
  | `draftgo context --task <task> --output json` | 精确读取任务 Reference 章节,并复用一个 MCP 会话并行查询项目、资源、API 和数据结构;保留原始结果及来源。 |
201
- | `draftgo map` | 通过 MCP 读取远端项目概览和资源列表,再叠加本地 checkout 状态。 |
204
+ | `draftgo map` | 并行读取 pages、navigations、docs/articles 元数据,再叠加本地 checkout 状态;`--type` 可限制一种类型。 |
202
205
  | `draftgo checkout <pages|nav|docs> <id...>` | 下载完整正文和 base,校验 hash 后原子落盘;`--force` 可显式覆盖已修改的本地正文。 |
203
206
  | `draftgo check` | 只检查 checkout manifest、worktree 文件、base/hash、扩展名、HTML 结构和已 checkout 导航引用。 |
204
207
  | `draftgo diff <pages|nav|docs> <id>` | 显示 checkout base 与当前本地正文的差异。 |
@@ -338,6 +341,7 @@ checkout 会通过 MCP 获取元数据和专用下载地址,流式写入临时
338
341
  - `--project <dir>`:操作指定项目目录,默认当前目录。
339
342
  - `--output json`:为 context、map、check、checkout、commit、diff、conflicts、conflict、api 或 delete 输出 JSON。
340
343
  - `--task <task>`:选择 context 路由;支持 frontend、data、custom-service、aihub、content、project。
344
+ - `--type <type>`:限制 `map` 查询一种正文资源类型;支持 pages、nav/navigations、docs/articles 及其别名。
341
345
  - `--strict`:check/deploy/auto-push 将 warnings 视为失败。
342
346
  - `--force`:init 覆盖已有 Skill;checkout 显式覆盖已修改的本地正文。update 始终刷新目标 Skill。
343
347
  - `--purge`:uninstall 时同时删除整个 `.draftgo/` 运行时目录。
@@ -345,6 +349,7 @@ checkout 会通过 MCP 获取元数据和专用下载地址,流式写入临时
345
349
  - `--connect` / `--local-dev` / `--no-setup`:控制 init 安装 Skill 后的基座处理流程。
346
350
  - `--target <name,...>`:为 MCP setup/status 指定一个或多个宿主。
347
351
  - `--server <url>` / `--token <sat>`:非交互 connect;不传时安全提示输入。
352
+ - `--mcp-url <url>`:为 connect 显式指定完整 MCP endpoint;标准 `/mcp` 可自动推导基础地址,自定义路径需同时传 `--server`。
348
353
  - `--allow-offline` / `--no-mcp-setup`:允许 connect 在 MCP 暂不可用时保存,或跳过宿主配置。
349
354
  - `--yes`:跳过支持该选项的交互确认。
350
355
  - `--mobile-check auto|always|never`:控制 `verify-ui` 是否执行。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "draftgo-cli",
3
- "version": "3.0.41",
3
+ "version": "3.0.43",
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"
@@ -2,7 +2,7 @@
2
2
  "schema_version": "1.0",
3
3
  "id": "draftgo",
4
4
  "name": "DraftGo 开发助手",
5
- "version": "3.0.41",
5
+ "version": "3.0.43",
6
6
  "entry": "SKILL.md",
7
7
  "description": "以 context 聚合编排、Skill/reference 原文、MCP 实时发现、长正文 checkout/commit、统一验证和完成日志为边界的 DraftGo 工作流。",
8
8
  "license": "MIT",
@@ -38,10 +38,10 @@ draftgo mcp serve
38
38
  - `context` 是开发任务的一次性准备入口;不保存业务镜像,不传输完整正文。
39
39
  - `setup` 写入项目级宿主配置,只替换 `draftgo` MCP 条目并保留其他配置。
40
40
  - `status` 检查配置是否存在、格式是否有效,以及是否误写了凭据或远端 URL。
41
- - `test` 从 `.draftgo/config.json` 读取连接,验证 initialize、tools/list 和关键 tools/call
41
+ - `test` 从 `.draftgo/config.json` 读取连接,验证 initialize、tools/list、三类正文 resource_list,以及 db_meta 的 api_search、api_describe 和只读 api_call
42
42
  - `serve` 启动 stdio bridge,把宿主请求代理到当前项目配置的远端 Streamable HTTP `/mcp`。
43
43
 
44
- `draftgo connect` 保存并验证 server/SAT、探测 `/mcp` 和关键工具,并可提示宿主 setup;它不下载业务资源,
44
+ `draftgo connect` 保存并验证 server/SAT、探测 `/mcp` 和关键工具,并可提示宿主 setup;`--server` 表示基础地址,完整 MCP endpoint 使用 `--mcp-url` 显式传入。它不下载业务资源,
45
45
  也不创建 pages、navigation、docs、db_meta 等本地镜像。底座仍在开发或暂不可达时,报告诊断结果即可,
46
46
  不得回退到 `pull --all`。
47
47
 
package/src/cli.js CHANGED
@@ -7,6 +7,7 @@ const VALUE_FLAGS = new Set([
7
7
  'project',
8
8
  'target',
9
9
  'server',
10
+ 'mcp-url',
10
11
  'token',
11
12
  'output',
12
13
  'url',
@@ -2,9 +2,10 @@
2
2
 
3
3
  const log = require('../logger');
4
4
  const { ask, askPassword } = require('../prompt');
5
- const { normalizeServer, writeProjectConfig } = require('../projectConfig');
5
+ const { normalizeConnection, normalizeServer, writeProjectConfig } = require('../projectConfig');
6
6
  const { testConnection } = require('../mcp/client');
7
7
  const { redactText } = require('../mcp/protocol');
8
+ const { parseTimeout } = require('../timeout');
8
9
 
9
10
  async function promptServer(defaultValue) {
10
11
  while (true) {
@@ -28,19 +29,32 @@ async function promptToken() {
28
29
 
29
30
  async function connect(projectDir, positional, flags = {}) {
30
31
  log.title('draftgo connect');
31
- const server = flags.server ? normalizeServer(flags.server) : await promptServer('https://');
32
+ const endpointConnection = flags['mcp-url']
33
+ ? normalizeConnection(flags['mcp-url'])
34
+ : null;
35
+ if (endpointConnection && !endpointConnection.server && !flags.server) {
36
+ throw new Error('--server is required when --mcp-url does not use the standard /mcp path.');
37
+ }
38
+ const connection = {
39
+ server: flags.server
40
+ ? normalizeServer(flags.server)
41
+ : endpointConnection && endpointConnection.server || await promptServer('https://'),
42
+ mcp_url: endpointConnection && endpointConnection.mcp_url || '',
43
+ };
44
+ const server = connection.server;
32
45
  const token = flags.token ? String(flags.token).trim() : await promptToken();
33
46
  if (!token) {
34
47
  log.err('A non-empty SAT is required.');
35
48
  return 1;
36
49
  }
50
+ const timeoutMs = parseTimeout(flags.timeout);
37
51
 
38
52
  log.step('Validating SAT and DraftGo MCP capabilities...');
39
53
  try {
40
- const diagnostic = await testConnection({ server, token }, {
41
- timeoutMs: flags.timeout ? Number(flags.timeout) : undefined,
54
+ const diagnostic = await testConnection({ ...connection, server, token }, {
55
+ timeoutMs,
42
56
  });
43
- log.ok(`MCP ready (${diagnostic.tools.length} tools; tested ${diagnostic.testedTool}).`);
57
+ log.ok(`MCP ready (${diagnostic.tools.length} tools; tested ${diagnostic.testedCalls.join(', ')}).`);
44
58
  } catch (error) {
45
59
  const message = redactText(error && error.message ? error.message : error, [token]);
46
60
  if (!flags['allow-offline']) {
@@ -52,7 +66,7 @@ async function connect(projectDir, positional, flags = {}) {
52
66
  log.warn('Continuing only because --allow-offline was explicitly supplied.');
53
67
  }
54
68
 
55
- const configFile = writeProjectConfig(projectDir, server, token);
69
+ const configFile = writeProjectConfig(projectDir, server, token, { mcp_url: connection.mcp_url });
56
70
  log.ok(`Project configuration written: ${configFile}`);
57
71
 
58
72
  if (!flags['no-mcp-setup']) {
@@ -66,6 +80,7 @@ async function connect(projectDir, positional, flags = {}) {
66
80
 
67
81
  log.title('Connected');
68
82
  log.plain(` server: ${server}`);
83
+ if (connection.mcp_url) log.plain(` mcp endpoint: ${connection.mcp_url}`);
69
84
  log.plain(` config: ${configFile}`);
70
85
  log.dim(' No DraftGo business resources were downloaded. Use MCP discovery or checkout long content explicitly.');
71
86
  return 0;
@@ -2,21 +2,7 @@
2
2
 
3
3
  const log = require('../logger');
4
4
  const { collectContext, TASK_PROFILES } = require('../context');
5
-
6
- const MAX_TIMEOUT_MS = 2_147_483_647;
7
-
8
- function parseTimeout(value) {
9
- if (value == null) return undefined;
10
- const text = String(value).trim();
11
- if (!/^\d+$/.test(text)) {
12
- throw new TypeError('--timeout must be a positive integer in milliseconds.');
13
- }
14
- const timeoutMs = Number(text);
15
- if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_TIMEOUT_MS) {
16
- throw new TypeError(`--timeout must be between 1 and ${MAX_TIMEOUT_MS} milliseconds.`);
17
- }
18
- return timeoutMs;
19
- }
5
+ const { parseTimeout } = require('../timeout');
20
6
 
21
7
  function printUsage() {
22
8
  log.err('Usage: draftgo context --task <task> --output json');
@@ -34,7 +34,7 @@ Usage:
34
34
  downloads DraftGo business resources.
35
35
  draftgo mcp setup [<target>...] Merge project-level stdio MCP configuration.
36
36
  draftgo mcp status [<target>...] Check host configuration and secret safety.
37
- draftgo mcp test Test initialize, tools/list, and tools/call.
37
+ draftgo mcp test Test project, resource, and API MCP calls.
38
38
  draftgo mcp serve Bridge local stdio to the remote /mcp endpoint.
39
39
 
40
40
  draftgo context --task <task> Read exact task Reference sections, then reuse
@@ -88,6 +88,8 @@ Important flags:
88
88
  --project <dir> Operate on <dir> instead of the current directory.
89
89
  --target <name,...> Select one or more MCP setup/status targets.
90
90
  --server <url> (connect) DraftGo base URL.
91
+ --mcp-url <url> (connect) Explicit endpoint; standard /mcp derives the
92
+ base URL; custom paths also need --server.
91
93
  --token <sat> (connect) SAT for non-interactive use.
92
94
  --timeout <ms> (connect/mcp test/context) Network timeout.
93
95
  --allow-offline (connect) Save only after explicitly accepting a
@@ -102,6 +104,7 @@ Important flags:
102
104
  --output json Print machine-readable output where supported.
103
105
  --task <task> (context) frontend | data | custom-service | aihub |
104
106
  content | project.
107
+ --type <type> (map) pages | nav/navigations | docs/articles.
105
108
  --strict Treat check warnings as failures.
106
109
  --yes Skip supported confirmation prompts.
107
110
  --operation-id <id> (delete) Select an operation explicitly.
@@ -6,7 +6,16 @@ const log = require('../logger');
6
6
  const { loadProjectConfig } = require('../projectConfig');
7
7
  const { loadManifest, absolutePath } = require('../worktree/manifest');
8
8
  const { hashFile } = require('../worktree/streams');
9
+ const { canonicalResourceType } = require('../worktree/types');
9
10
  const { TOOL_NAMES, openToolSession, callStructured } = require('../mcp/tools');
11
+ const { allWithAbort } = require('../mcp/parallel');
12
+
13
+ const REMOTE_RESOURCE_TYPES = Object.freeze({
14
+ pages: 'pages',
15
+ navigations: 'navigations',
16
+ docs: 'docs/articles',
17
+ });
18
+ const DEFAULT_REMOTE_RESOURCE_TYPES = Object.freeze(Object.values(REMOTE_RESOURCE_TYPES));
10
19
 
11
20
  function itemsFrom(value) {
12
21
  if (Array.isArray(value)) return value;
@@ -18,26 +27,59 @@ function itemsFrom(value) {
18
27
  }
19
28
 
20
29
  function nextCursor(value) {
21
- return value && (value.next_cursor || value.nextCursor || value.cursor && value.has_more && value.cursor);
30
+ if (!value || typeof value !== 'object') return null;
31
+ if (Object.prototype.hasOwnProperty.call(value, 'next_cursor')) return value.next_cursor;
32
+ if (Object.prototype.hasOwnProperty.call(value, 'nextCursor')) return value.nextCursor;
33
+ return (value.has_more === true || value.hasMore === true)
34
+ && Object.prototype.hasOwnProperty.call(value, 'cursor')
35
+ ? value.cursor
36
+ : null;
37
+ }
38
+
39
+ function claimsMorePages(value) {
40
+ return !!value && typeof value === 'object'
41
+ && (value.has_more === true || value.hasMore === true);
42
+ }
43
+
44
+ function normalizeMapResourceType(value) {
45
+ return REMOTE_RESOURCE_TYPES[canonicalResourceType(value)];
22
46
  }
23
47
 
24
- async function listRemoteResources(session, flags) {
48
+ function requestedResourceTypes(flags = {}) {
49
+ return flags.type == null
50
+ ? DEFAULT_REMOTE_RESOURCE_TYPES
51
+ : [normalizeMapResourceType(flags.type)];
52
+ }
53
+
54
+ async function listRemoteResourceType(session, resourceType, options = {}) {
25
55
  const resources = [];
26
56
  const seen = new Set();
27
57
  let cursor = null;
28
58
  for (let page = 0; page < 100; page += 1) {
29
- const args = {};
30
- if (cursor) args.cursor = cursor;
31
- if (flags.type) args.resource_type = String(flags.type);
32
- const payload = await callStructured(session, TOOL_NAMES.resourceList, args);
59
+ const args = { resource_type: resourceType, limit: 100 };
60
+ if (cursor != null && cursor !== '') args.cursor = cursor;
61
+ const payload = await callStructured(session, TOOL_NAMES.resourceList, args, options);
33
62
  resources.push(...itemsFrom(payload));
34
63
  cursor = nextCursor(payload);
35
- if (!cursor) break;
36
- if (seen.has(cursor)) throw new Error('DraftGo resource_list repeated a cursor.');
37
- seen.add(cursor);
64
+ if (cursor == null || cursor === '') {
65
+ if (claimsMorePages(payload)) {
66
+ throw new Error(`DraftGo resource_list (${resourceType}) reported more pages without a cursor.`);
67
+ }
68
+ return resources;
69
+ }
70
+ const cursorKey = String(cursor);
71
+ if (seen.has(cursorKey)) {
72
+ throw new Error(`DraftGo resource_list (${resourceType}) repeated a cursor.`);
73
+ }
74
+ seen.add(cursorKey);
38
75
  }
39
- if (cursor) throw new Error('DraftGo resource_list exceeded 100 pages.');
40
- return resources;
76
+ throw new Error(`DraftGo resource_list (${resourceType}) exceeded 100 pages.`);
77
+ }
78
+
79
+ async function listRemoteResources(session, resourceTypes = DEFAULT_REMOTE_RESOURCE_TYPES, options = {}) {
80
+ const groups = await allWithAbort(resourceTypes.map((resourceType) =>
81
+ (queryOptions) => listRemoteResourceType(session, resourceType, queryOptions)), options);
82
+ return groups.flat();
41
83
  }
42
84
 
43
85
  async function localCheckouts(projectDir) {
@@ -63,13 +105,17 @@ function legacyCaches(projectDir) {
63
105
  }
64
106
 
65
107
  async function mapCommand(projectDir, flags = {}) {
108
+ const resourceTypes = requestedResourceTypes(flags);
66
109
  const config = loadProjectConfig(projectDir);
67
110
  const session = await openToolSession(config, [TOOL_NAMES.projectOverview, TOOL_NAMES.resourceList]);
68
- const [overview, resources, checkouts] = await Promise.all([
69
- callStructured(session, TOOL_NAMES.projectOverview, {}),
70
- listRemoteResources(session, flags),
71
- localCheckouts(projectDir),
111
+ const [remote, checkouts] = await allWithAbort([
112
+ (options) => allWithAbort([
113
+ (options) => callStructured(session, TOOL_NAMES.projectOverview, {}, options),
114
+ (options) => listRemoteResources(session, resourceTypes, options),
115
+ ], options),
116
+ () => localCheckouts(projectDir),
72
117
  ]);
118
+ const [overview, resources] = remote;
73
119
  const result = {
74
120
  server: config.server,
75
121
  overview,
@@ -103,3 +149,6 @@ async function mapCommand(projectDir, flags = {}) {
103
149
 
104
150
  module.exports = mapCommand;
105
151
  module.exports.itemsFrom = itemsFrom;
152
+ module.exports.normalizeMapResourceType = normalizeMapResourceType;
153
+ module.exports.requestedResourceTypes = requestedResourceTypes;
154
+ module.exports.listRemoteResources = listRemoteResources;
@@ -11,6 +11,7 @@ const {
11
11
  statusHosts,
12
12
  } = require('../mcp/hosts');
13
13
  const { serveStdio } = require('../mcp/stdio');
14
+ const { parseTimeout } = require('../timeout');
14
15
 
15
16
  function targetArgs(positional, flags) {
16
17
  const values = positional.slice();
@@ -89,11 +90,11 @@ async function test(projectDir, _positional = [], flags = {}) {
89
90
  try {
90
91
  config = loadProjectConfig(projectDir);
91
92
  const result = await testConnection(config, {
92
- timeoutMs: flags.timeout ? Number(flags.timeout) : undefined,
93
+ timeoutMs: parseTimeout(flags.timeout),
93
94
  });
94
95
  log.ok('DraftGo MCP initialize succeeded.');
95
96
  log.ok(`tools/list returned ${result.tools.length} tools.`);
96
- log.ok(`tools/call succeeded: ${result.testedTool}`);
97
+ log.ok(`tools/call succeeded: ${result.testedCalls.join(', ')}`);
97
98
  log.dim(` protocol: ${result.protocolVersion}`);
98
99
  return 0;
99
100
  } catch (error) {
@@ -11,6 +11,13 @@ const { readInstalledVersion } = require('../skill');
11
11
 
12
12
  const SKILL_ROOT = path.join(RESOURCES_DIR, 'skill');
13
13
  const MAX_STRUCTURED_TEXT_BYTES = 1024 * 1024;
14
+ const LONG_CONTENT_RESOURCE_TYPES = Object.freeze([
15
+ 'pages',
16
+ 'navigations',
17
+ 'docs/articles',
18
+ ]);
19
+ const DB_META_LIST_PATH = '/api/db-meta';
20
+ const DB_META_PAGE_SIZE = 100;
14
21
  const ROOT_SECTIONS = [
15
22
  '核心边界',
16
23
  '强制预读:Reference 优先于 MCP',
@@ -22,7 +29,11 @@ const ROOT_SECTIONS = [
22
29
 
23
30
  const TASK_PROFILES = Object.freeze({
24
31
  frontend: {
25
- apiQuery: 'frontend pages navigation',
32
+ apiQueries: [
33
+ { resource_type: 'pages' },
34
+ { resource_type: 'navigations' },
35
+ { resource_type: 'db' },
36
+ ],
26
37
  references: [
27
38
  { file: 'SKILL.md', headings: ROOT_SECTIONS },
28
39
  { file: 'references/architecture.md' },
@@ -36,7 +47,7 @@ const TASK_PROFILES = Object.freeze({
36
47
  ],
37
48
  },
38
49
  data: {
39
- apiQuery: 'db_meta schema data',
50
+ apiQueries: [{ resource_type: 'db' }],
40
51
  references: [
41
52
  { file: 'SKILL.md', headings: ROOT_SECTIONS },
42
53
  { file: 'references/modules.md' },
@@ -48,7 +59,7 @@ const TASK_PROFILES = Object.freeze({
48
59
  ],
49
60
  },
50
61
  'custom-service': {
51
- apiQuery: 'custom services routes permissions',
62
+ apiQueries: [{ resource_type: 'custom_scripts' }],
52
63
  references: [
53
64
  { file: 'SKILL.md', headings: ROOT_SECTIONS },
54
65
  { file: 'references/modules.md' },
@@ -59,7 +70,10 @@ const TASK_PROFILES = Object.freeze({
59
70
  ],
60
71
  },
61
72
  aihub: {
62
- apiQuery: 'AIHub agents chat',
73
+ apiQueries: [
74
+ { resource_type: 'aihub' },
75
+ { resource_type: 'agents' },
76
+ ],
63
77
  references: [
64
78
  { file: 'SKILL.md', headings: ROOT_SECTIONS },
65
79
  { file: 'references/aihub.md' },
@@ -69,7 +83,11 @@ const TASK_PROFILES = Object.freeze({
69
83
  ],
70
84
  },
71
85
  content: {
72
- apiQuery: 'pages navigations docs',
86
+ apiQueries: [
87
+ { resource_type: 'pages' },
88
+ { resource_type: 'navigations' },
89
+ { resource_type: 'docs/articles' },
90
+ ],
73
91
  references: [
74
92
  { file: 'SKILL.md', headings: ROOT_SECTIONS },
75
93
  { file: 'references/architecture.md' },
@@ -80,7 +98,12 @@ const TASK_PROFILES = Object.freeze({
80
98
  ],
81
99
  },
82
100
  project: {
83
- apiQuery: 'project resources',
101
+ apiQueries: [
102
+ { resource_type: 'pages' },
103
+ { resource_type: 'navigations' },
104
+ { resource_type: 'docs/articles' },
105
+ { resource_type: 'db' },
106
+ ],
84
107
  references: [
85
108
  { file: 'SKILL.md', headings: ROOT_SECTIONS },
86
109
  { file: 'references/architecture.md' },
@@ -204,7 +227,7 @@ function rawQuery(session, expectedName, args, server, options = {}) {
204
227
  }));
205
228
  }
206
229
 
207
- function nextCursorFromRaw(result) {
230
+ function structuredValueFromRaw(result) {
208
231
  let value = result;
209
232
  if (value && value.result) value = value.result;
210
233
  if (value && value.structuredContent) value = value.structuredContent;
@@ -225,12 +248,112 @@ function nextCursorFromRaw(result) {
225
248
  }
226
249
  if (value && value.data) value = value.data;
227
250
  if (value && Object.prototype.hasOwnProperty.call(value, 'value')) value = value.value;
251
+ return value;
252
+ }
253
+
254
+ function nextCursorFromRaw(result) {
255
+ const value = structuredValueFromRaw(result);
228
256
  if (!value || typeof value !== 'object') return null;
229
- if (Object.prototype.hasOwnProperty.call(value, 'next_cursor')) return value.next_cursor;
230
- if (Object.prototype.hasOwnProperty.call(value, 'nextCursor')) return value.nextCursor;
231
- return value.has_more && Object.prototype.hasOwnProperty.call(value, 'cursor')
232
- ? value.cursor
233
- : null;
257
+ const hasMore = value.has_more === true || value.hasMore === true;
258
+ let cursor = null;
259
+ let hasCursor = false;
260
+ if (Object.prototype.hasOwnProperty.call(value, 'next_cursor')) {
261
+ cursor = value.next_cursor;
262
+ hasCursor = true;
263
+ } else if (Object.prototype.hasOwnProperty.call(value, 'nextCursor')) {
264
+ cursor = value.nextCursor;
265
+ hasCursor = true;
266
+ } else if (hasMore && Object.prototype.hasOwnProperty.call(value, 'cursor')) {
267
+ cursor = value.cursor;
268
+ hasCursor = true;
269
+ }
270
+ if (hasMore && (!hasCursor || cursor == null || cursor === '')) {
271
+ throw new Error('DraftGo MCP pagination reported more results without a cursor.');
272
+ }
273
+ return hasCursor ? cursor : null;
274
+ }
275
+
276
+ async function collectLongContentResources(session, server, options = {}) {
277
+ const sequences = await Promise.all(LONG_CONTENT_RESOURCE_TYPES.map((resourceType) =>
278
+ rawPagedQuery(
279
+ session,
280
+ TOOL_NAMES.resourceList,
281
+ { resource_type: resourceType, limit: 100 },
282
+ server,
283
+ options,
284
+ )));
285
+ return {
286
+ source: {
287
+ kind: 'mcp_tool_batch',
288
+ server,
289
+ canonical_tool: TOOL_NAMES.resourceList,
290
+ arguments: LONG_CONTENT_RESOURCE_TYPES.map((resourceType) => ({
291
+ resource_type: resourceType,
292
+ limit: 100,
293
+ })),
294
+ },
295
+ pages: sequences.flatMap((sequence) => sequence.pages),
296
+ };
297
+ }
298
+
299
+ async function collectTaskAPIs(session, queries, server, options = {}) {
300
+ const argumentsList = queries.map((query) => ({ ...query, limit: 100 }));
301
+ const sequences = await Promise.all(argumentsList.map((args) =>
302
+ rawPagedQuery(session, TOOL_NAMES.apiSearch, args, server, options)));
303
+ return {
304
+ source: {
305
+ kind: 'mcp_tool_batch',
306
+ server,
307
+ canonical_tool: TOOL_NAMES.apiSearch,
308
+ arguments: argumentsList,
309
+ },
310
+ pages: sequences.flatMap((sequence) => sequence.pages),
311
+ };
312
+ }
313
+
314
+ function itemsFromRawSequence(sequence) {
315
+ if (!sequence || !Array.isArray(sequence.pages)) return [];
316
+ return sequence.pages.flatMap((page) => {
317
+ const value = structuredValueFromRaw(page);
318
+ if (Array.isArray(value)) return value;
319
+ if (value && Array.isArray(value.items)) return value.items;
320
+ if (value && Array.isArray(value.operations)) return value.operations;
321
+ return [];
322
+ });
323
+ }
324
+
325
+ function selectDBMetaListOperation(sequence) {
326
+ return itemsFromRawSequence(sequence).find((operation) => {
327
+ if (!operation || typeof operation !== 'object') return false;
328
+ const method = String(operation.method || '').trim().toUpperCase();
329
+ const resourceType = String(operation.resource_type || '').trim().toLowerCase();
330
+ const operationPath = String(operation.path || operation.path_template || '').trim();
331
+ return method === 'GET'
332
+ && resourceType === 'db_meta'
333
+ && operationPath === DB_META_LIST_PATH
334
+ && operation.destructive !== true;
335
+ }) || null;
336
+ }
337
+
338
+ function optionalPageInteger(value, field, minimum) {
339
+ if (value == null) return null;
340
+ if (!Number.isSafeInteger(value) || value < minimum) {
341
+ throw new Error(`DraftGo db_meta API returned invalid ${field}.`);
342
+ }
343
+ return value;
344
+ }
345
+
346
+ function dbMetaPageInfo(response) {
347
+ let value = structuredValueFromRaw(response);
348
+ if (value && value.response) value = value.response;
349
+ if (value && value.data) value = value.data;
350
+ if (!value || typeof value !== 'object' || !Array.isArray(value.items)) return null;
351
+ return {
352
+ items: value.items,
353
+ total: optionalPageInteger(value.total, 'total', 0),
354
+ page: optionalPageInteger(value.page, 'page', 1),
355
+ pageSize: optionalPageInteger(value.page_size, 'page_size', 1),
356
+ };
234
357
  }
235
358
 
236
359
  async function rawPagedQuery(session, expectedName, baseArgs, server, options = {}) {
@@ -266,24 +389,102 @@ async function rawPagedQuery(session, expectedName, baseArgs, server, options =
266
389
  throw new Error(expectedName + ' exceeded ' + maxPages + ' pages.');
267
390
  }
268
391
 
392
+ async function collectDBMetaResources(session, operationID, server, options = {}) {
393
+ const pages = [];
394
+ let fetchedItems = 0;
395
+ let reportedTotal = null;
396
+ const maxPages = options.maxPages == null ? 100 : Number(options.maxPages);
397
+ if (!Number.isSafeInteger(maxPages) || maxPages < 1) {
398
+ throw new TypeError('maxPages must be a positive integer.');
399
+ }
400
+ for (let page = 1; page <= maxPages; page += 1) {
401
+ const args = {
402
+ operation_id: operationID,
403
+ query: { page, page_size: DB_META_PAGE_SIZE },
404
+ };
405
+ const response = await rawQuery(session, TOOL_NAMES.apiCall, args, server, options);
406
+ pages.push(response);
407
+ const info = dbMetaPageInfo(response);
408
+ if (!info) {
409
+ return {
410
+ source: {
411
+ kind: 'mcp_tool_sequence',
412
+ server,
413
+ canonical_tool: TOOL_NAMES.apiCall,
414
+ operation_id: operationID,
415
+ },
416
+ pages,
417
+ pagination: { complete: false, reason: 'response_shape_unavailable' },
418
+ };
419
+ }
420
+ const responsePage = info.page || page;
421
+ const responsePageSize = info.pageSize || DB_META_PAGE_SIZE;
422
+ if (responsePage !== page) {
423
+ throw new Error(`DraftGo db_meta API returned page ${responsePage} while page ${page} was requested.`);
424
+ }
425
+ fetchedItems += info.items.length;
426
+ if (info.total != null) {
427
+ if (reportedTotal != null && info.total !== reportedTotal) {
428
+ throw new Error(`DraftGo db_meta API changed total from ${reportedTotal} to ${info.total} during pagination.`);
429
+ }
430
+ reportedTotal = info.total;
431
+ }
432
+ if (reportedTotal != null) {
433
+ if (fetchedItems >= reportedTotal) {
434
+ return {
435
+ source: {
436
+ kind: 'mcp_tool_sequence',
437
+ server,
438
+ canonical_tool: TOOL_NAMES.apiCall,
439
+ operation_id: operationID,
440
+ },
441
+ pages,
442
+ pagination: { complete: true, total: reportedTotal },
443
+ };
444
+ }
445
+ if (!info.items.length) {
446
+ throw new Error('DraftGo db_meta API ended before the reported total was reached.');
447
+ }
448
+ } else if (info.items.length < responsePageSize) {
449
+ return {
450
+ source: {
451
+ kind: 'mcp_tool_sequence',
452
+ server,
453
+ canonical_tool: TOOL_NAMES.apiCall,
454
+ operation_id: operationID,
455
+ },
456
+ pages,
457
+ pagination: { complete: true, total: null },
458
+ };
459
+ }
460
+ }
461
+ throw new Error(`DraftGo db_meta API exceeded ${maxPages} pages.`);
462
+ }
463
+
269
464
  async function collectDataStructures(session, server, options = {}) {
270
- const [resources, api] = await Promise.all([
271
- rawPagedQuery(
272
- session,
273
- TOOL_NAMES.resourceList,
274
- { resource_type: 'db_meta' },
275
- server,
276
- options,
277
- ),
465
+ const api = await rawPagedQuery(
466
+ session,
467
+ TOOL_NAMES.apiSearch,
468
+ { resource_type: 'db_meta', limit: 100 },
469
+ server,
470
+ options,
471
+ );
472
+ const operation = selectDBMetaListOperation(api);
473
+ if (!operation || !operation.operation_id) {
474
+ throw new Error('DraftGo MCP does not expose a read-only GET /api/db-meta operation.');
475
+ }
476
+ const operationID = String(operation.operation_id);
477
+ const [contract, resources] = await Promise.all([
278
478
  rawQuery(
279
479
  session,
280
- TOOL_NAMES.apiSearch,
281
- { query: 'db_meta schema' },
480
+ TOOL_NAMES.apiDescribe,
481
+ { operation_id: operationID },
282
482
  server,
283
483
  options,
284
484
  ),
485
+ collectDBMetaResources(session, operationID, server, options),
285
486
  ]);
286
- return { resources, api };
487
+ return { api, contract, resources };
287
488
  }
288
489
 
289
490
  async function collectContext(projectDir, taskValue, options = {}) {
@@ -292,7 +493,13 @@ async function collectContext(projectDir, taskValue, options = {}) {
292
493
  const references = readTaskReferences(task, options);
293
494
  const installedSkillVersion = readInstalledSkillVersion(projectDir);
294
495
  const config = options.config || loadProjectConfig(projectDir);
295
- const expected = [TOOL_NAMES.projectOverview, TOOL_NAMES.resourceList, TOOL_NAMES.apiSearch];
496
+ const expected = [
497
+ TOOL_NAMES.projectOverview,
498
+ TOOL_NAMES.resourceList,
499
+ TOOL_NAMES.apiSearch,
500
+ TOOL_NAMES.apiDescribe,
501
+ TOOL_NAMES.apiCall,
502
+ ];
296
503
  const openSession = options.openToolSession || openToolSession;
297
504
  const session = options.session || await openSession(config, expected, options);
298
505
  const controller = new AbortController();
@@ -311,14 +518,8 @@ async function collectContext(projectDir, taskValue, options = {}) {
311
518
  const queryOptions = { ...options, signal: controller.signal };
312
519
  [project, resources, api, dataStructures] = await Promise.all([
313
520
  rawQuery(session, TOOL_NAMES.projectOverview, {}, config.server, queryOptions),
314
- rawPagedQuery(session, TOOL_NAMES.resourceList, {}, config.server, queryOptions),
315
- rawQuery(
316
- session,
317
- TOOL_NAMES.apiSearch,
318
- { query: profile.apiQuery },
319
- config.server,
320
- queryOptions,
321
- ),
521
+ collectLongContentResources(session, config.server, queryOptions),
522
+ collectTaskAPIs(session, profile.apiQueries, config.server, queryOptions),
322
523
  collectDataStructures(session, config.server, queryOptions),
323
524
  ]);
324
525
  } catch (error) {
@@ -329,7 +530,7 @@ async function collectContext(projectDir, taskValue, options = {}) {
329
530
  }
330
531
 
331
532
  return {
332
- schema_version: '1.0',
533
+ schema_version: '1.1',
333
534
  task,
334
535
  reference_bundle: {
335
536
  source: 'draftgo-cli',
@@ -360,8 +561,15 @@ module.exports = {
360
561
  normalizeTask,
361
562
  extractSections,
362
563
  readTaskReferences,
564
+ structuredValueFromRaw,
363
565
  nextCursorFromRaw,
364
566
  rawPagedQuery,
567
+ collectLongContentResources,
568
+ collectTaskAPIs,
569
+ itemsFromRawSequence,
570
+ selectDBMetaListOperation,
571
+ dbMetaPageInfo,
572
+ collectDBMetaResources,
365
573
  collectDataStructures,
366
574
  readInstalledSkillVersion,
367
575
  collectContext,
package/src/mcp/client.js CHANGED
@@ -7,11 +7,14 @@ const {
7
7
  postJsonRpc,
8
8
  redactText,
9
9
  } = require('./protocol');
10
+ const { allWithAbort } = require('./parallel');
10
11
 
11
12
  const SAFE_TEST_TOOLS = [
12
13
  'draftgo_project_overview',
13
14
  'draftgo_resource_list',
14
15
  'draftgo_api_search',
16
+ 'draftgo_api_describe',
17
+ 'draftgo_api_call',
15
18
  ];
16
19
 
17
20
  const REQUIRED_DRAFTGO_TOOLS = [
@@ -25,6 +28,33 @@ const REQUIRED_DRAFTGO_TOOLS = [
25
28
  'draftgo_api_call',
26
29
  ];
27
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
+
28
58
  class McpRpcError extends Error {
29
59
  constructor(message, { code = -32000, data, id = null } = {}) {
30
60
  super(message);
@@ -35,6 +65,40 @@ class McpRpcError extends Error {
35
65
  }
36
66
  }
37
67
 
68
+ function diagnosticNextCursor(result) {
69
+ const value = diagnosticData(result);
70
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
71
+ const hasMore = value.has_more === true || value.hasMore === true;
72
+
73
+ let cursor;
74
+ let present = false;
75
+ if (Object.prototype.hasOwnProperty.call(value, 'next_cursor')) {
76
+ cursor = value.next_cursor;
77
+ present = true;
78
+ } else if (Object.prototype.hasOwnProperty.call(value, 'nextCursor')) {
79
+ cursor = value.nextCursor;
80
+ present = true;
81
+ } else if (hasMore) {
82
+ if (!Object.prototype.hasOwnProperty.call(value, 'cursor')) {
83
+ throw new McpRpcError('DraftGo api_search reported more pages without a cursor.');
84
+ }
85
+ cursor = value.cursor;
86
+ present = true;
87
+ }
88
+
89
+ if (!present) return null;
90
+ if (cursor == null || cursor === '') {
91
+ if (hasMore) {
92
+ throw new McpRpcError('DraftGo api_search reported more pages with an empty cursor.');
93
+ }
94
+ return null;
95
+ }
96
+ if (typeof cursor !== 'string' || !cursor.trim()) {
97
+ throw new McpRpcError('DraftGo api_search returned an invalid cursor.');
98
+ }
99
+ return cursor;
100
+ }
101
+
38
102
  function redactValue(value, secrets = [], seen = new WeakMap()) {
39
103
  if (typeof value === 'string') return redactText(value, secrets);
40
104
  if (!value || typeof value !== 'object') return value;
@@ -222,30 +286,118 @@ class DraftGoMcpClient {
222
286
  if (missing.length) {
223
287
  throw new McpRpcError(`DraftGo MCP is missing required tools: ${missing.join(', ')}.`, { code: -32601 });
224
288
  }
225
- const required = options.safeTools || SAFE_TEST_TOOLS;
226
- const selected = tools.find((tool) => tool && typeof tool.name === 'string'
227
- && required.some((candidate) => toolMatches(tool.name, candidate)));
228
- if (!selected) {
289
+ const safeTools = options.safeTools || SAFE_TEST_TOOLS;
290
+ const hasDiagnosticOverrides = Boolean(options.requiredTools || options.safeTools);
291
+ const firstSafeTool = tools.find((tool) => tool && typeof tool.name === 'string'
292
+ && safeTools.some((canonical) => toolMatches(tool.name, canonical)));
293
+ if (!firstSafeTool) {
229
294
  throw new McpRpcError(
230
- `DraftGo MCP exposes no safe diagnostic tool (${required.join(', ')}).`,
295
+ `DraftGo MCP exposes no safe diagnostic tool (${safeTools.join(', ')}).`,
231
296
  { code: -32601 },
232
297
  );
233
298
  }
234
299
 
235
- const canonical = required.find((candidate) => toolMatches(selected.name, candidate));
236
- const defaultArguments = canonical === 'draftgo_api_search' ? { query: 'project' } : {};
237
- const toolResult = await this.toolsCall(
238
- selected.name,
239
- options.toolArguments || defaultArguments,
240
- options,
241
- );
300
+ const selected = safeTools.map((canonical) => ({
301
+ canonical,
302
+ tool: tools.find((candidate) => candidate && typeof candidate.name === 'string'
303
+ && toolMatches(candidate.name, canonical)),
304
+ })).filter((entry) => entry.tool);
305
+ const byCanonical = Object.fromEntries(selected.map((entry) => [entry.canonical, entry.tool]));
306
+ const overrideCanonical = safeTools.find((canonical) => toolMatches(firstSafeTool.name, canonical));
307
+ const defaultPlan = hasDiagnosticOverrides ? [{
308
+ label: overrideCanonical,
309
+ canonical: overrideCanonical,
310
+ tool: firstSafeTool,
311
+ args: diagnosticArguments(overrideCanonical),
312
+ }] : [
313
+ { label: 'project', canonical: 'draftgo_project_overview', tool: byCanonical.draftgo_project_overview, args: {} },
314
+ ...['pages', 'navigations', 'docs/articles'].map((resourceType) => ({
315
+ label: `resource:${resourceType}`,
316
+ canonical: 'draftgo_resource_list',
317
+ tool: byCanonical.draftgo_resource_list,
318
+ args: { resource_type: resourceType, limit: 1 },
319
+ })),
320
+ {
321
+ label: 'api:db_meta',
322
+ canonical: 'draftgo_api_search',
323
+ tool: byCanonical.draftgo_api_search,
324
+ args: { resource_type: 'db_meta', limit: 100 },
325
+ },
326
+ ];
327
+ const runPlan = (plan, startIndex = 0) => allWithAbort(plan.map((entry, offset) =>
328
+ async (queryOptions) => ({
329
+ label: entry.label,
330
+ canonical: entry.canonical,
331
+ name: entry.tool.name,
332
+ arguments: options.toolArguments && startIndex + offset === 0
333
+ ? options.toolArguments
334
+ : entry.args,
335
+ result: await this.toolsCall(
336
+ entry.tool.name,
337
+ options.toolArguments && startIndex + offset === 0
338
+ ? options.toolArguments
339
+ : entry.args,
340
+ queryOptions,
341
+ ),
342
+ })), options);
343
+ const tested = await runPlan(defaultPlan);
344
+ if (!hasDiagnosticOverrides) {
345
+ let discovery = tested.find((entry) => entry.label === 'api:db_meta');
346
+ let operation = discovery && findDBMetaListOperation(discovery.result);
347
+ const seenCursors = new Set();
348
+ const maxPages = Number(options.maxPages || 100);
349
+ let page = 1;
350
+ while (!operation) {
351
+ const cursor = diagnosticNextCursor(discovery && discovery.result);
352
+ if (cursor == null) break;
353
+ if (seenCursors.has(cursor)) {
354
+ throw new McpRpcError('DraftGo api_search repeated a db_meta cursor.');
355
+ }
356
+ seenCursors.add(cursor);
357
+ if (page >= maxPages) {
358
+ throw new McpRpcError(`DraftGo api_search exceeded ${maxPages} db_meta pages.`);
359
+ }
360
+ page += 1;
361
+ [discovery] = await runPlan([{
362
+ label: `api:db_meta:page-${page}`,
363
+ canonical: 'draftgo_api_search',
364
+ tool: byCanonical.draftgo_api_search,
365
+ args: { resource_type: 'db_meta', limit: 100, cursor },
366
+ }], tested.length);
367
+ tested.push(discovery);
368
+ operation = findDBMetaListOperation(discovery.result);
369
+ }
370
+ if (!operation || !operation.operation_id) {
371
+ throw new McpRpcError('DraftGo MCP exposes no read-only GET /api/db-meta operation.', {
372
+ code: -32601,
373
+ });
374
+ }
375
+ const operationID = String(operation.operation_id);
376
+ tested.push(...await runPlan([
377
+ {
378
+ label: 'api:describe-db_meta',
379
+ canonical: 'draftgo_api_describe',
380
+ tool: byCanonical.draftgo_api_describe,
381
+ args: { operation_id: operationID },
382
+ },
383
+ {
384
+ label: 'api:call-db_meta',
385
+ canonical: 'draftgo_api_call',
386
+ tool: byCanonical.draftgo_api_call,
387
+ args: { operation_id: operationID, query: { page: 1, page_size: 1 } },
388
+ },
389
+ ], tested.length));
390
+ }
242
391
  return {
243
392
  initialized,
244
393
  protocolVersion: this.protocolVersion,
245
394
  sessionId: this.sessionId,
246
395
  tools,
247
- testedTool: selected.name,
248
- toolResult,
396
+ testedTool: tested[0].name,
397
+ toolResult: tested[0].result,
398
+ testedTools: [...new Set(tested.map((entry) => entry.name))],
399
+ testedCalls: tested.map((entry) => entry.label),
400
+ diagnosticResults: tested,
249
401
  };
250
402
  }
251
403
  }
@@ -268,6 +420,10 @@ module.exports = {
268
420
  McpHttpError,
269
421
  SAFE_TEST_TOOLS,
270
422
  REQUIRED_DRAFTGO_TOOLS,
423
+ diagnosticArguments,
424
+ diagnosticData,
425
+ diagnosticNextCursor,
426
+ findDBMetaListOperation,
271
427
  redactValue,
272
428
  testConnection,
273
429
  callTool,
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+
3
+ async function allWithAbort(taskFactories, options = {}) {
4
+ if (!Array.isArray(taskFactories) || taskFactories.some((task) => typeof task !== 'function')) {
5
+ throw new TypeError('Parallel MCP tasks must be functions.');
6
+ }
7
+ const externalSignal = options.signal;
8
+ if (externalSignal && externalSignal.aborted) {
9
+ if (typeof externalSignal.throwIfAborted === 'function') externalSignal.throwIfAborted();
10
+ const error = new Error('Parallel MCP tasks were aborted before they started.');
11
+ error.name = 'AbortError';
12
+ throw error;
13
+ }
14
+
15
+ const controller = new AbortController();
16
+ const forwardAbort = () => controller.abort(externalSignal.reason);
17
+ if (externalSignal) {
18
+ externalSignal.addEventListener('abort', forwardAbort, { once: true });
19
+ }
20
+ try {
21
+ const taskOptions = { ...options, signal: controller.signal };
22
+ return await Promise.all(taskFactories.map((task) => task(taskOptions)));
23
+ } catch (error) {
24
+ if (!controller.signal.aborted) controller.abort(error);
25
+ throw error;
26
+ } finally {
27
+ if (externalSignal) externalSignal.removeEventListener('abort', forwardAbort);
28
+ }
29
+ }
30
+
31
+ module.exports = { allWithAbort };
@@ -22,9 +22,10 @@ function redactText(value, secrets = []) {
22
22
 
23
23
  function endpointFor(config) {
24
24
  const server = new URL(String(config.server || config.mcp_url));
25
+ const serverPath = server.pathname.replace(/\/+$/, '');
25
26
  const endpoint = config.mcp_url
26
27
  ? new URL(String(config.mcp_url), server)
27
- : new URL(`${server.pathname.replace(/\/+$/, '')}/mcp`, server);
28
+ : new URL(`${serverPath}/mcp`, server);
28
29
  if (!['http:', 'https:'].includes(endpoint.protocol) || endpoint.username || endpoint.password) {
29
30
  throw new McpHttpError('DraftGo MCP endpoint must be a credential-free HTTP(S) URL.');
30
31
  }
@@ -11,16 +11,42 @@ const IGNORE_ENTRIES = [
11
11
  '.draftgo/conflicts/',
12
12
  ];
13
13
 
14
- function normalizeServer(raw) {
14
+ function parseHttpUrl(raw) {
15
15
  let value = String(raw || '').trim();
16
- if (!value) return '';
16
+ if (!value) return null;
17
17
  if (!/^https?:\/\//i.test(value)) value = `http://${value}`;
18
18
  const parsed = new URL(value);
19
19
  if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password
20
20
  || parsed.search || parsed.hash) {
21
21
  throw new Error('DraftGo server must be an absolute HTTP(S) URL without credentials, query, or fragment.');
22
22
  }
23
- return value.replace(/\/+$/, '');
23
+ return parsed;
24
+ }
25
+
26
+ function normalizeServer(raw) {
27
+ const parsed = parseHttpUrl(raw);
28
+ if (!parsed) return '';
29
+ return parsed.toString().replace(/\/+$/, '');
30
+ }
31
+
32
+ function normalizeMcpEndpoint(raw) {
33
+ const parsed = parseHttpUrl(raw);
34
+ return parsed ? parsed.toString() : '';
35
+ }
36
+
37
+ function normalizeConnection(raw) {
38
+ const value = normalizeMcpEndpoint(raw);
39
+ if (!value) return { server: '', mcp_url: '' };
40
+ const parsed = new URL(value);
41
+ const pathname = parsed.pathname.replace(/\/+$/, '');
42
+ if (!pathname.toLowerCase().endsWith('/mcp')) {
43
+ return { server: '', mcp_url: value };
44
+ }
45
+ parsed.pathname = pathname.slice(0, -4) || '/';
46
+ return {
47
+ server: parsed.toString().replace(/\/+$/, ''),
48
+ mcp_url: value,
49
+ };
24
50
  }
25
51
 
26
52
  function configPath(projectDir) {
@@ -65,7 +91,7 @@ function writePrivateJson(file, value) {
65
91
  try { fs.chmodSync(file, 0o600); } catch { /* Windows ACLs are inherited. */ }
66
92
  }
67
93
 
68
- function writeProjectConfig(projectDir, server, token) {
94
+ function writeProjectConfig(projectDir, server, token, options = {}) {
69
95
  const normalizedServer = normalizeServer(server);
70
96
  const sat = String(token || '').trim();
71
97
  if (!normalizedServer) throw new Error('DraftGo server is required.');
@@ -77,6 +103,8 @@ function writeProjectConfig(projectDir, server, token) {
77
103
  token: sat,
78
104
  auto_push: existing.auto_push === true,
79
105
  };
106
+ if (options.mcp_url) config.mcp_url = normalizeMcpEndpoint(options.mcp_url);
107
+ else delete config.mcp_url;
80
108
  const file = configPath(projectDir);
81
109
  writePrivateJson(file, config);
82
110
  ensureProjectIgnores(projectDir);
@@ -88,16 +116,22 @@ function loadProjectConfig(projectDir, { requireToken = true } = {}) {
88
116
  if (!exists(file)) throw new Error('Missing .draftgo/config.json; run `draftgo connect` first.');
89
117
  const config = readExisting(projectDir);
90
118
  const server = normalizeServer(config.server);
119
+ const mcpUrl = config.mcp_url ? normalizeMcpEndpoint(config.mcp_url) : '';
91
120
  const token = String(config.token || config.sat || '').trim();
92
121
  if (!server) throw new Error('DraftGo project config is missing server.');
93
122
  if (requireToken && !token) throw new Error('DraftGo project config is missing SAT.');
94
- return { ...config, server, token, path: file };
123
+ const result = { ...config, server, token, path: file };
124
+ if (mcpUrl) result.mcp_url = mcpUrl;
125
+ else delete result.mcp_url;
126
+ return result;
95
127
  }
96
128
 
97
129
  module.exports = {
98
130
  IGNORE_ENTRIES,
99
131
  configPath,
100
132
  normalizeServer,
133
+ normalizeMcpEndpoint,
134
+ normalizeConnection,
101
135
  ensureProjectIgnores,
102
136
  writeProjectConfig,
103
137
  loadProjectConfig,
package/src/timeout.js ADDED
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+
3
+ const MAX_TIMEOUT_MS = 2_147_483_647;
4
+
5
+ function parseTimeout(value) {
6
+ if (value == null) return undefined;
7
+ const text = String(value).trim();
8
+ if (!/^\d+$/.test(text)) {
9
+ throw new TypeError('--timeout must be a positive integer in milliseconds.');
10
+ }
11
+ const timeoutMs = Number(text);
12
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_TIMEOUT_MS) {
13
+ throw new TypeError(`--timeout must be between 1 and ${MAX_TIMEOUT_MS} milliseconds.`);
14
+ }
15
+ return timeoutMs;
16
+ }
17
+
18
+ module.exports = { MAX_TIMEOUT_MS, parseTimeout };