jui-tools-mcp-server 2.15.11 → 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 +15 -1
- package/dist/index.js +15 -2
- package/dist/tools/viz/viz_common.d.ts +13 -0
- package/dist/tools/viz/viz_common.js +50 -0
- package/dist/tools/viz/viz_doctor.d.ts +2 -0
- package/dist/tools/viz/viz_doctor.js +14 -0
- package/dist/tools/viz/viz_open.d.ts +2 -0
- package/dist/tools/viz/viz_open.js +14 -0
- package/dist/tools/viz/viz_start.d.ts +2 -0
- package/dist/tools/viz/viz_start.js +23 -0
- package/dist/tools/viz/viz_status.d.ts +2 -0
- package/dist/tools/viz/viz_status.js +21 -0
- package/dist/tools/viz/viz_stop.d.ts +2 -0
- package/dist/tools/viz/viz_stop.js +13 -0
- package/package.json +1 -1
- package/src/index.ts +17 -2
- package/src/tools/viz/viz_common.ts +61 -0
- package/src/tools/viz/viz_doctor.ts +20 -0
- package/src/tools/viz/viz_open.ts +20 -0
- package/src/tools/viz/viz_start.ts +27 -0
- package/src/tools/viz/viz_status.ts +27 -0
- package/src/tools/viz/viz_stop.ts +19 -0
- package/tests/tool_count.test.ts +53 -0
- package/tests/tools_viz.test.ts +154 -0
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 (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 ネイティブで本リポジトリのモジュール構成とそのまま噛み合うため採用)。
|
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 (
|
|
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
|
-
|
|
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,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,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,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,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,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
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 (
|
|
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
|
-
|
|
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
|
+
});
|