jui-tools-mcp-server 2.15.11 → 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.
Files changed (63) hide show
  1. package/.github/workflows/test.yml +2 -1
  2. package/README.md +30 -1
  3. package/dist/index.js +30 -2
  4. package/dist/tools/infra/infra_check.d.ts +10 -0
  5. package/dist/tools/infra/infra_check.js +43 -0
  6. package/dist/tools/infra/infra_common.d.ts +40 -0
  7. package/dist/tools/infra/infra_common.js +44 -0
  8. package/dist/tools/infra/infra_cost.d.ts +3 -0
  9. package/dist/tools/infra/infra_cost.js +118 -0
  10. package/dist/tools/infra/infra_load_validate.d.ts +8 -0
  11. package/dist/tools/infra/infra_load_validate.js +39 -0
  12. package/dist/tools/infra/infra_prices_validate.d.ts +7 -0
  13. package/dist/tools/infra/infra_prices_validate.js +34 -0
  14. package/dist/tools/infra/infra_seed.d.ts +27 -0
  15. package/dist/tools/infra/infra_seed.js +81 -0
  16. package/dist/tools/infra/infra_validate.d.ts +8 -0
  17. package/dist/tools/infra/infra_validate.js +37 -0
  18. package/dist/tools/viz/viz_common.d.ts +13 -0
  19. package/dist/tools/viz/viz_common.js +50 -0
  20. package/dist/tools/viz/viz_doctor.d.ts +2 -0
  21. package/dist/tools/viz/viz_doctor.js +14 -0
  22. package/dist/tools/viz/viz_open.d.ts +2 -0
  23. package/dist/tools/viz/viz_open.js +14 -0
  24. package/dist/tools/viz/viz_start.d.ts +2 -0
  25. package/dist/tools/viz/viz_start.js +23 -0
  26. package/dist/tools/viz/viz_status.d.ts +2 -0
  27. package/dist/tools/viz/viz_status.js +21 -0
  28. package/dist/tools/viz/viz_stop.d.ts +2 -0
  29. package/dist/tools/viz/viz_stop.js +13 -0
  30. package/package.json +1 -1
  31. package/scripts/record-infra-fixtures.sh +78 -0
  32. package/src/index.ts +33 -2
  33. package/src/tools/infra/infra_check.ts +49 -0
  34. package/src/tools/infra/infra_common.ts +69 -0
  35. package/src/tools/infra/infra_cost.ts +122 -0
  36. package/src/tools/infra/infra_load_validate.ts +46 -0
  37. package/src/tools/infra/infra_prices_validate.ts +41 -0
  38. package/src/tools/infra/infra_seed.ts +88 -0
  39. package/src/tools/infra/infra_validate.ts +44 -0
  40. package/src/tools/viz/viz_common.ts +61 -0
  41. package/src/tools/viz/viz_doctor.ts +20 -0
  42. package/src/tools/viz/viz_open.ts +20 -0
  43. package/src/tools/viz/viz_start.ts +27 -0
  44. package/src/tools/viz/viz_status.ts +27 -0
  45. package/src/tools/viz/viz_stop.ts +19 -0
  46. package/tests/fixtures/infra/RECORDED_FROM +1 -0
  47. package/tests/fixtures/infra/check-infra.json +101 -0
  48. package/tests/fixtures/infra/cost-infra.day.json +10080 -0
  49. package/tests/fixtures/infra/cost-infra.month-by-node.json +484 -0
  50. package/tests/fixtures/infra/cost-infra.month.json +381 -0
  51. package/tests/fixtures/infra/cost-infra.refused.json +5 -0
  52. package/tests/fixtures/infra/seed-infra.draft.json +233 -0
  53. package/tests/fixtures/infra/seed-infra.report.json +46 -0
  54. package/tests/fixtures/infra/validate-infra-load.errors.json +39 -0
  55. package/tests/fixtures/infra/validate-infra-load.ok.json +29 -0
  56. package/tests/fixtures/infra/validate-infra-load.unreadable.json +6 -0
  57. package/tests/fixtures/infra/validate-infra-prices.ok.json +9 -0
  58. package/tests/fixtures/infra/validate-infra.errors.json +39 -0
  59. package/tests/fixtures/infra/validate-infra.ok.json +30 -0
  60. package/tests/fixtures/infra/validate-infra.unreadable.json +6 -0
  61. package/tests/tool_count.test.ts +53 -0
  62. package/tests/tools_infra.test.ts +332 -0
  63. package/tests/tools_viz.test.ts +154 -0
@@ -0,0 +1,13 @@
1
+ import { CliResult } from "../../cli_runner.js";
2
+ /**
3
+ * Shared by the viz_* tools (`jui viz`, the local viewer of Claude Code
4
+ * sessions). jui viz reads ~/.claude and ~/.jsonui-cli/viz, not a project, so
5
+ * every call runs in the home folder — never JUI_PROJECT_DIR.
6
+ */
7
+ /** Said in every viz_* description: other agents' sessions (Codex, …) are not shown. */
8
+ export declare const CLAUDE_CODE_ONLY = "jui viz shows Claude Code sessions only (Claude Code \u306E\u30BB\u30C3\u30B7\u30E7\u30F3\u3060\u3051\u3092\u898B\u305B\u308B).";
9
+ export declare const UPDATE_JUI = "curl -fsSL https://raw.githubusercontent.com/Tai-Kimura/jsonui-cli/main/installer/bootstrap.sh | bash";
10
+ export declare function outdatedJui(result: CliResult): string | undefined;
11
+ export declare function runViz(args: string[], timeout?: number): Promise<CliResult>;
12
+ /** The tool's text: the CLI's JSON (when it printed one) inside the usual envelope. */
13
+ export declare function vizResult(result: CliResult): string;
@@ -0,0 +1,50 @@
1
+ import { homedir } from "os";
2
+ import { runCli } from "../../cli_runner.js";
3
+ /**
4
+ * Shared by the viz_* tools (`jui viz`, the local viewer of Claude Code
5
+ * sessions). jui viz reads ~/.claude and ~/.jsonui-cli/viz, not a project, so
6
+ * every call runs in the home folder — never JUI_PROJECT_DIR.
7
+ */
8
+ /** Said in every viz_* description: other agents' sessions (Codex, …) are not shown. */
9
+ export const CLAUDE_CODE_ONLY = "jui viz shows Claude Code sessions only (Claude Code のセッションだけを見せる).";
10
+ export const UPDATE_JUI = "curl -fsSL https://raw.githubusercontent.com/Tai-Kimura/jsonui-cli/main/installer/bootstrap.sh | bash";
11
+ /**
12
+ * The installed jui predates what the tool asks for. Matched on argparse's
13
+ * own words, as printed by the jui versions that lack each piece (measured
14
+ * 2026-10-18: jui 1.9.37 has no `viz`; feature/jui-viz before `start` has
15
+ * neither `start` nor `status --with-relay`).
16
+ */
17
+ const OUTDATED = [
18
+ { pattern: /invalid choice: 'viz'/, missing: "`jui viz`" },
19
+ { pattern: /invalid choice: 'start'/, missing: "`jui viz start`" },
20
+ { pattern: /unrecognized arguments: .*--with-relay/, missing: "`jui viz status --with-relay`" },
21
+ ];
22
+ export function outdatedJui(result) {
23
+ const hit = OUTDATED.find((o) => o.pattern.test(result.stderr));
24
+ if (!hit)
25
+ return undefined;
26
+ return `The installed jui has no ${hit.missing}. Update jui, then call this tool again: ${UPDATE_JUI}`;
27
+ }
28
+ export async function runViz(args, timeout) {
29
+ return runCli("jui", ["viz", ...args], { cwd: homedir(), timeout });
30
+ }
31
+ /** The tool's text: the CLI's JSON (when it printed one) inside the usual envelope. */
32
+ export function vizResult(result) {
33
+ let parsed = undefined;
34
+ try {
35
+ parsed = JSON.parse(result.stdout);
36
+ }
37
+ catch {
38
+ // not JSON (stop / open print a line)
39
+ }
40
+ const notInstalled = result.exitCode === "ENOENT";
41
+ return JSON.stringify({
42
+ success: result.exitCode === 0,
43
+ result: parsed,
44
+ output: parsed === undefined ? result.stdout || undefined : undefined,
45
+ errors: result.stderr || undefined,
46
+ update_jui: notInstalled
47
+ ? `jui is not installed (not found on PATH). Install it: ${UPDATE_JUI}`
48
+ : outdatedJui(result),
49
+ }, null, 2);
50
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function register(server: McpServer): void;
@@ -0,0 +1,14 @@
1
+ import { CLAUDE_CODE_ONLY, runViz, vizResult } from "./viz_common.js";
2
+ export function register(server) {
3
+ server.tool("viz_doctor", "Check the environment for jui viz (the local viewer of Claude Code sessions): Python packages, the hooks and " +
4
+ "the Python they run, the port, the claude CLI. Each check has name, ok and detail. " +
5
+ CLAUDE_CODE_ONLY, {}, async () => {
6
+ try {
7
+ const result = await runViz(["doctor", "--json"], 30_000);
8
+ return { content: [{ type: "text", text: vizResult(result) }] };
9
+ }
10
+ catch (e) {
11
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
12
+ }
13
+ });
14
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function register(server: McpServer): void;
@@ -0,0 +1,14 @@
1
+ import { CLAUDE_CODE_ONLY, runViz, vizResult } from "./viz_common.js";
2
+ export function register(server) {
3
+ server.tool("viz_open", "Open the running jui viz page (the local viewer of Claude Code sessions) in the user's browser. " +
4
+ "Fails when no server runs: call viz_start first. " +
5
+ CLAUDE_CODE_ONLY, {}, async () => {
6
+ try {
7
+ const result = await runViz(["open"], 30_000);
8
+ return { content: [{ type: "text", text: vizResult(result) }] };
9
+ }
10
+ catch (e) {
11
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
12
+ }
13
+ });
14
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function register(server: McpServer): void;
@@ -0,0 +1,23 @@
1
+ import { z } from "zod";
2
+ import { CLAUDE_CODE_ONLY, runViz, vizResult } from "./viz_common.js";
3
+ export function register(server) {
4
+ server.tool("viz_start", "Start the jui viz server (the local viewer of Claude Code sessions) in the background and return its URL " +
5
+ "on 127.0.0.1. A server already running is reported (alreadyRunning), not started again. Hooks and the " +
6
+ "relay are installed with the jui CLI, not here. " +
7
+ CLAUDE_CODE_ONLY, {
8
+ port: z.number().int().min(1).max(65535).optional().describe("Port on 127.0.0.1 (default: the viz config, 7788)"),
9
+ }, async (params) => {
10
+ try {
11
+ const args = ["start", "--json"];
12
+ if (params.port !== undefined) {
13
+ args.push("--port", String(params.port));
14
+ }
15
+ // jui waits up to 15 s for the server to answer.
16
+ const result = await runViz(args, 45_000);
17
+ return { content: [{ type: "text", text: vizResult(result) }] };
18
+ }
19
+ catch (e) {
20
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
21
+ }
22
+ });
23
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function register(server: McpServer): void;
@@ -0,0 +1,21 @@
1
+ import { z } from "zod";
2
+ import { CLAUDE_CODE_ONLY, runViz, vizResult } from "./viz_common.js";
3
+ export function register(server) {
4
+ server.tool("viz_status", "Report jui viz (the local viewer of Claude Code sessions): whether its server runs (pid, URL), the hooks in " +
5
+ "~/.claude/settings.json (installed of 18), and whether the permission relay is registered. " +
6
+ CLAUDE_CODE_ONLY, {
7
+ relay: z.boolean().optional().describe("Also ask claude whether the permission relay is registered (default true; runs `claude mcp get`, a few seconds)"),
8
+ }, async (params) => {
9
+ try {
10
+ const args = ["status", "--json"];
11
+ if (params.relay !== false) {
12
+ args.push("--with-relay");
13
+ }
14
+ const result = await runViz(args, 30_000);
15
+ return { content: [{ type: "text", text: vizResult(result) }] };
16
+ }
17
+ catch (e) {
18
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
19
+ }
20
+ });
21
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function register(server: McpServer): void;
@@ -0,0 +1,13 @@
1
+ import { CLAUDE_CODE_ONLY, runViz, vizResult } from "./viz_common.js";
2
+ export function register(server) {
3
+ server.tool("viz_stop", "Stop the jui viz server (the local viewer of Claude Code sessions). Only the server jui recorded is stopped. " +
4
+ CLAUDE_CODE_ONLY, {}, async () => {
5
+ try {
6
+ const result = await runViz(["stop"], 30_000);
7
+ return { content: [{ type: "text", text: vizResult(result) }] };
8
+ }
9
+ catch (e) {
10
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
11
+ }
12
+ });
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jui-tools-mcp-server",
3
- "version": "2.15.11",
3
+ "version": "2.17.0",
4
4
  "description": "MCP server for JsonUI project management - wraps jui and jsonui-doc CLIs plus component spec lookup",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -0,0 +1,78 @@
1
+ #!/bin/bash
2
+ # Record what the jsonui-doc infra commands print with --json, as the infra tools' test fixtures
3
+ # (tests/fixtures/infra/*.json). The tools summarise those outputs, so the fixtures are the CLI's contract: when the
4
+ # CLI's JSON changes, record again and the tools' tests say what no longer holds.
5
+ #
6
+ # scripts/record-infra-fixtures.sh <jsonui-cli checkout> # e.g. a worktree of integrate/2.3.15
7
+ #
8
+ # Rooms: jsonui-cli's own document_tools/tests/fixtures/infra (aws-three-tier: a model, two load scenarios, a price
9
+ # file; compose-and-cloud: what seed reads), copied to a temp folder; one copy is broken on purpose (a load file that
10
+ # is not JSON, a model file with a schema error) so the error shapes are recorded too. The fetched-price cache is kept
11
+ # out (JUI_VIZ_HOME points nowhere), so the bundled table prices them.
12
+ set -euo pipefail
13
+ CLI_ROOT="$(cd "${1:?usage: $0 <jsonui-cli checkout>}" && pwd)"
14
+ HERE="$(cd "$(dirname "$0")/.." && pwd)"
15
+ OUT="${HERE}/tests/fixtures/infra"
16
+ FIX="${CLI_ROOT}/document_tools/tests/fixtures/infra"
17
+ WORK="$(mktemp -d)"
18
+ trap 'rm -rf "${WORK}"' EXIT
19
+
20
+ export PYTHONPATH="${CLI_ROOT}/document_tools:${CLI_ROOT}/jui_tools:${CLI_ROOT}/test_tools"
21
+ export JUI_VIZ_HOME=/nonexistent
22
+ doc() { (cd "$1" && shift && python3 -m jsonui_doc_cli.cli "$@") || true; }
23
+
24
+ cp -R "${FIX}/aws-three-tier" "${WORK}/room"
25
+ cp -R "${FIX}/aws-three-tier" "${WORK}/broken"
26
+ printf '{' > "${WORK}/broken/docs/infra/load/broken.json"
27
+ printf '{"version": 0, "nodes": [{"id": "x", "category": "nope"}]}' > "${WORK}/broken/docs/infra/bad.json"
28
+ mkdir -p "${WORK}/seed/app"
29
+ cat > "${WORK}/seed/docker-compose.yml" <<'EOF'
30
+ services:
31
+ web:
32
+ image: example/web
33
+ depends_on: [db, cache]
34
+ db:
35
+ image: postgres:16
36
+ cache:
37
+ image: redis:7
38
+ EOF
39
+ cat > "${WORK}/seed/main.tf" <<'EOF'
40
+ provider "aws" { region = "ap-northeast-1" }
41
+ resource "aws_db_instance" "main" { engine = "postgres" }
42
+ resource "aws_lb" "web" { load_balancer_type = "application" }
43
+ resource "aws_s3_bucket" "assets" {}
44
+ resource "aws_glue_job" "etl" {}
45
+ EOF
46
+ printf 'import boto3\nimport stripe\nQUEUE_URL = "SQS_QUEUE_URL"\n' > "${WORK}/seed/app/main.py"
47
+ cat > "${WORK}/room/jui.config.json" <<'EOF'
48
+ {"checks": [{"name": "infra-load", "type": "builtin:infra-load"}, {"name": "infra-code", "type": "builtin:infra-code"}]}
49
+ EOF
50
+
51
+ mkdir -p "${OUT}" "${WORK}/empty"
52
+ doc "${WORK}/empty" validate infra --json > "${OUT}/validate-infra.unreadable.json"
53
+ doc "${WORK}/empty" validate infra-load --json > "${OUT}/validate-infra-load.unreadable.json"
54
+ doc "${WORK}/room" validate infra --json > "${OUT}/validate-infra.ok.json"
55
+ doc "${WORK}/broken" validate infra --json > "${OUT}/validate-infra.errors.json"
56
+ doc "${WORK}/room" validate infra-load --json > "${OUT}/validate-infra-load.ok.json"
57
+ doc "${WORK}/broken" validate infra-load --json > "${OUT}/validate-infra-load.errors.json"
58
+ doc "${WORK}/room" validate infra-prices --json > "${OUT}/validate-infra-prices.ok.json"
59
+ doc "${WORK}/seed" seed infra -o "${WORK}/seed-out" > /dev/null
60
+ cp "${WORK}/seed-out/draft.json" "${OUT}/seed-infra.draft.json"
61
+ cp "${WORK}/seed-out/seed-report.json" "${OUT}/seed-infra.report.json"
62
+ doc "${WORK}/room" cost infra --json > "${OUT}/cost-infra.month.json"
63
+ doc "${WORK}/room" cost infra --by node --scenario sale --json > "${OUT}/cost-infra.month-by-node.json"
64
+ doc "${WORK}/room" cost infra --date 2026-11-03 --json > "${OUT}/cost-infra.day.json"
65
+ doc "${WORK}/room" cost infra --scenario nope --json > "${OUT}/cost-infra.refused.json"
66
+ doc "${WORK}/room" check infra --json 2>/dev/null > "${OUT}/check-infra.json"
67
+
68
+ # The temp folder's path is not part of the contract
69
+ for f in "${OUT}"/*.json; do
70
+ python3 - "$f" "${WORK}" <<'EOF'
71
+ import sys
72
+ p, work = sys.argv[1:]
73
+ s = open(p, encoding="utf-8").read().replace(work, "<ROOT>")
74
+ open(p, "w", encoding="utf-8").write(s)
75
+ EOF
76
+ done
77
+ git -C "${CLI_ROOT}" log -1 --format='%h %s' > "${OUT}/RECORDED_FROM"
78
+ ls -1 "${OUT}"
package/src/index.ts CHANGED
@@ -53,6 +53,19 @@ import { register as registerDocGenerateHtml } from "./tools/doc/doc_generate_ht
53
53
  import { register as registerDocRulesInit } from "./tools/doc/doc_rules_init.js";
54
54
  import { register as registerDocRulesShow } from "./tools/doc/doc_rules_show.js";
55
55
 
56
+ // --- Group G: jui viz (Claude Code sessions viewer) ---
57
+ import { register as registerVizStatus } from "./tools/viz/viz_status.js";
58
+ import { register as registerVizDoctor } from "./tools/viz/viz_doctor.js";
59
+ import { register as registerVizStart } from "./tools/viz/viz_start.js";
60
+ import { register as registerVizStop } from "./tools/viz/viz_stop.js";
61
+ import { register as registerVizOpen } from "./tools/viz/viz_open.js";
62
+ import { register as registerInfraValidate } from "./tools/infra/infra_validate.js";
63
+ import { register as registerInfraLoadValidate } from "./tools/infra/infra_load_validate.js";
64
+ import { register as registerInfraPricesValidate } from "./tools/infra/infra_prices_validate.js";
65
+ import { register as registerInfraSeed } from "./tools/infra/infra_seed.js";
66
+ import { register as registerInfraCheck } from "./tools/infra/infra_check.js";
67
+ import { register as registerInfraCost } from "./tools/infra/infra_cost.js";
68
+
56
69
  // --- Group F: jsonui-test CLI ---
57
70
  import { register as registerTestValidate } from "./tools/test/test_validate.js";
58
71
  import { register as registerTestGenerateScreen } from "./tools/test/test_generate_screen.js";
@@ -144,7 +157,7 @@ registerListApiSpecs(server, config);
144
157
  registerListApiModels(server, config);
145
158
  registerPreviewApiModelSync(server, config);
146
159
 
147
- // Register Group F: jsonui-test CLI (8 tools)
160
+ // Register Group F: jsonui-test CLI (10 tools)
148
161
  // mock serve is intentionally NOT exposed (long-running local HTTP server that
149
162
  // executes run-targets — kept to direct CLI use only). test_mock_generate
150
163
  // covers both scaffold (default) and drift-check (check=true) modes.
@@ -159,7 +172,25 @@ registerTestArtifactsPull(server, config);
159
172
  registerTestArtifactsStatus(server, config);
160
173
  registerTestContractsCoverage(server, config);
161
174
 
162
- log(`Registered 41 tools (7 spec + 6 context + 8 jui + 9 doc + 3 api + 8 test)`);
175
+ // Register Group G: jui viz (5 tools). Hooks / relay install stay with the
176
+ // jui CLI (they write ~/.claude/settings.json and the user's MCP config).
177
+ registerVizStatus(server);
178
+ registerVizDoctor(server);
179
+ registerVizStart(server);
180
+ registerVizStop(server);
181
+ registerVizOpen(server);
182
+
183
+ // Register Group H: the server room's infra (6 tools) — jsonui-doc's validate / seed / check / cost infra, each
184
+ // summarised from its --json (jsonui-cli 2.3.15+). `jui viz cloud-prices fetch` stays with the CLI (it goes to the
185
+ // network and writes the viz home).
186
+ registerInfraValidate(server, config);
187
+ registerInfraLoadValidate(server, config);
188
+ registerInfraPricesValidate(server, config);
189
+ registerInfraSeed(server, config);
190
+ registerInfraCheck(server, config);
191
+ registerInfraCost(server, config);
192
+
193
+ log(`Registered 57 tools (9 spec + 7 context + 8 jui + 9 doc + 3 api + 10 test + 5 viz + 6 infra)`);
163
194
 
164
195
  async function main() {
165
196
  const transport = new StdioServerTransport();
@@ -0,0 +1,49 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { ServerConfig } from "../../config.js";
4
+ import { FIRST, failure, need, runDocJson, text } from "./infra_common.js";
5
+
6
+ /**
7
+ * `jsonui-doc check infra -p <project> --json`: the declared infra checks (jui.config.json → checks:
8
+ * builtin:infra-iac, builtin:infra-code, builtin:infra-load) — the model against the IaC, the code, and the load
9
+ * definitions. This is the one jsonui-doc command that runs project-declared checks; the infra ones read files only.
10
+ * Exit 0: clean (warnings alone are 0); 1: a mismatch (a finding of a check declared with gate: true); 2: an
11
+ * execution error, or no infra check is declared.
12
+ */
13
+ export function register(server: McpServer, config: ServerConfig) {
14
+ server.tool(
15
+ "infra_check",
16
+ "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+.",
17
+ {
18
+ project_dir: z.string().optional().describe("Project directory with jui.config.json (overrides JUI_PROJECT_DIR env)"),
19
+ },
20
+ async ({ project_dir }) => {
21
+ try {
22
+ const projectDir = config.resolveProjectDir(project_dir);
23
+ const { exitCode, data, stderr } = await runDocJson(["check", "infra", "-p", projectDir, "--json"], projectDir, 180_000);
24
+ need(data, ["ok", "exit", "checks"], "check infra");
25
+ if (!data.checks.length) {
26
+ return text({ ok: false, exit: exitCode, checks: [],
27
+ message: stderr.trim() || "no builtin:infra-* check is declared in jui.config.json → checks",
28
+ declared: data.declared ?? [] });
29
+ }
30
+ return text({
31
+ ok: data.ok,
32
+ exit: exitCode,
33
+ checks: data.checks.map((c: any) => {
34
+ need(c, ["name", "type", "status"], "check infra check");
35
+ if (c.status === "error") return { name: c.name, type: c.type, status: "error", error: c.error };
36
+ need(c, ["summary", "results", "warnings", "report"], "check infra check");
37
+ return {
38
+ name: c.name, type: c.type, status: c.status, summary: c.summary,
39
+ results: c.results.slice(0, FIRST), resultCount: c.results.length,
40
+ warnings: c.warnings.slice(0, FIRST), report: c.report,
41
+ };
42
+ }),
43
+ });
44
+ } catch (e: any) {
45
+ return failure(e);
46
+ }
47
+ }
48
+ );
49
+ }
@@ -0,0 +1,69 @@
1
+ import { runCli } from "../../cli_runner.js";
2
+
3
+ /**
4
+ * Shared by the Group H tools: run a `jsonui-doc … --json` infra command and read its one JSON object.
5
+ *
6
+ * The tools summarise that object for an agent (counts, the first problems, the totals), so the keys they read are
7
+ * the CLI's contract. `need` names a key the object lacks instead of summarising nothing — a jsonui-doc older or
8
+ * newer than these tools says so rather than reading as an empty room. The tests feed the CLI's own recorded output
9
+ * (tests/fixtures/infra, scripts/record-infra-fixtures.sh).
10
+ */
11
+
12
+ export interface JsonRun {
13
+ exitCode: number;
14
+ data: any;
15
+ stderr: string;
16
+ }
17
+
18
+ export class ContractError extends Error {}
19
+
20
+ export const FIRST = 20;
21
+
22
+ export async function runDocJson(args: string[], cwd: string, timeout?: number): Promise<JsonRun> {
23
+ const result = await runCli("jsonui-doc", args, { cwd, timeout });
24
+ let data: any;
25
+ try {
26
+ data = JSON.parse(result.stdout);
27
+ } catch {
28
+ throw new ContractError(
29
+ `jsonui-doc ${args.slice(0, 2).join(" ")} printed no JSON (exit ${result.exitCode})` +
30
+ (result.stderr ? `: ${result.stderr.trim().split("\n").slice(-3).join(" / ")}` : "") +
31
+ " — it needs jsonui-cli 2.3.15 or later for --json here"
32
+ );
33
+ }
34
+ return { exitCode: result.exitCode, data, stderr: result.stderr };
35
+ }
36
+
37
+ export function need(obj: any, keys: string[], what: string): void {
38
+ const missing = keys.filter((k) => obj == null || !(k in obj));
39
+ if (missing.length) {
40
+ throw new ContractError(
41
+ `${what}: the JSON has no ${missing.map((k) => `"${k}"`).join(", ")} — a jsonui-doc these tools do not know; ` +
42
+ "update jsonui-cli or this server"
43
+ );
44
+ }
45
+ }
46
+
47
+ /** A problem list as counts (by severity, by code) and the first FIRST, each as written. */
48
+ export function problems(list: any[], what: string, keep: string[] = ["severity", "code", "file", "path", "message"]) {
49
+ if (!Array.isArray(list)) throw new ContractError(`${what}: "problems" is not a list`);
50
+ const bySeverity: Record<string, number> = {};
51
+ const byCode: Record<string, number> = {};
52
+ for (const p of list) {
53
+ need(p, ["severity", "code"], `${what} problem`);
54
+ bySeverity[p.severity] = (bySeverity[p.severity] ?? 0) + 1;
55
+ byCode[p.code] = (byCode[p.code] ?? 0) + 1;
56
+ }
57
+ const first = list.slice(0, FIRST).map((p) =>
58
+ Object.fromEntries(keep.filter((k) => p[k] !== undefined && p[k] !== null).map((k) => [k, p[k]]))
59
+ );
60
+ return { count: list.length, errors: bySeverity.error ?? 0, warnings: bySeverity.warning ?? 0, byCode, first };
61
+ }
62
+
63
+ export function text(value: unknown) {
64
+ return { content: [{ type: "text" as const, text: JSON.stringify(value, null, 2) }] };
65
+ }
66
+
67
+ export function failure(e: any) {
68
+ return { content: [{ type: "text" as const, text: `Error: ${e.message}` }] };
69
+ }
@@ -0,0 +1,122 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { ServerConfig } from "../../config.js";
4
+ import { FIRST, failure, need, problems, runDocJson, text } from "./infra_common.js";
5
+
6
+ /**
7
+ * `jsonui-doc cost infra <project> --json`: the cost simulator's month (or one day) for a load scenario, the same
8
+ * simulation and shapes as jui viz's コスト. The month's whole reply (every day's stack) and above all a day's (every
9
+ * node's 24 hours with each meter's price trace, ~300 KB) are too long for an agent; this keeps what answers "what
10
+ * does it cost and why": the total, the totals per stack key, the top nodes, what has no price / rule / load, the
11
+ * hours over capacity, the notes, the load problems and the price-book warnings — and for a day, each top node's
12
+ * charge by meter with the price layer it came from. Exit 0: simulated; 1: simulated, a load file has an error (left
13
+ * out); 2: nothing simulated (reason).
14
+ */
15
+
16
+ function monthSummary(m: any) {
17
+ need(m, ["scenario", "month", "usd", "by", "keys", "days", "noPrice", "noRule", "loadUndefined", "overflowHours",
18
+ "warnings", "warningCount"], "cost infra month");
19
+ const totals: Record<string, number> = {};
20
+ for (const d of m.days) {
21
+ need(d, ["date", "usd", "stack"], "cost infra day of the month");
22
+ for (const [k, v] of Object.entries(d.stack)) totals[k] = (totals[k] ?? 0) + (v as number);
23
+ }
24
+ const label = Object.fromEntries(m.keys.map((k: any) => [k.key, k.label]));
25
+ return {
26
+ scenario: m.scenario,
27
+ month: m.month,
28
+ usd: m.usd,
29
+ by: m.by,
30
+ totals: Object.entries(totals).sort((a, b) => b[1] - a[1])
31
+ .map(([key, usd]) => ({ key: key === "__others__" ? "others" : key, label: label[key] ?? key, usd })),
32
+ days: m.days.length,
33
+ dearestDay: m.days.reduce((a: any, b: any) => (b.usd > a.usd ? b : a), m.days[0])?.date ?? null,
34
+ noPrice: m.noPrice,
35
+ noRule: m.noRule,
36
+ loadUndefined: m.loadUndefined,
37
+ overflowHours: m.overflowHours,
38
+ notes: m.warnings.slice(0, FIRST).map((w: any) => (typeof w === "string" ? w : w.message)),
39
+ noteCount: m.warningCount,
40
+ };
41
+ }
42
+
43
+ function daySummary(day: any) {
44
+ need(day, ["scenario", "date", "usd", "nodes", "others"], "cost infra day");
45
+ return {
46
+ scenario: day.scenario,
47
+ date: day.date,
48
+ usd: day.usd,
49
+ nodes: day.nodes.map((n: any) => {
50
+ need(n, ["node", "usd", "hours"], "cost infra day node");
51
+ const meters: Record<string, { usd: number; amount: number; unit: string; sku: string; layer: string | null }> = {};
52
+ let peak = { hour: 0, usd: -1 };
53
+ n.hours.forEach((h: any, i: number) => {
54
+ if (h.usd > peak.usd) peak = { hour: day.hours?.[i] ?? i, usd: h.usd };
55
+ for (const s of h.segments ?? []) {
56
+ const m = (meters[s.meter] ??= { usd: 0, amount: 0, unit: s.unit, sku: s.sku, layer: s.price?.layer ?? null });
57
+ m.usd += s.usd ?? 0;
58
+ m.amount += s.amount ?? 0;
59
+ }
60
+ });
61
+ const overflowHours = n.hours.filter((h: any) => h.load && h.load.overflow > 0).length;
62
+ return {
63
+ node: n.node, label: n.label, provider: n.provider, rule: n.rule, note: n.note || undefined, usd: n.usd,
64
+ peakHour: peak.usd >= 0 ? peak.hour : null, meters, overflowHours: overflowHours || undefined,
65
+ };
66
+ }),
67
+ others: { count: day.others.count, usd: (day.others.usd as number[]).reduce((a, b) => a + b, 0) },
68
+ };
69
+ }
70
+
71
+ export function register(server: McpServer, config: ServerConfig) {
72
+ server.tool(
73
+ "infra_cost",
74
+ "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+.",
75
+ {
76
+ project_dir: z.string().optional().describe("Project directory with jui.config.json (overrides JUI_PROJECT_DIR env)"),
77
+ scenario: z.string().optional().describe("Load scenario id (default: the first)"),
78
+ date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("One day of the scenario's month (YYYY-MM-DD)"),
79
+ by: z.enum(["provider", "node"]).optional().describe("Group the month's totals by provider (default) or node"),
80
+ },
81
+ async ({ project_dir, scenario, date, by }) => {
82
+ try {
83
+ const projectDir = config.resolveProjectDir(project_dir);
84
+ const args = ["cost", "infra", projectDir, "--json"];
85
+ if (scenario) args.push("--scenario", scenario);
86
+ if (date) args.push("--date", date);
87
+ if (by) args.push("--by", by);
88
+ const { exitCode, data } = await runDocJson(args, projectDir, 180_000);
89
+ need(data, ["ok"], "cost infra");
90
+ if (!data.ok) {
91
+ need(data, ["reason", "message"], "cost infra");
92
+ return text({ ok: false, exit: exitCode, reason: data.reason, message: data.message,
93
+ ...(data.problems ? { problems: problems(data.problems, "cost infra") } : {}) });
94
+ }
95
+ need(data, ["dir", "scenario", "scenarios", "problems", "prices"], "cost infra");
96
+ need(data.problems, ["count", "errors", "first"], "cost infra problems");
97
+ need(data.prices, ["warnings", "warningCount"], "cost infra prices");
98
+ const out: Record<string, unknown> = {
99
+ ok: exitCode === 0,
100
+ exit: exitCode,
101
+ dir: data.dir,
102
+ scenario: data.scenario,
103
+ scenarios: data.scenarios,
104
+ loadProblems: data.problems,
105
+ priceWarnings: data.prices.warnings,
106
+ priceWarningCount: data.prices.warningCount,
107
+ };
108
+ if (date) {
109
+ need(data, ["day"], "cost infra");
110
+ out.day = daySummary(data.day);
111
+ } else {
112
+ need(data, ["month", "topNodes"], "cost infra");
113
+ out.month = monthSummary(data.month);
114
+ out.topNodes = data.topNodes;
115
+ }
116
+ return text(out);
117
+ } catch (e: any) {
118
+ return failure(e);
119
+ }
120
+ }
121
+ );
122
+ }
@@ -0,0 +1,46 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { ServerConfig } from "../../config.js";
4
+ import { failure, need, problems, runDocJson, text } from "./infra_common.js";
5
+
6
+ /**
7
+ * `jsonui-doc validate infra-load <project> --json`: the load scenarios the cost simulator reads (<infra.dir>/load,
8
+ * or infra.load_dir) against their schema and against the model. Exit 0: no error; 1: errors; 2: the model or the
9
+ * folder cannot be read (reason).
10
+ */
11
+ export function register(server: McpServer, config: ServerConfig) {
12
+ server.tool(
13
+ "infra_load_validate",
14
+ "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+.",
15
+ {
16
+ project_dir: z.string().optional().describe("Project directory with jui.config.json (overrides JUI_PROJECT_DIR env)"),
17
+ },
18
+ async ({ project_dir }) => {
19
+ try {
20
+ const projectDir = config.resolveProjectDir(project_dir);
21
+ const { exitCode, data } = await runDocJson(["validate", "infra-load", projectDir, "--json"], projectDir);
22
+ need(data, ["ok"], "validate infra-load");
23
+ if (!data.ok) {
24
+ need(data, ["reason", "message"], "validate infra-load");
25
+ return text({ ok: false, exit: exitCode, readable: false, reason: data.reason, message: data.message });
26
+ }
27
+ need(data, ["dir", "files", "problems", "scenarios"], "validate infra-load");
28
+ for (const s of data.scenarios) need(s, ["id", "environment", "month", "hours", "loads", "edges"], "validate infra-load scenario");
29
+ return text({
30
+ ok: exitCode === 0,
31
+ exit: exitCode,
32
+ readable: true,
33
+ dir: data.dir,
34
+ files: data.files,
35
+ scenarios: data.scenarios.map((s: any) => ({
36
+ id: s.id, file: s.file, environment: s.environment, month: s.month, hours: s.hours, loads: s.loads, edges: s.edges,
37
+ })),
38
+ problems: problems(data.problems, "validate infra-load",
39
+ ["severity", "code", "file", "path", "message", "node", "hour", "quantity"]),
40
+ });
41
+ } catch (e: any) {
42
+ return failure(e);
43
+ }
44
+ }
45
+ );
46
+ }