@pecot/tools-mcp 1.0.1 → 1.1.0

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.
Files changed (39) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/cli/dist/cli.d.ts +1 -2
  3. package/cli/dist/cli.js +9 -13
  4. package/cli/dist/diagnostics.d.ts +38 -0
  5. package/cli/dist/diagnostics.js +46 -0
  6. package/cli/dist/domain/arguments.d.ts +2 -15
  7. package/cli/dist/domain/arguments.js +38 -99
  8. package/cli/dist/domain/athena-query.d.ts +2 -8
  9. package/cli/dist/domain/athena-query.js +10 -35
  10. package/cli/dist/domain/s3-input.d.ts +2 -9
  11. package/cli/dist/domain/s3-input.js +8 -21
  12. package/cli/dist/infra/awscli.d.ts +8 -36
  13. package/cli/dist/infra/awscli.js +15 -55
  14. package/cli/dist/infra/sso/login-registry.d.ts +4 -21
  15. package/cli/dist/infra/sso/login-registry.js +13 -56
  16. package/cli/dist/mcp/serve.d.ts +6 -28
  17. package/cli/dist/mcp/serve.js +20 -64
  18. package/cli/dist/mcp/server.d.ts +4 -20
  19. package/cli/dist/mcp/server.js +9 -35
  20. package/cli/dist/mcp/tools/_shared.d.ts +6 -27
  21. package/cli/dist/mcp/tools/_shared.js +36 -65
  22. package/cli/dist/mcp/tools/athena.js +17 -27
  23. package/cli/dist/mcp/tools/cloudwatch.js +13 -15
  24. package/cli/dist/mcp/tools/ec2.js +7 -10
  25. package/cli/dist/mcp/tools/ecr.js +10 -16
  26. package/cli/dist/mcp/tools/index.js +1 -5
  27. package/cli/dist/mcp/tools/lambda.js +10 -10
  28. package/cli/dist/mcp/tools/logs.js +22 -22
  29. package/cli/dist/mcp/tools/rds.js +15 -18
  30. package/cli/dist/mcp/tools/s3.js +19 -29
  31. package/cli/dist/mcp/tools/scheduler.js +10 -13
  32. package/cli/dist/mcp/tools/sqs.js +8 -8
  33. package/cli/dist/mcp/tools/sso.d.ts +1 -5
  34. package/cli/dist/mcp/tools/sso.js +33 -59
  35. package/cli/dist/mcp/tools/sts.js +2 -2
  36. package/cli/dist/node-runtime.d.ts +6 -1
  37. package/cli/dist/node-runtime.js +16 -8
  38. package/npm-shrinkwrap.json +3 -3
  39. package/package.json +2 -2
@@ -1,11 +1,5 @@
1
- // S3 の入力値(オブジェクト URI・一覧 URI・ローカル出力先)の書式検証。
2
- //
3
- // research R7 の方向制約は「入力の形で表現できない」ことで担保しているが、値そのものが
4
- // 期待どおりの形かはここで見る。書式は contracts/mcp-tools.md の正規表現を逐語で写す。
5
- //
6
- // 戻り値は「入力名に続く述部」で、入力名と値の連結は arguments.ts が行う(`<name> <述部>: <value>`)。
7
- // 述部に助詞まで含めるのは、`s3://…` のような欧文始まりの語で助詞との間に空白が要るかどうかが
8
- // 語ごとに変わるため。文言の組み立てを 1 箇所に寄せると、規則を足したときに書式が割れない。
1
+ // S3 の入力値(オブジェクト URI・一覧 URI・ローカル出力先)の書式検証。書式は contracts/mcp-tools.md の正規表現を写す。
2
+ // 戻り値は入力名に続く述部で、助詞の前の空白の要否を 1 箇所に寄せるため、連結は arguments.ts が行う。
9
3
  import path from 'node:path';
10
4
  /**
11
5
  `aws_s3_cp` の取得元。バケットとキーの双方を要求する(契約表 aws_s3_cp の s3-uri)。
@@ -21,30 +15,23 @@ const S3_LIST_URI = /^s3:\/\/[^/]*(?:\/.*)?$/;
21
15
  */
22
16
  const S3_SCHEME = /^s3:\/\//i;
23
17
  /**
24
- `s3://<bucket>/<key>` 形式か。
25
-
26
- 空白を含む値は拒否しない。引数は shell を介さず、1 要素 1 値で awscli へ渡る。
27
- そのため `s3://b/k --recursive` のような値は「そういう名前のキー」として扱われ、
28
- 別オプションには化けない(research R3)。
18
+ `s3://<bucket>/<key>` 形式か。空白を含む値は、shell を介さず 1 値で渡るので拒否しない(research R3)。
29
19
  */
30
20
  export function validateS3Uri(value) {
31
- return S3_OBJECT_URI.test(value) ? undefined : 'は s3://<bucket>/<key> の形式で指定してください';
21
+ return S3_OBJECT_URI.test(value) ? undefined : 'must be in the form s3://<bucket>/<key>';
32
22
  }
33
23
  /**
34
24
  `s3 ls` の位置引数として妥当か(バケット一覧・prefix 指定の双方を許す)。
35
25
  */
36
26
  export function validateS3ListUri(value) {
37
- return S3_LIST_URI.test(value) ? undefined : 'は s3://<bucket>[/<prefix>] の形式で指定してください';
27
+ return S3_LIST_URI.test(value) ? undefined : 'must be in the form s3://<bucket>[/<prefix>]';
38
28
  }
39
29
  /**
40
- 書き出し先のローカルパスとして妥当か。
41
-
42
- `s3://…` を先に見るのは、絶対パスの検証だけでも弾けるが「相対パスだ」という案内になり、
43
- 取得元と書き出し先を取り違えた利用者に誤った直し方を促すため。
30
+ 書き出し先のローカルパスとして妥当か。`s3://…` を先に弾くのは、取得元と取り違えた利用者に誤案内しないため。
44
31
  */
45
32
  export function validateLocalPath(value) {
46
33
  if (S3_SCHEME.test(value))
47
- return 'はローカルの絶対パスで指定してください(s3:// は書き出し先にできません)';
48
- return path.isAbsolute(value) ? undefined : 'は絶対パスで指定してください';
34
+ return 'must be a local absolute path (s3:// cannot be a destination)';
35
+ return path.isAbsolute(value) ? undefined : 'must be an absolute path';
49
36
  }
50
37
  //# sourceMappingURL=s3-input.js.map
@@ -1,30 +1,17 @@
1
1
  /**
2
- 起動するコマンド。PATH 解決に委ねる(絶対パス指定はしない)。
3
-
4
- SSO のログインレジストリ(infra/sso/login-registry.ts)も同じコマンドで spawn するため
5
- export する。片方だけ絶対パス化するような差が入らないようにしておく。
2
+ 起動するコマンド。PATH 解決に委ねる。SSO のログインレジストリも同じコマンドで spawn するので export する。
6
3
  */
7
4
  export declare const AWSCLI_COMMAND = "aws";
8
5
  /**
9
- awscli 出力の上限(バイト、research R8)。
10
-
11
- 応答へ載せる際の切り詰め(mcp/result.ts の limitOutput)と、本モジュールの捕捉上限が
12
- 同じ値を基準にする。依存方向が infra → mcp を許さないため、定義は infra に置く。
6
+ awscli 出力の上限(バイト、research R8)。依存方向が infra → mcp を許さないため、定義は infra に置く。
13
7
  */
14
8
  export declare const OUTPUT_LIMIT_BYTES: number;
15
9
  /**
16
- 子プロセスの stdout / stderr を捕捉する上限(バイト)。超過分は捨てる。
17
-
18
- research R8 の `filter-log-events` は既定 10,000 件で数 MiB になり得る。全件を溜めると
19
- 同時実行数(4)と掛かってサーバの常駐メモリに直結するため、応答上限の 4 倍で打ち切る
20
- (切り詰め後の応答に必要な分は必ず残る)。
10
+ 子プロセスの stdout / stderr を捕捉する上限(バイト)。超過分は捨てる。常駐メモリを抑えるため、応答上限の 4 倍で打ち切る。
21
11
  */
22
12
  export declare const CAPTURE_LIMIT_BYTES: number;
23
13
  /**
24
- 生存中の awscli 子プロセス全てへ SIGTERM を送る。
25
-
26
- 終了処理(mcp/serve.ts の onShutdown)から呼ぶ。既に終了した子への kill は no-op で、
27
- finalize 側が Set から外すため二重送出にはならない。
14
+ 生存中の awscli 子プロセス全てへ SIGTERM を送る。終了済みの子への kill は no-op なので二重送出にならない。
28
15
  */
29
16
  export declare function killRunningAwscli(): void;
30
17
  /**
@@ -44,10 +31,7 @@ export interface SpawnResult {
44
31
  export interface RunAwscliOptions {
45
32
  readonly timeoutMs: number;
46
33
  /**
47
- 呼び出し元がキャンセルされたら子プロセスへ SIGTERM を送る(任意)。
48
-
49
- MCP の tools/call は ToolCallContext.signal でキャンセルされる。配線しないと、クライアントが
50
- 諦めたあとも awscli が孤児として動き続ける。
34
+ 呼び出し元がキャンセルされたら子プロセスへ SIGTERM を送る。配線しないと awscli が孤児として動き続ける。
51
35
  */
52
36
  readonly signal?: AbortSignal | undefined;
53
37
  }
@@ -63,18 +47,11 @@ export interface OutputCapture {
63
47
  text(): string;
64
48
  }
65
49
  /**
66
- CAPTURE_LIMIT_BYTES まで溜め、超過分は捨てる capture を作る。
67
-
68
- SSO のログインレジストリ(infra/sso/login-registry.ts)も同じ上限で子プロセスの出力を
69
- 溜めるため export する。上限が呼び出し元ごとに割れると、常駐メモリの見積もりが立たなくなる。
50
+ CAPTURE_LIMIT_BYTES まで溜め、超過分は捨てる capture を作る。SSO のログインレジストリも同じ上限を使うので export する。
70
51
  */
71
52
  export declare function createCapture(): OutputCapture;
72
53
  /**
73
- `aws <args>` を起動し、CAPTURE_LIMIT_BYTES まで capture した stdout / stderr を返す。
74
-
75
- timeout 到達・abort では SIGTERM を送り、`timedOut` / signal を結果へ載せる。
76
- 起動 error は reject せず `exitCode: null` + `spawnErrorCode` に詰める。
77
- そのため呼び出し側は、失敗種別の判定を 1 か所(classifyFailure)に集約できる。
54
+ `aws <args>` を起動し、capture した stdout / stderr を返す。起動 error は reject せず spawnErrorCode に詰める。
78
55
  */
79
56
  export declare function runAwscli(arguments_: readonly string[], options: Readonly<RunAwscliOptions>): Promise<SpawnResult>;
80
57
  /**
@@ -89,12 +66,7 @@ export type AwscliProbe = {
89
66
  readonly message: string;
90
67
  };
91
68
  /**
92
- awscli の存在と版を確認し、成功結果だけをプロセス内に記憶する。
93
-
94
- 毎回 `aws --version` を起こすとツール 1 件あたりの子プロセスが 2 本になるため、
95
- Promise 自体を memo する(並行呼び出しでも実行は 1 回)。
96
- ただし失敗(未導入・版不足)を memo すると、利用者が awscli を導入してもサーバ再起動まで全ツールが awscli-missing を返し続ける。
97
- そのため失敗時は memo を捨てて、次の呼び出しで再判定する。
69
+ awscli の存在と版を確認し、成功結果だけを memo する。失敗を memo すると導入後も再起動まで awscli-missing が続くため捨てる。
98
70
  */
99
71
  export declare function ensureAwscli(): Promise<AwscliProbe>;
100
72
  /**
@@ -1,14 +1,7 @@
1
- // awscli の子プロセス実行。
2
- //
3
- // shell:false で起動するため、引数配列の要素はそのまま 1 値として awscli に渡る
4
- // (連結や特殊文字による別コマンド委譲が構造的に起きない)。timeout / AbortSignal は
5
- // SIGTERM で子を落とし、起動失敗(ENOENT 等)は reject せず spawnErrorCode に詰める。
1
+ // awscli の子プロセス実行。shell:false で起動し、起動失敗は reject せず spawnErrorCode に詰める。
6
2
  import { spawn } from 'node:child_process';
7
3
  /**
8
- 起動するコマンド。PATH 解決に委ねる(絶対パス指定はしない)。
9
-
10
- SSO のログインレジストリ(infra/sso/login-registry.ts)も同じコマンドで spawn するため
11
- export する。片方だけ絶対パス化するような差が入らないようにしておく。
4
+ 起動するコマンド。PATH 解決に委ねる。SSO のログインレジストリも同じコマンドで spawn するので export する。
12
5
  */
13
6
  export const AWSCLI_COMMAND = 'aws';
14
7
  /**
@@ -25,34 +18,19 @@ message に載せる最低版の表記。
25
18
  const MINIMUM_VERSION_TEXT = MINIMUM_VERSION.join('.');
26
19
  const VERSION_PATTERN = /aws-cli\/(\d+)\.(\d+)\.(\d+)/;
27
20
  /**
28
- awscli 出力の上限(バイト、research R8)。
29
-
30
- 応答へ載せる際の切り詰め(mcp/result.ts の limitOutput)と、本モジュールの捕捉上限が
31
- 同じ値を基準にする。依存方向が infra → mcp を許さないため、定義は infra に置く。
21
+ awscli 出力の上限(バイト、research R8)。依存方向が infra → mcp を許さないため、定義は infra に置く。
32
22
  */
33
23
  export const OUTPUT_LIMIT_BYTES = 256 * 1024;
34
24
  /**
35
- 子プロセスの stdout / stderr を捕捉する上限(バイト)。超過分は捨てる。
36
-
37
- research R8 の `filter-log-events` は既定 10,000 件で数 MiB になり得る。全件を溜めると
38
- 同時実行数(4)と掛かってサーバの常駐メモリに直結するため、応答上限の 4 倍で打ち切る
39
- (切り詰め後の応答に必要な分は必ず残る)。
25
+ 子プロセスの stdout / stderr を捕捉する上限(バイト)。超過分は捨てる。常駐メモリを抑えるため、応答上限の 4 倍で打ち切る。
40
26
  */
41
27
  export const CAPTURE_LIMIT_BYTES = OUTPUT_LIMIT_BYTES * 4;
42
28
  /**
43
- 生存中の awscli 子プロセス。
44
-
45
- runAwscli が spawn 直後に足し、finalize(close / error のどちらか先)で外す。
46
- サーバが SIGTERM で畳まれるときは親が先に消える。
47
- そのため、ここを辿って子へも SIGTERM を送らないと、子が PPID 1 へ再親付けされて動き続ける。
48
- timeout を張っていた親が居なくなるので、打ち切りも働かない。
29
+ 生存中の awscli 子プロセス。サーバが SIGTERM で畳まれるとき、ここを辿って子へも送らないと PPID 1 で動き続ける。
49
30
  */
50
31
  const runningChildren = new Set();
51
32
  /**
52
- 生存中の awscli 子プロセス全てへ SIGTERM を送る。
53
-
54
- 終了処理(mcp/serve.ts の onShutdown)から呼ぶ。既に終了した子への kill は no-op で、
55
- finalize 側が Set から外すため二重送出にはならない。
33
+ 生存中の awscli 子プロセス全てへ SIGTERM を送る。終了済みの子への kill は no-op なので二重送出にならない。
56
34
  */
57
35
  export function killRunningAwscli() {
58
36
  for (const child of runningChildren)
@@ -72,10 +50,7 @@ export function mergeStderr(captured, extra) {
72
50
  return `${captured}\n${extra}`;
73
51
  }
74
52
  /**
75
- CAPTURE_LIMIT_BYTES まで溜め、超過分は捨てる capture を作る。
76
-
77
- SSO のログインレジストリ(infra/sso/login-registry.ts)も同じ上限で子プロセスの出力を
78
- 溜めるため export する。上限が呼び出し元ごとに割れると、常駐メモリの見積もりが立たなくなる。
53
+ CAPTURE_LIMIT_BYTES まで溜め、超過分は捨てる capture を作る。SSO のログインレジストリも同じ上限を使うので export する。
79
54
  */
80
55
  export function createCapture() {
81
56
  const chunks = [];
@@ -95,11 +70,7 @@ export function createCapture() {
95
70
  };
96
71
  }
97
72
  /**
98
- `aws <args>` を起動し、CAPTURE_LIMIT_BYTES まで capture した stdout / stderr を返す。
99
-
100
- timeout 到達・abort では SIGTERM を送り、`timedOut` / signal を結果へ載せる。
101
- 起動 error は reject せず `exitCode: null` + `spawnErrorCode` に詰める。
102
- そのため呼び出し側は、失敗種別の判定を 1 か所(classifyFailure)に集約できる。
73
+ `aws <args>` を起動し、capture した stdout / stderr を返す。起動 error は reject せず spawnErrorCode に詰める。
103
74
  */
104
75
  export async function runAwscli(arguments_,
105
76
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- AbortSignal は内部状態を持つ
@@ -140,11 +111,8 @@ options) {
140
111
  runningChildren.delete(child);
141
112
  clearTimeout(timer);
142
113
  options.signal?.removeEventListener('abort', onAbort);
143
- // ENOENT 等で 'error' の直後に 'close' も発火する Node の仕様に対し、二重 finalize は
144
- // settled で抑止している。稀に child が生存したまま残る場合への防御で kill しておく。
145
- // 通常の close 後もここを通る(Node 24 の確認で child.killed は false のまま)。
146
- // ただし _handle が既に null で kill() は no-op のため無害。
147
- // catch は防御網で、通常は到達しない。
114
+ // 'error' の直後に 'close' も発火するため、二重 finalize は settled で抑止する。
115
+ // child が生存したまま残る稀な場合に備えて kill する。_handle が null なら no-op で無害。
148
116
  /* c8 ignore start -- catch 節(既に終了済みの kill が throw する場合)は観測されない防御網 */
149
117
  if (!child.killed) {
150
118
  try {
@@ -178,10 +146,7 @@ options) {
178
146
  */
179
147
  const probeCache = { pending: undefined };
180
148
  /**
181
- 検出した版が最低版以上か。
182
-
183
- major / minor / patch を 1 つの数へ畳む比較は minor / patch が 1000 以上で桁が衝突するため、
184
- 上位から順に見るタプル比較にする。
149
+ 検出した版が最低版以上か。minor / patch が 1000 以上で桁が衝突しないよう、上位から順に比べる。
185
150
  */
186
151
  function isAtLeastMinimum(major, minor, patch) {
187
152
  const [minimumMajor, minimumMinor, minimumPatch] = MINIMUM_VERSION;
@@ -197,7 +162,7 @@ async function probeAwscli() {
197
162
  return {
198
163
  ok: false,
199
164
  category: 'awscli-missing',
200
- message: `awscli が見つかりません。awscli v2 (${MINIMUM_VERSION_TEXT} 以上) を導入して PATH を通してください`,
165
+ message: `awscli not found. Install awscli v2 (${MINIMUM_VERSION_TEXT} or later) and add it to PATH`,
201
166
  };
202
167
  }
203
168
  const matched = VERSION_PATTERN.exec(`${result.stdout}\n${result.stderr}`) ?? undefined;
@@ -205,7 +170,7 @@ async function probeAwscli() {
205
170
  return {
206
171
  ok: false,
207
172
  category: 'awscli-missing',
208
- message: `awscli の版を判定できませんでした: ${result.stdout.trim()}`,
173
+ message: `Could not determine the awscli version: ${result.stdout.trim()}`,
209
174
  };
210
175
  }
211
176
  const major = Number(matched[1]);
@@ -217,16 +182,11 @@ async function probeAwscli() {
217
182
  : {
218
183
  ok: false,
219
184
  category: 'awscli-missing',
220
- message: `awscli ${MINIMUM_VERSION_TEXT} 以上が必要です(検出: ${version})`,
185
+ message: `awscli ${MINIMUM_VERSION_TEXT} or later is required (detected: ${version})`,
221
186
  };
222
187
  }
223
188
  /**
224
- awscli の存在と版を確認し、成功結果だけをプロセス内に記憶する。
225
-
226
- 毎回 `aws --version` を起こすとツール 1 件あたりの子プロセスが 2 本になるため、
227
- Promise 自体を memo する(並行呼び出しでも実行は 1 回)。
228
- ただし失敗(未導入・版不足)を memo すると、利用者が awscli を導入してもサーバ再起動まで全ツールが awscli-missing を返し続ける。
229
- そのため失敗時は memo を捨てて、次の呼び出しで再判定する。
189
+ awscli の存在と版を確認し、成功結果だけを memo する。失敗を memo すると導入後も再起動まで awscli-missing が続くため捨てる。
230
190
  */
231
191
  export async function ensureAwscli() {
232
192
  const pending = probeCache.pending ?? probeAwscli();
@@ -4,15 +4,7 @@ import type { FailureCategory } from '../../domain/allowlist.js';
4
4
  */
5
5
  export declare const SSO_LOGIN_TIMEOUT_MS = 600000;
6
6
  /**
7
- URL とコードが確定するまでの上限。
8
-
9
- research R5 と契約書が定めるのは 10 分だけ。
10
- だが `sso login` が何も出さずに生き続けると、呼び出し元の tools/call が 10 分ブロックする。
11
- 応答を返せない状態を早く畳むための上限で、契約書側には未反映の差分。
12
-
13
- 確定までの計測値は 0.65 秒。
14
- この待ちは、mcp/executor.ts の createBoundedExecutor(4) の実行枠を占有し続ける。
15
- そのため `aws --version` の待ち時間(infra/awscli.ts の PROBE_TIMEOUT_MS)と同じ上限に揃える。
7
+ URL とコードが確定するまでの上限。実行枠を占有し続けないよう、`aws --version` の PROBE_TIMEOUT_MS と揃える。
16
8
  */
17
9
  export declare const SSO_PROMPT_TIMEOUT_MS = 15000;
18
10
  /**
@@ -47,10 +39,7 @@ export interface SsoLoginSession {
47
39
  */
48
40
  readonly category?: FailureCategory;
49
41
  /**
50
- awscli の終了コード。子がまだ終わっていなければキーごと無い。
51
-
52
- 応答の exitCode は許可リストの呼び出しと同じく、awscli の終了コードをそのまま返す規約。
53
- ここで持たないと、ツール層が値を作れない。シグナルで終わった子は null になる。
42
+ awscli の終了コード。子がまだ終わっていなければキーごと無い。シグナルで終わった子は null。
54
43
  */
55
44
  readonly exitCode?: number | null;
56
45
  }
@@ -79,17 +68,11 @@ export interface LoginRegistry {
79
68
  shutdown(): void;
80
69
  }
81
70
  /**
82
- ログインレジストリを作る。
83
-
84
- MCP サーバは 1 プロセスなので実運用のインスタンスは 1 つ(loginRegistry)だが、
85
- テストが状態を持ち越さずに検証できるようファクトリとして公開する。
71
+ ログインレジストリを作る。実運用は loginRegistry の 1 つだが、テストが状態を持ち越さないようファクトリで公開する。
86
72
  */
87
73
  export declare function createLoginRegistry(options?: Readonly<LoginRegistryOptions>): LoginRegistry;
88
74
  /**
89
- MCP サーバプロセスで共有するレジストリ。
90
-
91
- ツール(mcp/tools/sso.ts)と終了配線(mcp/serve.ts)が同じインスタンスを見ないと、
92
- 開始したフローを status が引けず、終了時に子も落ちない。
75
+ MCP サーバプロセスで共有するレジストリ。ツールと終了配線が同じインスタンスを見ないと、status が引けず子も落ちない。
93
76
  */
94
77
  export declare function loginRegistry(): LoginRegistry;
95
78
  /**
@@ -1,18 +1,7 @@
1
- // `aws sso login` の子プロセスを預かるレジストリ(research R5 / FR-007 / data-model §SsoLoginSession)。
2
- //
3
- // `sso login` はデバイス認証が終わるまで戻らないため、close まで全量を溜める runAwscli では
4
- // URL とコードを即時に返せない。ここでは自前で spawn し、stdout が届くたびに累積出力を
5
- // parseSsoLoginOutput へ掛けて、URL とコードが揃った時点で start() を resolve する。
6
- // 子はそのまま生かし、完了・失敗・時間切れは close とタイマで拾う。
7
- //
8
- // 子は spawn 直後に生存集合へ登録する。URL / コードの確定前も後始末の対象で、登録が遅れると
9
- // サーバ終了時に PPID 1 へ再親付けされた子が最大 10 分動き続ける。
10
- //
11
- // 出力に対する加工の分担は「秘匿化はここ、応答の上限はツール層」。infra から mcp を import
12
- // できないため、limitOutput は mcp/tools/sso.ts が掛ける。
13
- /* eslint-disable @typescript-eslint/prefer-readonly-parameter-types -- LoginProcess は mutable。
14
- Node 由来の型を抱える。中身は ChildProcess・出力 capture・タイマ配列。
15
- Readonly<T> では deep readonly の要求を満たせない。読み取りしかしない関数として扱う。 */
1
+ // `aws sso login` の子プロセスを預かるレジストリ(research R5 / FR-007)。
2
+ // URL とコードが揃った時点で start() を resolve し、子は生かしたまま close とタイマで拾う。
3
+ // 秘匿化はここで、応答の上限はツール層で掛ける(infra から mcp は import できない)。
4
+ /* eslint-disable @typescript-eslint/prefer-readonly-parameter-types -- LoginProcess は mutable */
16
5
  import { spawn } from 'node:child_process';
17
6
  import { classifyFailure } from '../../domain/failure.js';
18
7
  import { redactSecrets } from '../../domain/redact.js';
@@ -23,15 +12,7 @@ import { AWSCLI_COMMAND, createCapture, mergeStderr } from '../awscli.js';
23
12
  */
24
13
  export const SSO_LOGIN_TIMEOUT_MS = 600_000;
25
14
  /**
26
- URL とコードが確定するまでの上限。
27
-
28
- research R5 と契約書が定めるのは 10 分だけ。
29
- だが `sso login` が何も出さずに生き続けると、呼び出し元の tools/call が 10 分ブロックする。
30
- 応答を返せない状態を早く畳むための上限で、契約書側には未反映の差分。
31
-
32
- 確定までの計測値は 0.65 秒。
33
- この待ちは、mcp/executor.ts の createBoundedExecutor(4) の実行枠を占有し続ける。
34
- そのため `aws --version` の待ち時間(infra/awscli.ts の PROBE_TIMEOUT_MS)と同じ上限に揃える。
15
+ URL とコードが確定するまでの上限。実行枠を占有し続けないよう、`aws --version` の PROBE_TIMEOUT_MS と揃える。
35
16
  */
36
17
  export const SSO_PROMPT_TIMEOUT_MS = 15_000;
37
18
  function isTerminal(state) {
@@ -61,14 +42,7 @@ function outputOf(record) {
61
42
  return record.finalOutput ?? computeOutput(record);
62
43
  }
63
44
  /**
64
- 子プロセスの stdio が閉じた(close / error)ら呼ぶ。
65
-
66
- sessions は終端状態のレコードを次の start か shutdown() まで保持し続ける。
67
- capture(最大 CAPTURE_LIMIT_BYTES 分の Buffer)を持たせたままにすると、
68
- 常駐する profile 数に比例して解放されない領域が積み上がる。output を確定させた時点で capture を手放す。
69
-
70
- Node は 'error' の後に 'close' も発火する(awscli.ts の finalize と同じ)。
71
- そのため二重呼び出しは finalOutput の有無で防ぐ(2 回目は capture が既に無く output が痩せるため)。
45
+ 子の stdio が閉じた(close / error)ら呼ぶ。output を確定させた時点で capture を手放し、二重呼び出しは finalOutput の有無で防ぐ。
72
46
  */
73
47
  function finalizeOutput(record) {
74
48
  if (record.finalOutput !== undefined)
@@ -93,10 +67,7 @@ function snapshotOf(record) {
93
67
  };
94
68
  }
95
69
  /**
96
- ログインレジストリを作る。
97
-
98
- MCP サーバは 1 プロセスなので実運用のインスタンスは 1 つ(loginRegistry)だが、
99
- テストが状態を持ち越さずに検証できるようファクトリとして公開する。
70
+ ログインレジストリを作る。実運用は loginRegistry の 1 つだが、テストが状態を持ち越さないようファクトリで公開する。
100
71
  */
101
72
  export function createLoginRegistry(options = {}) {
102
73
  const timeoutMs = options.timeoutMs ?? SSO_LOGIN_TIMEOUT_MS;
@@ -114,11 +85,7 @@ export function createLoginRegistry(options = {}) {
114
85
  */
115
86
  const running = new Set();
116
87
  /**
117
- shutdown() 済みの印。
118
-
119
- startLogin は registry.start() の前に ensureAwscli()(最長 15 秒)を await する。
120
- その待ち中に stdin の EOF が来て runShutdown が先に実行されると、再開した start() が誰も回収しない子を spawn する。
121
- 子の stdio パイプがイベントループを保持し、親は loginTimer が最長 10 分後に発火するまで終了できない。start() の冒頭でここを見て spawn 自体をしない。
88
+ shutdown() 済みの印。ensureAwscli() の待ち中に終了処理が先に動くと、start() が回収されない子を spawn するため、冒頭で見る。
122
89
  */
123
90
  let isStopped = false;
124
91
  const beginLogin = (key, profile) => new Promise((resolve) => {
@@ -175,14 +142,10 @@ export function createLoginRegistry(options = {}) {
175
142
  child.stderr.on('error', ignoreStreamError);
176
143
  child.stdout.on('data', (chunk) => {
177
144
  record.stdout?.push(chunk);
178
- // 出力は chunk 単位で届く。累積した全文をそのつど掛け、URL とコードが揃った時点で応答する。
179
- // autofill URL はその後の行に届き得るので、揃うまでは再 parse を続けて prompt を差し替える
180
- // (確定そのものは遅らせない)。全部そろえば以後の chunk では何もしない。
145
+ // 累積全文をそのつど掛ける。autofill URL は後の行に届き得るので、揃うまで再 parse して prompt を差し替える。
181
146
  if (record.prompt?.verificationUrlComplete !== undefined)
182
147
  return;
183
- // 基本の URL / コードが確定した後は、autofill 行を含む chunk が来たときだけ再 parse する。
184
- // 含まない awscli(確認結果: 2.36.40 は含む)では、以後の chunk を処理せず返す。
185
- // 累積全文(最大 CAPTURE_LIMIT_BYTES)の split と走査を、毎 chunk 実行しないため。
148
+ // URL / コード確定後は、autofill 行を含む chunk だけ再 parse する。毎 chunk の split と走査を避けるため。
186
149
  if (record.prompt !== undefined && !chunk.toString('utf8').includes(AUTOFILL_MARKER))
187
150
  return;
188
151
  const prompt = parseSsoLoginOutput(record.stdout?.text() ?? '');
@@ -220,10 +183,7 @@ export function createLoginRegistry(options = {}) {
220
183
  // 時間切れ・prompt 待ちの打ち切りで既に終端なら、その理由を SIGTERM 由来の失敗で
221
184
  // 上書きしない。
222
185
  if (!isTerminal(record.state)) {
223
- // 確定前の exit 0 は「デバイスフローを経ず成功した」場合。
224
- // awscli 2.36.40 で確認したところ、--use-device-code を指定するとキャッシュトークンの有無に依らず
225
- // 毎回デバイスフローへ入る。そのため未観測で、ここは防御として completed に倒している。
226
- // research R5 の「抽出できずに終了 = failed」は、SSO 設定なし(exit ≠ 0)を想定した記述。
186
+ // 確定前の exit 0 は防御として completed に倒す。2.36.40 は --use-device-code で毎回デバイスフローに入り、未観測。
227
187
  if (code === 0)
228
188
  record.state = 'completed';
229
189
  else
@@ -258,7 +218,7 @@ export function createLoginRegistry(options = {}) {
258
218
  profile: key,
259
219
  state: 'failed',
260
220
  startedAt: Date.now(),
261
- output: 'サーバ終了中のため新しいログインを開始できません',
221
+ output: 'Cannot start a new login because the server is shutting down',
262
222
  category: 'other',
263
223
  };
264
224
  }
@@ -296,10 +256,7 @@ export function createLoginRegistry(options = {}) {
296
256
  }
297
257
  const sharedRegistry = {};
298
258
  /**
299
- MCP サーバプロセスで共有するレジストリ。
300
-
301
- ツール(mcp/tools/sso.ts)と終了配線(mcp/serve.ts)が同じインスタンスを見ないと、
302
- 開始したフローを status が引けず、終了時に子も落ちない。
259
+ MCP サーバプロセスで共有するレジストリ。ツールと終了配線が同じインスタンスを見ないと、status が引けず子も落ちない。
303
260
  */
304
261
  export function loginRegistry() {
305
262
  sharedRegistry.current ??= createLoginRegistry();
@@ -9,16 +9,8 @@ export interface WritableTarget {
9
9
  write(chunk: string | Uint8Array, callback?: (error?: Error | null) => void): boolean;
10
10
  }
11
11
  /**
12
- stdio の stdout を JSON-RPC 専有にするガードを張る。
13
-
14
- 返した Writable だけが本物の stdout へ届き、それ以外の stdout 書き込み
15
- (console.log・依存ライブラリの進捗表示など) は stderr へ逃がす。
16
-
17
- 「process.stdout.write を差し替えるだけ」では、transport 自身の JSON-RPC 応答まで stderr に流れる。
18
- するとクライアントが無応答になる。
19
- そのため、差し替え前に束縛した元の write を使う専用 Writable を transport へ渡す、という二段構えが必須になる。
20
-
21
- 同じ stdout に対しては冪等で、2 回目以降は初回に作った Writable を返す。
12
+ stdout を JSON-RPC 専有にする。返した Writable だけが本物の stdout へ届き、ほかの書き込みは stderr へ逃がす。
13
+ 同じ stdout に対しては冪等。
22
14
  */
23
15
  export declare function installStdoutGuard(stdout: WritableTarget, stderr: WritableTarget): Writable;
24
16
  /**
@@ -38,32 +30,18 @@ export interface McpServeDependencies {
38
30
  readonly stdin: Readable;
39
31
  readonly stderr: WritableTarget;
40
32
  /**
41
- 終了時に呼ぶ後始末。
42
-
43
- 実行中の awscli 子プロセスのように「子プロセスを持つ機能」がここへ登録する。
44
- 子を kill しない限り親は終了できない (子の stdio パイプが Node のイベントループを
45
- 保持するため、stdin の EOF で server.close() を呼んでも待ち続ける)。
33
+ 終了時の後始末。子の stdio パイプがイベントループを保持するため、子を kill しないと親は終了できない
46
34
  */
47
35
  readonly onShutdown: readonly (() => void)[];
48
36
  readonly signals: ProcessSignals;
49
37
  }
50
38
  /**
51
- MCP サーバを stdio で待ち受ける。
52
-
53
- serveStdio が接続ごとに 2025 系と 2026-07-28 系のどちらかを選び、factory のインスタンスを
54
- 1 つ固定して配る。factory は 1 接続で複数回呼ばれ得る (2026-07-28 は探査用インスタンスを捨てることがある)。
55
- そのため createServer() と onerror の設定以外の副作用は持たせない。
56
- 後始末の配線 (signals / stdin) は factory の外で 1 回だけ行う。
57
-
58
- 接続が完了しても待ち受けは続く。クライアントが stdin を閉じたら子プロセスを落として
59
- 接続を閉じ、SIGTERM を待たずにプロセスが自力で終了できるようにする。
39
+ MCP サーバを stdio で待ち受ける。factory は 1 接続で複数回呼ばれ得るため、createServer() と onerror 以外の副作用を持たせない。
40
+ stdin が閉じたら子プロセスを落として接続を閉じる
60
41
  */
61
42
  export declare function runMcpServe(dependencies: Readonly<McpServeDependencies>): StdioServerHandle;
62
43
  /**
63
- `pecot-tools mcp` の action 本体。
64
-
65
- program.parse は action の Promise を await しない。
66
- そのため例外はここで捕まえて、process.exitCode へ反映する (unhandled rejection にしない)。
44
+ `pecot-tools mcp` の action 本体。program.parse は Promise を await しないので、例外はここで process.exitCode へ反映する
67
45
  */
68
46
  export declare function executeMcpServe(buildDependencies: () => McpServeDependencies): void;
69
47
  export declare function mcpServeDefaultDependencies(): McpServeDependencies;