sonobat 0.5.1 → 0.5.2

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/dist/index.js CHANGED
@@ -3539,6 +3539,1989 @@ function registerKbTools(server2, db2) {
3539
3539
  );
3540
3540
  }
3541
3541
 
3542
+ // src/mcp/tools/ops.ts
3543
+ import { z as z7 } from "zod";
3544
+
3545
+ // src/db/repository/engagement-repository.ts
3546
+ import crypto6 from "crypto";
3547
+ function rowToEngagement(row) {
3548
+ return {
3549
+ id: row.id,
3550
+ name: row.name,
3551
+ environment: row.environment,
3552
+ scopeJson: row.scope_json,
3553
+ policyJson: row.policy_json,
3554
+ ...row.schedule_cron !== null ? { scheduleCron: row.schedule_cron } : {},
3555
+ status: row.status,
3556
+ createdAt: row.created_at,
3557
+ updatedAt: row.updated_at
3558
+ };
3559
+ }
3560
+ var FIELD_TO_COLUMN = {
3561
+ name: "name",
3562
+ environment: "environment",
3563
+ scopeJson: "scope_json",
3564
+ policyJson: "policy_json",
3565
+ scheduleCron: "schedule_cron",
3566
+ status: "status"
3567
+ };
3568
+ var EngagementRepository = class {
3569
+ db;
3570
+ insertStmt;
3571
+ selectByIdStmt;
3572
+ selectByStatusStmt;
3573
+ selectAllStmt;
3574
+ deleteStmt;
3575
+ constructor(db2) {
3576
+ this.db = db2;
3577
+ this.insertStmt = this.db.prepare(
3578
+ `INSERT INTO engagements (id, name, environment, scope_json, policy_json, schedule_cron, status, created_at, updated_at)
3579
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
3580
+ );
3581
+ this.selectByIdStmt = this.db.prepare(
3582
+ `SELECT id, name, environment, scope_json, policy_json, schedule_cron, status, created_at, updated_at
3583
+ FROM engagements WHERE id = ?`
3584
+ );
3585
+ this.selectByStatusStmt = this.db.prepare(
3586
+ `SELECT id, name, environment, scope_json, policy_json, schedule_cron, status, created_at, updated_at
3587
+ FROM engagements WHERE status = ?`
3588
+ );
3589
+ this.selectAllStmt = this.db.prepare(
3590
+ `SELECT id, name, environment, scope_json, policy_json, schedule_cron, status, created_at, updated_at
3591
+ FROM engagements`
3592
+ );
3593
+ this.deleteStmt = this.db.prepare(`DELETE FROM engagements WHERE id = ?`);
3594
+ }
3595
+ /**
3596
+ * Engagement を新規作成して返す。
3597
+ *
3598
+ * デフォルト値:
3599
+ * - environment: 'stg'
3600
+ * - scopeJson: '{}'
3601
+ * - policyJson: '{}'
3602
+ * - status: 'active'
3603
+ */
3604
+ create(input) {
3605
+ const id = crypto6.randomUUID();
3606
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
3607
+ const environment = input.environment ?? "stg";
3608
+ const scopeJson = input.scopeJson ?? "{}";
3609
+ const policyJson = input.policyJson ?? "{}";
3610
+ const scheduleCron = input.scheduleCron ?? null;
3611
+ const status = input.status ?? "active";
3612
+ this.insertStmt.run(
3613
+ id,
3614
+ input.name,
3615
+ environment,
3616
+ scopeJson,
3617
+ policyJson,
3618
+ scheduleCron,
3619
+ status,
3620
+ timestamp,
3621
+ timestamp
3622
+ );
3623
+ return {
3624
+ id,
3625
+ name: input.name,
3626
+ environment,
3627
+ scopeJson,
3628
+ policyJson,
3629
+ ...scheduleCron !== null ? { scheduleCron } : {},
3630
+ status,
3631
+ createdAt: timestamp,
3632
+ updatedAt: timestamp
3633
+ };
3634
+ }
3635
+ /**
3636
+ * ID で Engagement を取得する。存在しなければ undefined。
3637
+ */
3638
+ findById(id) {
3639
+ const row = this.selectByIdStmt.get(id);
3640
+ if (row === void 0) {
3641
+ return void 0;
3642
+ }
3643
+ return rowToEngagement(row);
3644
+ }
3645
+ /**
3646
+ * status で Engagement 一覧を取得する。
3647
+ */
3648
+ findByStatus(status) {
3649
+ const rows = this.selectByStatusStmt.all(status);
3650
+ return rows.map(rowToEngagement);
3651
+ }
3652
+ /**
3653
+ * 全 Engagement を取得する。
3654
+ */
3655
+ list() {
3656
+ const rows = this.selectAllStmt.all();
3657
+ return rows.map(rowToEngagement);
3658
+ }
3659
+ /**
3660
+ * Engagement の指定フィールドを更新する。
3661
+ *
3662
+ * 提供されたフィールドのみ SET し、updated_at を自動更新する。
3663
+ * 存在しない ID の場合 undefined を返す。
3664
+ */
3665
+ update(id, fields) {
3666
+ const setClauses = [];
3667
+ const params = [];
3668
+ for (const [key, value] of Object.entries(fields)) {
3669
+ const column = FIELD_TO_COLUMN[key];
3670
+ if (column !== void 0) {
3671
+ setClauses.push(`${column} = ?`);
3672
+ params.push(value ?? null);
3673
+ }
3674
+ }
3675
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
3676
+ setClauses.push("updated_at = ?");
3677
+ params.push(timestamp);
3678
+ params.push(id);
3679
+ const sql = `UPDATE engagements SET ${setClauses.join(", ")} WHERE id = ?`;
3680
+ const result = this.db.prepare(sql).run(...params);
3681
+ if (result.changes === 0) {
3682
+ return void 0;
3683
+ }
3684
+ return this.findById(id);
3685
+ }
3686
+ /**
3687
+ * Engagement を削除する。
3688
+ *
3689
+ * CASCADE により関連する runs なども同時に削除される。
3690
+ *
3691
+ * @returns 削除成功時 true、id が存在しない場合 false。
3692
+ */
3693
+ delete(id) {
3694
+ const result = this.deleteStmt.run(id);
3695
+ return result.changes > 0;
3696
+ }
3697
+ };
3698
+
3699
+ // src/db/repository/run-repository.ts
3700
+ import crypto7 from "crypto";
3701
+ function rowToRun(row) {
3702
+ return {
3703
+ id: row.id,
3704
+ engagementId: row.engagement_id,
3705
+ triggerKind: row.trigger_kind,
3706
+ ...row.trigger_ref !== null ? { triggerRef: row.trigger_ref } : {},
3707
+ status: row.status,
3708
+ ...row.started_at !== null ? { startedAt: row.started_at } : {},
3709
+ ...row.finished_at !== null ? { finishedAt: row.finished_at } : {},
3710
+ summaryJson: row.summary_json,
3711
+ createdAt: row.created_at
3712
+ };
3713
+ }
3714
+ var RunRepository = class {
3715
+ db;
3716
+ insertStmt;
3717
+ selectByIdStmt;
3718
+ selectByEngagementStmt;
3719
+ selectByStatusStmt;
3720
+ deleteStmt;
3721
+ constructor(db2) {
3722
+ this.db = db2;
3723
+ this.insertStmt = this.db.prepare(
3724
+ `INSERT INTO runs (id, engagement_id, trigger_kind, trigger_ref, status, started_at, finished_at, summary_json, created_at)
3725
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
3726
+ );
3727
+ this.selectByIdStmt = this.db.prepare(
3728
+ `SELECT id, engagement_id, trigger_kind, trigger_ref, status, started_at, finished_at, summary_json, created_at
3729
+ FROM runs WHERE id = ?`
3730
+ );
3731
+ this.selectByEngagementStmt = this.db.prepare(
3732
+ `SELECT id, engagement_id, trigger_kind, trigger_ref, status, started_at, finished_at, summary_json, created_at
3733
+ FROM runs WHERE engagement_id = ? ORDER BY created_at DESC LIMIT ?`
3734
+ );
3735
+ this.selectByStatusStmt = this.db.prepare(
3736
+ `SELECT id, engagement_id, trigger_kind, trigger_ref, status, started_at, finished_at, summary_json, created_at
3737
+ FROM runs WHERE status = ?`
3738
+ );
3739
+ this.deleteStmt = this.db.prepare(`DELETE FROM runs WHERE id = ?`);
3740
+ }
3741
+ /**
3742
+ * Run を新規作成して返す。
3743
+ *
3744
+ * - created_at は現在時刻に設定
3745
+ * - status が 'running' の場合、started_at も現在時刻に設定
3746
+ * - summaryJson はデフォルト '{}'
3747
+ */
3748
+ create(input) {
3749
+ const id = crypto7.randomUUID();
3750
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3751
+ const startedAt = input.status === "running" ? now : null;
3752
+ this.insertStmt.run(
3753
+ id,
3754
+ input.engagementId,
3755
+ input.triggerKind,
3756
+ input.triggerRef ?? null,
3757
+ input.status,
3758
+ startedAt,
3759
+ null,
3760
+ // finished_at
3761
+ "{}",
3762
+ // summary_json
3763
+ now
3764
+ // created_at
3765
+ );
3766
+ return {
3767
+ id,
3768
+ engagementId: input.engagementId,
3769
+ triggerKind: input.triggerKind,
3770
+ ...input.triggerRef !== void 0 ? { triggerRef: input.triggerRef } : {},
3771
+ status: input.status,
3772
+ ...startedAt !== null ? { startedAt } : {},
3773
+ summaryJson: "{}",
3774
+ createdAt: now
3775
+ };
3776
+ }
3777
+ /**
3778
+ * ID で Run を取得する。存在しなければ undefined。
3779
+ */
3780
+ findById(id) {
3781
+ const row = this.selectByIdStmt.get(id);
3782
+ if (row === void 0) {
3783
+ return void 0;
3784
+ }
3785
+ return rowToRun(row);
3786
+ }
3787
+ /**
3788
+ * engagement_id で Run 一覧を取得する。
3789
+ * ORDER BY created_at DESC、LIMIT デフォルト 100。
3790
+ */
3791
+ findByEngagement(engagementId, limit) {
3792
+ const rows = this.selectByEngagementStmt.all(engagementId, limit ?? 100);
3793
+ return rows.map(rowToRun);
3794
+ }
3795
+ /**
3796
+ * status で Run 一覧を取得する。
3797
+ */
3798
+ findByStatus(status) {
3799
+ const rows = this.selectByStatusStmt.all(status);
3800
+ return rows.map(rowToRun);
3801
+ }
3802
+ /**
3803
+ * Run のステータスを更新する。
3804
+ *
3805
+ * - 'succeeded' または 'failed' の場合、finished_at を現在時刻に設定
3806
+ * - 'running' の場合、started_at が未設定なら現在時刻に設定
3807
+ * - summaryJson が指定された場合、summary_json も更新
3808
+ *
3809
+ * @returns 更新後の Run。id が存在しなければ undefined。
3810
+ */
3811
+ updateStatus(id, status, summaryJson) {
3812
+ const existing = this.findById(id);
3813
+ if (existing === void 0) {
3814
+ return void 0;
3815
+ }
3816
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3817
+ const setClauses = ["status = ?"];
3818
+ const params = [status];
3819
+ if (status === "succeeded" || status === "failed") {
3820
+ setClauses.push("finished_at = ?");
3821
+ params.push(now);
3822
+ }
3823
+ if (status === "running" && existing.startedAt === void 0) {
3824
+ setClauses.push("started_at = ?");
3825
+ params.push(now);
3826
+ }
3827
+ if (summaryJson !== void 0) {
3828
+ setClauses.push("summary_json = ?");
3829
+ params.push(summaryJson);
3830
+ }
3831
+ params.push(id);
3832
+ const sql = `UPDATE runs SET ${setClauses.join(", ")} WHERE id = ?`;
3833
+ this.db.prepare(sql).run(...params);
3834
+ return this.findById(id);
3835
+ }
3836
+ /**
3837
+ * Run を削除する。
3838
+ *
3839
+ * @returns 削除成功時 true、id が存在しない場合 false。
3840
+ */
3841
+ delete(id) {
3842
+ const result = this.deleteStmt.run(id);
3843
+ return result.changes > 0;
3844
+ }
3845
+ };
3846
+
3847
+ // src/db/repository/action-queue-repository.ts
3848
+ import crypto8 from "crypto";
3849
+ function rowToActionQueueItem(row) {
3850
+ return {
3851
+ id: row.id,
3852
+ engagementId: row.engagement_id,
3853
+ ...row.run_id !== null ? { runId: row.run_id } : {},
3854
+ ...row.parent_action_id !== null ? { parentActionId: row.parent_action_id } : {},
3855
+ kind: row.kind,
3856
+ priority: row.priority,
3857
+ dedupeKey: row.dedupe_key,
3858
+ paramsJson: row.params_json,
3859
+ state: row.state,
3860
+ attemptCount: row.attempt_count,
3861
+ maxAttempts: row.max_attempts,
3862
+ availableAt: row.available_at,
3863
+ ...row.lease_owner !== null ? { leaseOwner: row.lease_owner } : {},
3864
+ ...row.lease_expires_at !== null ? { leaseExpiresAt: row.lease_expires_at } : {},
3865
+ ...row.last_error !== null ? { lastError: row.last_error } : {},
3866
+ createdAt: row.created_at,
3867
+ updatedAt: row.updated_at
3868
+ };
3869
+ }
3870
+ var ActionQueueRepository = class {
3871
+ db;
3872
+ insertStmt;
3873
+ selectByIdStmt;
3874
+ pollStmt;
3875
+ completeStmt;
3876
+ requeueStmt;
3877
+ deadLetterStmt;
3878
+ selectByEngagementStmt;
3879
+ selectByEngagementStateStmt;
3880
+ cancelStmt;
3881
+ failTx;
3882
+ constructor(db2) {
3883
+ this.db = db2;
3884
+ this.insertStmt = this.db.prepare(
3885
+ `INSERT INTO action_queue
3886
+ (id, engagement_id, run_id, parent_action_id, kind, priority,
3887
+ dedupe_key, params_json, state, attempt_count, max_attempts,
3888
+ available_at, lease_owner, lease_expires_at, last_error,
3889
+ created_at, updated_at)
3890
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
3891
+ );
3892
+ this.selectByIdStmt = this.db.prepare(
3893
+ `SELECT id, engagement_id, run_id, parent_action_id, kind, priority,
3894
+ dedupe_key, params_json, state, attempt_count, max_attempts,
3895
+ available_at, lease_owner, lease_expires_at, last_error,
3896
+ created_at, updated_at
3897
+ FROM action_queue WHERE id = ?`
3898
+ );
3899
+ this.pollStmt = this.db.prepare(
3900
+ `UPDATE action_queue
3901
+ SET state = 'running',
3902
+ lease_owner = ?,
3903
+ lease_expires_at = ?,
3904
+ attempt_count = attempt_count + 1,
3905
+ updated_at = ?
3906
+ WHERE id = (
3907
+ SELECT id FROM action_queue
3908
+ WHERE state = 'queued' AND available_at <= ?
3909
+ ORDER BY priority ASC, created_at ASC
3910
+ LIMIT 1
3911
+ )
3912
+ RETURNING *`
3913
+ );
3914
+ this.completeStmt = this.db.prepare(
3915
+ `UPDATE action_queue
3916
+ SET state = 'succeeded', updated_at = ?
3917
+ WHERE id = ? AND state = 'running'`
3918
+ );
3919
+ this.requeueStmt = this.db.prepare(
3920
+ `UPDATE action_queue
3921
+ SET state = 'queued',
3922
+ available_at = ?,
3923
+ last_error = ?,
3924
+ lease_owner = NULL,
3925
+ lease_expires_at = NULL,
3926
+ updated_at = ?
3927
+ WHERE id = ?`
3928
+ );
3929
+ this.deadLetterStmt = this.db.prepare(
3930
+ `UPDATE action_queue
3931
+ SET state = 'failed',
3932
+ last_error = ?,
3933
+ updated_at = ?
3934
+ WHERE id = ?`
3935
+ );
3936
+ this.selectByEngagementStmt = this.db.prepare(
3937
+ `SELECT id, engagement_id, run_id, parent_action_id, kind, priority,
3938
+ dedupe_key, params_json, state, attempt_count, max_attempts,
3939
+ available_at, lease_owner, lease_expires_at, last_error,
3940
+ created_at, updated_at
3941
+ FROM action_queue
3942
+ WHERE engagement_id = ?
3943
+ ORDER BY created_at DESC`
3944
+ );
3945
+ this.selectByEngagementStateStmt = this.db.prepare(
3946
+ `SELECT id, engagement_id, run_id, parent_action_id, kind, priority,
3947
+ dedupe_key, params_json, state, attempt_count, max_attempts,
3948
+ available_at, lease_owner, lease_expires_at, last_error,
3949
+ created_at, updated_at
3950
+ FROM action_queue
3951
+ WHERE engagement_id = ? AND state = ?
3952
+ ORDER BY created_at DESC`
3953
+ );
3954
+ this.cancelStmt = this.db.prepare(
3955
+ `UPDATE action_queue SET state = 'cancelled', updated_at = ? WHERE id = ?`
3956
+ );
3957
+ this.failTx = this.db.transaction((id, error) => {
3958
+ const item = this.findById(id);
3959
+ if (!item || item.state !== "running") return false;
3960
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3961
+ if (item.attemptCount < item.maxAttempts) {
3962
+ const backoffSec = item.attemptCount * 30;
3963
+ const availableAt = new Date(Date.now() + backoffSec * 1e3).toISOString();
3964
+ this.requeueStmt.run(availableAt, error, now, id);
3965
+ } else {
3966
+ this.deadLetterStmt.run(error, now, id);
3967
+ }
3968
+ return true;
3969
+ });
3970
+ }
3971
+ /**
3972
+ * アクションをキューに追加して返す。
3973
+ *
3974
+ * デフォルト値:
3975
+ * - priority: 100
3976
+ * - state: 'queued'
3977
+ * - paramsJson: '{}'
3978
+ * - maxAttempts: 3
3979
+ * - availableAt: 現在時刻
3980
+ */
3981
+ enqueue(input) {
3982
+ const id = crypto8.randomUUID();
3983
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3984
+ const priority = input.priority ?? 100;
3985
+ const state = input.state ?? "queued";
3986
+ const paramsJson = input.paramsJson ?? "{}";
3987
+ const maxAttempts = input.maxAttempts ?? 3;
3988
+ const availableAt = input.availableAt ?? now;
3989
+ this.insertStmt.run(
3990
+ id,
3991
+ input.engagementId,
3992
+ input.runId ?? null,
3993
+ input.parentActionId ?? null,
3994
+ input.kind,
3995
+ priority,
3996
+ input.dedupeKey,
3997
+ paramsJson,
3998
+ state,
3999
+ 0,
4000
+ // attempt_count
4001
+ maxAttempts,
4002
+ availableAt,
4003
+ null,
4004
+ // lease_owner
4005
+ null,
4006
+ // lease_expires_at
4007
+ null,
4008
+ // last_error
4009
+ now,
4010
+ // created_at
4011
+ now
4012
+ // updated_at
4013
+ );
4014
+ return {
4015
+ id,
4016
+ engagementId: input.engagementId,
4017
+ ...input.runId !== void 0 ? { runId: input.runId } : {},
4018
+ ...input.parentActionId !== void 0 ? { parentActionId: input.parentActionId } : {},
4019
+ kind: input.kind,
4020
+ priority,
4021
+ dedupeKey: input.dedupeKey,
4022
+ paramsJson,
4023
+ state,
4024
+ attemptCount: 0,
4025
+ maxAttempts,
4026
+ availableAt,
4027
+ createdAt: now,
4028
+ updatedAt: now
4029
+ };
4030
+ }
4031
+ /**
4032
+ * ID でアクションを取得する。存在しなければ undefined。
4033
+ */
4034
+ findById(id) {
4035
+ const row = this.selectByIdStmt.get(id);
4036
+ if (row === void 0) {
4037
+ return void 0;
4038
+ }
4039
+ return rowToActionQueueItem(row);
4040
+ }
4041
+ /**
4042
+ * キューから次のアクションをポーリングする。
4043
+ *
4044
+ * アトミックに state='running' に遷移し、リースを設定する。
4045
+ * available_at が現在時刻以前のもののうち、priority ASC → created_at ASC で最初の1件を取得。
4046
+ *
4047
+ * @param leaseOwner リースオーナーの識別子(例: 'worker-1')
4048
+ * @param leaseDurationSec リース期間(秒)。デフォルト 300秒。
4049
+ * @returns ポーリングしたアクション。キューが空なら undefined。
4050
+ */
4051
+ poll(leaseOwner, leaseDurationSec) {
4052
+ const duration = leaseDurationSec ?? 300;
4053
+ const now = /* @__PURE__ */ new Date();
4054
+ const nowIso = now.toISOString();
4055
+ const leaseExpiresAt = new Date(now.getTime() + duration * 1e3).toISOString();
4056
+ const row = this.pollStmt.get(
4057
+ leaseOwner,
4058
+ leaseExpiresAt,
4059
+ nowIso,
4060
+ nowIso
4061
+ );
4062
+ if (row === void 0) {
4063
+ return void 0;
4064
+ }
4065
+ return rowToActionQueueItem(row);
4066
+ }
4067
+ /**
4068
+ * アクションを正常完了にする。
4069
+ *
4070
+ * state を 'succeeded' に遷移する。running 状態でない場合は更新されない。
4071
+ *
4072
+ * @returns 更新成功時 true、id が存在しないか running でない場合 false。
4073
+ */
4074
+ complete(id) {
4075
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4076
+ const result = this.completeStmt.run(now, id);
4077
+ return result.changes > 0;
4078
+ }
4079
+ /**
4080
+ * アクションを失敗にする。
4081
+ *
4082
+ * attempt_count < max_attempts の場合: state='queued' に戻し、バックオフ付きで再スケジュール。
4083
+ * attempt_count >= max_attempts の場合: state='failed'(dead letter)。
4084
+ *
4085
+ * @param id アクション ID
4086
+ * @param error エラーメッセージ
4087
+ * @returns 更新成功時 true、id が存在しないか running でない場合 false。
4088
+ */
4089
+ fail(id, error) {
4090
+ return this.failTx(id, error);
4091
+ }
4092
+ /**
4093
+ * エンゲージメントに紐づくアクション一覧を取得する。
4094
+ *
4095
+ * @param engagementId エンゲージメント ID
4096
+ * @param state フィルタする状態(省略時は全状態)
4097
+ * @returns アクション一覧(created_at DESC 順)
4098
+ */
4099
+ findByEngagement(engagementId, state) {
4100
+ if (state !== void 0) {
4101
+ const rows2 = this.selectByEngagementStateStmt.all(engagementId, state);
4102
+ return rows2.map(rowToActionQueueItem);
4103
+ }
4104
+ const rows = this.selectByEngagementStmt.all(engagementId);
4105
+ return rows.map(rowToActionQueueItem);
4106
+ }
4107
+ /**
4108
+ * アクションをキャンセルする。
4109
+ *
4110
+ * @returns キャンセル成功時 true、id が存在しない場合 false。
4111
+ */
4112
+ cancel(id) {
4113
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4114
+ const result = this.cancelStmt.run(now, id);
4115
+ return result.changes > 0;
4116
+ }
4117
+ };
4118
+
4119
+ // src/db/repository/action-execution-repository.ts
4120
+ import crypto9 from "crypto";
4121
+ function rowToExecution(row) {
4122
+ return {
4123
+ id: row.id,
4124
+ actionId: row.action_id,
4125
+ ...row.run_id !== null ? { runId: row.run_id } : {},
4126
+ executor: row.executor,
4127
+ ...row.command !== null ? { command: row.command } : {},
4128
+ inputJson: row.input_json,
4129
+ outputJson: row.output_json,
4130
+ ...row.stdout_artifact_id !== null ? { stdoutArtifactId: row.stdout_artifact_id } : {},
4131
+ ...row.stderr_artifact_id !== null ? { stderrArtifactId: row.stderr_artifact_id } : {},
4132
+ ...row.exit_code !== null ? { exitCode: row.exit_code } : {},
4133
+ ...row.error_type !== null ? { errorType: row.error_type } : {},
4134
+ ...row.error_message !== null ? { errorMessage: row.error_message } : {},
4135
+ startedAt: row.started_at,
4136
+ ...row.finished_at !== null ? { finishedAt: row.finished_at } : {},
4137
+ ...row.duration_ms !== null ? { durationMs: row.duration_ms } : {}
4138
+ };
4139
+ }
4140
+ var ActionExecutionRepository = class {
4141
+ db;
4142
+ insertStmt;
4143
+ selectByIdStmt;
4144
+ selectByActionStmt;
4145
+ selectByRunStmt;
4146
+ constructor(db2) {
4147
+ this.db = db2;
4148
+ this.insertStmt = this.db.prepare(
4149
+ `INSERT INTO action_executions (id, action_id, run_id, executor, command, input_json, output_json, started_at)
4150
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
4151
+ );
4152
+ this.selectByIdStmt = this.db.prepare(
4153
+ `SELECT id, action_id, run_id, executor, command, input_json, output_json,
4154
+ stdout_artifact_id, stderr_artifact_id, exit_code, error_type, error_message,
4155
+ started_at, finished_at, duration_ms
4156
+ FROM action_executions WHERE id = ?`
4157
+ );
4158
+ this.selectByActionStmt = this.db.prepare(
4159
+ `SELECT id, action_id, run_id, executor, command, input_json, output_json,
4160
+ stdout_artifact_id, stderr_artifact_id, exit_code, error_type, error_message,
4161
+ started_at, finished_at, duration_ms
4162
+ FROM action_executions WHERE action_id = ? ORDER BY started_at DESC`
4163
+ );
4164
+ this.selectByRunStmt = this.db.prepare(
4165
+ `SELECT id, action_id, run_id, executor, command, input_json, output_json,
4166
+ stdout_artifact_id, stderr_artifact_id, exit_code, error_type, error_message,
4167
+ started_at, finished_at, duration_ms
4168
+ FROM action_executions WHERE run_id = ? ORDER BY started_at DESC`
4169
+ );
4170
+ }
4171
+ /**
4172
+ * ActionExecution を新規作成して返す。
4173
+ *
4174
+ * - started_at は現在時刻に自動設定
4175
+ * - inputJson デフォルト '{}'
4176
+ * - outputJson デフォルト '{}'
4177
+ */
4178
+ create(input) {
4179
+ const id = crypto9.randomUUID();
4180
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4181
+ const inputJson = input.inputJson ?? "{}";
4182
+ this.insertStmt.run(
4183
+ id,
4184
+ input.actionId,
4185
+ input.runId ?? null,
4186
+ input.executor,
4187
+ input.command ?? null,
4188
+ inputJson,
4189
+ "{}",
4190
+ // output_json default
4191
+ now
4192
+ // started_at
4193
+ );
4194
+ return {
4195
+ id,
4196
+ actionId: input.actionId,
4197
+ ...input.runId !== void 0 ? { runId: input.runId } : {},
4198
+ executor: input.executor,
4199
+ ...input.command !== void 0 ? { command: input.command } : {},
4200
+ inputJson,
4201
+ outputJson: "{}",
4202
+ startedAt: now
4203
+ };
4204
+ }
4205
+ /**
4206
+ * ID で ActionExecution を取得する。存在しなければ undefined。
4207
+ */
4208
+ findById(id) {
4209
+ const row = this.selectByIdStmt.get(id);
4210
+ if (row === void 0) {
4211
+ return void 0;
4212
+ }
4213
+ return rowToExecution(row);
4214
+ }
4215
+ /**
4216
+ * action_id で ActionExecution 一覧を取得する。
4217
+ * ORDER BY started_at DESC。
4218
+ */
4219
+ findByAction(actionId) {
4220
+ const rows = this.selectByActionStmt.all(actionId);
4221
+ return rows.map(rowToExecution);
4222
+ }
4223
+ /**
4224
+ * run_id で ActionExecution 一覧を取得する。
4225
+ * ORDER BY started_at DESC。
4226
+ */
4227
+ findByRun(runId) {
4228
+ const rows = this.selectByRunStmt.all(runId);
4229
+ return rows.map(rowToExecution);
4230
+ }
4231
+ /**
4232
+ * ActionExecution を完了状態にする。
4233
+ *
4234
+ * - finished_at を現在時刻に設定
4235
+ * - duration_ms を started_at からの差分で自動計算
4236
+ * - outputJson, exitCode, errorType, errorMessage を更新
4237
+ *
4238
+ * @returns 更新後の ActionExecution。id が存在しなければ undefined。
4239
+ */
4240
+ complete(id, output) {
4241
+ const existing = this.findById(id);
4242
+ if (existing === void 0) {
4243
+ return void 0;
4244
+ }
4245
+ const now = /* @__PURE__ */ new Date();
4246
+ const finishedAt = now.toISOString();
4247
+ const durationMs = now.getTime() - new Date(existing.startedAt).getTime();
4248
+ const setClauses = ["finished_at = ?", "duration_ms = ?"];
4249
+ const params = [finishedAt, durationMs];
4250
+ if (output.outputJson !== void 0) {
4251
+ setClauses.push("output_json = ?");
4252
+ params.push(output.outputJson);
4253
+ }
4254
+ if (output.exitCode !== void 0) {
4255
+ setClauses.push("exit_code = ?");
4256
+ params.push(output.exitCode);
4257
+ }
4258
+ if (output.errorType !== void 0) {
4259
+ setClauses.push("error_type = ?");
4260
+ params.push(output.errorType);
4261
+ }
4262
+ if (output.errorMessage !== void 0) {
4263
+ setClauses.push("error_message = ?");
4264
+ params.push(output.errorMessage);
4265
+ }
4266
+ if (output.stdoutArtifactId !== void 0) {
4267
+ setClauses.push("stdout_artifact_id = ?");
4268
+ params.push(output.stdoutArtifactId);
4269
+ }
4270
+ if (output.stderrArtifactId !== void 0) {
4271
+ setClauses.push("stderr_artifact_id = ?");
4272
+ params.push(output.stderrArtifactId);
4273
+ }
4274
+ params.push(id);
4275
+ const sql = `UPDATE action_executions SET ${setClauses.join(", ")} WHERE id = ?`;
4276
+ this.db.prepare(sql).run(...params);
4277
+ return this.findById(id);
4278
+ }
4279
+ };
4280
+
4281
+ // src/mcp/tools/ops.ts
4282
+ function registerOpsTools(server2, db2) {
4283
+ const engagementRepo = new EngagementRepository(db2);
4284
+ const runRepo = new RunRepository(db2);
4285
+ const actionQueueRepo = new ActionQueueRepository(db2);
4286
+ const actionExecRepo = new ActionExecutionRepository(db2);
4287
+ server2.tool(
4288
+ "ops",
4289
+ "Manage operational entities. Actions: create_engagement, list_engagements, get_engagement, update_engagement, delete_engagement, create_run, list_runs, get_run, update_run_status, enqueue_action, poll_action, complete_action, fail_action, cancel_action, list_actions, get_execution, list_executions",
4290
+ {
4291
+ action: z7.enum([
4292
+ "create_engagement",
4293
+ "list_engagements",
4294
+ "get_engagement",
4295
+ "update_engagement",
4296
+ "delete_engagement",
4297
+ "create_run",
4298
+ "list_runs",
4299
+ "get_run",
4300
+ "update_run_status",
4301
+ "enqueue_action",
4302
+ "poll_action",
4303
+ "complete_action",
4304
+ "fail_action",
4305
+ "cancel_action",
4306
+ "list_actions",
4307
+ "get_execution",
4308
+ "list_executions"
4309
+ ]),
4310
+ id: z7.string().optional().describe("Entity ID"),
4311
+ engagementId: z7.string().optional().describe("Engagement ID"),
4312
+ runId: z7.string().optional().describe("Run ID"),
4313
+ actionId: z7.string().optional().describe("Action queue item ID"),
4314
+ name: z7.string().optional().describe("Engagement name"),
4315
+ environment: z7.string().optional().describe("Environment (e.g. stg, prod)"),
4316
+ status: z7.string().optional().describe("Status value"),
4317
+ triggerKind: z7.string().optional().describe("Run trigger kind"),
4318
+ triggerRef: z7.string().optional().describe("Run trigger reference"),
4319
+ kind: z7.string().optional().describe("Action kind"),
4320
+ dedupeKey: z7.string().optional().describe("Action deduplication key"),
4321
+ leaseOwner: z7.string().optional().describe("Lease owner for poll"),
4322
+ executor: z7.string().optional().describe("Executor name"),
4323
+ errorMessage: z7.string().optional().describe("Error message for fail"),
4324
+ scopeJson: z7.string().optional().describe("Scope as JSON"),
4325
+ policyJson: z7.string().optional().describe("Policy as JSON"),
4326
+ scheduleCron: z7.string().optional().describe("Schedule cron expression"),
4327
+ paramsJson: z7.string().optional().describe("Action parameters as JSON"),
4328
+ summaryJson: z7.string().optional().describe("Run summary as JSON"),
4329
+ inputJson: z7.string().optional().describe("Execution input as JSON"),
4330
+ priority: z7.number().optional().describe("Action priority (lower = higher)"),
4331
+ maxAttempts: z7.number().optional().describe("Max retry attempts"),
4332
+ leaseDurationSec: z7.number().optional().describe("Lease duration in seconds"),
4333
+ limit: z7.number().optional().describe("Result limit"),
4334
+ state: z7.string().optional().describe("Action state filter"),
4335
+ parentActionId: z7.string().optional().describe("Parent action ID"),
4336
+ availableAt: z7.string().optional().describe("Available at timestamp")
4337
+ },
4338
+ async ({
4339
+ action,
4340
+ id,
4341
+ engagementId,
4342
+ runId,
4343
+ actionId,
4344
+ name,
4345
+ environment,
4346
+ status,
4347
+ triggerKind,
4348
+ triggerRef,
4349
+ kind,
4350
+ dedupeKey,
4351
+ leaseOwner,
4352
+ errorMessage,
4353
+ scopeJson,
4354
+ policyJson,
4355
+ scheduleCron,
4356
+ paramsJson,
4357
+ summaryJson,
4358
+ priority,
4359
+ maxAttempts,
4360
+ leaseDurationSec,
4361
+ limit,
4362
+ state,
4363
+ parentActionId,
4364
+ availableAt
4365
+ }) => {
4366
+ switch (action) {
4367
+ // ----------------------------------------------------------------
4368
+ // Engagement actions
4369
+ // ----------------------------------------------------------------
4370
+ case "create_engagement": {
4371
+ if (!name) {
4372
+ return {
4373
+ content: [
4374
+ { type: "text", text: "name parameter is required for create_engagement" }
4375
+ ],
4376
+ isError: true
4377
+ };
4378
+ }
4379
+ const engagement = engagementRepo.create({
4380
+ name,
4381
+ environment,
4382
+ scopeJson,
4383
+ policyJson,
4384
+ scheduleCron,
4385
+ status
4386
+ });
4387
+ return { content: [{ type: "text", text: JSON.stringify(engagement, null, 2) }] };
4388
+ }
4389
+ case "list_engagements": {
4390
+ const engagements = status ? engagementRepo.findByStatus(status) : engagementRepo.list();
4391
+ return { content: [{ type: "text", text: JSON.stringify(engagements, null, 2) }] };
4392
+ }
4393
+ case "get_engagement": {
4394
+ if (!id) {
4395
+ return {
4396
+ content: [
4397
+ { type: "text", text: "id parameter is required for get_engagement" }
4398
+ ],
4399
+ isError: true
4400
+ };
4401
+ }
4402
+ const engagement = engagementRepo.findById(id);
4403
+ if (!engagement) {
4404
+ return {
4405
+ content: [{ type: "text", text: `Engagement not found: ${id}` }],
4406
+ isError: true
4407
+ };
4408
+ }
4409
+ return { content: [{ type: "text", text: JSON.stringify(engagement, null, 2) }] };
4410
+ }
4411
+ case "update_engagement": {
4412
+ if (!id) {
4413
+ return {
4414
+ content: [
4415
+ { type: "text", text: "id parameter is required for update_engagement" }
4416
+ ],
4417
+ isError: true
4418
+ };
4419
+ }
4420
+ const fields = {};
4421
+ if (name !== void 0) fields.name = name;
4422
+ if (environment !== void 0) fields.environment = environment;
4423
+ if (scopeJson !== void 0) fields.scopeJson = scopeJson;
4424
+ if (policyJson !== void 0) fields.policyJson = policyJson;
4425
+ if (scheduleCron !== void 0) fields.scheduleCron = scheduleCron;
4426
+ if (status !== void 0) fields.status = status;
4427
+ const updated = engagementRepo.update(id, fields);
4428
+ if (!updated) {
4429
+ return {
4430
+ content: [{ type: "text", text: `Engagement not found: ${id}` }],
4431
+ isError: true
4432
+ };
4433
+ }
4434
+ return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
4435
+ }
4436
+ case "delete_engagement": {
4437
+ if (!id) {
4438
+ return {
4439
+ content: [
4440
+ { type: "text", text: "id parameter is required for delete_engagement" }
4441
+ ],
4442
+ isError: true
4443
+ };
4444
+ }
4445
+ const deleted = engagementRepo.delete(id);
4446
+ if (!deleted) {
4447
+ return {
4448
+ content: [{ type: "text", text: `Engagement not found: ${id}` }],
4449
+ isError: true
4450
+ };
4451
+ }
4452
+ return {
4453
+ content: [{ type: "text", text: `Engagement ${id} deleted successfully.` }]
4454
+ };
4455
+ }
4456
+ // ----------------------------------------------------------------
4457
+ // Run actions
4458
+ // ----------------------------------------------------------------
4459
+ case "create_run": {
4460
+ if (!engagementId) {
4461
+ return {
4462
+ content: [
4463
+ { type: "text", text: "engagementId parameter is required for create_run" }
4464
+ ],
4465
+ isError: true
4466
+ };
4467
+ }
4468
+ if (!triggerKind) {
4469
+ return {
4470
+ content: [
4471
+ { type: "text", text: "triggerKind parameter is required for create_run" }
4472
+ ],
4473
+ isError: true
4474
+ };
4475
+ }
4476
+ if (!status) {
4477
+ return {
4478
+ content: [
4479
+ { type: "text", text: "status parameter is required for create_run" }
4480
+ ],
4481
+ isError: true
4482
+ };
4483
+ }
4484
+ const run = runRepo.create({
4485
+ engagementId,
4486
+ triggerKind,
4487
+ triggerRef,
4488
+ status
4489
+ });
4490
+ return { content: [{ type: "text", text: JSON.stringify(run, null, 2) }] };
4491
+ }
4492
+ case "list_runs": {
4493
+ if (!engagementId) {
4494
+ return {
4495
+ content: [
4496
+ { type: "text", text: "engagementId parameter is required for list_runs" }
4497
+ ],
4498
+ isError: true
4499
+ };
4500
+ }
4501
+ const runs = runRepo.findByEngagement(engagementId, limit);
4502
+ return { content: [{ type: "text", text: JSON.stringify(runs, null, 2) }] };
4503
+ }
4504
+ case "get_run": {
4505
+ if (!id) {
4506
+ return {
4507
+ content: [{ type: "text", text: "id parameter is required for get_run" }],
4508
+ isError: true
4509
+ };
4510
+ }
4511
+ const run = runRepo.findById(id);
4512
+ if (!run) {
4513
+ return {
4514
+ content: [{ type: "text", text: `Run not found: ${id}` }],
4515
+ isError: true
4516
+ };
4517
+ }
4518
+ return { content: [{ type: "text", text: JSON.stringify(run, null, 2) }] };
4519
+ }
4520
+ case "update_run_status": {
4521
+ if (!id) {
4522
+ return {
4523
+ content: [
4524
+ { type: "text", text: "id parameter is required for update_run_status" }
4525
+ ],
4526
+ isError: true
4527
+ };
4528
+ }
4529
+ if (!status) {
4530
+ return {
4531
+ content: [
4532
+ { type: "text", text: "status parameter is required for update_run_status" }
4533
+ ],
4534
+ isError: true
4535
+ };
4536
+ }
4537
+ const updatedRun = runRepo.updateStatus(id, status, summaryJson);
4538
+ if (!updatedRun) {
4539
+ return {
4540
+ content: [{ type: "text", text: `Run not found: ${id}` }],
4541
+ isError: true
4542
+ };
4543
+ }
4544
+ return { content: [{ type: "text", text: JSON.stringify(updatedRun, null, 2) }] };
4545
+ }
4546
+ // ----------------------------------------------------------------
4547
+ // ActionQueue actions
4548
+ // ----------------------------------------------------------------
4549
+ case "enqueue_action": {
4550
+ if (!engagementId) {
4551
+ return {
4552
+ content: [
4553
+ {
4554
+ type: "text",
4555
+ text: "engagementId parameter is required for enqueue_action"
4556
+ }
4557
+ ],
4558
+ isError: true
4559
+ };
4560
+ }
4561
+ if (!kind) {
4562
+ return {
4563
+ content: [
4564
+ { type: "text", text: "kind parameter is required for enqueue_action" }
4565
+ ],
4566
+ isError: true
4567
+ };
4568
+ }
4569
+ if (!dedupeKey) {
4570
+ return {
4571
+ content: [
4572
+ { type: "text", text: "dedupeKey parameter is required for enqueue_action" }
4573
+ ],
4574
+ isError: true
4575
+ };
4576
+ }
4577
+ const item = actionQueueRepo.enqueue({
4578
+ engagementId,
4579
+ runId,
4580
+ parentActionId,
4581
+ kind,
4582
+ priority,
4583
+ dedupeKey,
4584
+ paramsJson,
4585
+ state,
4586
+ maxAttempts,
4587
+ availableAt
4588
+ });
4589
+ return { content: [{ type: "text", text: JSON.stringify(item, null, 2) }] };
4590
+ }
4591
+ case "poll_action": {
4592
+ if (!leaseOwner) {
4593
+ return {
4594
+ content: [
4595
+ { type: "text", text: "leaseOwner parameter is required for poll_action" }
4596
+ ],
4597
+ isError: true
4598
+ };
4599
+ }
4600
+ const polled = actionQueueRepo.poll(leaseOwner, leaseDurationSec);
4601
+ if (!polled) {
4602
+ return {
4603
+ content: [{ type: "text", text: "No action available to poll" }]
4604
+ };
4605
+ }
4606
+ return { content: [{ type: "text", text: JSON.stringify(polled, null, 2) }] };
4607
+ }
4608
+ case "complete_action": {
4609
+ if (!id) {
4610
+ return {
4611
+ content: [
4612
+ { type: "text", text: "id parameter is required for complete_action" }
4613
+ ],
4614
+ isError: true
4615
+ };
4616
+ }
4617
+ const completed = actionQueueRepo.complete(id);
4618
+ if (!completed) {
4619
+ return {
4620
+ content: [
4621
+ { type: "text", text: `Action not found or not in running state: ${id}` }
4622
+ ],
4623
+ isError: true
4624
+ };
4625
+ }
4626
+ return {
4627
+ content: [{ type: "text", text: `Action ${id} completed successfully.` }]
4628
+ };
4629
+ }
4630
+ case "fail_action": {
4631
+ if (!id) {
4632
+ return {
4633
+ content: [
4634
+ { type: "text", text: "id parameter is required for fail_action" }
4635
+ ],
4636
+ isError: true
4637
+ };
4638
+ }
4639
+ if (!errorMessage) {
4640
+ return {
4641
+ content: [
4642
+ { type: "text", text: "errorMessage parameter is required for fail_action" }
4643
+ ],
4644
+ isError: true
4645
+ };
4646
+ }
4647
+ const failed = actionQueueRepo.fail(id, errorMessage);
4648
+ if (!failed) {
4649
+ return {
4650
+ content: [
4651
+ { type: "text", text: `Action not found or not in running state: ${id}` }
4652
+ ],
4653
+ isError: true
4654
+ };
4655
+ }
4656
+ const afterFail = actionQueueRepo.findById(id);
4657
+ return {
4658
+ content: [{ type: "text", text: JSON.stringify(afterFail, null, 2) }]
4659
+ };
4660
+ }
4661
+ case "cancel_action": {
4662
+ if (!id) {
4663
+ return {
4664
+ content: [
4665
+ { type: "text", text: "id parameter is required for cancel_action" }
4666
+ ],
4667
+ isError: true
4668
+ };
4669
+ }
4670
+ const cancelled = actionQueueRepo.cancel(id);
4671
+ if (!cancelled) {
4672
+ return {
4673
+ content: [{ type: "text", text: `Action not found: ${id}` }],
4674
+ isError: true
4675
+ };
4676
+ }
4677
+ return {
4678
+ content: [{ type: "text", text: `Action ${id} cancelled successfully.` }]
4679
+ };
4680
+ }
4681
+ case "list_actions": {
4682
+ if (!engagementId) {
4683
+ return {
4684
+ content: [
4685
+ { type: "text", text: "engagementId parameter is required for list_actions" }
4686
+ ],
4687
+ isError: true
4688
+ };
4689
+ }
4690
+ const actions = actionQueueRepo.findByEngagement(engagementId, state);
4691
+ return { content: [{ type: "text", text: JSON.stringify(actions, null, 2) }] };
4692
+ }
4693
+ // ----------------------------------------------------------------
4694
+ // ActionExecution actions
4695
+ // ----------------------------------------------------------------
4696
+ case "get_execution": {
4697
+ if (!id) {
4698
+ return {
4699
+ content: [
4700
+ { type: "text", text: "id parameter is required for get_execution" }
4701
+ ],
4702
+ isError: true
4703
+ };
4704
+ }
4705
+ const execution = actionExecRepo.findById(id);
4706
+ if (!execution) {
4707
+ return {
4708
+ content: [{ type: "text", text: `Execution not found: ${id}` }],
4709
+ isError: true
4710
+ };
4711
+ }
4712
+ return { content: [{ type: "text", text: JSON.stringify(execution, null, 2) }] };
4713
+ }
4714
+ case "list_executions": {
4715
+ if (actionId) {
4716
+ const executions = actionExecRepo.findByAction(actionId);
4717
+ return {
4718
+ content: [{ type: "text", text: JSON.stringify(executions, null, 2) }]
4719
+ };
4720
+ }
4721
+ if (runId) {
4722
+ const executions = actionExecRepo.findByRun(runId);
4723
+ return {
4724
+ content: [{ type: "text", text: JSON.stringify(executions, null, 2) }]
4725
+ };
4726
+ }
4727
+ return {
4728
+ content: [
4729
+ {
4730
+ type: "text",
4731
+ text: "actionId or runId parameter is required for list_executions"
4732
+ }
4733
+ ],
4734
+ isError: true
4735
+ };
4736
+ }
4737
+ }
4738
+ }
4739
+ );
4740
+ }
4741
+
4742
+ // src/mcp/tools/findings.ts
4743
+ import { z as z8 } from "zod";
4744
+
4745
+ // src/db/repository/finding-repository.ts
4746
+ import crypto10 from "crypto";
4747
+ function rowToFinding(row) {
4748
+ return {
4749
+ id: row.id,
4750
+ engagementId: row.engagement_id,
4751
+ canonicalKey: row.canonical_key,
4752
+ ...row.node_id !== null ? { nodeId: row.node_id } : {},
4753
+ title: row.title,
4754
+ severity: row.severity,
4755
+ confidence: row.confidence,
4756
+ state: row.state,
4757
+ ...row.state_reason !== null ? { stateReason: row.state_reason } : {},
4758
+ ...row.owner !== null ? { owner: row.owner } : {},
4759
+ ...row.ticket_ref !== null ? { ticketRef: row.ticket_ref } : {},
4760
+ ...row.first_seen_run_id !== null ? { firstSeenRunId: row.first_seen_run_id } : {},
4761
+ ...row.last_seen_run_id !== null ? { lastSeenRunId: row.last_seen_run_id } : {},
4762
+ firstSeenAt: row.first_seen_at,
4763
+ lastSeenAt: row.last_seen_at,
4764
+ ...row.sla_due_at !== null ? { slaDueAt: row.sla_due_at } : {},
4765
+ attrsJson: row.attrs_json
4766
+ };
4767
+ }
4768
+ function rowToFindingEvent(row) {
4769
+ return {
4770
+ id: row.id,
4771
+ findingId: row.finding_id,
4772
+ ...row.run_id !== null ? { runId: row.run_id } : {},
4773
+ eventType: row.event_type,
4774
+ beforeJson: row.before_json,
4775
+ afterJson: row.after_json,
4776
+ ...row.artifact_id !== null ? { artifactId: row.artifact_id } : {},
4777
+ createdAt: row.created_at
4778
+ };
4779
+ }
4780
+ var FindingRepository = class {
4781
+ db;
4782
+ insertFindingStmt;
4783
+ selectFindingByIdStmt;
4784
+ deleteFindingStmt;
4785
+ updateLastSeenStmt;
4786
+ updateStateStmt;
4787
+ insertEventStmt;
4788
+ selectEventsByFindingStmt;
4789
+ constructor(db2) {
4790
+ this.db = db2;
4791
+ this.insertFindingStmt = this.db.prepare(
4792
+ `INSERT INTO findings
4793
+ (id, engagement_id, canonical_key, node_id, title, severity, confidence,
4794
+ state, state_reason, owner, ticket_ref,
4795
+ first_seen_run_id, last_seen_run_id, first_seen_at, last_seen_at,
4796
+ sla_due_at, attrs_json)
4797
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
4798
+ );
4799
+ this.selectFindingByIdStmt = this.db.prepare(
4800
+ `SELECT id, engagement_id, canonical_key, node_id, title, severity, confidence,
4801
+ state, state_reason, owner, ticket_ref,
4802
+ first_seen_run_id, last_seen_run_id, first_seen_at, last_seen_at,
4803
+ sla_due_at, attrs_json
4804
+ FROM findings WHERE id = ?`
4805
+ );
4806
+ this.deleteFindingStmt = this.db.prepare(`DELETE FROM findings WHERE id = ?`);
4807
+ this.updateLastSeenStmt = this.db.prepare(
4808
+ `UPDATE findings
4809
+ SET last_seen_at = ?, last_seen_run_id = ?,
4810
+ title = ?, severity = ?, confidence = ?, attrs_json = ?
4811
+ WHERE id = ?`
4812
+ );
4813
+ this.updateStateStmt = this.db.prepare(
4814
+ `UPDATE findings SET state = ?, state_reason = ? WHERE id = ?`
4815
+ );
4816
+ this.insertEventStmt = this.db.prepare(
4817
+ `INSERT INTO finding_events
4818
+ (id, finding_id, run_id, event_type, before_json, after_json, artifact_id, created_at)
4819
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
4820
+ );
4821
+ this.selectEventsByFindingStmt = this.db.prepare(
4822
+ `SELECT id, finding_id, run_id, event_type, before_json, after_json, artifact_id, created_at
4823
+ FROM finding_events
4824
+ WHERE finding_id = ?
4825
+ ORDER BY created_at DESC`
4826
+ );
4827
+ }
4828
+ /**
4829
+ * Finding を upsert する。
4830
+ *
4831
+ * engagement_id + canonical_key が一致する既存レコードがあれば更新(re_observed)、
4832
+ * なければ新規作成(discovered)。いずれの場合も finding_events にイベントを追加する。
4833
+ *
4834
+ * @returns { finding, created } — created は新規作成時 true、既存更新時 false
4835
+ */
4836
+ upsert(input) {
4837
+ const upsertTx = this.db.transaction((inp) => {
4838
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4839
+ const existing = this.db.prepare("SELECT * FROM findings WHERE engagement_id = ? AND canonical_key = ?").get(inp.engagementId, inp.canonicalKey);
4840
+ if (existing) {
4841
+ this.updateLastSeenStmt.run(
4842
+ now,
4843
+ inp.runId ?? null,
4844
+ inp.title,
4845
+ inp.severity,
4846
+ inp.confidence,
4847
+ inp.attrsJson ?? existing.attrs_json,
4848
+ existing.id
4849
+ );
4850
+ this.insertEventStmt.run(
4851
+ crypto10.randomUUID(),
4852
+ existing.id,
4853
+ inp.runId ?? null,
4854
+ "re_observed",
4855
+ "{}",
4856
+ "{}",
4857
+ null,
4858
+ now
4859
+ );
4860
+ return { finding: this.findById(existing.id), created: false };
4861
+ } else {
4862
+ const id = crypto10.randomUUID();
4863
+ this.insertFindingStmt.run(
4864
+ id,
4865
+ inp.engagementId,
4866
+ inp.canonicalKey,
4867
+ inp.nodeId ?? null,
4868
+ inp.title,
4869
+ inp.severity,
4870
+ inp.confidence,
4871
+ inp.state ?? "open",
4872
+ null,
4873
+ // state_reason
4874
+ null,
4875
+ // owner
4876
+ null,
4877
+ // ticket_ref
4878
+ inp.runId ?? null,
4879
+ // first_seen_run_id
4880
+ inp.runId ?? null,
4881
+ // last_seen_run_id
4882
+ now,
4883
+ // first_seen_at
4884
+ now,
4885
+ // last_seen_at
4886
+ null,
4887
+ // sla_due_at
4888
+ inp.attrsJson ?? "{}"
4889
+ );
4890
+ this.insertEventStmt.run(
4891
+ crypto10.randomUUID(),
4892
+ id,
4893
+ inp.runId ?? null,
4894
+ "discovered",
4895
+ "{}",
4896
+ "{}",
4897
+ null,
4898
+ now
4899
+ );
4900
+ return { finding: this.findById(id), created: true };
4901
+ }
4902
+ });
4903
+ return upsertTx(input);
4904
+ }
4905
+ /**
4906
+ * ID で Finding を取得する。存在しなければ undefined。
4907
+ */
4908
+ findById(id) {
4909
+ const row = this.selectFindingByIdStmt.get(id);
4910
+ if (row === void 0) {
4911
+ return void 0;
4912
+ }
4913
+ return rowToFinding(row);
4914
+ }
4915
+ /**
4916
+ * エンゲージメント ID で Finding 一覧を取得する。
4917
+ *
4918
+ * opts で state, severity を指定して絞り込み可能。
4919
+ * ORDER BY last_seen_at DESC。
4920
+ */
4921
+ findByEngagement(engagementId, opts) {
4922
+ const whereClauses = ["engagement_id = ?"];
4923
+ const params = [engagementId];
4924
+ if (opts?.state !== void 0) {
4925
+ whereClauses.push("state = ?");
4926
+ params.push(opts.state);
4927
+ }
4928
+ if (opts?.severity !== void 0) {
4929
+ whereClauses.push("severity = ?");
4930
+ params.push(opts.severity);
4931
+ }
4932
+ const sql = `SELECT id, engagement_id, canonical_key, node_id, title, severity, confidence,
4933
+ state, state_reason, owner, ticket_ref,
4934
+ first_seen_run_id, last_seen_run_id, first_seen_at, last_seen_at,
4935
+ sla_due_at, attrs_json
4936
+ FROM findings
4937
+ WHERE ${whereClauses.join(" AND ")}
4938
+ ORDER BY last_seen_at DESC`;
4939
+ const rows = this.db.prepare(sql).all(...params);
4940
+ return rows.map(rowToFinding);
4941
+ }
4942
+ /**
4943
+ * Finding の状態を更新する。
4944
+ *
4945
+ * state と state_reason を更新し、'state_change' イベントを自動追加する。
4946
+ * 存在しない ID の場合 undefined を返す。
4947
+ */
4948
+ updateState(id, state, reason) {
4949
+ const updateStateTx = this.db.transaction(
4950
+ (findingId, newState, stateReason) => {
4951
+ const existing = this.findById(findingId);
4952
+ if (existing === void 0) {
4953
+ return void 0;
4954
+ }
4955
+ const oldState = existing.state;
4956
+ this.updateStateStmt.run(newState, stateReason ?? null, findingId);
4957
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4958
+ this.insertEventStmt.run(
4959
+ crypto10.randomUUID(),
4960
+ findingId,
4961
+ null,
4962
+ // run_id
4963
+ "state_change",
4964
+ JSON.stringify({ state: oldState }),
4965
+ JSON.stringify({ state: newState }),
4966
+ null,
4967
+ // artifact_id
4968
+ now
4969
+ );
4970
+ return this.findById(findingId);
4971
+ }
4972
+ );
4973
+ return updateStateTx(id, state, reason);
4974
+ }
4975
+ /**
4976
+ * Finding にイベントを手動追加する。
4977
+ */
4978
+ addEvent(findingId, event) {
4979
+ const id = crypto10.randomUUID();
4980
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4981
+ this.insertEventStmt.run(
4982
+ id,
4983
+ findingId,
4984
+ event.runId ?? null,
4985
+ event.eventType,
4986
+ event.beforeJson ?? "{}",
4987
+ event.afterJson ?? "{}",
4988
+ event.artifactId ?? null,
4989
+ now
4990
+ );
4991
+ return {
4992
+ id,
4993
+ findingId,
4994
+ ...event.runId !== void 0 ? { runId: event.runId } : {},
4995
+ eventType: event.eventType,
4996
+ beforeJson: event.beforeJson ?? "{}",
4997
+ afterJson: event.afterJson ?? "{}",
4998
+ ...event.artifactId !== void 0 ? { artifactId: event.artifactId } : {},
4999
+ createdAt: now
5000
+ };
5001
+ }
5002
+ /**
5003
+ * Finding のイベント一覧を取得する。
5004
+ *
5005
+ * ORDER BY created_at DESC(最新が先頭)。
5006
+ */
5007
+ getEvents(findingId) {
5008
+ const rows = this.selectEventsByFindingStmt.all(findingId);
5009
+ return rows.map(rowToFindingEvent);
5010
+ }
5011
+ /**
5012
+ * Finding を削除する。
5013
+ *
5014
+ * CASCADE により関連する finding_events も同時に削除される。
5015
+ *
5016
+ * @returns 削除成功時 true、id が存在しない場合 false。
5017
+ */
5018
+ delete(id) {
5019
+ const result = this.deleteFindingStmt.run(id);
5020
+ return result.changes > 0;
5021
+ }
5022
+ };
5023
+
5024
+ // src/db/repository/risk-snapshot-repository.ts
5025
+ import crypto11 from "crypto";
5026
+ function rowToRiskSnapshot(row) {
5027
+ return {
5028
+ id: row.id,
5029
+ engagementId: row.engagement_id,
5030
+ ...row.run_id !== null ? { runId: row.run_id } : {},
5031
+ score: row.score,
5032
+ openCritical: row.open_critical,
5033
+ openHigh: row.open_high,
5034
+ openMedium: row.open_medium,
5035
+ openLow: row.open_low,
5036
+ openInfo: row.open_info,
5037
+ openTotal: row.open_total,
5038
+ attackPathCount: row.attack_path_count,
5039
+ exposedCredCount: row.exposed_cred_count,
5040
+ ...row.model_version !== null ? { modelVersion: row.model_version } : {},
5041
+ attrsJson: row.attrs_json,
5042
+ createdAt: row.created_at
5043
+ };
5044
+ }
5045
+ var RiskSnapshotRepository = class {
5046
+ db;
5047
+ insertStmt;
5048
+ selectByIdStmt;
5049
+ selectByEngagementStmt;
5050
+ latestStmt;
5051
+ constructor(db2) {
5052
+ this.db = db2;
5053
+ this.insertStmt = this.db.prepare(
5054
+ `INSERT INTO risk_snapshots
5055
+ (id, engagement_id, run_id, score,
5056
+ open_critical, open_high, open_medium, open_low, open_info, open_total,
5057
+ attack_path_count, exposed_cred_count, model_version,
5058
+ attrs_json, created_at)
5059
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
5060
+ );
5061
+ this.selectByIdStmt = this.db.prepare(
5062
+ `SELECT id, engagement_id, run_id, score,
5063
+ open_critical, open_high, open_medium, open_low, open_info, open_total,
5064
+ attack_path_count, exposed_cred_count, model_version,
5065
+ attrs_json, created_at
5066
+ FROM risk_snapshots WHERE id = ?`
5067
+ );
5068
+ this.selectByEngagementStmt = this.db.prepare(
5069
+ `SELECT id, engagement_id, run_id, score,
5070
+ open_critical, open_high, open_medium, open_low, open_info, open_total,
5071
+ attack_path_count, exposed_cred_count, model_version,
5072
+ attrs_json, created_at
5073
+ FROM risk_snapshots
5074
+ WHERE engagement_id = ?
5075
+ ORDER BY created_at DESC
5076
+ LIMIT ?`
5077
+ );
5078
+ this.latestStmt = this.db.prepare(
5079
+ `SELECT id, engagement_id, run_id, score,
5080
+ open_critical, open_high, open_medium, open_low, open_info, open_total,
5081
+ attack_path_count, exposed_cred_count, model_version,
5082
+ attrs_json, created_at
5083
+ FROM risk_snapshots
5084
+ WHERE engagement_id = ?
5085
+ ORDER BY created_at DESC
5086
+ LIMIT 1`
5087
+ );
5088
+ }
5089
+ /**
5090
+ * RiskSnapshot を新規作成して返す。
5091
+ *
5092
+ * デフォルト値:
5093
+ * - openCritical, openHigh, openMedium, openLow, openInfo, openTotal: 0
5094
+ * - attackPathCount: 0
5095
+ * - exposedCredCount: 0
5096
+ * - attrsJson: '{}'
5097
+ * - createdAt: 現在時刻(ISO 8601)
5098
+ */
5099
+ create(input) {
5100
+ const id = crypto11.randomUUID();
5101
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
5102
+ const runId = input.runId ?? null;
5103
+ const openCritical = input.openCritical ?? 0;
5104
+ const openHigh = input.openHigh ?? 0;
5105
+ const openMedium = input.openMedium ?? 0;
5106
+ const openLow = input.openLow ?? 0;
5107
+ const openInfo = input.openInfo ?? 0;
5108
+ const openTotal = input.openTotal ?? 0;
5109
+ const attackPathCount = input.attackPathCount ?? 0;
5110
+ const exposedCredCount = input.exposedCredCount ?? 0;
5111
+ const modelVersion = input.modelVersion ?? null;
5112
+ const attrsJson = input.attrsJson ?? "{}";
5113
+ this.insertStmt.run(
5114
+ id,
5115
+ input.engagementId,
5116
+ runId,
5117
+ input.score,
5118
+ openCritical,
5119
+ openHigh,
5120
+ openMedium,
5121
+ openLow,
5122
+ openInfo,
5123
+ openTotal,
5124
+ attackPathCount,
5125
+ exposedCredCount,
5126
+ modelVersion,
5127
+ attrsJson,
5128
+ createdAt
5129
+ );
5130
+ return {
5131
+ id,
5132
+ engagementId: input.engagementId,
5133
+ ...runId !== null ? { runId } : {},
5134
+ score: input.score,
5135
+ openCritical,
5136
+ openHigh,
5137
+ openMedium,
5138
+ openLow,
5139
+ openInfo,
5140
+ openTotal,
5141
+ attackPathCount,
5142
+ exposedCredCount,
5143
+ ...modelVersion !== null ? { modelVersion } : {},
5144
+ attrsJson,
5145
+ createdAt
5146
+ };
5147
+ }
5148
+ /**
5149
+ * ID で RiskSnapshot を取得する。存在しなければ undefined。
5150
+ */
5151
+ findById(id) {
5152
+ const row = this.selectByIdStmt.get(id);
5153
+ if (row === void 0) {
5154
+ return void 0;
5155
+ }
5156
+ return rowToRiskSnapshot(row);
5157
+ }
5158
+ /**
5159
+ * エンゲージメントに紐づく RiskSnapshot 一覧を取得する。
5160
+ *
5161
+ * created_at DESC でソートし、limit で件数を制限する(デフォルト 100)。
5162
+ *
5163
+ * @param engagementId エンゲージメント ID
5164
+ * @param limit 最大取得件数(デフォルト 100)
5165
+ * @returns RiskSnapshot 一覧(created_at DESC 順)
5166
+ */
5167
+ findByEngagement(engagementId, limit) {
5168
+ const effectiveLimit = limit ?? 100;
5169
+ const rows = this.selectByEngagementStmt.all(engagementId, effectiveLimit);
5170
+ return rows.map(rowToRiskSnapshot);
5171
+ }
5172
+ /**
5173
+ * エンゲージメントの最新 RiskSnapshot を取得する。
5174
+ *
5175
+ * スナップショットが存在しない場合は undefined を返す。
5176
+ *
5177
+ * @param engagementId エンゲージメント ID
5178
+ * @returns 最新の RiskSnapshot、または undefined
5179
+ */
5180
+ latest(engagementId) {
5181
+ const row = this.latestStmt.get(engagementId);
5182
+ if (row === void 0) {
5183
+ return void 0;
5184
+ }
5185
+ return rowToRiskSnapshot(row);
5186
+ }
5187
+ };
5188
+
5189
+ // src/mcp/tools/findings.ts
5190
+ function registerFindingsTools(server2, db2) {
5191
+ const findingRepo = new FindingRepository(db2);
5192
+ const riskSnapshotRepo = new RiskSnapshotRepository(db2);
5193
+ server2.tool(
5194
+ "findings",
5195
+ "Manage findings and risk snapshots. Actions: upsert_finding, get_finding, list_findings, update_finding_state, list_finding_events, create_risk_snapshot, get_risk_snapshot, list_risk_snapshots, latest_risk_snapshot",
5196
+ {
5197
+ action: z8.enum([
5198
+ "upsert_finding",
5199
+ "get_finding",
5200
+ "list_findings",
5201
+ "update_finding_state",
5202
+ "list_finding_events",
5203
+ "create_risk_snapshot",
5204
+ "get_risk_snapshot",
5205
+ "list_risk_snapshots",
5206
+ "latest_risk_snapshot"
5207
+ ]),
5208
+ id: z8.string().optional().describe("Entity ID"),
5209
+ engagementId: z8.string().optional().describe("Engagement ID"),
5210
+ canonicalKey: z8.string().optional().describe("Finding canonical key"),
5211
+ nodeId: z8.string().optional().describe("Associated node ID"),
5212
+ title: z8.string().optional().describe("Finding title"),
5213
+ severity: z8.string().optional().describe("Finding severity"),
5214
+ confidence: z8.string().optional().describe("Finding confidence"),
5215
+ state: z8.string().optional().describe("Finding state"),
5216
+ stateReason: z8.string().optional().describe("Reason for state change"),
5217
+ runId: z8.string().optional().describe("Run ID"),
5218
+ findingId: z8.string().optional().describe("Finding ID for events"),
5219
+ attrsJson: z8.string().optional().describe("Attributes as JSON"),
5220
+ score: z8.number().optional().describe("Risk score"),
5221
+ openCritical: z8.number().optional().describe("Open critical count"),
5222
+ openHigh: z8.number().optional().describe("Open high count"),
5223
+ openMedium: z8.number().optional().describe("Open medium count"),
5224
+ openLow: z8.number().optional().describe("Open low count"),
5225
+ openInfo: z8.number().optional().describe("Open info count"),
5226
+ openTotal: z8.number().optional().describe("Open total count"),
5227
+ attackPathCount: z8.number().optional().describe("Attack path count"),
5228
+ exposedCredCount: z8.number().optional().describe("Exposed credential count"),
5229
+ modelVersion: z8.string().optional().describe("Risk model version"),
5230
+ limit: z8.number().optional().describe("Result limit")
5231
+ },
5232
+ async ({
5233
+ action,
5234
+ id,
5235
+ engagementId,
5236
+ canonicalKey,
5237
+ nodeId,
5238
+ title,
5239
+ severity,
5240
+ confidence,
5241
+ state,
5242
+ stateReason,
5243
+ runId,
5244
+ findingId,
5245
+ attrsJson,
5246
+ score,
5247
+ openCritical,
5248
+ openHigh,
5249
+ openMedium,
5250
+ openLow,
5251
+ openInfo,
5252
+ openTotal,
5253
+ attackPathCount,
5254
+ exposedCredCount,
5255
+ modelVersion,
5256
+ limit
5257
+ }) => {
5258
+ switch (action) {
5259
+ // ----------------------------------------------------------------
5260
+ // Finding actions
5261
+ // ----------------------------------------------------------------
5262
+ case "upsert_finding": {
5263
+ if (!engagementId) {
5264
+ return {
5265
+ content: [
5266
+ {
5267
+ type: "text",
5268
+ text: "engagementId parameter is required for upsert_finding"
5269
+ }
5270
+ ],
5271
+ isError: true
5272
+ };
5273
+ }
5274
+ if (!canonicalKey) {
5275
+ return {
5276
+ content: [
5277
+ {
5278
+ type: "text",
5279
+ text: "canonicalKey parameter is required for upsert_finding"
5280
+ }
5281
+ ],
5282
+ isError: true
5283
+ };
5284
+ }
5285
+ if (!title) {
5286
+ return {
5287
+ content: [
5288
+ { type: "text", text: "title parameter is required for upsert_finding" }
5289
+ ],
5290
+ isError: true
5291
+ };
5292
+ }
5293
+ if (!severity) {
5294
+ return {
5295
+ content: [
5296
+ { type: "text", text: "severity parameter is required for upsert_finding" }
5297
+ ],
5298
+ isError: true
5299
+ };
5300
+ }
5301
+ if (!confidence) {
5302
+ return {
5303
+ content: [
5304
+ {
5305
+ type: "text",
5306
+ text: "confidence parameter is required for upsert_finding"
5307
+ }
5308
+ ],
5309
+ isError: true
5310
+ };
5311
+ }
5312
+ const result = findingRepo.upsert({
5313
+ engagementId,
5314
+ canonicalKey,
5315
+ title,
5316
+ severity,
5317
+ confidence,
5318
+ nodeId,
5319
+ state,
5320
+ runId,
5321
+ attrsJson
5322
+ });
5323
+ return {
5324
+ content: [
5325
+ {
5326
+ type: "text",
5327
+ text: JSON.stringify({ ...result.finding, created: result.created }, null, 2)
5328
+ }
5329
+ ]
5330
+ };
5331
+ }
5332
+ case "get_finding": {
5333
+ if (!id) {
5334
+ return {
5335
+ content: [
5336
+ { type: "text", text: "id parameter is required for get_finding" }
5337
+ ],
5338
+ isError: true
5339
+ };
5340
+ }
5341
+ const finding = findingRepo.findById(id);
5342
+ if (!finding) {
5343
+ return {
5344
+ content: [{ type: "text", text: `Finding not found: ${id}` }],
5345
+ isError: true
5346
+ };
5347
+ }
5348
+ return { content: [{ type: "text", text: JSON.stringify(finding, null, 2) }] };
5349
+ }
5350
+ case "list_findings": {
5351
+ if (!engagementId) {
5352
+ return {
5353
+ content: [
5354
+ {
5355
+ type: "text",
5356
+ text: "engagementId parameter is required for list_findings"
5357
+ }
5358
+ ],
5359
+ isError: true
5360
+ };
5361
+ }
5362
+ const opts = {};
5363
+ if (state) opts.state = state;
5364
+ if (severity) opts.severity = severity;
5365
+ const findings = findingRepo.findByEngagement(engagementId, opts);
5366
+ return {
5367
+ content: [{ type: "text", text: JSON.stringify(findings, null, 2) }]
5368
+ };
5369
+ }
5370
+ case "update_finding_state": {
5371
+ if (!id) {
5372
+ return {
5373
+ content: [
5374
+ { type: "text", text: "id parameter is required for update_finding_state" }
5375
+ ],
5376
+ isError: true
5377
+ };
5378
+ }
5379
+ if (!state) {
5380
+ return {
5381
+ content: [
5382
+ {
5383
+ type: "text",
5384
+ text: "state parameter is required for update_finding_state"
5385
+ }
5386
+ ],
5387
+ isError: true
5388
+ };
5389
+ }
5390
+ const updated = findingRepo.updateState(id, state, stateReason);
5391
+ if (!updated) {
5392
+ return {
5393
+ content: [{ type: "text", text: `Finding not found: ${id}` }],
5394
+ isError: true
5395
+ };
5396
+ }
5397
+ return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
5398
+ }
5399
+ case "list_finding_events": {
5400
+ if (!findingId) {
5401
+ return {
5402
+ content: [
5403
+ {
5404
+ type: "text",
5405
+ text: "findingId parameter is required for list_finding_events"
5406
+ }
5407
+ ],
5408
+ isError: true
5409
+ };
5410
+ }
5411
+ const events = findingRepo.getEvents(findingId);
5412
+ return { content: [{ type: "text", text: JSON.stringify(events, null, 2) }] };
5413
+ }
5414
+ // ----------------------------------------------------------------
5415
+ // RiskSnapshot actions
5416
+ // ----------------------------------------------------------------
5417
+ case "create_risk_snapshot": {
5418
+ if (!engagementId) {
5419
+ return {
5420
+ content: [
5421
+ {
5422
+ type: "text",
5423
+ text: "engagementId parameter is required for create_risk_snapshot"
5424
+ }
5425
+ ],
5426
+ isError: true
5427
+ };
5428
+ }
5429
+ if (score === void 0 || score === null) {
5430
+ return {
5431
+ content: [
5432
+ {
5433
+ type: "text",
5434
+ text: "score parameter is required for create_risk_snapshot"
5435
+ }
5436
+ ],
5437
+ isError: true
5438
+ };
5439
+ }
5440
+ const snapshot = riskSnapshotRepo.create({
5441
+ engagementId,
5442
+ score,
5443
+ runId,
5444
+ openCritical,
5445
+ openHigh,
5446
+ openMedium,
5447
+ openLow,
5448
+ openInfo,
5449
+ openTotal,
5450
+ attackPathCount,
5451
+ exposedCredCount,
5452
+ modelVersion,
5453
+ attrsJson
5454
+ });
5455
+ return {
5456
+ content: [{ type: "text", text: JSON.stringify(snapshot, null, 2) }]
5457
+ };
5458
+ }
5459
+ case "get_risk_snapshot": {
5460
+ if (!id) {
5461
+ return {
5462
+ content: [
5463
+ { type: "text", text: "id parameter is required for get_risk_snapshot" }
5464
+ ],
5465
+ isError: true
5466
+ };
5467
+ }
5468
+ const snapshot = riskSnapshotRepo.findById(id);
5469
+ if (!snapshot) {
5470
+ return {
5471
+ content: [{ type: "text", text: `Risk snapshot not found: ${id}` }],
5472
+ isError: true
5473
+ };
5474
+ }
5475
+ return { content: [{ type: "text", text: JSON.stringify(snapshot, null, 2) }] };
5476
+ }
5477
+ case "list_risk_snapshots": {
5478
+ if (!engagementId) {
5479
+ return {
5480
+ content: [
5481
+ {
5482
+ type: "text",
5483
+ text: "engagementId parameter is required for list_risk_snapshots"
5484
+ }
5485
+ ],
5486
+ isError: true
5487
+ };
5488
+ }
5489
+ const snapshots = riskSnapshotRepo.findByEngagement(engagementId, limit);
5490
+ return {
5491
+ content: [{ type: "text", text: JSON.stringify(snapshots, null, 2) }]
5492
+ };
5493
+ }
5494
+ case "latest_risk_snapshot": {
5495
+ if (!engagementId) {
5496
+ return {
5497
+ content: [
5498
+ {
5499
+ type: "text",
5500
+ text: "engagementId parameter is required for latest_risk_snapshot"
5501
+ }
5502
+ ],
5503
+ isError: true
5504
+ };
5505
+ }
5506
+ const latest = riskSnapshotRepo.latest(engagementId);
5507
+ if (!latest) {
5508
+ return {
5509
+ content: [
5510
+ {
5511
+ type: "text",
5512
+ text: `No risk snapshot found for engagement: ${engagementId}`
5513
+ }
5514
+ ],
5515
+ isError: true
5516
+ };
5517
+ }
5518
+ return { content: [{ type: "text", text: JSON.stringify(latest, null, 2) }] };
5519
+ }
5520
+ }
5521
+ }
5522
+ );
5523
+ }
5524
+
3542
5525
  // src/mcp/resources.ts
3543
5526
  function registerResources(server2, db2) {
3544
5527
  const nodeRepo = new NodeRepository(db2);
@@ -3669,6 +5652,8 @@ function createMcpServer(db2, version) {
3669
5652
  registerIngestTool(server2, db2);
3670
5653
  registerProposeTool(server2, db2);
3671
5654
  registerKbTools(server2, db2);
5655
+ registerOpsTools(server2, db2);
5656
+ registerFindingsTools(server2, db2);
3672
5657
  registerResources(server2, db2);
3673
5658
  return server2;
3674
5659
  }