jui-tools-mcp-server 2.16.0 → 2.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/test.yml +2 -1
- package/README.md +16 -1
- package/dist/index.js +16 -1
- package/dist/tools/infra/infra_check.d.ts +10 -0
- package/dist/tools/infra/infra_check.js +43 -0
- package/dist/tools/infra/infra_common.d.ts +40 -0
- package/dist/tools/infra/infra_common.js +44 -0
- package/dist/tools/infra/infra_cost.d.ts +3 -0
- package/dist/tools/infra/infra_cost.js +118 -0
- package/dist/tools/infra/infra_load_validate.d.ts +8 -0
- package/dist/tools/infra/infra_load_validate.js +39 -0
- package/dist/tools/infra/infra_prices_validate.d.ts +7 -0
- package/dist/tools/infra/infra_prices_validate.js +34 -0
- package/dist/tools/infra/infra_seed.d.ts +27 -0
- package/dist/tools/infra/infra_seed.js +81 -0
- package/dist/tools/infra/infra_validate.d.ts +8 -0
- package/dist/tools/infra/infra_validate.js +37 -0
- package/package.json +1 -1
- package/scripts/record-infra-fixtures.sh +78 -0
- package/src/index.ts +17 -1
- package/src/tools/infra/infra_check.ts +49 -0
- package/src/tools/infra/infra_common.ts +69 -0
- package/src/tools/infra/infra_cost.ts +122 -0
- package/src/tools/infra/infra_load_validate.ts +46 -0
- package/src/tools/infra/infra_prices_validate.ts +41 -0
- package/src/tools/infra/infra_seed.ts +88 -0
- package/src/tools/infra/infra_validate.ts +44 -0
- package/tests/fixtures/infra/RECORDED_FROM +1 -0
- package/tests/fixtures/infra/check-infra.json +101 -0
- package/tests/fixtures/infra/cost-infra.day.json +10080 -0
- package/tests/fixtures/infra/cost-infra.month-by-node.json +484 -0
- package/tests/fixtures/infra/cost-infra.month.json +381 -0
- package/tests/fixtures/infra/cost-infra.refused.json +5 -0
- package/tests/fixtures/infra/seed-infra.draft.json +233 -0
- package/tests/fixtures/infra/seed-infra.report.json +46 -0
- package/tests/fixtures/infra/validate-infra-load.errors.json +39 -0
- package/tests/fixtures/infra/validate-infra-load.ok.json +29 -0
- package/tests/fixtures/infra/validate-infra-load.unreadable.json +6 -0
- package/tests/fixtures/infra/validate-infra-prices.ok.json +9 -0
- package/tests/fixtures/infra/validate-infra.errors.json +39 -0
- package/tests/fixtures/infra/validate-infra.ok.json +30 -0
- package/tests/fixtures/infra/validate-infra.unreadable.json +6 -0
- package/tests/tool_count.test.ts +1 -1
- package/tests/tools_infra.test.ts +332 -0
|
@@ -11,13 +11,14 @@ jobs:
|
|
|
11
11
|
strategy:
|
|
12
12
|
fail-fast: false
|
|
13
13
|
matrix:
|
|
14
|
-
node-version: [
|
|
14
|
+
node-version: [22, 24]
|
|
15
15
|
steps:
|
|
16
16
|
- uses: actions/checkout@v4
|
|
17
17
|
|
|
18
18
|
- uses: actions/setup-node@v4
|
|
19
19
|
with:
|
|
20
20
|
node-version: ${{ matrix.node-version }}
|
|
21
|
+
check-latest: true # pick up security releases before the runner image's toolcache does
|
|
21
22
|
cache: npm
|
|
22
23
|
|
|
23
24
|
# --ignore-scripts keeps CI hermetic: the postinstall hook
|
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ bash ~/.jsonui-mcp-server/install.sh
|
|
|
46
46
|
bash ~/.jsonui-mcp-server/uninstall.sh
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
## Available Tools (
|
|
49
|
+
## Available Tools (57)
|
|
50
50
|
|
|
51
51
|
### Group A: コンポーネント仕様参照 (9)
|
|
52
52
|
|
|
@@ -140,6 +140,21 @@ Claude Code のセッションを見るローカルのビューア(`jui viz`
|
|
|
140
140
|
|
|
141
141
|
> フックと中継の導入(`jui viz hooks install` / `jui viz relay install`)は `~/.claude/settings.json` と利用者の MCP 設定を書くので **MCP 非公開**(CLI で行う)。jui が古いと(`viz` / `start` / `--with-relay` が無い)、結果の `update_jui` に更新の手順が出る。
|
|
142
142
|
|
|
143
|
+
### Group H: サーバールーム(infra) (6)
|
|
144
|
+
|
|
145
|
+
`docs/infra`(モデル・flow・負荷・単価)を確かめ、下書きし、値段を付ける。どれも `jsonui-doc … --json` を呼び、その JSON を短くまとめて返す(**jsonui-cli 2.3.15 以降**)。書き方は jsonui-cli の `document_tools/INFRA.md`。
|
|
146
|
+
|
|
147
|
+
| Tool | Description | CLI |
|
|
148
|
+
|------|-------------|-----|
|
|
149
|
+
| `infra_validate` | モデルを読めるか(読めない時は理由)、使ったファイル・数・flow の数、problems を重さとコードで数え、最初の 20 件 | `jsonui-doc validate infra <project> --json` |
|
|
150
|
+
| `infra_load_validate` | 負荷のシナリオ(id・環境・月・時間・行・edge)と problems(node・hour・quantity つき) | `jsonui-doc validate infra-load <project> --json` |
|
|
151
|
+
| `infra_prices_validate` | 部屋の単価のファイル・単価の数・problems | `jsonui-doc validate infra-prices <project> --json` |
|
|
152
|
+
| `infra_seed` | IaC・設定ファイル・コードから下書き。数・node・読んだ物・置けなかった物(unmatched)・skipped・notes。既定では一時フォルダに書いて読み戻し、プロジェクトは変えない(`write: true` で `<infra.dir>/seed`) | `jsonui-doc seed infra <project> -o <dir> [--force]` |
|
|
153
|
+
| `infra_check` | 宣言した `builtin:infra-iac` / `infra-code` / `infra-load` の結果(status・数・ok でない結果の最初の 20 件・レポートの道) | `jsonui-doc check infra -p <project> --json` |
|
|
154
|
+
| `infra_cost` | 月の合計・provider(か node)ごとの合計・上位 node・一番高い日・単価なし/規則なし/負荷なし・容量を超えた時間・注意。`date` で 1 日(上位 node の meter ごとの額と単価の層、ピークの時刻)。負荷の problems と単価の警告も | `jsonui-doc cost infra <project> --json [--scenario] [--date] [--by]` |
|
|
155
|
+
|
|
156
|
+
> 単価の取得(`jui viz cloud-prices fetch`)はネットへ出て viz のホームに書くので **MCP 非公開**(CLI で行う)。jsonui-doc が古くて `--json` の形が違うと、どのキーが無いかを名指しして失敗する。
|
|
157
|
+
|
|
143
158
|
## Testing
|
|
144
159
|
|
|
145
160
|
単体テストは [vitest](https://vitest.dev/)(ESM ネイティブで本リポジトリのモジュール構成とそのまま噛み合うため採用)。
|
package/dist/index.js
CHANGED
|
@@ -52,6 +52,12 @@ import { register as registerVizDoctor } from "./tools/viz/viz_doctor.js";
|
|
|
52
52
|
import { register as registerVizStart } from "./tools/viz/viz_start.js";
|
|
53
53
|
import { register as registerVizStop } from "./tools/viz/viz_stop.js";
|
|
54
54
|
import { register as registerVizOpen } from "./tools/viz/viz_open.js";
|
|
55
|
+
import { register as registerInfraValidate } from "./tools/infra/infra_validate.js";
|
|
56
|
+
import { register as registerInfraLoadValidate } from "./tools/infra/infra_load_validate.js";
|
|
57
|
+
import { register as registerInfraPricesValidate } from "./tools/infra/infra_prices_validate.js";
|
|
58
|
+
import { register as registerInfraSeed } from "./tools/infra/infra_seed.js";
|
|
59
|
+
import { register as registerInfraCheck } from "./tools/infra/infra_check.js";
|
|
60
|
+
import { register as registerInfraCost } from "./tools/infra/infra_cost.js";
|
|
55
61
|
// --- Group F: jsonui-test CLI ---
|
|
56
62
|
import { register as registerTestValidate } from "./tools/test/test_validate.js";
|
|
57
63
|
import { register as registerTestGenerateScreen } from "./tools/test/test_generate_screen.js";
|
|
@@ -147,7 +153,16 @@ registerVizDoctor(server);
|
|
|
147
153
|
registerVizStart(server);
|
|
148
154
|
registerVizStop(server);
|
|
149
155
|
registerVizOpen(server);
|
|
150
|
-
|
|
156
|
+
// Register Group H: the server room's infra (6 tools) — jsonui-doc's validate / seed / check / cost infra, each
|
|
157
|
+
// summarised from its --json (jsonui-cli 2.3.15+). `jui viz cloud-prices fetch` stays with the CLI (it goes to the
|
|
158
|
+
// network and writes the viz home).
|
|
159
|
+
registerInfraValidate(server, config);
|
|
160
|
+
registerInfraLoadValidate(server, config);
|
|
161
|
+
registerInfraPricesValidate(server, config);
|
|
162
|
+
registerInfraSeed(server, config);
|
|
163
|
+
registerInfraCheck(server, config);
|
|
164
|
+
registerInfraCost(server, config);
|
|
165
|
+
log(`Registered 57 tools (9 spec + 7 context + 8 jui + 9 doc + 3 api + 10 test + 5 viz + 6 infra)`);
|
|
151
166
|
async function main() {
|
|
152
167
|
const transport = new StdioServerTransport();
|
|
153
168
|
await server.connect(transport);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { ServerConfig } from "../../config.js";
|
|
3
|
+
/**
|
|
4
|
+
* `jsonui-doc check infra -p <project> --json`: the declared infra checks (jui.config.json → checks:
|
|
5
|
+
* builtin:infra-iac, builtin:infra-code, builtin:infra-load) — the model against the IaC, the code, and the load
|
|
6
|
+
* definitions. This is the one jsonui-doc command that runs project-declared checks; the infra ones read files only.
|
|
7
|
+
* Exit 0: clean (warnings alone are 0); 1: a mismatch (a finding of a check declared with gate: true); 2: an
|
|
8
|
+
* execution error, or no infra check is declared.
|
|
9
|
+
*/
|
|
10
|
+
export declare function register(server: McpServer, config: ServerConfig): void;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { FIRST, failure, need, runDocJson, text } from "./infra_common.js";
|
|
3
|
+
/**
|
|
4
|
+
* `jsonui-doc check infra -p <project> --json`: the declared infra checks (jui.config.json → checks:
|
|
5
|
+
* builtin:infra-iac, builtin:infra-code, builtin:infra-load) — the model against the IaC, the code, and the load
|
|
6
|
+
* definitions. This is the one jsonui-doc command that runs project-declared checks; the infra ones read files only.
|
|
7
|
+
* Exit 0: clean (warnings alone are 0); 1: a mismatch (a finding of a check declared with gate: true); 2: an
|
|
8
|
+
* execution error, or no infra check is declared.
|
|
9
|
+
*/
|
|
10
|
+
export function register(server, config) {
|
|
11
|
+
server.tool("infra_check", "Run the project's declared infra checks with `jsonui-doc check infra --json` (jui.config.json → checks: builtin:infra-iac = the model against Terraform/Compose/CloudFormation/k8s/wrangler, builtin:infra-code = against the code's evidence, SDK imports and env names, builtin:infra-load = against the load definitions). Returns each check's status (ok / mismatch / error), counts, the first 20 results that are not ok (target, status, expected, actual, message), warnings, and its report file. Findings are warnings unless the check sets gate: true. Needs jsonui-cli 2.3.15+.", {
|
|
12
|
+
project_dir: z.string().optional().describe("Project directory with jui.config.json (overrides JUI_PROJECT_DIR env)"),
|
|
13
|
+
}, async ({ project_dir }) => {
|
|
14
|
+
try {
|
|
15
|
+
const projectDir = config.resolveProjectDir(project_dir);
|
|
16
|
+
const { exitCode, data, stderr } = await runDocJson(["check", "infra", "-p", projectDir, "--json"], projectDir, 180_000);
|
|
17
|
+
need(data, ["ok", "exit", "checks"], "check infra");
|
|
18
|
+
if (!data.checks.length) {
|
|
19
|
+
return text({ ok: false, exit: exitCode, checks: [],
|
|
20
|
+
message: stderr.trim() || "no builtin:infra-* check is declared in jui.config.json → checks",
|
|
21
|
+
declared: data.declared ?? [] });
|
|
22
|
+
}
|
|
23
|
+
return text({
|
|
24
|
+
ok: data.ok,
|
|
25
|
+
exit: exitCode,
|
|
26
|
+
checks: data.checks.map((c) => {
|
|
27
|
+
need(c, ["name", "type", "status"], "check infra check");
|
|
28
|
+
if (c.status === "error")
|
|
29
|
+
return { name: c.name, type: c.type, status: "error", error: c.error };
|
|
30
|
+
need(c, ["summary", "results", "warnings", "report"], "check infra check");
|
|
31
|
+
return {
|
|
32
|
+
name: c.name, type: c.type, status: c.status, summary: c.summary,
|
|
33
|
+
results: c.results.slice(0, FIRST), resultCount: c.results.length,
|
|
34
|
+
warnings: c.warnings.slice(0, FIRST), report: c.report,
|
|
35
|
+
};
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
return failure(e);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared by the Group H tools: run a `jsonui-doc … --json` infra command and read its one JSON object.
|
|
3
|
+
*
|
|
4
|
+
* The tools summarise that object for an agent (counts, the first problems, the totals), so the keys they read are
|
|
5
|
+
* the CLI's contract. `need` names a key the object lacks instead of summarising nothing — a jsonui-doc older or
|
|
6
|
+
* newer than these tools says so rather than reading as an empty room. The tests feed the CLI's own recorded output
|
|
7
|
+
* (tests/fixtures/infra, scripts/record-infra-fixtures.sh).
|
|
8
|
+
*/
|
|
9
|
+
export interface JsonRun {
|
|
10
|
+
exitCode: number;
|
|
11
|
+
data: any;
|
|
12
|
+
stderr: string;
|
|
13
|
+
}
|
|
14
|
+
export declare class ContractError extends Error {
|
|
15
|
+
}
|
|
16
|
+
export declare const FIRST = 20;
|
|
17
|
+
export declare function runDocJson(args: string[], cwd: string, timeout?: number): Promise<JsonRun>;
|
|
18
|
+
export declare function need(obj: any, keys: string[], what: string): void;
|
|
19
|
+
/** A problem list as counts (by severity, by code) and the first FIRST, each as written. */
|
|
20
|
+
export declare function problems(list: any[], what: string, keep?: string[]): {
|
|
21
|
+
count: number;
|
|
22
|
+
errors: number;
|
|
23
|
+
warnings: number;
|
|
24
|
+
byCode: Record<string, number>;
|
|
25
|
+
first: {
|
|
26
|
+
[k: string]: any;
|
|
27
|
+
}[];
|
|
28
|
+
};
|
|
29
|
+
export declare function text(value: unknown): {
|
|
30
|
+
content: {
|
|
31
|
+
type: "text";
|
|
32
|
+
text: string;
|
|
33
|
+
}[];
|
|
34
|
+
};
|
|
35
|
+
export declare function failure(e: any): {
|
|
36
|
+
content: {
|
|
37
|
+
type: "text";
|
|
38
|
+
text: string;
|
|
39
|
+
}[];
|
|
40
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { runCli } from "../../cli_runner.js";
|
|
2
|
+
export class ContractError extends Error {
|
|
3
|
+
}
|
|
4
|
+
export const FIRST = 20;
|
|
5
|
+
export async function runDocJson(args, cwd, timeout) {
|
|
6
|
+
const result = await runCli("jsonui-doc", args, { cwd, timeout });
|
|
7
|
+
let data;
|
|
8
|
+
try {
|
|
9
|
+
data = JSON.parse(result.stdout);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
throw new ContractError(`jsonui-doc ${args.slice(0, 2).join(" ")} printed no JSON (exit ${result.exitCode})` +
|
|
13
|
+
(result.stderr ? `: ${result.stderr.trim().split("\n").slice(-3).join(" / ")}` : "") +
|
|
14
|
+
" — it needs jsonui-cli 2.3.15 or later for --json here");
|
|
15
|
+
}
|
|
16
|
+
return { exitCode: result.exitCode, data, stderr: result.stderr };
|
|
17
|
+
}
|
|
18
|
+
export function need(obj, keys, what) {
|
|
19
|
+
const missing = keys.filter((k) => obj == null || !(k in obj));
|
|
20
|
+
if (missing.length) {
|
|
21
|
+
throw new ContractError(`${what}: the JSON has no ${missing.map((k) => `"${k}"`).join(", ")} — a jsonui-doc these tools do not know; ` +
|
|
22
|
+
"update jsonui-cli or this server");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** A problem list as counts (by severity, by code) and the first FIRST, each as written. */
|
|
26
|
+
export function problems(list, what, keep = ["severity", "code", "file", "path", "message"]) {
|
|
27
|
+
if (!Array.isArray(list))
|
|
28
|
+
throw new ContractError(`${what}: "problems" is not a list`);
|
|
29
|
+
const bySeverity = {};
|
|
30
|
+
const byCode = {};
|
|
31
|
+
for (const p of list) {
|
|
32
|
+
need(p, ["severity", "code"], `${what} problem`);
|
|
33
|
+
bySeverity[p.severity] = (bySeverity[p.severity] ?? 0) + 1;
|
|
34
|
+
byCode[p.code] = (byCode[p.code] ?? 0) + 1;
|
|
35
|
+
}
|
|
36
|
+
const first = list.slice(0, FIRST).map((p) => Object.fromEntries(keep.filter((k) => p[k] !== undefined && p[k] !== null).map((k) => [k, p[k]])));
|
|
37
|
+
return { count: list.length, errors: bySeverity.error ?? 0, warnings: bySeverity.warning ?? 0, byCode, first };
|
|
38
|
+
}
|
|
39
|
+
export function text(value) {
|
|
40
|
+
return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
|
|
41
|
+
}
|
|
42
|
+
export function failure(e) {
|
|
43
|
+
return { content: [{ type: "text", text: `Error: ${e.message}` }] };
|
|
44
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { FIRST, failure, need, problems, runDocJson, text } from "./infra_common.js";
|
|
3
|
+
/**
|
|
4
|
+
* `jsonui-doc cost infra <project> --json`: the cost simulator's month (or one day) for a load scenario, the same
|
|
5
|
+
* simulation and shapes as jui viz's コスト. The month's whole reply (every day's stack) and above all a day's (every
|
|
6
|
+
* node's 24 hours with each meter's price trace, ~300 KB) are too long for an agent; this keeps what answers "what
|
|
7
|
+
* does it cost and why": the total, the totals per stack key, the top nodes, what has no price / rule / load, the
|
|
8
|
+
* hours over capacity, the notes, the load problems and the price-book warnings — and for a day, each top node's
|
|
9
|
+
* charge by meter with the price layer it came from. Exit 0: simulated; 1: simulated, a load file has an error (left
|
|
10
|
+
* out); 2: nothing simulated (reason).
|
|
11
|
+
*/
|
|
12
|
+
function monthSummary(m) {
|
|
13
|
+
need(m, ["scenario", "month", "usd", "by", "keys", "days", "noPrice", "noRule", "loadUndefined", "overflowHours",
|
|
14
|
+
"warnings", "warningCount"], "cost infra month");
|
|
15
|
+
const totals = {};
|
|
16
|
+
for (const d of m.days) {
|
|
17
|
+
need(d, ["date", "usd", "stack"], "cost infra day of the month");
|
|
18
|
+
for (const [k, v] of Object.entries(d.stack))
|
|
19
|
+
totals[k] = (totals[k] ?? 0) + v;
|
|
20
|
+
}
|
|
21
|
+
const label = Object.fromEntries(m.keys.map((k) => [k.key, k.label]));
|
|
22
|
+
return {
|
|
23
|
+
scenario: m.scenario,
|
|
24
|
+
month: m.month,
|
|
25
|
+
usd: m.usd,
|
|
26
|
+
by: m.by,
|
|
27
|
+
totals: Object.entries(totals).sort((a, b) => b[1] - a[1])
|
|
28
|
+
.map(([key, usd]) => ({ key: key === "__others__" ? "others" : key, label: label[key] ?? key, usd })),
|
|
29
|
+
days: m.days.length,
|
|
30
|
+
dearestDay: m.days.reduce((a, b) => (b.usd > a.usd ? b : a), m.days[0])?.date ?? null,
|
|
31
|
+
noPrice: m.noPrice,
|
|
32
|
+
noRule: m.noRule,
|
|
33
|
+
loadUndefined: m.loadUndefined,
|
|
34
|
+
overflowHours: m.overflowHours,
|
|
35
|
+
notes: m.warnings.slice(0, FIRST).map((w) => (typeof w === "string" ? w : w.message)),
|
|
36
|
+
noteCount: m.warningCount,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function daySummary(day) {
|
|
40
|
+
need(day, ["scenario", "date", "usd", "nodes", "others"], "cost infra day");
|
|
41
|
+
return {
|
|
42
|
+
scenario: day.scenario,
|
|
43
|
+
date: day.date,
|
|
44
|
+
usd: day.usd,
|
|
45
|
+
nodes: day.nodes.map((n) => {
|
|
46
|
+
need(n, ["node", "usd", "hours"], "cost infra day node");
|
|
47
|
+
const meters = {};
|
|
48
|
+
let peak = { hour: 0, usd: -1 };
|
|
49
|
+
n.hours.forEach((h, i) => {
|
|
50
|
+
if (h.usd > peak.usd)
|
|
51
|
+
peak = { hour: day.hours?.[i] ?? i, usd: h.usd };
|
|
52
|
+
for (const s of h.segments ?? []) {
|
|
53
|
+
const m = (meters[s.meter] ??= { usd: 0, amount: 0, unit: s.unit, sku: s.sku, layer: s.price?.layer ?? null });
|
|
54
|
+
m.usd += s.usd ?? 0;
|
|
55
|
+
m.amount += s.amount ?? 0;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
const overflowHours = n.hours.filter((h) => h.load && h.load.overflow > 0).length;
|
|
59
|
+
return {
|
|
60
|
+
node: n.node, label: n.label, provider: n.provider, rule: n.rule, note: n.note || undefined, usd: n.usd,
|
|
61
|
+
peakHour: peak.usd >= 0 ? peak.hour : null, meters, overflowHours: overflowHours || undefined,
|
|
62
|
+
};
|
|
63
|
+
}),
|
|
64
|
+
others: { count: day.others.count, usd: day.others.usd.reduce((a, b) => a + b, 0) },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export function register(server, config) {
|
|
68
|
+
server.tool("infra_cost", "Simulate the server room's cost with `jsonui-doc cost infra --json` (the model, a load scenario under docs/infra/load, prices: the room's own, then fetched, then bundled — USD, on demand). Without date: the month's total, totals by provider (or by node), the top nodes, the dearest day, what has no price / no rule / no load, hours over capacity, notes. With date (YYYY-MM-DD in the scenario's month): that day's top nodes, each with its charge by meter, the price layer, and its peak hour. Also the scenarios there are, the load problems and the price-book warnings (prices.free_dropped: a room's price row dropping a bundled free amount). Needs jsonui-cli 2.3.15+.", {
|
|
69
|
+
project_dir: z.string().optional().describe("Project directory with jui.config.json (overrides JUI_PROJECT_DIR env)"),
|
|
70
|
+
scenario: z.string().optional().describe("Load scenario id (default: the first)"),
|
|
71
|
+
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("One day of the scenario's month (YYYY-MM-DD)"),
|
|
72
|
+
by: z.enum(["provider", "node"]).optional().describe("Group the month's totals by provider (default) or node"),
|
|
73
|
+
}, async ({ project_dir, scenario, date, by }) => {
|
|
74
|
+
try {
|
|
75
|
+
const projectDir = config.resolveProjectDir(project_dir);
|
|
76
|
+
const args = ["cost", "infra", projectDir, "--json"];
|
|
77
|
+
if (scenario)
|
|
78
|
+
args.push("--scenario", scenario);
|
|
79
|
+
if (date)
|
|
80
|
+
args.push("--date", date);
|
|
81
|
+
if (by)
|
|
82
|
+
args.push("--by", by);
|
|
83
|
+
const { exitCode, data } = await runDocJson(args, projectDir, 180_000);
|
|
84
|
+
need(data, ["ok"], "cost infra");
|
|
85
|
+
if (!data.ok) {
|
|
86
|
+
need(data, ["reason", "message"], "cost infra");
|
|
87
|
+
return text({ ok: false, exit: exitCode, reason: data.reason, message: data.message,
|
|
88
|
+
...(data.problems ? { problems: problems(data.problems, "cost infra") } : {}) });
|
|
89
|
+
}
|
|
90
|
+
need(data, ["dir", "scenario", "scenarios", "problems", "prices"], "cost infra");
|
|
91
|
+
need(data.problems, ["count", "errors", "first"], "cost infra problems");
|
|
92
|
+
need(data.prices, ["warnings", "warningCount"], "cost infra prices");
|
|
93
|
+
const out = {
|
|
94
|
+
ok: exitCode === 0,
|
|
95
|
+
exit: exitCode,
|
|
96
|
+
dir: data.dir,
|
|
97
|
+
scenario: data.scenario,
|
|
98
|
+
scenarios: data.scenarios,
|
|
99
|
+
loadProblems: data.problems,
|
|
100
|
+
priceWarnings: data.prices.warnings,
|
|
101
|
+
priceWarningCount: data.prices.warningCount,
|
|
102
|
+
};
|
|
103
|
+
if (date) {
|
|
104
|
+
need(data, ["day"], "cost infra");
|
|
105
|
+
out.day = daySummary(data.day);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
need(data, ["month", "topNodes"], "cost infra");
|
|
109
|
+
out.month = monthSummary(data.month);
|
|
110
|
+
out.topNodes = data.topNodes;
|
|
111
|
+
}
|
|
112
|
+
return text(out);
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
return failure(e);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { ServerConfig } from "../../config.js";
|
|
3
|
+
/**
|
|
4
|
+
* `jsonui-doc validate infra-load <project> --json`: the load scenarios the cost simulator reads (<infra.dir>/load,
|
|
5
|
+
* or infra.load_dir) against their schema and against the model. Exit 0: no error; 1: errors; 2: the model or the
|
|
6
|
+
* folder cannot be read (reason).
|
|
7
|
+
*/
|
|
8
|
+
export declare function register(server: McpServer, config: ServerConfig): void;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { failure, need, problems, runDocJson, text } from "./infra_common.js";
|
|
3
|
+
/**
|
|
4
|
+
* `jsonui-doc validate infra-load <project> --json`: the load scenarios the cost simulator reads (<infra.dir>/load,
|
|
5
|
+
* or infra.load_dir) against their schema and against the model. Exit 0: no error; 1: errors; 2: the model or the
|
|
6
|
+
* folder cannot be read (reason).
|
|
7
|
+
*/
|
|
8
|
+
export function register(server, config) {
|
|
9
|
+
server.tool("infra_load_validate", "Validate the server room's load scenarios (docs/infra/load/*.json) with `jsonui-doc validate infra-load --json`. Returns each scenario (id, environment, month, hours, loads, edges) and the problems by severity and code with the first 20 (file, path, code, message, node, hour, quantity). load.overlap means a node has two rows and only one counts. Needs jsonui-cli 2.3.15+.", {
|
|
10
|
+
project_dir: z.string().optional().describe("Project directory with jui.config.json (overrides JUI_PROJECT_DIR env)"),
|
|
11
|
+
}, async ({ project_dir }) => {
|
|
12
|
+
try {
|
|
13
|
+
const projectDir = config.resolveProjectDir(project_dir);
|
|
14
|
+
const { exitCode, data } = await runDocJson(["validate", "infra-load", projectDir, "--json"], projectDir);
|
|
15
|
+
need(data, ["ok"], "validate infra-load");
|
|
16
|
+
if (!data.ok) {
|
|
17
|
+
need(data, ["reason", "message"], "validate infra-load");
|
|
18
|
+
return text({ ok: false, exit: exitCode, readable: false, reason: data.reason, message: data.message });
|
|
19
|
+
}
|
|
20
|
+
need(data, ["dir", "files", "problems", "scenarios"], "validate infra-load");
|
|
21
|
+
for (const s of data.scenarios)
|
|
22
|
+
need(s, ["id", "environment", "month", "hours", "loads", "edges"], "validate infra-load scenario");
|
|
23
|
+
return text({
|
|
24
|
+
ok: exitCode === 0,
|
|
25
|
+
exit: exitCode,
|
|
26
|
+
readable: true,
|
|
27
|
+
dir: data.dir,
|
|
28
|
+
files: data.files,
|
|
29
|
+
scenarios: data.scenarios.map((s) => ({
|
|
30
|
+
id: s.id, file: s.file, environment: s.environment, month: s.month, hours: s.hours, loads: s.loads, edges: s.edges,
|
|
31
|
+
})),
|
|
32
|
+
problems: problems(data.problems, "validate infra-load", ["severity", "code", "file", "path", "message", "node", "hour", "quantity"]),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
return failure(e);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { ServerConfig } from "../../config.js";
|
|
3
|
+
/**
|
|
4
|
+
* `jsonui-doc validate infra-prices <project> --json`: the room's own price files (<infra.dir>/prices, or
|
|
5
|
+
* infra.prices_dir). Exit 0: no error; 1: errors; 2: the folder cannot be read (reason).
|
|
6
|
+
*/
|
|
7
|
+
export declare function register(server: McpServer, config: ServerConfig): void;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { failure, need, problems, runDocJson, text } from "./infra_common.js";
|
|
3
|
+
/**
|
|
4
|
+
* `jsonui-doc validate infra-prices <project> --json`: the room's own price files (<infra.dir>/prices, or
|
|
5
|
+
* infra.prices_dir). Exit 0: no error; 1: errors; 2: the folder cannot be read (reason).
|
|
6
|
+
*/
|
|
7
|
+
export function register(server, config) {
|
|
8
|
+
server.tool("infra_prices_validate", "Validate the server room's own price files (docs/infra/prices/*.json — only for what the bundled prices lack) with `jsonui-doc validate infra-prices --json`. Returns the files used, how many prices, and the problems by severity and code with the first 20. Needs jsonui-cli 2.3.15+.", {
|
|
9
|
+
project_dir: z.string().optional().describe("Project directory with jui.config.json (overrides JUI_PROJECT_DIR env)"),
|
|
10
|
+
}, async ({ project_dir }) => {
|
|
11
|
+
try {
|
|
12
|
+
const projectDir = config.resolveProjectDir(project_dir);
|
|
13
|
+
const { exitCode, data } = await runDocJson(["validate", "infra-prices", projectDir, "--json"], projectDir);
|
|
14
|
+
need(data, ["ok"], "validate infra-prices");
|
|
15
|
+
if (!data.ok) {
|
|
16
|
+
need(data, ["reason", "message"], "validate infra-prices");
|
|
17
|
+
return text({ ok: false, exit: exitCode, readable: false, reason: data.reason, message: data.message });
|
|
18
|
+
}
|
|
19
|
+
need(data, ["dir", "files", "problems", "count"], "validate infra-prices");
|
|
20
|
+
return text({
|
|
21
|
+
ok: exitCode === 0,
|
|
22
|
+
exit: exitCode,
|
|
23
|
+
readable: true,
|
|
24
|
+
dir: data.dir,
|
|
25
|
+
files: data.files,
|
|
26
|
+
count: data.count,
|
|
27
|
+
problems: problems(data.problems, "validate infra-prices"),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
return failure(e);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { ServerConfig } from "../../config.js";
|
|
3
|
+
export declare function summarize(draft: any, report: any): {
|
|
4
|
+
counts: {
|
|
5
|
+
nodes: any;
|
|
6
|
+
groups: any;
|
|
7
|
+
edges: any;
|
|
8
|
+
flows: any;
|
|
9
|
+
};
|
|
10
|
+
environments: any;
|
|
11
|
+
nodes: any;
|
|
12
|
+
inputs: any;
|
|
13
|
+
unmatched: {
|
|
14
|
+
[k: string]: {
|
|
15
|
+
count: any;
|
|
16
|
+
items: any;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
notDrawn: any;
|
|
20
|
+
skipped: {
|
|
21
|
+
count: any;
|
|
22
|
+
first: any;
|
|
23
|
+
};
|
|
24
|
+
notes: any;
|
|
25
|
+
withheld: any;
|
|
26
|
+
};
|
|
27
|
+
export declare function register(server: McpServer, config: ServerConfig): void;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from "fs";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { ContractError, FIRST, failure, need, text } from "./infra_common.js";
|
|
6
|
+
import { runCli } from "../../cli_runner.js";
|
|
7
|
+
/**
|
|
8
|
+
* `jsonui-doc seed infra <project> -o <dir>`: a draft model from what the project already says (Compose, Terraform /
|
|
9
|
+
* OpenTofu, Kubernetes, CloudFormation / SAM / CDK, wrangler, PaaS files, SDK imports and env names; never state,
|
|
10
|
+
* tfvars or .env values), and its report (what it read, what it could not place). By default the draft is written to
|
|
11
|
+
* a temp folder and read back — nothing in the project changes; write: true writes <infra.dir>/seed (the draft is a
|
|
12
|
+
* starting point, never read as the model). Exit 0: drafted; 2: refused (bad config, the draft fails its own check,
|
|
13
|
+
* or a draft is already there without force).
|
|
14
|
+
*/
|
|
15
|
+
function infraDir(projectDir) {
|
|
16
|
+
try {
|
|
17
|
+
const cfg = JSON.parse(readFileSync(join(projectDir, "jui.config.json"), "utf-8"));
|
|
18
|
+
if (typeof cfg?.infra?.dir === "string")
|
|
19
|
+
return cfg.infra.dir;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// no config, or not JSON: the CLI says so itself
|
|
23
|
+
}
|
|
24
|
+
return "docs/infra";
|
|
25
|
+
}
|
|
26
|
+
function readJson(path, what) {
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
throw new ContractError(`seed infra wrote no readable ${what} at ${path}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function summarize(draft, report) {
|
|
35
|
+
need(draft, ["nodes", "groups", "edges"], "seed infra draft");
|
|
36
|
+
need(report, ["inputs", "unmatched", "skipped", "skippedCount", "notes", "withheld"], "seed infra report");
|
|
37
|
+
return {
|
|
38
|
+
counts: { nodes: draft.nodes.length, groups: draft.groups.length, edges: draft.edges.length,
|
|
39
|
+
flows: Array.isArray(draft.flows) ? draft.flows.length : 0 },
|
|
40
|
+
environments: (draft.environments ?? []).map((e) => e.id),
|
|
41
|
+
nodes: draft.nodes.slice(0, 50).map((n) => ({
|
|
42
|
+
id: n.id, category: n.category, provider: n.provider, service: n.service, iac: n.iac, environments: n.environments,
|
|
43
|
+
})),
|
|
44
|
+
inputs: report.inputs,
|
|
45
|
+
unmatched: Object.fromEntries(Object.entries(report.unmatched).map(([provider, u]) => [provider, { count: u.count, items: (u.items ?? []).slice(0, FIRST) }])),
|
|
46
|
+
notDrawn: report.notDrawn ?? {},
|
|
47
|
+
skipped: { count: report.skippedCount, first: report.skipped.slice(0, FIRST) },
|
|
48
|
+
notes: report.notes.slice(0, FIRST),
|
|
49
|
+
withheld: report.withheld,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function register(server, config) {
|
|
53
|
+
server.tool("infra_seed", "Draft the server room's model from the project's IaC, config files and code with `jsonui-doc seed infra` (Compose, Terraform/OpenTofu, Kubernetes, CloudFormation/SAM/CDK, wrangler, Firebase/PaaS files, SDK imports and env var names — never Terraform state, tfvars or .env values). Returns the draft's counts and nodes (id, category, provider, service, iac), what was read, what could not be placed (unmatched, by provider) or was skipped, and notes. By default nothing in the project is written; write: true writes <infra.dir>/seed/draft.json and seed-report.json (a starting point to copy from — the model is docs/infra/*.json).", {
|
|
54
|
+
project_dir: z.string().optional().describe("Project directory with jui.config.json (overrides JUI_PROJECT_DIR env)"),
|
|
55
|
+
write: z.boolean().optional().describe("Write the draft to <infra.dir>/seed in the project (default false: read only)"),
|
|
56
|
+
force: z.boolean().optional().describe("With write: replace a draft already there"),
|
|
57
|
+
}, async ({ project_dir, write, force }) => {
|
|
58
|
+
const temp = write ? null : mkdtempSync(join(tmpdir(), "jui-mcp-seed-"));
|
|
59
|
+
try {
|
|
60
|
+
const projectDir = config.resolveProjectDir(project_dir);
|
|
61
|
+
const out = temp ?? join(projectDir, infraDir(projectDir), "seed");
|
|
62
|
+
const args = ["seed", "infra", projectDir, "-o", out];
|
|
63
|
+
if (write && force)
|
|
64
|
+
args.push("--force");
|
|
65
|
+
const result = await runCli("jsonui-doc", args, { cwd: projectDir, timeout: 180_000 });
|
|
66
|
+
if (result.exitCode !== 0 || !existsSync(join(out, "draft.json"))) {
|
|
67
|
+
return text({ ok: false, exit: result.exitCode,
|
|
68
|
+
message: (result.stderr || result.stdout).trim().split("\n").slice(-5).join("\n") });
|
|
69
|
+
}
|
|
70
|
+
const summary = summarize(readJson(join(out, "draft.json"), "draft.json"), readJson(join(out, "seed-report.json"), "seed-report.json"));
|
|
71
|
+
return text({ ok: true, exit: 0, written: write ? out : null, ...summary });
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
return failure(e);
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
if (temp)
|
|
78
|
+
rmSync(temp, { recursive: true, force: true });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { ServerConfig } from "../../config.js";
|
|
3
|
+
/**
|
|
4
|
+
* `jsonui-doc validate infra <project> --json`: the room's model (docs/infra/*.json, or jui.config.json's infra.dir)
|
|
5
|
+
* read as jui viz reads it — each file against the schema and the shapes of secrets, then merged, and its flows
|
|
6
|
+
* walked. Exit 0: no error (warnings alone are 0); 1: errors; 2: nothing could be read (reason).
|
|
7
|
+
*/
|
|
8
|
+
export declare function register(server: McpServer, config: ServerConfig): void;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { failure, need, problems, runDocJson, text } from "./infra_common.js";
|
|
3
|
+
/**
|
|
4
|
+
* `jsonui-doc validate infra <project> --json`: the room's model (docs/infra/*.json, or jui.config.json's infra.dir)
|
|
5
|
+
* read as jui viz reads it — each file against the schema and the shapes of secrets, then merged, and its flows
|
|
6
|
+
* walked. Exit 0: no error (warnings alone are 0); 1: errors; 2: nothing could be read (reason).
|
|
7
|
+
*/
|
|
8
|
+
export function register(server, config) {
|
|
9
|
+
server.tool("infra_validate", "Validate the server room's infra model (docs/infra/*.json: environments, groups, nodes, edges, flows) with `jsonui-doc validate infra --json`. Returns whether it could be read (and why not), the files used, the counts, and the problems by severity and code with the first 20 (file, path, code, message). Flow problems (flow.*) name the rule a step most likely missed. Needs jsonui-cli 2.3.15+.", {
|
|
10
|
+
project_dir: z.string().optional().describe("Project directory with jui.config.json (overrides JUI_PROJECT_DIR env)"),
|
|
11
|
+
}, async ({ project_dir }) => {
|
|
12
|
+
try {
|
|
13
|
+
const projectDir = config.resolveProjectDir(project_dir);
|
|
14
|
+
const { exitCode, data } = await runDocJson(["validate", "infra", projectDir, "--json"], projectDir);
|
|
15
|
+
need(data, ["ok"], "validate infra");
|
|
16
|
+
if (!data.ok && data.reason !== "invalid") {
|
|
17
|
+
need(data, ["reason", "message"], "validate infra");
|
|
18
|
+
return text({ ok: false, exit: exitCode, readable: false, reason: data.reason, message: data.message });
|
|
19
|
+
}
|
|
20
|
+
need(data, ["dir", "files", "problems"], "validate infra");
|
|
21
|
+
return text({
|
|
22
|
+
ok: exitCode === 0,
|
|
23
|
+
exit: exitCode,
|
|
24
|
+
readable: data.ok,
|
|
25
|
+
dir: data.dir,
|
|
26
|
+
files: data.files,
|
|
27
|
+
environments: (data.environments ?? []).map((e) => e.id),
|
|
28
|
+
counts: data.counts ?? null,
|
|
29
|
+
flows: Array.isArray(data.flows) ? data.flows.length : null,
|
|
30
|
+
problems: problems(data.problems, "validate infra"),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
return failure(e);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
package/package.json
CHANGED