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 +1985 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
}
|