@argosvix/mcp-server 0.26.2-alpha.1 → 0.28.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,45 +19,56 @@ describe("MCP version source of truth", () => {
19
19
  });
20
20
  });
21
21
  describe("MCP tools metadata", () => {
22
- it("exposes 57 tools (56 prior + axis 4 Tier 2 第二弾: auto_silence_noisy_alert)", () => {
22
+ it("exposes 71 tools (70 prior + Team read 1 件: list_members。 招待/ロール変更/削除の mutation は承認ゲート経由前提で非提供)", () => {
23
23
  expect(tools.map((t) => t.name).sort()).toEqual([
24
24
  "acknowledge_alert",
25
25
  "aggregate_calls",
26
+ "apply_promo_code_to_customer",
26
27
  "auto_silence_noisy_alert",
27
28
  "bulk_delete_calls",
28
29
  "classify_calls_batch",
29
30
  "compare_eval_runs",
30
31
  "create_alert",
31
32
  "create_annotation",
33
+ "create_budget_gate",
32
34
  "create_eval_criterion",
35
+ "create_policy_gate",
33
36
  "create_project",
34
37
  "create_prompt",
35
38
  "create_saved_view",
36
39
  "delete_alert",
37
40
  "delete_annotation",
41
+ "delete_budget_gate",
38
42
  "delete_eval_criterion",
43
+ "delete_policy_gate",
39
44
  "delete_project",
40
45
  "delete_prompt",
41
46
  "delete_saved_view",
42
47
  "detect_anomaly",
43
48
  "export_calls",
49
+ "extend_customer_trial",
44
50
  "get_account_health",
45
51
  "get_alert",
46
52
  "get_annotation",
53
+ "get_approval",
54
+ "get_budget_gate",
47
55
  "get_cost_summary",
48
56
  "get_eval_criterion",
49
57
  "get_eval_run",
50
58
  "get_llm_budget",
51
59
  "get_percentiles",
60
+ "get_policy_gate",
52
61
  "get_prompt",
53
62
  "get_safety_assessment",
54
63
  "list_alert_events",
55
64
  "list_alerts",
56
65
  "list_annotations_by_label",
57
66
  "list_annotations_for_call",
67
+ "list_approvals",
58
68
  "list_audit_log",
59
69
  "list_eval_criteria",
60
70
  "list_eval_runs",
71
+ "list_members",
61
72
  "list_projects",
62
73
  "list_prompts",
63
74
  "list_safety_assessments",
@@ -69,6 +80,7 @@ describe("MCP tools metadata", () => {
69
80
  "raise_llm_budget",
70
81
  "rename_project",
71
82
  "rename_prompt",
83
+ "request_approval",
72
84
  "retry_failed_webhook",
73
85
  "run_eval",
74
86
  "silence_alert",
@@ -76,7 +88,9 @@ describe("MCP tools metadata", () => {
76
88
  "unsilence_alert",
77
89
  "update_alert",
78
90
  "update_annotation",
91
+ "update_budget_gate",
79
92
  "update_eval_criterion",
93
+ "update_policy_gate",
80
94
  "update_prompt",
81
95
  ]);
82
96
  });
@@ -90,21 +104,22 @@ describe("MCP tools metadata", () => {
90
104
  it("create_alert.alertType enum matches backend ALERT_TYPES (= enum drift 回帰防止)", () => {
91
105
  // backend packages/backend/src/types.ts ALERT_TYPES と完全一致させる。
92
106
  // ここが drift すると mismatch した type の create_alert が backend で 400 になる。
93
- // 2026-06-06 narrative gap fix carry: 既存 hard-coded list は backend と drift
94
- // していた (= cost_daily / cost_monthly / latency_p95 backend に存在しない、
95
- // backend は cost_threshold / monthly_budget / latency_degradation)。
96
- // gate constant も 同じ MCP description から copy していたため drift を検出
97
- // できていなかった。 backend `src/alerts/types.ts` の ALERT_TYPES を source of
98
- // truth として hard-code 更新。
99
- const BACKEND_ALERT_TYPES = [
100
- "cost_threshold",
101
- "error_rate",
102
- "latency_degradation",
103
- "monthly_budget",
104
- "anomaly_cost",
105
- "anomaly_latency",
106
- "anomaly_error_rate",
107
- ];
107
+ // 2026-06-06 R37 medium #1 fix carry: hard-coded list は backend と drift
108
+ // していた (= 旧 list: cost_daily / cost_monthly / latency_p95 backend reality:
109
+ // cost_threshold / monthly_budget / latency_degradation)。 同 hard-code を MCP
110
+ // description から copy していたため drift gate test 自体が drift を検出できない
111
+ // 構造軸だった。 backend `src/alerts/types.ts` の ALERT_TYPES を test 時点で
112
+ // fs.readFileSync で 読み出して regex 抽出 → これで source of truth backend
113
+ // 側に固定、 backend が enum を 追加すると 本 test が 自動的に拾って fail する。
114
+ const here2 = dirname(fileURLToPath(import.meta.url));
115
+ const backendTypesPath = resolve(here2, "..", "..", "backend", "src", "alerts", "types.ts");
116
+ const backendTypesContent = readFileSync(backendTypesPath, "utf8");
117
+ const arrayMatch = backendTypesContent.match(/export const ALERT_TYPES = \[([\s\S]*?)\] as const;/);
118
+ if (!arrayMatch) {
119
+ throw new Error("ALERT_TYPES export not found in backend/src/alerts/types.ts (= drift gate が source-of-truth を 見失った、 backend の ALERT_TYPES export 形を 確認してください)");
120
+ }
121
+ const BACKEND_ALERT_TYPES = Array.from(arrayMatch[1].matchAll(/"([^"]+)"/g)).map((m) => m[1]);
122
+ expect(BACKEND_ALERT_TYPES.length).toBeGreaterThan(0);
108
123
  const createAlert = tools.find((t) => t.name === "create_alert");
109
124
  const schema = createAlert?.inputSchema;
110
125
  const enumValues = schema?.properties?.alertType?.enum ?? [];
@@ -166,6 +181,20 @@ describe("dispatchTool", () => {
166
181
  // 7d 後ろから now まで = 終端より始端が前
167
182
  expect(new Date(body.startTime).getTime()).toBeLessThan(new Date(body.endTime).getTime());
168
183
  });
184
+ it("R74 HIGH 1 regression: query_calls の latencyMin/Max が outgoing body に乗る (= allowlist 落ち防止)", async () => {
185
+ const res = await dispatchTool({
186
+ name: "query_calls",
187
+ args: { latencyMin: 1500, latencyMax: 2500 },
188
+ apiKey: "argosvix_live_test",
189
+ apiBase: "https://ingest.example.com",
190
+ });
191
+ expect(res.isError).toBeUndefined();
192
+ const fetchMock = global.fetch;
193
+ const init = fetchMock.mock.calls[0]?.[1];
194
+ const body = JSON.parse(init.body);
195
+ expect(body.latencyMin).toBe(1500);
196
+ expect(body.latencyMax).toBe(2500);
197
+ });
169
198
  it("get_cost_summary uses /v1/query/aggregate endpoint", async () => {
170
199
  const res = await dispatchTool({
171
200
  name: "get_cost_summary",
@@ -177,6 +206,28 @@ describe("dispatchTool", () => {
177
206
  const parsed = JSON.parse(res.content[0]?.text ?? "{}");
178
207
  expect(parsed.groups[0].provider).toBe("openai");
179
208
  });
209
+ it("list_members GETs /v1/memberships (= #31 Team read tool)", async () => {
210
+ vi.stubGlobal("fetch", vi.fn(async () => new Response(JSON.stringify({
211
+ members: [
212
+ { id: "mem_1", email: "a@example.com", role: "admin", status: "active" },
213
+ ],
214
+ }), { status: 200, headers: { "content-type": "application/json" } })));
215
+ const res = await dispatchTool({
216
+ name: "list_members",
217
+ args: {},
218
+ apiKey: "argosvix_live_test",
219
+ apiBase: "https://ingest.example.com",
220
+ });
221
+ expect(res.isError).toBeUndefined();
222
+ const fetchMock = global.fetch;
223
+ const fetchedUrl = String(fetchMock.mock.calls[0]?.[0]);
224
+ expect(fetchedUrl).toContain("/v1/memberships");
225
+ // read tool = GET (= 明示 method 指定なし → callApi default GET)
226
+ const init = fetchMock.mock.calls[0]?.[1];
227
+ expect(init?.method ?? "GET").toBe("GET");
228
+ const parsed = JSON.parse(res.content[0]?.text ?? "{}");
229
+ expect(parsed.members[0].role).toBe("admin");
230
+ });
180
231
  it("classify_calls_batch POSTs to /v1/safety-assessments/scan-batch with maxRecords", async () => {
181
232
  vi.stubGlobal("fetch", vi.fn(async () => new Response(JSON.stringify({ scanned: 10, assessed: 9, flagged: 1, failures: 0, skipped: 1 }), { status: 200 })));
182
233
  const res = await dispatchTool({
@@ -1443,5 +1494,151 @@ describe("dispatchTool", () => {
1443
1494
  expect(url).toContain("limit=5");
1444
1495
  expect(url).not.toContain("account_id");
1445
1496
  });
1497
+ // 2026-06-09 v1.7 hygiene #13 carry = v1.5/v1.6 で追加した 7 safety/eval
1498
+ // read tools の path + method 固定 test。 既存は metadata gate のみで dispatch
1499
+ // path/body 軸の structural drift 防御がなかった軸を carry。
1500
+ // R51 LOW carry (2026-06-10): toContain → new URL(...).pathname + exact searchParams
1501
+ // で suffix/prefix drift を 構造的に検出 (= 旧 path = `/v1/eval-runs` が
1502
+ // `/v1/eval-runs/foo` でも pass する弱さを narrow)。
1503
+ function urlOf(call) {
1504
+ return new URL(String(call));
1505
+ }
1506
+ it("list_eval_criteria GETs /v1/eval-criteria", async () => {
1507
+ vi.stubGlobal("fetch", vi.fn(async () => new Response(JSON.stringify({ criteria: [] }), {
1508
+ status: 200,
1509
+ headers: { "content-type": "application/json" },
1510
+ })));
1511
+ await dispatchTool({
1512
+ name: "list_eval_criteria",
1513
+ args: {},
1514
+ apiKey: "argosvix_live_test",
1515
+ apiBase: "https://ingest.example.com",
1516
+ });
1517
+ const fetchMock = global.fetch;
1518
+ const u = urlOf(fetchMock.mock.calls[0]?.[0]);
1519
+ const init = fetchMock.mock.calls[0]?.[1];
1520
+ expect(u.pathname).toBe("/v1/eval-criteria");
1521
+ expect(Array.from(u.searchParams.keys())).toEqual([]);
1522
+ expect(init.method ?? "GET").toBe("GET");
1523
+ });
1524
+ it("get_eval_criterion GETs /v1/eval-criteria/:id", async () => {
1525
+ vi.stubGlobal("fetch", vi.fn(async () => new Response(JSON.stringify({ id: 42, name: "accuracy" }), {
1526
+ status: 200,
1527
+ headers: { "content-type": "application/json" },
1528
+ })));
1529
+ await dispatchTool({
1530
+ name: "get_eval_criterion",
1531
+ args: { criterionId: 42 },
1532
+ apiKey: "argosvix_live_test",
1533
+ apiBase: "https://ingest.example.com",
1534
+ });
1535
+ const fetchMock = global.fetch;
1536
+ const u = urlOf(fetchMock.mock.calls[0]?.[0]);
1537
+ const init = fetchMock.mock.calls[0]?.[1];
1538
+ expect(u.pathname).toBe("/v1/eval-criteria/42");
1539
+ expect(Array.from(u.searchParams.keys())).toEqual([]);
1540
+ expect(init.method ?? "GET").toBe("GET");
1541
+ });
1542
+ it("list_safety_assessments GETs /v1/safety-assessments with call_id + limit", async () => {
1543
+ vi.stubGlobal("fetch", vi.fn(async () => new Response(JSON.stringify({ assessments: [] }), {
1544
+ status: 200,
1545
+ headers: { "content-type": "application/json" },
1546
+ })));
1547
+ await dispatchTool({
1548
+ name: "list_safety_assessments",
1549
+ args: { callId: "call_abc123", limit: 25 },
1550
+ apiKey: "argosvix_live_test",
1551
+ apiBase: "https://ingest.example.com",
1552
+ });
1553
+ const fetchMock = global.fetch;
1554
+ const u = urlOf(fetchMock.mock.calls[0]?.[0]);
1555
+ const init = fetchMock.mock.calls[0]?.[1];
1556
+ expect(u.pathname).toBe("/v1/safety-assessments");
1557
+ expect(u.searchParams.get("call_id")).toBe("call_abc123");
1558
+ expect(u.searchParams.get("limit")).toBe("25");
1559
+ expect(init.method ?? "GET").toBe("GET");
1560
+ });
1561
+ it("get_safety_assessment GETs /v1/safety-assessments/:id", async () => {
1562
+ vi.stubGlobal("fetch", vi.fn(async () => new Response(JSON.stringify({ id: 7, flagged: 0 }), {
1563
+ status: 200,
1564
+ headers: { "content-type": "application/json" },
1565
+ })));
1566
+ await dispatchTool({
1567
+ name: "get_safety_assessment",
1568
+ args: { assessmentId: 7 },
1569
+ apiKey: "argosvix_live_test",
1570
+ apiBase: "https://ingest.example.com",
1571
+ });
1572
+ const fetchMock = global.fetch;
1573
+ const u = urlOf(fetchMock.mock.calls[0]?.[0]);
1574
+ const init = fetchMock.mock.calls[0]?.[1];
1575
+ expect(u.pathname).toBe("/v1/safety-assessments/7");
1576
+ expect(Array.from(u.searchParams.keys())).toEqual([]);
1577
+ expect(init.method ?? "GET").toBe("GET");
1578
+ });
1579
+ it("list_eval_runs GETs /v1/eval-runs with limit", async () => {
1580
+ vi.stubGlobal("fetch", vi.fn(async () => new Response(JSON.stringify({ runs: [] }), {
1581
+ status: 200,
1582
+ headers: { "content-type": "application/json" },
1583
+ })));
1584
+ await dispatchTool({
1585
+ name: "list_eval_runs",
1586
+ args: { limit: 15 },
1587
+ apiKey: "argosvix_live_test",
1588
+ apiBase: "https://ingest.example.com",
1589
+ });
1590
+ const fetchMock = global.fetch;
1591
+ const u = urlOf(fetchMock.mock.calls[0]?.[0]);
1592
+ const init = fetchMock.mock.calls[0]?.[1];
1593
+ expect(u.pathname).toBe("/v1/eval-runs");
1594
+ expect(u.searchParams.get("limit")).toBe("15");
1595
+ expect(init.method ?? "GET").toBe("GET");
1596
+ });
1597
+ it("get_eval_run GETs /v1/eval-runs/:id", async () => {
1598
+ vi.stubGlobal("fetch", vi.fn(async () => new Response(JSON.stringify({ id: 99, status: "completed" }), {
1599
+ status: 200,
1600
+ headers: { "content-type": "application/json" },
1601
+ })));
1602
+ await dispatchTool({
1603
+ name: "get_eval_run",
1604
+ args: { runId: 99 },
1605
+ apiKey: "argosvix_live_test",
1606
+ apiBase: "https://ingest.example.com",
1607
+ });
1608
+ const fetchMock = global.fetch;
1609
+ const u = urlOf(fetchMock.mock.calls[0]?.[0]);
1610
+ const init = fetchMock.mock.calls[0]?.[1];
1611
+ expect(u.pathname).toBe("/v1/eval-runs/99");
1612
+ expect(Array.from(u.searchParams.keys())).toEqual([]);
1613
+ expect(init.method ?? "GET").toBe("GET");
1614
+ });
1615
+ it("run_eval POSTs to /v1/eval-runs with name + recentCount + idempotencyKey", async () => {
1616
+ vi.stubGlobal("fetch", vi.fn(async () => new Response(JSON.stringify({ id: 100, status: "queued" }), {
1617
+ status: 200,
1618
+ headers: { "content-type": "application/json" },
1619
+ })));
1620
+ await dispatchTool({
1621
+ name: "run_eval",
1622
+ args: {
1623
+ name: "baseline-2026-06-09",
1624
+ recentCount: 30,
1625
+ idempotencyKey: "idem-abc",
1626
+ },
1627
+ apiKey: "argosvix_live_test",
1628
+ apiBase: "https://ingest.example.com",
1629
+ });
1630
+ const fetchMock = global.fetch;
1631
+ const u = urlOf(fetchMock.mock.calls[0]?.[0]);
1632
+ const init = fetchMock.mock.calls[0]?.[1];
1633
+ expect(u.pathname).toBe("/v1/eval-runs");
1634
+ expect(Array.from(u.searchParams.keys())).toEqual([]);
1635
+ expect(init.method).toBe("POST");
1636
+ const body = JSON.parse(init.body);
1637
+ expect(body).toEqual({
1638
+ name: "baseline-2026-06-09",
1639
+ recentCount: 30,
1640
+ idempotencyKey: "idem-abc",
1641
+ });
1642
+ });
1446
1643
  });
1447
1644
  //# sourceMappingURL=tools.test.js.map