@pecot/tools-mcp 0.0.0-stage → 1.0.1

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 (71) hide show
  1. package/.claude-plugin/plugin.json +8 -0
  2. package/LICENSE +21 -0
  3. package/README.md +113 -2
  4. package/bin/pecot-tools-mcp.mjs +10 -0
  5. package/cli/dist/cli.d.ts +9 -0
  6. package/cli/dist/cli.js +65 -0
  7. package/cli/dist/domain/allowlist.d.ts +228 -0
  8. package/cli/dist/domain/allowlist.js +657 -0
  9. package/cli/dist/domain/arguments.d.ts +37 -0
  10. package/cli/dist/domain/arguments.js +453 -0
  11. package/cli/dist/domain/athena-query.d.ts +22 -0
  12. package/cli/dist/domain/athena-query.js +191 -0
  13. package/cli/dist/domain/failure.d.ts +22 -0
  14. package/cli/dist/domain/failure.js +99 -0
  15. package/cli/dist/domain/redact.d.ts +5 -0
  16. package/cli/dist/domain/redact.js +38 -0
  17. package/cli/dist/domain/s3-input.d.ts +20 -0
  18. package/cli/dist/domain/s3-input.js +50 -0
  19. package/cli/dist/domain/sso-output.d.ts +23 -0
  20. package/cli/dist/domain/sso-output.js +70 -0
  21. package/cli/dist/infra/awscli.d.ts +104 -0
  22. package/cli/dist/infra/awscli.js +245 -0
  23. package/cli/dist/infra/ignore-rejection.d.ts +6 -0
  24. package/cli/dist/infra/ignore-rejection.js +13 -0
  25. package/cli/dist/infra/local-file.d.ts +9 -0
  26. package/cli/dist/infra/local-file.js +21 -0
  27. package/cli/dist/infra/sso/login-registry.d.ts +99 -0
  28. package/cli/dist/infra/sso/login-registry.js +314 -0
  29. package/cli/dist/mcp/executor.d.ts +14 -0
  30. package/cli/dist/mcp/executor.js +44 -0
  31. package/cli/dist/mcp/result.d.ts +57 -0
  32. package/cli/dist/mcp/result.js +53 -0
  33. package/cli/dist/mcp/serve.d.ts +70 -0
  34. package/cli/dist/mcp/serve.js +185 -0
  35. package/cli/dist/mcp/server.d.ts +69 -0
  36. package/cli/dist/mcp/server.js +115 -0
  37. package/cli/dist/mcp/tools/_shared.d.ts +141 -0
  38. package/cli/dist/mcp/tools/_shared.js +290 -0
  39. package/cli/dist/mcp/tools/athena.d.ts +4 -0
  40. package/cli/dist/mcp/tools/athena.js +82 -0
  41. package/cli/dist/mcp/tools/cloudwatch.d.ts +4 -0
  42. package/cli/dist/mcp/tools/cloudwatch.js +46 -0
  43. package/cli/dist/mcp/tools/ec2.d.ts +4 -0
  44. package/cli/dist/mcp/tools/ec2.js +31 -0
  45. package/cli/dist/mcp/tools/ecr.d.ts +4 -0
  46. package/cli/dist/mcp/tools/ecr.js +37 -0
  47. package/cli/dist/mcp/tools/index.d.ts +7 -0
  48. package/cli/dist/mcp/tools/index.js +37 -0
  49. package/cli/dist/mcp/tools/lambda.d.ts +4 -0
  50. package/cli/dist/mcp/tools/lambda.js +36 -0
  51. package/cli/dist/mcp/tools/logs.d.ts +4 -0
  52. package/cli/dist/mcp/tools/logs.js +74 -0
  53. package/cli/dist/mcp/tools/rds.d.ts +4 -0
  54. package/cli/dist/mcp/tools/rds.js +60 -0
  55. package/cli/dist/mcp/tools/s3.d.ts +4 -0
  56. package/cli/dist/mcp/tools/s3.js +96 -0
  57. package/cli/dist/mcp/tools/scheduler.d.ts +4 -0
  58. package/cli/dist/mcp/tools/scheduler.js +44 -0
  59. package/cli/dist/mcp/tools/sqs.d.ts +4 -0
  60. package/cli/dist/mcp/tools/sqs.js +33 -0
  61. package/cli/dist/mcp/tools/sso.d.ts +11 -0
  62. package/cli/dist/mcp/tools/sso.js +240 -0
  63. package/cli/dist/mcp/tools/sts.d.ts +4 -0
  64. package/cli/dist/mcp/tools/sts.js +15 -0
  65. package/cli/dist/node-runtime.d.ts +20 -0
  66. package/cli/dist/node-runtime.js +84 -0
  67. package/cli/dist/package.json +1 -0
  68. package/cli/dist/version.d.ts +19 -0
  69. package/cli/dist/version.js +44 -0
  70. package/npm-shrinkwrap.json +67 -0
  71. package/package.json +21 -4
@@ -0,0 +1,13 @@
1
+ /**
2
+ promise の完了を待ち、失敗は捨てる。
3
+ 後始末のように、失敗しても呼び出し側の結果に影響しない処理で使う。
4
+ */
5
+ export async function ignoreRejection(promise) {
6
+ try {
7
+ await promise;
8
+ }
9
+ catch {
10
+ // 呼び出し側は失敗を扱わない契約のため、理由は捨てる。
11
+ }
12
+ }
13
+ //# sourceMappingURL=ignore-rejection.js.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ 絶対パスが既に存在するか(ファイル・ディレクトリのどちらでも true)。
3
+ */
4
+ export declare function doesFileExist(absolutePath: string): boolean;
5
+ /**
6
+ 絶対パスがディレクトリか。存在しなければ false。
7
+ */
8
+ export declare function isDirectory(absolutePath: string): boolean;
9
+ //# sourceMappingURL=local-file.d.ts.map
@@ -0,0 +1,21 @@
1
+ // ローカルファイルの実在確認。
2
+ //
3
+ // 上書き規則(research R7)の判定は、「既にある書き出し先には overwrite を要する」の 1 点だけ。
4
+ // ただしディレクトリは、overwrite を付けても救えない。
5
+ // `aws s3 cp s3://b/k /dir` は `/dir/k` を書くので、artifacts に載せる書き出し先(`/dir`)と実体がずれる。
6
+ // 呼び出し側が種別で分岐できるよう、存在確認とディレクトリ判定を別に公開する。
7
+ /* eslint-disable security/detect-non-literal-fs-filename -- 引数は許可リストの abs-path 検証を通った書き出し先のみ */
8
+ import { existsSync, statSync } from 'node:fs';
9
+ /**
10
+ 絶対パスが既に存在するか(ファイル・ディレクトリのどちらでも true)。
11
+ */
12
+ export function doesFileExist(absolutePath) {
13
+ return existsSync(absolutePath);
14
+ }
15
+ /**
16
+ 絶対パスがディレクトリか。存在しなければ false。
17
+ */
18
+ export function isDirectory(absolutePath) {
19
+ return statSync(absolutePath, { throwIfNoEntry: false })?.isDirectory() ?? false;
20
+ }
21
+ //# sourceMappingURL=local-file.js.map
@@ -0,0 +1,99 @@
1
+ import type { FailureCategory } from '../../domain/allowlist.js';
2
+ /**
3
+ 開始から時間切れ(`timed-out`)にするまでの上限。値の出所は research R5 の 10 分。
4
+ */
5
+ export declare const SSO_LOGIN_TIMEOUT_MS = 600000;
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)と同じ上限に揃える。
16
+ */
17
+ export declare const SSO_PROMPT_TIMEOUT_MS = 15000;
18
+ /**
19
+ セッションの状態(data-model §SsoLoginSession)。
20
+ */
21
+ export type SsoLoginState = 'pending' | 'completed' | 'failed' | 'timed-out';
22
+ /**
23
+ レジストリが外へ出すセッション 1 件のスナップショット。start() の戻り値も同じ形。
24
+ */
25
+ export interface SsoLoginSession {
26
+ /**
27
+ 対象プロファイル。空文字は awscli の既定解決に委ねた場合。
28
+ */
29
+ readonly profile: string;
30
+ readonly state: SsoLoginState;
31
+ readonly verificationUrl?: string;
32
+ /**
33
+ コードを自動入力する認証 URL。これを開けば利用者はコード入力が要らない。
34
+ */
35
+ readonly verificationUrlComplete?: string;
36
+ readonly userCode?: string;
37
+ /**
38
+ 開始時刻(epoch ms)。
39
+ */
40
+ readonly startedAt: number;
41
+ /**
42
+ 子プロセスの stdout + stderr(秘匿化済み)。
43
+ */
44
+ readonly output: string;
45
+ /**
46
+ `failed` のときの失敗種別。
47
+ */
48
+ readonly category?: FailureCategory;
49
+ /**
50
+ awscli の終了コード。子がまだ終わっていなければキーごと無い。
51
+
52
+ 応答の exitCode は許可リストの呼び出しと同じく、awscli の終了コードをそのまま返す規約。
53
+ ここで持たないと、ツール層が値を作れない。シグナルで終わった子は null になる。
54
+ */
55
+ readonly exitCode?: number | null;
56
+ }
57
+ export interface LoginRegistryOptions {
58
+ /**
59
+ 既定は SSO_LOGIN_TIMEOUT_MS。
60
+ */
61
+ readonly timeoutMs?: number;
62
+ /**
63
+ 既定は SSO_PROMPT_TIMEOUT_MS。
64
+ */
65
+ readonly promptTimeoutMs?: number;
66
+ }
67
+ export interface LoginRegistry {
68
+ /**
69
+ ログインを開始し、URL / コードの確定か終端状態のどちらかまで待って返す。
70
+ */
71
+ start(profile?: string): Promise<SsoLoginSession>;
72
+ /**
73
+ 開始済みセッションの現在値。無ければ undefined。
74
+ */
75
+ get(profile?: string): SsoLoginSession | undefined;
76
+ /**
77
+ 生存中の子へ SIGTERM を送り、預かりを空にする。process の 'exit' からも呼ぶため同期で完結する。
78
+ */
79
+ shutdown(): void;
80
+ }
81
+ /**
82
+ ログインレジストリを作る。
83
+
84
+ MCP サーバは 1 プロセスなので実運用のインスタンスは 1 つ(loginRegistry)だが、
85
+ テストが状態を持ち越さずに検証できるようファクトリとして公開する。
86
+ */
87
+ export declare function createLoginRegistry(options?: Readonly<LoginRegistryOptions>): LoginRegistry;
88
+ /**
89
+ MCP サーバプロセスで共有するレジストリ。
90
+
91
+ ツール(mcp/tools/sso.ts)と終了配線(mcp/serve.ts)が同じインスタンスを見ないと、
92
+ 開始したフローを status が引けず、終了時に子も落ちない。
93
+ */
94
+ export declare function loginRegistry(): LoginRegistry;
95
+ /**
96
+ serve.ts の onShutdown へ登録する後始末。1 度も開始していなければ何もしない。
97
+ */
98
+ export declare function shutdownLoginRegistry(): void;
99
+ //# sourceMappingURL=login-registry.d.ts.map
@@ -0,0 +1,314 @@
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 の要求を満たせない。読み取りしかしない関数として扱う。 */
16
+ import { spawn } from 'node:child_process';
17
+ import { classifyFailure } from '../../domain/failure.js';
18
+ import { redactSecrets } from '../../domain/redact.js';
19
+ import { AUTOFILL_MARKER, parseSsoLoginOutput } from '../../domain/sso-output.js';
20
+ import { AWSCLI_COMMAND, createCapture, mergeStderr } from '../awscli.js';
21
+ /**
22
+ 開始から時間切れ(`timed-out`)にするまでの上限。値の出所は research R5 の 10 分。
23
+ */
24
+ export const SSO_LOGIN_TIMEOUT_MS = 600_000;
25
+ /**
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)と同じ上限に揃える。
35
+ */
36
+ export const SSO_PROMPT_TIMEOUT_MS = 15_000;
37
+ function isTerminal(state) {
38
+ return state !== 'pending';
39
+ }
40
+ /**
41
+ pipe の読み出し側で上がる 'error' の受け皿。張らないと未処理の 'error' がサーバごと落とす。
42
+ */
43
+ function ignoreStreamError() {
44
+ // 何もしない。リスナを張ること自体が目的。
45
+ }
46
+ function clearTimers(record) {
47
+ for (const timer of record.timers)
48
+ clearTimeout(timer);
49
+ }
50
+ /**
51
+ capture から output を組み立てる。stdout に stderr(spawn の error 行を含む)を継いで秘匿化する。
52
+ */
53
+ function computeOutput(record) {
54
+ const stderr = mergeStderr(record.stderr?.text() ?? '', record.spawnErrorMessage);
55
+ return redactSecrets(mergeStderr(record.stdout?.text() ?? '', stderr));
56
+ }
57
+ /**
58
+ 応答へ出す output。確定済みなら finalOutput を、未確定なら capture から都度組み立てる。
59
+ */
60
+ function outputOf(record) {
61
+ return record.finalOutput ?? computeOutput(record);
62
+ }
63
+ /**
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 が痩せるため)。
72
+ */
73
+ function finalizeOutput(record) {
74
+ if (record.finalOutput !== undefined)
75
+ return;
76
+ record.finalOutput = computeOutput(record);
77
+ record.stdout = undefined;
78
+ record.stderr = undefined;
79
+ }
80
+ function snapshotOf(record) {
81
+ return {
82
+ profile: record.profile,
83
+ state: record.state,
84
+ ...(record.prompt !== undefined && {
85
+ verificationUrl: record.prompt.verificationUrl,
86
+ userCode: record.prompt.userCode,
87
+ ...(record.prompt.verificationUrlComplete !== undefined && { verificationUrlComplete: record.prompt.verificationUrlComplete }),
88
+ }),
89
+ startedAt: record.startedAt,
90
+ output: outputOf(record),
91
+ ...(record.category !== undefined && { category: record.category }),
92
+ ...(record.exitCode !== undefined && { exitCode: record.exitCode }),
93
+ };
94
+ }
95
+ /**
96
+ ログインレジストリを作る。
97
+
98
+ MCP サーバは 1 プロセスなので実運用のインスタンスは 1 つ(loginRegistry)だが、
99
+ テストが状態を持ち越さずに検証できるようファクトリとして公開する。
100
+ */
101
+ export function createLoginRegistry(options = {}) {
102
+ const timeoutMs = options.timeoutMs ?? SSO_LOGIN_TIMEOUT_MS;
103
+ const promptTimeoutMs = options.promptTimeoutMs ?? SSO_PROMPT_TIMEOUT_MS;
104
+ /**
105
+ キー(`profile ?? ''`)ごとの最新セッション。終端状態のものは次の start で置き換える。
106
+ */
107
+ const sessions = new Map();
108
+ /**
109
+ 確定前の start。同一キーの 2 回目はこれを await して二重起動を避ける。
110
+ */
111
+ const starting = new Map();
112
+ /**
113
+ 生存中の子。spawn 直後に足し、close で外す。shutdown はここを辿る。
114
+ */
115
+ const running = new Set();
116
+ /**
117
+ shutdown() 済みの印。
118
+
119
+ startLogin は registry.start() の前に ensureAwscli()(最長 15 秒)を await する。
120
+ その待ち中に stdin の EOF が来て runShutdown が先に実行されると、再開した start() が誰も回収しない子を spawn する。
121
+ 子の stdio パイプがイベントループを保持し、親は loginTimer が最長 10 分後に発火するまで終了できない。start() の冒頭でここを見て spawn 自体をしない。
122
+ */
123
+ let isStopped = false;
124
+ const beginLogin = (key, profile) => new Promise((resolve) => {
125
+ // `--profile=<value>` の 1 トークンにする。許可リストの組み立てと同じ形。
126
+ // 分離して渡すと、先頭ハイフンを含む値の扱いが 2 か所で割れる。
127
+ const arguments_ = [
128
+ 'sso',
129
+ 'login',
130
+ ...(profile === undefined ? [] : [`--profile=${profile}`]),
131
+ '--no-browser',
132
+ '--use-device-code',
133
+ ];
134
+ const child = spawn(AWSCLI_COMMAND, arguments_, {
135
+ stdio: ['ignore', 'pipe', 'pipe'],
136
+ shell: false,
137
+ });
138
+ const record = {
139
+ profile: key,
140
+ startedAt: Date.now(),
141
+ child,
142
+ stdout: createCapture(),
143
+ stderr: createCapture(),
144
+ timers: [],
145
+ state: 'pending',
146
+ prompt: undefined,
147
+ category: undefined,
148
+ exitCode: undefined,
149
+ spawnErrorCode: undefined,
150
+ spawnErrorMessage: '',
151
+ finalOutput: undefined,
152
+ };
153
+ running.add(record);
154
+ sessions.set(key, record);
155
+ let isSettled = false;
156
+ const settle = () => {
157
+ if (isSettled)
158
+ return;
159
+ isSettled = true;
160
+ resolve(snapshotOf(record));
161
+ };
162
+ /**
163
+ close が受けた終了コードをそのまま渡す(record.child.exitCode を別の定義元として持たない)。
164
+ */
165
+ const fail = (exitCode) => {
166
+ record.state = 'failed';
167
+ record.category = classifyFailure({
168
+ exitCode,
169
+ stderr: mergeStderr(record.stderr?.text() ?? '', record.spawnErrorMessage),
170
+ timedOut: false,
171
+ ...(record.spawnErrorCode !== undefined && { spawnErrorCode: record.spawnErrorCode }),
172
+ });
173
+ };
174
+ child.stdout.on('error', ignoreStreamError);
175
+ child.stderr.on('error', ignoreStreamError);
176
+ child.stdout.on('data', (chunk) => {
177
+ record.stdout?.push(chunk);
178
+ // 出力は chunk 単位で届く。累積した全文をそのつど掛け、URL とコードが揃った時点で応答する。
179
+ // autofill URL はその後の行に届き得るので、揃うまでは再 parse を続けて prompt を差し替える
180
+ // (確定そのものは遅らせない)。全部そろえば以後の chunk では何もしない。
181
+ if (record.prompt?.verificationUrlComplete !== undefined)
182
+ return;
183
+ // 基本の URL / コードが確定した後は、autofill 行を含む chunk が来たときだけ再 parse する。
184
+ // 含まない awscli(確認結果: 2.36.40 は含む)では、以後の chunk を処理せず返す。
185
+ // 累積全文(最大 CAPTURE_LIMIT_BYTES)の split と走査を、毎 chunk 実行しないため。
186
+ if (record.prompt !== undefined && !chunk.toString('utf8').includes(AUTOFILL_MARKER))
187
+ return;
188
+ const prompt = parseSsoLoginOutput(record.stdout?.text() ?? '');
189
+ if (prompt === undefined)
190
+ return;
191
+ record.prompt = prompt;
192
+ // 確定した promptTimer はここで役目を終える。settled のみを見ていると、close/error が
193
+ // 遅れて届くまでの最長 promptTimeoutMs をタイマが無駄に生かし続ける。
194
+ clearTimeout(promptTimer);
195
+ settle();
196
+ });
197
+ child.stderr.on('data', (chunk) => {
198
+ record.stderr?.push(chunk);
199
+ });
200
+ child.on('error', (error) => {
201
+ // 既に終端状態(時間切れ・prompt 待ちの打ち切り)なら、遅れて届いた 'error' で
202
+ // spawnErrorMessage を上書きしない(outputOf の結果が確定後に変わってしまう)。
203
+ if (!isTerminal(record.state)) {
204
+ record.spawnErrorMessage = error.message;
205
+ record.spawnErrorCode
206
+ = 'code' in error && typeof error.code === 'string' ? error.code : undefined;
207
+ fail(record.child.exitCode);
208
+ }
209
+ // kill() の失敗で出る 'error' には後続の close が無い。ここでも running から外さないと
210
+ // shutdown() の対象に残り続ける。
211
+ running.delete(record);
212
+ clearTimers(record);
213
+ finalizeOutput(record);
214
+ settle();
215
+ });
216
+ child.on('close', (code) => {
217
+ running.delete(record);
218
+ clearTimers(record);
219
+ record.exitCode = code;
220
+ // 時間切れ・prompt 待ちの打ち切りで既に終端なら、その理由を SIGTERM 由来の失敗で
221
+ // 上書きしない。
222
+ if (!isTerminal(record.state)) {
223
+ // 確定前の exit 0 は「デバイスフローを経ず成功した」場合。
224
+ // awscli 2.36.40 で確認したところ、--use-device-code を指定するとキャッシュトークンの有無に依らず
225
+ // 毎回デバイスフローへ入る。そのため未観測で、ここは防御として completed に倒している。
226
+ // research R5 の「抽出できずに終了 = failed」は、SSO 設定なし(exit ≠ 0)を想定した記述。
227
+ if (code === 0)
228
+ record.state = 'completed';
229
+ else
230
+ fail(code);
231
+ }
232
+ finalizeOutput(record);
233
+ settle();
234
+ });
235
+ const promptTimer = setTimeout(() => {
236
+ if (isSettled)
237
+ return;
238
+ record.state = 'failed';
239
+ record.category = 'timeout';
240
+ child.kill('SIGTERM');
241
+ settle();
242
+ }, promptTimeoutMs);
243
+ promptTimer.unref();
244
+ const loginTimer = setTimeout(() => {
245
+ if (isTerminal(record.state))
246
+ return;
247
+ record.state = 'timed-out';
248
+ child.kill('SIGTERM');
249
+ settle();
250
+ }, timeoutMs);
251
+ loginTimer.unref();
252
+ record.timers.push(promptTimer, loginTimer);
253
+ });
254
+ const start = async (profile) => {
255
+ const key = profile ?? '';
256
+ if (isStopped) {
257
+ return {
258
+ profile: key,
259
+ state: 'failed',
260
+ startedAt: Date.now(),
261
+ output: 'サーバ終了中のため新しいログインを開始できません',
262
+ category: 'other',
263
+ };
264
+ }
265
+ const inFlight = starting.get(key);
266
+ if (inFlight !== undefined)
267
+ return inFlight;
268
+ // 同じキーで pending のフローがあれば新規起動せず既存の URL / コードを返す(R5)。
269
+ const existing = sessions.get(key);
270
+ if (existing !== undefined && !isTerminal(existing.state))
271
+ return snapshotOf(existing);
272
+ const pending = beginLogin(key, profile);
273
+ starting.set(key, pending);
274
+ try {
275
+ return await pending;
276
+ }
277
+ finally {
278
+ starting.delete(key);
279
+ }
280
+ };
281
+ const get = (profile = '') => {
282
+ const record = sessions.get(profile);
283
+ return record === undefined ? undefined : snapshotOf(record);
284
+ };
285
+ const shutdown = () => {
286
+ isStopped = true;
287
+ for (const record of running) {
288
+ clearTimers(record);
289
+ record.child.kill('SIGTERM');
290
+ }
291
+ running.clear();
292
+ sessions.clear();
293
+ starting.clear();
294
+ };
295
+ return { start, get, shutdown };
296
+ }
297
+ const sharedRegistry = {};
298
+ /**
299
+ MCP サーバプロセスで共有するレジストリ。
300
+
301
+ ツール(mcp/tools/sso.ts)と終了配線(mcp/serve.ts)が同じインスタンスを見ないと、
302
+ 開始したフローを status が引けず、終了時に子も落ちない。
303
+ */
304
+ export function loginRegistry() {
305
+ sharedRegistry.current ??= createLoginRegistry();
306
+ return sharedRegistry.current;
307
+ }
308
+ /**
309
+ serve.ts の onShutdown へ登録する後始末。1 度も開始していなければ何もしない。
310
+ */
311
+ export function shutdownLoginRegistry() {
312
+ sharedRegistry.current?.shutdown();
313
+ }
314
+ //# sourceMappingURL=login-registry.js.map
@@ -0,0 +1,14 @@
1
+ export interface BoundedExecutor {
2
+ /**
3
+ task を上限内で実行し、その結果を返す。
4
+
5
+ - 枠が空いていれば即座に、埋まっていれば到着順(FIFO)で待ってから実行する。
6
+ - task の失敗は呼び出し元へそのまま伝播する。枠は finally で解放するので、後続は実行される。
7
+ */
8
+ run<T>(task: () => Promise<T>): Promise<T>;
9
+ }
10
+ /**
11
+ 同時実行数 limit の実行器を生成する。
12
+ */
13
+ export declare function createBoundedExecutor(limit: number): BoundedExecutor;
14
+ //# sourceMappingURL=executor.d.ts.map
@@ -0,0 +1,44 @@
1
+ // 上限付き並行実行器(FR-018 / research R9)。
2
+ //
3
+ // MCP の Protocol 層は受信したリクエストを並行に捌く。
4
+ // そのためツールハンドラを run() で包み、「同時に実行されるのは limit 件まで」を保証する。
5
+ // awscli の各呼び出しは独立プロセスなので、逐次化までは要らない。
6
+ // 上限だけを構造的に強制する(ツール実装側の善意には依存しない)。
7
+ /**
8
+ 同時実行数 limit の実行器を生成する。
9
+ */
10
+ export function createBoundedExecutor(limit) {
11
+ let active = 0;
12
+ const waiting = [];
13
+ // 枠の取得までは呼び出しと同じ tick で実行されるため、active の増加と waiting への積み方が到着順になる。
14
+ const acquire = async () => {
15
+ if (active < limit) {
16
+ active += 1;
17
+ return;
18
+ }
19
+ return new Promise((resolve) => {
20
+ waiting.push(resolve);
21
+ });
22
+ };
23
+ // 待機列があれば枠を明け渡す(active は減らさない)。無ければ枠を返す。
24
+ const release = () => {
25
+ const next = waiting.shift();
26
+ if (next === undefined) {
27
+ active -= 1;
28
+ return;
29
+ }
30
+ next();
31
+ };
32
+ return {
33
+ async run(task) {
34
+ await acquire();
35
+ try {
36
+ return await task();
37
+ }
38
+ finally {
39
+ release();
40
+ }
41
+ },
42
+ };
43
+ }
44
+ //# sourceMappingURL=executor.js.map
@@ -0,0 +1,57 @@
1
+ import type { CallToolResult } from '@modelcontextprotocol/server';
2
+ import type { FailureCategory } from '../domain/allowlist.js';
3
+ /**
4
+ ツール 1 回分の実行結果(data-model.md の ExecutionResult)。
5
+ */
6
+ export interface ExecutionResult {
7
+ readonly ok: boolean;
8
+ /**
9
+ awscli の終了コード。実行前拒否は 2、時間切れは 124、awscli 不在は 127。
10
+ */
11
+ readonly exitCode: number;
12
+ /**
13
+ 失敗時のみ。ok が true のときはキーごと無い。
14
+ */
15
+ readonly category?: FailureCategory;
16
+ readonly data: Readonly<Record<string, unknown>>;
17
+ /**
18
+ 書き出したローカルファイルの絶対パス。
19
+ */
20
+ readonly artifacts: readonly string[];
21
+ /**
22
+ エージェント向けの 1 行サマリ(失敗時は再試行方法を含む)。
23
+ */
24
+ readonly message: string;
25
+ /**
26
+ 失敗時の awscli stderr(秘匿化済み)。
27
+ */
28
+ readonly stderr?: string;
29
+ }
30
+ /**
31
+ limitOutput の戻り値。
32
+ */
33
+ export interface LimitedOutput {
34
+ readonly text: string;
35
+ readonly truncated: boolean;
36
+ }
37
+ /**
38
+ ExecutionResult を MCP の CallToolResult へ変換する。
39
+
40
+ content には structuredContent と同じ内容の JSON テキストを載せる。structuredContent を
41
+ 読めないクライアントでも結果を解釈できるようにするための二重化で、MCP 仕様が推奨する形。
42
+
43
+ 引数は deep readonly だが、戻り値は SDK 側の mutable 型のまま返す。
44
+ SDK が readonly 型を公開していないため、この非対称は境界として許容する。
45
+ */
46
+ export declare function toCallToolResult(result: Readonly<ExecutionResult>): CallToolResult;
47
+ /**
48
+ 出力を上限まで切り詰める。
49
+
50
+ 残すのは **先頭側**。pecotv の「末尾を残す」は進捗ログ向けで、JSON 出力では先頭を失うと
51
+ 構造ごと読めなくなる。先頭を残せば少なくとも先頭要素は読める。
52
+
53
+ バイト数で切るため、末尾が多バイト文字の途中にかかると U+FFFD が 1 文字だけ残る。
54
+ 元の出力に無い文字を足さないよう、末尾の U+FFFD は落とす。
55
+ */
56
+ export declare function limitOutput(text: string): LimitedOutput;
57
+ //# sourceMappingURL=result.d.ts.map
@@ -0,0 +1,53 @@
1
+ // ツールの実行結果表現と、SDK の CallToolResult への変換。
2
+ //
3
+ // 各ツールは、「許可リストのエントリに従って awscli を呼び、その成否・出力を一様な形で返す」構造を持つ。
4
+ // そのためコマンド固有の戻り値ではなく、ExecutionResult に正規化してからプロトコル表現へ変換する。
5
+ // 上限(research R8)の定義は infra 側に置く。子プロセスの捕捉上限が同じ値を基準にするが、
6
+ // 依存方向が infra → mcp を許さないため。
7
+ import { OUTPUT_LIMIT_BYTES } from '../infra/awscli.js';
8
+ /**
9
+ ExecutionResult を MCP の CallToolResult へ変換する。
10
+
11
+ content には structuredContent と同じ内容の JSON テキストを載せる。structuredContent を
12
+ 読めないクライアントでも結果を解釈できるようにするための二重化で、MCP 仕様が推奨する形。
13
+
14
+ 引数は deep readonly だが、戻り値は SDK 側の mutable 型のまま返す。
15
+ SDK が readonly 型を公開していないため、この非対称は境界として許容する。
16
+ */
17
+ export function toCallToolResult(result) {
18
+ const structuredContent = toStructuredContent(result);
19
+ return {
20
+ content: [{ type: 'text', text: JSON.stringify(structuredContent) }],
21
+ // exactOptionalPropertyTypes 下で「成功時は isError キー自体を持たない」を表現する。
22
+ ...(!result.ok && { isError: true }),
23
+ structuredContent,
24
+ };
25
+ }
26
+ function toStructuredContent(result) {
27
+ return {
28
+ ok: result.ok,
29
+ exitCode: result.exitCode,
30
+ ...(result.category !== undefined && { category: result.category }),
31
+ data: { ...result.data },
32
+ artifacts: [...result.artifacts],
33
+ message: result.message,
34
+ ...(result.stderr !== undefined && { stderr: result.stderr }),
35
+ };
36
+ }
37
+ /**
38
+ 出力を上限まで切り詰める。
39
+
40
+ 残すのは **先頭側**。pecotv の「末尾を残す」は進捗ログ向けで、JSON 出力では先頭を失うと
41
+ 構造ごと読めなくなる。先頭を残せば少なくとも先頭要素は読める。
42
+
43
+ バイト数で切るため、末尾が多バイト文字の途中にかかると U+FFFD が 1 文字だけ残る。
44
+ 元の出力に無い文字を足さないよう、末尾の U+FFFD は落とす。
45
+ */
46
+ export function limitOutput(text) {
47
+ const buffer = Buffer.from(text, 'utf8');
48
+ if (buffer.length <= OUTPUT_LIMIT_BYTES)
49
+ return { text, truncated: false };
50
+ const head = buffer.subarray(0, OUTPUT_LIMIT_BYTES).toString('utf8');
51
+ return { text: head.endsWith('�') ? head.slice(0, -1) : head, truncated: true };
52
+ }
53
+ //# sourceMappingURL=result.js.map