jui-tools-mcp-server 2.15.10 → 2.16.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/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 (46)
49
+ ## Available Tools (51)
50
50
 
51
51
  ### Group A: コンポーネント仕様参照 (9)
52
52
 
@@ -126,6 +126,20 @@ bash ~/.jsonui-mcp-server/uninstall.sh
126
126
 
127
127
  > `jsonui-test mock serve`(常駐 HTTP + run-target 実行)は RCE 面のため **MCP 非公開**(CLI 直叩き限定)。
128
128
 
129
+ ### Group G: jui viz (5)
130
+
131
+ Claude Code のセッションを見るローカルのビューア(`jui viz`)。**Claude Code のセッションだけを見せる**(Codex など他のエージェントのセッションは出ない)。どれもホームフォルダで走り、`JUI_PROJECT_DIR` は使わない。
132
+
133
+ | Tool | Description | CLI |
134
+ |------|-------------|-----|
135
+ | `viz_status` | サーバーが動いているか(pid・URL)、`~/.claude/settings.json` のフック(18 個中いくつ)、権限の中継が登録済みか(`relay=false` で中継の確認を省く) | `jui viz status --json [--with-relay]` |
136
+ | `viz_doctor` | viz の環境の点検(Python パッケージ・フックとその Python・ポート・claude CLI) | `jui viz doctor --json` |
137
+ | `viz_start` | サーバーをバックグラウンドで起動し、`/health` が応答したら URL を返す。動いていれば起動せずに `alreadyRunning` で返す | `jui viz start --json [--port N]` |
138
+ | `viz_stop` | jui が記録したサーバーだけを止める | `jui viz stop` |
139
+ | `viz_open` | 動いているサーバーのページをブラウザで開く(動いていなければ失敗。先に `viz_start`) | `jui viz open` |
140
+
141
+ > フックと中継の導入(`jui viz hooks install` / `jui viz relay install`)は `~/.claude/settings.json` と利用者の MCP 設定を書くので **MCP 非公開**(CLI で行う)。jui が古いと(`viz` / `start` / `--with-relay` が無い)、結果の `update_jui` に更新の手順が出る。
142
+
129
143
  ## Testing
130
144
 
131
145
  単体テストは [vitest](https://vitest.dev/)(ESM ネイティブで本リポジトリのモジュール構成とそのまま噛み合うため採用)。
@@ -1019,12 +1019,14 @@
1019
1019
  "onAppear": {
1020
1020
  "type": "string",
1021
1021
  "description": "Lifecycle callback when view appears (SwiftUI/Compose only). The handler's name (e.g. \"screenAppeared\"); written as a binding (`@{screenAppeared}`) or with UIKit's sender mark (`screenAppeared:`, which means nothing in SwiftUI or Compose) it is read as the same name, as the other event handlers are. Called as its declared closure type asks: `()` with nothing, `(String)` with the viewId.",
1022
- "mode": ["swiftui", "compose"]
1022
+ "mode": ["swiftui", "compose"],
1023
+ "warn_outside_mode": true
1023
1024
  },
1024
1025
  "onDisappear": {
1025
1026
  "type": "string",
1026
1027
  "description": "Lifecycle callback when view disappears (SwiftUI/Compose only). The handler's name (e.g. \"screenDisappeared\"); written as a binding (`@{screenDisappeared}`) or with UIKit's sender mark (`screenDisappeared:`, which means nothing in SwiftUI or Compose) it is read as the same name, as the other event handlers are. Called as its declared closure type asks: `()` with nothing, `(String)` with the viewId.",
1027
- "mode": ["swiftui", "compose"]
1028
+ "mode": ["swiftui", "compose"],
1029
+ "warn_outside_mode": true
1028
1030
  },
1029
1031
  "className": {
1030
1032
  "type": "string",
package/dist/index.js CHANGED
@@ -46,6 +46,12 @@ import { register as registerDocGenerateComponent } from "./tools/doc/doc_genera
46
46
  import { register as registerDocGenerateHtml } from "./tools/doc/doc_generate_html.js";
47
47
  import { register as registerDocRulesInit } from "./tools/doc/doc_rules_init.js";
48
48
  import { register as registerDocRulesShow } from "./tools/doc/doc_rules_show.js";
49
+ // --- Group G: jui viz (Claude Code sessions viewer) ---
50
+ import { register as registerVizStatus } from "./tools/viz/viz_status.js";
51
+ import { register as registerVizDoctor } from "./tools/viz/viz_doctor.js";
52
+ import { register as registerVizStart } from "./tools/viz/viz_start.js";
53
+ import { register as registerVizStop } from "./tools/viz/viz_stop.js";
54
+ import { register as registerVizOpen } from "./tools/viz/viz_open.js";
49
55
  // --- Group F: jsonui-test CLI ---
50
56
  import { register as registerTestValidate } from "./tools/test/test_validate.js";
51
57
  import { register as registerTestGenerateScreen } from "./tools/test/test_generate_screen.js";
@@ -120,7 +126,7 @@ registerDocRulesShow(server, config);
120
126
  registerListApiSpecs(server, config);
121
127
  registerListApiModels(server, config);
122
128
  registerPreviewApiModelSync(server, config);
123
- // Register Group F: jsonui-test CLI (8 tools)
129
+ // Register Group F: jsonui-test CLI (10 tools)
124
130
  // mock serve is intentionally NOT exposed (long-running local HTTP server that
125
131
  // executes run-targets — kept to direct CLI use only). test_mock_generate
126
132
  // covers both scaffold (default) and drift-check (check=true) modes.
@@ -134,7 +140,14 @@ registerTestMockGenerate(server, config);
134
140
  registerTestArtifactsPull(server, config);
135
141
  registerTestArtifactsStatus(server, config);
136
142
  registerTestContractsCoverage(server, config);
137
- log(`Registered 41 tools (7 spec + 6 context + 8 jui + 9 doc + 3 api + 8 test)`);
143
+ // Register Group G: jui viz (5 tools). Hooks / relay install stay with the
144
+ // jui CLI (they write ~/.claude/settings.json and the user's MCP config).
145
+ registerVizStatus(server);
146
+ registerVizDoctor(server);
147
+ registerVizStart(server);
148
+ registerVizStop(server);
149
+ registerVizOpen(server);
150
+ log(`Registered 51 tools (9 spec + 7 context + 8 jui + 9 doc + 3 api + 10 test + 5 viz)`);
138
151
  async function main() {
139
152
  const transport = new StdioServerTransport();
140
153
  await server.connect(transport);
@@ -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.10",
3
+ "version": "2.16.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": {
@@ -44,7 +44,7 @@ const DATA_DIR = join(PROJECT_ROOT, "data");
44
44
  // 3. `npm run fetch-definitions` and commit the data/ diff together with
45
45
  // this pin in one commit
46
46
  // JSONUI_CLI_RAW_BASE still overrides the whole base URL for manual testing.
47
- const JSONUI_CLI_TAG = "v1.9.30";
47
+ const JSONUI_CLI_TAG = "v1.9.34";
48
48
  const BASE_URL =
49
49
  process.env.JSONUI_CLI_RAW_BASE ||
50
50
  `https://raw.githubusercontent.com/Tai-Kimura/jsonui-cli/${JSONUI_CLI_TAG}`;
package/src/index.ts CHANGED
@@ -53,6 +53,13 @@ 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
+
56
63
  // --- Group F: jsonui-test CLI ---
57
64
  import { register as registerTestValidate } from "./tools/test/test_validate.js";
58
65
  import { register as registerTestGenerateScreen } from "./tools/test/test_generate_screen.js";
@@ -144,7 +151,7 @@ registerListApiSpecs(server, config);
144
151
  registerListApiModels(server, config);
145
152
  registerPreviewApiModelSync(server, config);
146
153
 
147
- // Register Group F: jsonui-test CLI (8 tools)
154
+ // Register Group F: jsonui-test CLI (10 tools)
148
155
  // mock serve is intentionally NOT exposed (long-running local HTTP server that
149
156
  // executes run-targets — kept to direct CLI use only). test_mock_generate
150
157
  // covers both scaffold (default) and drift-check (check=true) modes.
@@ -159,7 +166,15 @@ registerTestArtifactsPull(server, config);
159
166
  registerTestArtifactsStatus(server, config);
160
167
  registerTestContractsCoverage(server, config);
161
168
 
162
- log(`Registered 41 tools (7 spec + 6 context + 8 jui + 9 doc + 3 api + 8 test)`);
169
+ // Register Group G: jui viz (5 tools). Hooks / relay install stay with the
170
+ // jui CLI (they write ~/.claude/settings.json and the user's MCP config).
171
+ registerVizStatus(server);
172
+ registerVizDoctor(server);
173
+ registerVizStart(server);
174
+ registerVizStop(server);
175
+ registerVizOpen(server);
176
+
177
+ log(`Registered 51 tools (9 spec + 7 context + 8 jui + 9 doc + 3 api + 10 test + 5 viz)`);
163
178
 
164
179
  async function main() {
165
180
  const transport = new StdioServerTransport();
@@ -0,0 +1,61 @@
1
+ import { homedir } from "os";
2
+ import { runCli, CliResult } from "../../cli_runner.js";
3
+
4
+ /**
5
+ * Shared by the viz_* tools (`jui viz`, the local viewer of Claude Code
6
+ * sessions). jui viz reads ~/.claude and ~/.jsonui-cli/viz, not a project, so
7
+ * every call runs in the home folder — never JUI_PROJECT_DIR.
8
+ */
9
+
10
+ /** Said in every viz_* description: other agents' sessions (Codex, …) are not shown. */
11
+ export const CLAUDE_CODE_ONLY =
12
+ "jui viz shows Claude Code sessions only (Claude Code のセッションだけを見せる).";
13
+
14
+ export const UPDATE_JUI =
15
+ "curl -fsSL https://raw.githubusercontent.com/Tai-Kimura/jsonui-cli/main/installer/bootstrap.sh | bash";
16
+
17
+ /**
18
+ * The installed jui predates what the tool asks for. Matched on argparse's
19
+ * own words, as printed by the jui versions that lack each piece (measured
20
+ * 2026-10-18: jui 1.9.37 has no `viz`; feature/jui-viz before `start` has
21
+ * neither `start` nor `status --with-relay`).
22
+ */
23
+ const OUTDATED: { pattern: RegExp; missing: string }[] = [
24
+ { pattern: /invalid choice: 'viz'/, missing: "`jui viz`" },
25
+ { pattern: /invalid choice: 'start'/, missing: "`jui viz start`" },
26
+ { pattern: /unrecognized arguments: .*--with-relay/, missing: "`jui viz status --with-relay`" },
27
+ ];
28
+
29
+ export function outdatedJui(result: CliResult): string | undefined {
30
+ const hit = OUTDATED.find((o) => o.pattern.test(result.stderr));
31
+ if (!hit) return undefined;
32
+ return `The installed jui has no ${hit.missing}. Update jui, then call this tool again: ${UPDATE_JUI}`;
33
+ }
34
+
35
+ export async function runViz(args: string[], timeout?: number): Promise<CliResult> {
36
+ return runCli("jui", ["viz", ...args], { cwd: homedir(), timeout });
37
+ }
38
+
39
+ /** The tool's text: the CLI's JSON (when it printed one) inside the usual envelope. */
40
+ export function vizResult(result: CliResult): string {
41
+ let parsed: unknown = undefined;
42
+ try {
43
+ parsed = JSON.parse(result.stdout);
44
+ } catch {
45
+ // not JSON (stop / open print a line)
46
+ }
47
+ const notInstalled = (result.exitCode as unknown) === "ENOENT";
48
+ return JSON.stringify(
49
+ {
50
+ success: result.exitCode === 0,
51
+ result: parsed,
52
+ output: parsed === undefined ? result.stdout || undefined : undefined,
53
+ errors: result.stderr || undefined,
54
+ update_jui: notInstalled
55
+ ? `jui is not installed (not found on PATH). Install it: ${UPDATE_JUI}`
56
+ : outdatedJui(result),
57
+ },
58
+ null,
59
+ 2
60
+ );
61
+ }
@@ -0,0 +1,20 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { CLAUDE_CODE_ONLY, runViz, vizResult } from "./viz_common.js";
3
+
4
+ export function register(server: McpServer) {
5
+ server.tool(
6
+ "viz_doctor",
7
+ "Check the environment for jui viz (the local viewer of Claude Code sessions): Python packages, the hooks and " +
8
+ "the Python they run, the port, the claude CLI. Each check has name, ok and detail. " +
9
+ CLAUDE_CODE_ONLY,
10
+ {},
11
+ async () => {
12
+ try {
13
+ const result = await runViz(["doctor", "--json"], 30_000);
14
+ return { content: [{ type: "text", text: vizResult(result) }] };
15
+ } catch (e: any) {
16
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
17
+ }
18
+ }
19
+ );
20
+ }
@@ -0,0 +1,20 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { CLAUDE_CODE_ONLY, runViz, vizResult } from "./viz_common.js";
3
+
4
+ export function register(server: McpServer) {
5
+ server.tool(
6
+ "viz_open",
7
+ "Open the running jui viz page (the local viewer of Claude Code sessions) in the user's browser. " +
8
+ "Fails when no server runs: call viz_start first. " +
9
+ CLAUDE_CODE_ONLY,
10
+ {},
11
+ async () => {
12
+ try {
13
+ const result = await runViz(["open"], 30_000);
14
+ return { content: [{ type: "text", text: vizResult(result) }] };
15
+ } catch (e: any) {
16
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
17
+ }
18
+ }
19
+ );
20
+ }
@@ -0,0 +1,27 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { CLAUDE_CODE_ONLY, runViz, vizResult } from "./viz_common.js";
4
+
5
+ export function register(server: McpServer) {
6
+ server.tool(
7
+ "viz_start",
8
+ "Start the jui viz server (the local viewer of Claude Code sessions) in the background and return its URL " +
9
+ "on 127.0.0.1. A server already running is reported (alreadyRunning), not started again. Hooks and the " +
10
+ "relay are installed with the jui CLI, not here. " +
11
+ CLAUDE_CODE_ONLY,
12
+ {
13
+ port: z.number().int().min(1).max(65535).optional().describe("Port on 127.0.0.1 (default: the viz config, 7788)"),
14
+ },
15
+ async (params) => {
16
+ try {
17
+ const args = ["start", "--json"];
18
+ if (params.port !== undefined) { args.push("--port", String(params.port)); }
19
+ // jui waits up to 15 s for the server to answer.
20
+ const result = await runViz(args, 45_000);
21
+ return { content: [{ type: "text", text: vizResult(result) }] };
22
+ } catch (e: any) {
23
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
24
+ }
25
+ }
26
+ );
27
+ }
@@ -0,0 +1,27 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { CLAUDE_CODE_ONLY, runViz, vizResult } from "./viz_common.js";
4
+
5
+ export function register(server: McpServer) {
6
+ server.tool(
7
+ "viz_status",
8
+ "Report jui viz (the local viewer of Claude Code sessions): whether its server runs (pid, URL), the hooks in " +
9
+ "~/.claude/settings.json (installed of 18), and whether the permission relay is registered. " +
10
+ CLAUDE_CODE_ONLY,
11
+ {
12
+ relay: z.boolean().optional().describe(
13
+ "Also ask claude whether the permission relay is registered (default true; runs `claude mcp get`, a few seconds)"
14
+ ),
15
+ },
16
+ async (params) => {
17
+ try {
18
+ const args = ["status", "--json"];
19
+ if (params.relay !== false) { args.push("--with-relay"); }
20
+ const result = await runViz(args, 30_000);
21
+ return { content: [{ type: "text", text: vizResult(result) }] };
22
+ } catch (e: any) {
23
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
24
+ }
25
+ }
26
+ );
27
+ }
@@ -0,0 +1,19 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { CLAUDE_CODE_ONLY, runViz, vizResult } from "./viz_common.js";
3
+
4
+ export function register(server: McpServer) {
5
+ server.tool(
6
+ "viz_stop",
7
+ "Stop the jui viz server (the local viewer of Claude Code sessions). Only the server jui recorded is stopped. " +
8
+ CLAUDE_CODE_ONLY,
9
+ {},
10
+ async () => {
11
+ try {
12
+ const result = await runViz(["stop"], 30_000);
13
+ return { content: [{ type: "text", text: vizResult(result) }] };
14
+ } catch (e: any) {
15
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
16
+ }
17
+ }
18
+ );
19
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The tool count is written in two places by hand — the startup log in
3
+ * src/index.ts and the README's "Available Tools (N)" with one "(n)" per
4
+ * group — and the log said 41 (8 test) for a while after the server had
5
+ * 46 (10 test). Both are checked against the tools the source registers:
6
+ * one `server.tool(` per file under src/tools/<group>/, and one register call
7
+ * per tool in index.ts.
8
+ */
9
+ import { readdirSync, readFileSync } from "fs";
10
+ import { join } from "path";
11
+ import { describe, expect, it } from "vitest";
12
+
13
+ const ROOT = join(__dirname, "..");
14
+ const GROUPS = ["spec", "context", "jui", "doc", "api", "test", "viz"]; // README Groups A..G, in order
15
+
16
+ function registeredIn(group: string): string[] {
17
+ const dir = join(ROOT, "src", "tools", group);
18
+ const names: string[] = [];
19
+ for (const file of readdirSync(dir).filter((f) => f.endsWith(".ts"))) {
20
+ const text = readFileSync(join(dir, file), "utf-8");
21
+ for (const m of text.matchAll(/server\.tool\(\s*"([a-z_]+)"/g)) names.push(m[1]);
22
+ }
23
+ return names;
24
+ }
25
+
26
+ const counts = Object.fromEntries(GROUPS.map((g) => [g, registeredIn(g).length]));
27
+ const total = Object.values(counts).reduce((a, b) => a + b, 0);
28
+
29
+ describe("tool count", () => {
30
+ it("every tool directory is a known group", () => {
31
+ const dirs = readdirSync(join(ROOT, "src", "tools"), { withFileTypes: true })
32
+ .filter((d) => d.isDirectory())
33
+ .map((d) => d.name);
34
+ expect(dirs.sort()).toEqual([...GROUPS].sort());
35
+ });
36
+
37
+ it("index.ts registers each tool once and its log line says how many, per group", () => {
38
+ const index = readFileSync(join(ROOT, "src", "index.ts"), "utf-8");
39
+ expect(index.match(/^register[A-Za-z]+\(server(, (config|loader))?\);$/gm)!.length).toBe(total);
40
+ const line = index.match(/Registered (\d+) tools \(([^)]*)\)/);
41
+ expect(line).not.toBeNull();
42
+ expect(Number(line![1])).toBe(total);
43
+ expect(line![2]).toBe(GROUPS.map((g) => `${counts[g]} ${g}`).join(" + "));
44
+ });
45
+
46
+ it("the README says the same", () => {
47
+ const readme = readFileSync(join(ROOT, "README.md"), "utf-8");
48
+ expect(readme.match(/^## Available Tools \((\d+)\)$/m)![1]).toBe(String(total));
49
+ const headers = [...readme.matchAll(/^### Group ([A-Z]): .*\((\d+)\)$/gm)];
50
+ expect(headers.map((h) => h[1])).toEqual(GROUPS.map((_, i) => String.fromCharCode(65 + i)));
51
+ expect(headers.map((h) => Number(h[2]))).toEqual(GROUPS.map((g) => counts[g]));
52
+ });
53
+ });
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Group G (jui viz) tool wrappers. child_process is mocked: the tests pin the
3
+ * argv each tool builds, that it runs in the home folder (never the project
4
+ * dir), the envelope it returns, and the update hint for a jui that predates
5
+ * viz / start / --with-relay. The stderr samples are what those jui versions
6
+ * print, copied from real runs (2026-10-18), not written from memory.
7
+ */
8
+ import { homedir } from "os";
9
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
10
+
11
+ const execFileMock = vi.hoisted(() => vi.fn());
12
+ vi.mock("child_process", () => ({ execFile: execFileMock }));
13
+ vi.mock("node:child_process", () => ({ execFile: execFileMock }));
14
+
15
+ import { register as registerVizStatus } from "../src/tools/viz/viz_status.js";
16
+ import { register as registerVizDoctor } from "../src/tools/viz/viz_doctor.js";
17
+ import { register as registerVizStart } from "../src/tools/viz/viz_start.js";
18
+ import { register as registerVizStop } from "../src/tools/viz/viz_stop.js";
19
+ import { register as registerVizOpen } from "../src/tools/viz/viz_open.js";
20
+ import { cleanupTempDirs, createToolHarness, makeTempDir, type ToolHarness } from "./helpers.js";
21
+
22
+ type ExecCallback = (error: any, stdout: string, stderr: string) => void;
23
+
24
+ let recorded: { command: string; args: string[]; options: Record<string, any> }[];
25
+ let nextResponse: { error?: any; stdout?: string; stderr?: string };
26
+ let harness: ToolHarness;
27
+
28
+ const VIZ_TOOLS = ["viz_status", "viz_doctor", "viz_start", "viz_stop", "viz_open"];
29
+
30
+ // jui 1.9.37 (no viz at all)
31
+ const NO_VIZ =
32
+ "usage: jui [-h] [--version]\n" +
33
+ " {init,i,generate,g,build,b,verify,migrate-layouts,lint-generated,lint-strings,ls,sync_tool,hotload,conformance,screens} ...\n" +
34
+ "jui: error: argument command: invalid choice: 'viz' (choose from init, i, generate, g, build, b, verify, migrate-layouts, lint-generated, lint-strings, ls, sync_tool, hotload, conformance, screens)\n";
35
+ // feature/jui-viz b69ac616 (viz, but no start)
36
+ const NO_START =
37
+ "usage: jui viz [-h]\n" +
38
+ " {serve,status,stop,open,doctor,hooks,export-demo,relay} ...\n" +
39
+ "jui viz: error: argument viz_cmd: invalid choice: 'start' (choose from serve, status, stop, open, doctor, hooks, export-demo, relay)\n";
40
+ // the same jui, asked for status --with-relay
41
+ const NO_WITH_RELAY =
42
+ "usage: jui [-h] [--version]\n" +
43
+ " {init,i,generate,g,build,b,verify,migrate-layouts,lint-generated,lint-strings,ls,sync_tool,hotload,conformance,screens,viz} ...\n" +
44
+ "jui: error: unrecognized arguments: --with-relay\n";
45
+
46
+ function failWithCode(code: number | string, stdout = "", stderr = ""): void {
47
+ nextResponse = { error: Object.assign(new Error(`exit ${code}`), { code }), stdout, stderr };
48
+ }
49
+
50
+ beforeEach(() => {
51
+ recorded = [];
52
+ nextResponse = {};
53
+ execFileMock.mockReset();
54
+ execFileMock.mockImplementation((command: string, args: string[], options: any, callback: ExecCallback) => {
55
+ recorded.push({ command, args, options });
56
+ callback(nextResponse.error ?? null, nextResponse.stdout ?? "", nextResponse.stderr ?? "");
57
+ return { stdin: { end() {} } };
58
+ });
59
+ harness = createToolHarness();
60
+ registerVizStatus(harness.server);
61
+ registerVizDoctor(harness.server);
62
+ registerVizStart(harness.server);
63
+ registerVizStop(harness.server);
64
+ registerVizOpen(harness.server);
65
+ });
66
+
67
+ afterEach(() => {
68
+ delete process.env.JUI_PROJECT_DIR;
69
+ cleanupTempDirs();
70
+ });
71
+
72
+ describe("viz tools", () => {
73
+ it("every description says only Claude Code sessions are shown", () => {
74
+ expect([...harness.tools.keys()]).toEqual(VIZ_TOOLS);
75
+ for (const name of VIZ_TOOLS) {
76
+ expect(harness.tools.get(name)!.description).toContain("Claude Code のセッションだけを見せる");
77
+ }
78
+ });
79
+
80
+ it.each([
81
+ ["viz_status", {}, ["viz", "status", "--json", "--with-relay"]],
82
+ ["viz_status", { relay: true }, ["viz", "status", "--json", "--with-relay"]],
83
+ ["viz_status", { relay: false }, ["viz", "status", "--json"]],
84
+ ["viz_doctor", {}, ["viz", "doctor", "--json"]],
85
+ ["viz_start", {}, ["viz", "start", "--json"]],
86
+ ["viz_start", { port: 18011 }, ["viz", "start", "--json", "--port", "18011"]],
87
+ ["viz_stop", {}, ["viz", "stop"]],
88
+ ["viz_open", {}, ["viz", "open"]],
89
+ ])("%s %j runs jui %j in the home folder, not the project", async (tool, params, argv) => {
90
+ process.env.JUI_PROJECT_DIR = makeTempDir("project");
91
+ await harness.call(tool, params);
92
+ expect(recorded).toHaveLength(1);
93
+ expect(recorded[0].command).toBe("jui");
94
+ expect(recorded[0].args).toEqual(argv);
95
+ expect(recorded[0].options.cwd).toBe(homedir());
96
+ expect(recorded[0].options.cwd).not.toBe(process.env.JUI_PROJECT_DIR);
97
+ });
98
+
99
+ it("viz_start waits longer than jui's own 15 s", async () => {
100
+ await harness.call("viz_start");
101
+ expect(recorded[0].options.timeout).toBeGreaterThan(15_000);
102
+ });
103
+
104
+ it("viz_start rejects a port outside 1-65535 before running anything", async () => {
105
+ await expect(harness.call("viz_start", { port: 0 })).rejects.toThrow();
106
+ await expect(harness.call("viz_start", { port: 70000 })).rejects.toThrow();
107
+ expect(execFileMock).not.toHaveBeenCalled();
108
+ });
109
+
110
+ it("passes the CLI's JSON through as result", async () => {
111
+ const started = { running: true, alreadyRunning: false, pid: 4242, url: "http://127.0.0.1:18011/", log: "/h/serve.log" };
112
+ nextResponse = { stdout: JSON.stringify(started) + "\n" };
113
+ expect(JSON.parse(await harness.call("viz_start"))).toEqual({ success: true, result: started });
114
+ });
115
+
116
+ it("a failed start keeps the CLI's JSON and reports failure, with no update hint", async () => {
117
+ const failed = { running: false, error: "jui viz serve が終了しました(終了コード 1)", logTail: "ポート 18011 は使用中です" };
118
+ failWithCode(1, JSON.stringify(failed));
119
+ expect(JSON.parse(await harness.call("viz_start"))).toEqual({ success: false, result: failed });
120
+ });
121
+
122
+ it("a line that is not JSON comes back as output", async () => {
123
+ nextResponse = { stdout: "jui viz: 止めました (pid 4242)\n" };
124
+ expect(JSON.parse(await harness.call("viz_stop"))).toEqual({ success: true, output: "jui viz: 止めました (pid 4242)\n" });
125
+ });
126
+
127
+ it.each([
128
+ ["viz_status", {}, NO_VIZ, "`jui viz`"],
129
+ ["viz_start", {}, NO_VIZ, "`jui viz`"],
130
+ ["viz_start", {}, NO_START, "`jui viz start`"],
131
+ ["viz_status", {}, NO_WITH_RELAY, "`jui viz status --with-relay`"],
132
+ ])("%s on an old jui names what is missing and how to update", async (tool, params, stderr, missing) => {
133
+ failWithCode(2, "", stderr);
134
+ const parsed = JSON.parse(await harness.call(tool, params));
135
+ expect(parsed.success).toBe(false);
136
+ expect(parsed.errors).toBe(stderr);
137
+ expect(parsed.update_jui).toBe(
138
+ `The installed jui has no ${missing}. Update jui, then call this tool again: ` +
139
+ "curl -fsSL https://raw.githubusercontent.com/Tai-Kimura/jsonui-cli/main/installer/bootstrap.sh | bash"
140
+ );
141
+ });
142
+
143
+ it("another argparse error is not taken for an old jui", async () => {
144
+ failWithCode(2, "", "jui viz: error: argument --port: invalid int value: 'x'\n");
145
+ expect(JSON.parse(await harness.call("viz_start")).update_jui).toBeUndefined();
146
+ });
147
+
148
+ it("no jui on PATH says so", async () => {
149
+ failWithCode("ENOENT");
150
+ const parsed = JSON.parse(await harness.call("viz_status"));
151
+ expect(parsed.success).toBe(false);
152
+ expect(parsed.update_jui).toMatch(/^jui is not installed \(not found on PATH\)\. Install it: curl /);
153
+ });
154
+ });