ladder-mcp 1.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,63 @@
1
+ # Changelog
2
+
3
+ All notable changes to Ladder_mcp are documented here. Format loosely follows
4
+ [Keep a Changelog](https://keepachangelog.com/); this project uses
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [1.0.0] - 2026-06-27
8
+
9
+ First release. Windows-first MCP bridge for Kimi Code CLI v24, rebuilt in a
10
+ fresh `./src` application (package `ladder-mcp`). Logic is ported from the
11
+ read-only `kimi-code-mcp/` reference; the legacy `CacheManager`/warmup layer is
12
+ not carried over.
13
+
14
+ ### Added — v1 core (Epics 1-3)
15
+
16
+ - Robust environment resolver (`environment.ts`): discovers `kimi.exe` via PATH
17
+ and `~/.kimi-code/bin/`, resolves the `~/.kimi-code/` catalog, credentials and
18
+ version with no hardcoded POSIX paths; no silent fallback to legacy `~/.kimi/`.
19
+ - CLI adapter (`kimi-runner.ts`): correct Kimi CLI v24 arguments
20
+ (`-p`, `--output-format stream-json`, `-S`, `-C`, `--auto`), `stream-json`
21
+ parsing, native session resume, Windows process-tree termination on timeout.
22
+ - API adapter (`kimi-api.ts`): contextless `kimi_query` / `kimi_verify` reading
23
+ key/endpoint from `KIMICODE_API_KEY` or `~/.kimi-code/config.toml`.
24
+ - Six v1 MCP tools: `kimi_analyze`, `kimi_query`, `kimi_verify`, `kimi_resume`,
25
+ `kimi_list_sessions`, `kimi_status`.
26
+
27
+ ### Added — vNext expansion (Epic 4)
28
+
29
+ - **CLI admin & capabilities** (`transports/cli-admin.ts`): `kimi_capabilities`,
30
+ `kimi_doctor`, `kimi_provider_list`, `kimi_export_session`,
31
+ `kimi_visualize_session`. Export requires an explicit `output_path`, is
32
+ confined to the working directory, and excludes the global diagnostic log by
33
+ default.
34
+ - **ACP MVP over stdio** (`transports/acp.ts`): JSON-RPC client for `kimi acp`
35
+ with newline-delimited and `Content-Length` framing; `kimi_chat`,
36
+ `kimi_acp_sessions`, `kimi_cancel`.
37
+ - **Background task lifecycle** (`task-store.ts`): in-process task store with
38
+ `kimi_chat background=true`, `kimi_task_status`, `kimi_task_output`,
39
+ `kimi_task_cancel`.
40
+ - **Kimi-hosted MCP config + read-only desktop probes**
41
+ (`kimi-mcp-config.ts`, `desktop-work.ts`): `kimi_generate_mcp_config`,
42
+ `kimi_desktop_status`, `kimi_budget_probe`. Desktop access is
43
+ experimental/read-only — no token-store reads, no web-auth replay, no desktop
44
+ Work task submission.
45
+
46
+ ### Security & robustness (adversarial review)
47
+
48
+ Hardened against path traversal/TOCTOU on export, writes under the read-only
49
+ reference tree, unbounded ACP frame/task memory, process crash on malformed ACP
50
+ JSON, non-idempotent task cancellation, and UTF-8 byte-boundary truncation.
51
+
52
+ The remaining lower-risk review findings (W1-W12) were also resolved before
53
+ release: raw ACP response-id matching (no `NaN` coercion), bounded ACP update
54
+ buffer, clamped ACP request timeouts, smaller buffers for parallel capability
55
+ probes, a timeout around task cancel hooks, a size cap on the desktop status
56
+ probe body, validated `kimi_chat` timeout input, and `kimi_cancel` rejecting
57
+ ambiguous task_id+session_id calls. Regression tests added (49 total).
58
+
59
+ ### Known limitations
60
+
61
+ - Windows 11 only (NFR-1); POSIX branches are not a target.
62
+
63
+ [1.0.0]: https://semver.org/
package/LICENSE ADDED
@@ -0,0 +1,27 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sherstnev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ This project ports logic from the kimi-code-mcp reference
26
+ (MIT License, Copyright (c) 2026 howardpen9), retained as a read-only
27
+ reference under ./kimi-code-mcp/.
package/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # Ladder_mcp
2
+
3
+ Windows-first [MCP](https://modelcontextprotocol.io/) bridge for the
4
+ [Kimi Code](https://kimi.com) CLI (v24). It exposes Kimi Code as MCP tools so a
5
+ client like Claude Code can run codebase analysis, native sessions, API
6
+ queries, ACP chat, background tasks, and CLI admin/diagnostics — all on Windows
7
+ without hardcoded POSIX assumptions.
8
+
9
+ > Status: **v1.0.0**. Supported platform is **Windows 11 only**.
10
+
11
+ ## Requirements
12
+
13
+ - Windows 11
14
+ - Node.js ≥ 18
15
+ - Kimi Code CLI installed (`kimi.exe` on PATH or at `~/.kimi-code/bin/kimi.exe`),
16
+ authenticated (`~/.kimi-code/`)
17
+
18
+ ## Install & build
19
+
20
+ ```bash
21
+ npm install
22
+ npm run build # compiles src/ -> dist/ (tests excluded)
23
+ ```
24
+
25
+ Quick checks:
26
+
27
+ ```bash
28
+ npm test # vitest (44 tests)
29
+ npm run typecheck # tsc --noEmit (incl. tests)
30
+ npm run dev # run the server from source via tsx
31
+ ```
32
+
33
+ ## Run as an MCP server
34
+
35
+ The server speaks MCP over stdio. Point your MCP client at the built entry:
36
+
37
+ ```jsonc
38
+ // e.g. Claude Code MCP config
39
+ {
40
+ "mcpServers": {
41
+ "kimi-code": {
42
+ "command": "node",
43
+ "args": ["C:\\path\\to\\Ladder_mcp\\dist\\index.js"]
44
+ }
45
+ }
46
+ }
47
+ ```
48
+
49
+ To let Kimi Code itself host this server, use the `kimi_generate_mcp_config`
50
+ tool to produce/merge a `.kimi-code/mcp.json` entry.
51
+
52
+ ## Tools
53
+
54
+ **Core (v1)** — `kimi_analyze`, `kimi_query`, `kimi_verify`, `kimi_resume`,
55
+ `kimi_list_sessions`, `kimi_status`
56
+
57
+ **CLI admin & capabilities** — `kimi_capabilities`, `kimi_doctor`,
58
+ `kimi_provider_list`, `kimi_export_session`, `kimi_visualize_session`
59
+
60
+ **ACP (chat over stdio)** — `kimi_chat`, `kimi_acp_sessions`, `kimi_cancel`
61
+
62
+ **Background tasks** — `kimi_task_status`, `kimi_task_output`, `kimi_task_cancel`
63
+ (set `background=true` on `kimi_chat`)
64
+
65
+ **Kimi-hosted config & desktop (read-only)** — `kimi_generate_mcp_config`,
66
+ `kimi_desktop_status`, `kimi_budget_probe`
67
+
68
+ ## Safety boundaries
69
+
70
+ - `kimi_export_session` requires an explicit `output_path`, stays within the
71
+ working directory, and excludes the global diagnostic log by default.
72
+ - Desktop Work tools are **experimental and read-only**: they do not read the
73
+ desktop token store, replay web auth, or submit desktop Work tasks.
74
+ - The vendored `kimi-code-mcp/` is a **read-only reference** and is never edited
75
+ or written to by the tools.
76
+
77
+ ## Project layout
78
+
79
+ - `src/` — the Ladder_mcp application (package `ladder-mcp`)
80
+ - `kimi-code-mcp/` — upstream reference (read-only, MIT)
81
+ - `_bmad-output/` — planning & implementation artifacts (BMAD workflow)
82
+
83
+ ## License
84
+
85
+ [MIT](./LICENSE). Ports logic from the MIT-licensed `kimi-code-mcp` reference.
@@ -0,0 +1,102 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ const execFileAsync = promisify(execFile);
4
+ export const MAX_PROBE_BODY_BYTES = 256 * 1024;
5
+ // Read a fetch response body but stop once MAX_PROBE_BODY_BYTES is reached, so a
6
+ // misbehaving or hostile local endpoint cannot stream an unbounded body into memory.
7
+ // The surrounding AbortController still bounds total time.
8
+ export async function readBodyCapped(response) {
9
+ const reader = response.body?.getReader();
10
+ if (!reader)
11
+ return { text: await response.text(), truncated: false };
12
+ const decoder = new TextDecoder();
13
+ let text = '';
14
+ let total = 0;
15
+ let truncated = false;
16
+ for (;;) {
17
+ const { done, value } = await reader.read();
18
+ if (done)
19
+ break;
20
+ total += value.byteLength;
21
+ if (total > MAX_PROBE_BODY_BYTES) {
22
+ text += decoder.decode(value.slice(0, Math.max(0, MAX_PROBE_BODY_BYTES - (total - value.byteLength))));
23
+ truncated = true;
24
+ await reader.cancel();
25
+ break;
26
+ }
27
+ text += decoder.decode(value, { stream: true });
28
+ }
29
+ if (!truncated)
30
+ text += decoder.decode();
31
+ return { text, truncated };
32
+ }
33
+ export async function getDesktopStatus() {
34
+ const safety = [
35
+ 'Experimental/read-only probe only.',
36
+ 'Does not read desktop token-store files.',
37
+ 'Does not replay web auth.',
38
+ 'Does not submit desktop Work tasks.',
39
+ ];
40
+ const status = { experimental: true, readOnly: true, safety };
41
+ try {
42
+ // Resolved from PATH. This is safe by construction: execFile runs the binary
43
+ // directly (no shell, so no metacharacter injection), the only argument is the
44
+ // fixed read-only `status` verb, and a missing binary simply rejects below.
45
+ const { stdout, stderr } = await execFileAsync('kimi-webbridge', ['status'], {
46
+ timeout: 5000,
47
+ windowsHide: true,
48
+ });
49
+ status.bridgeCommand = { ok: true, stdout: String(stdout), stderr: String(stderr) };
50
+ }
51
+ catch (error) {
52
+ status.bridgeCommand = { ok: false, error: error instanceof Error ? error.message : String(error) };
53
+ }
54
+ try {
55
+ const controller = new AbortController();
56
+ const timer = setTimeout(() => controller.abort(), 3000);
57
+ try {
58
+ const response = await fetch('http://127.0.0.1:10086/status', { signal: controller.signal });
59
+ const { text, truncated } = await readBodyCapped(response);
60
+ let body = text;
61
+ if (!truncated) {
62
+ try {
63
+ body = JSON.parse(text);
64
+ }
65
+ catch {
66
+ // Keep raw text.
67
+ }
68
+ }
69
+ status.bridgeHttp = { ok: response.ok, status: response.status, body, ...(truncated ? { truncated: true } : {}) };
70
+ }
71
+ finally {
72
+ clearTimeout(timer);
73
+ }
74
+ }
75
+ catch (error) {
76
+ status.bridgeHttp = { ok: false, error: error instanceof Error ? error.message : String(error) };
77
+ }
78
+ return status;
79
+ }
80
+ export function buildBudgetProbeGuide(includeCliProbe) {
81
+ const lines = [
82
+ '## Kimi Budget Probe (experimental evidence workflow)',
83
+ '',
84
+ 'Safety boundaries:',
85
+ '- This probe does not read desktop token-store files.',
86
+ '- This probe does not replay web auth.',
87
+ '- This probe does not submit desktop Work tasks.',
88
+ '- Treat budget separation as unproven until visible counters confirm it.',
89
+ '',
90
+ 'Manual evidence steps:',
91
+ '1. Record visible Kimi Code CLI usage/billing counters.',
92
+ '2. Record visible Desktop/OK Computer/Kimi Work counters from the app UI.',
93
+ '3. Run one tiny CLI prompt.',
94
+ '4. Run one tiny Desktop Work task manually in the visible app.',
95
+ '5. Compare which counters moved after refresh.',
96
+ '6. Repeat once to rule out delayed accounting.',
97
+ ];
98
+ if (includeCliProbe) {
99
+ lines.push('', 'CLI probe requested: run a tiny `kimi_chat`/`kimi_analyze` prompt separately and compare visible counters.');
100
+ }
101
+ return lines.join('\n');
102
+ }
@@ -0,0 +1,193 @@
1
+ import { execFile } from 'node:child_process';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import { promisify } from 'node:util';
5
+ const execFileAsync = promisify(execFile);
6
+ const DEFAULT_BASE_URL = 'https://api.kimi.com/coding/v1';
7
+ function getEnv(options) {
8
+ return options?.env ?? process.env;
9
+ }
10
+ function exists(candidate, options) {
11
+ return options?.existsSync ? options.existsSync(candidate) : fs.existsSync(candidate);
12
+ }
13
+ function readText(candidate, options) {
14
+ return (options?.readFileSync ?? fs.readFileSync)(candidate, 'utf-8');
15
+ }
16
+ export function getWindowsHome(env = process.env) {
17
+ if (env.USERPROFILE)
18
+ return env.USERPROFILE;
19
+ if (env.HOMEDRIVE && env.HOMEPATH)
20
+ return `${env.HOMEDRIVE}${env.HOMEPATH}`;
21
+ throw new Error('USERPROFILE is not set; Ladder_mcp v1 supports Windows only.');
22
+ }
23
+ function splitWindowsPath(pathValue) {
24
+ return (pathValue ?? '').split(';').map((part) => part.trim()).filter(Boolean);
25
+ }
26
+ export function findKimiOnPath(env = process.env, existsSync = fs.existsSync) {
27
+ for (const entry of splitWindowsPath(env.PATH ?? env.Path)) {
28
+ const exe = path.join(entry, 'kimi.exe');
29
+ if (existsSync(exe))
30
+ return exe;
31
+ }
32
+ return undefined;
33
+ }
34
+ export function resolveKimiPaths(options) {
35
+ const env = getEnv(options);
36
+ const homeDir = getWindowsHome(env);
37
+ const kimiDir = path.join(homeDir, '.kimi-code');
38
+ const defaultBinary = path.join(kimiDir, 'bin', 'kimi.exe');
39
+ const pathBinary = findKimiOnPath(env, (candidate) => exists(candidate, options));
40
+ const binaryPath = pathBinary ?? (exists(defaultBinary, options) ? defaultBinary : undefined);
41
+ return {
42
+ homeDir,
43
+ kimiDir,
44
+ configPath: path.join(kimiDir, 'config.toml'),
45
+ credentialsPath: path.join(kimiDir, 'credentials', 'kimi-code.json'),
46
+ sessionsDir: path.join(kimiDir, 'sessions'),
47
+ sessionIndexPath: path.join(kimiDir, 'session_index.jsonl'),
48
+ legacyDir: path.join(homeDir, '.kimi'),
49
+ pathBinary,
50
+ defaultBinary,
51
+ binaryPath,
52
+ };
53
+ }
54
+ export function interpolateEnv(value, env = process.env) {
55
+ return value.replace(/\$\{(\w+)\}/g, (_, name) => env[name] ?? '');
56
+ }
57
+ function readTomlString(raw, key) {
58
+ const pattern = new RegExp(`^\\s*${key}\\s*=\\s*["']([^"']*)["']`, 'm');
59
+ return raw.match(pattern)?.[1];
60
+ }
61
+ export function loadApiAuth(options) {
62
+ const env = getEnv(options);
63
+ const paths = resolveKimiPaths(options);
64
+ let apiKey = env.KIMICODE_API_KEY ?? '';
65
+ let baseUrl = env.KIMI_BASE_URL ?? '';
66
+ if ((!apiKey || !baseUrl) && exists(paths.configPath, options)) {
67
+ try {
68
+ const raw = readText(paths.configPath, options);
69
+ if (!apiKey)
70
+ apiKey = readTomlString(raw, 'api_key') ?? '';
71
+ if (!baseUrl)
72
+ baseUrl = readTomlString(raw, 'base_url') ?? '';
73
+ if (!apiKey || !baseUrl) {
74
+ const providers = parseProviderBlocks(raw);
75
+ const coding = providers.find((provider) => provider.baseUrl?.includes('/coding'));
76
+ const chosen = coding ?? providers.find((provider) => provider.apiKey) ?? providers[0];
77
+ if (chosen) {
78
+ if (!apiKey && chosen.apiKey)
79
+ apiKey = chosen.apiKey;
80
+ if (!baseUrl && chosen.baseUrl)
81
+ baseUrl = chosen.baseUrl;
82
+ }
83
+ }
84
+ }
85
+ catch {
86
+ // Environment variables are sufficient; malformed config simply means no file-based auth.
87
+ }
88
+ }
89
+ apiKey = interpolateEnv(apiKey, env).trim();
90
+ baseUrl = interpolateEnv(baseUrl || DEFAULT_BASE_URL, env).trim().replace(/\/$/, '');
91
+ return apiKey ? { apiKey, baseUrl } : null;
92
+ }
93
+ function parseProviderBlocks(raw) {
94
+ const providers = [];
95
+ let current = null;
96
+ for (const line of raw.split(/\r?\n/)) {
97
+ const trimmed = line.trim();
98
+ if (!trimmed || trimmed.startsWith('#'))
99
+ continue;
100
+ const section = trimmed.match(/^\[(.+?)]$/);
101
+ if (section) {
102
+ current = section[1].startsWith('providers.') ? {} : null;
103
+ if (current)
104
+ providers.push(current);
105
+ continue;
106
+ }
107
+ if (!current)
108
+ continue;
109
+ const kv = trimmed.match(/^(\w+)\s*=\s*["']([^"']*)["']/);
110
+ if (!kv)
111
+ continue;
112
+ if (kv[1] === 'api_key')
113
+ current.apiKey = kv[2];
114
+ if (kv[1] === 'base_url')
115
+ current.baseUrl = kv[2];
116
+ }
117
+ return providers;
118
+ }
119
+ export function isAuthenticated(options) {
120
+ const paths = resolveKimiPaths(options);
121
+ if (!exists(paths.credentialsPath, options))
122
+ return false;
123
+ try {
124
+ const raw = readText(paths.credentialsPath, options);
125
+ const data = JSON.parse(raw);
126
+ return Boolean(data.access_token || data.refresh_token || data.id_token || data.token);
127
+ }
128
+ catch {
129
+ return true;
130
+ }
131
+ }
132
+ export function isKimiInstalled(options) {
133
+ return Boolean(resolveKimiPaths(options).binaryPath);
134
+ }
135
+ export async function getKimiVersion(binaryPath, env = process.env) {
136
+ try {
137
+ const { stdout } = await execFileAsync(binaryPath, ['--version'], {
138
+ env: buildKimiEnv(env),
139
+ timeout: 5000,
140
+ windowsHide: true,
141
+ });
142
+ return String(stdout).trim() || undefined;
143
+ }
144
+ catch {
145
+ return undefined;
146
+ }
147
+ }
148
+ export function buildKimiEnv(env = process.env) {
149
+ const paths = resolveKimiPaths({ env });
150
+ const currentPath = env.PATH ?? env.Path ?? '';
151
+ const binDir = path.dirname(paths.defaultBinary);
152
+ const pathName = env.PATH !== undefined ? 'PATH' : 'Path';
153
+ const nextEnv = { ...env };
154
+ const entries = splitWindowsPath(currentPath);
155
+ if (!entries.some((entry) => path.normalize(entry).toLowerCase() === path.normalize(binDir).toLowerCase())) {
156
+ nextEnv[pathName] = `${binDir}${currentPath ? `;${currentPath}` : ''}`;
157
+ }
158
+ return nextEnv;
159
+ }
160
+ export async function getKimiStatus(options) {
161
+ const paths = resolveKimiPaths(options);
162
+ const catalogFound = exists(paths.kimiDir, options);
163
+ const credentialsFound = exists(paths.credentialsPath, options);
164
+ const configFound = exists(paths.configPath, options);
165
+ const apiConfigured = loadApiAuth(options) !== null;
166
+ const installed = Boolean(paths.binaryPath);
167
+ const authenticated = isAuthenticated(options);
168
+ let error;
169
+ if (!catalogFound) {
170
+ error = exists(paths.legacyDir, options)
171
+ ? 'Found legacy ~/.kimi but not ~/.kimi-code. Update Kimi CLI to the current kimi-code version.'
172
+ : 'Kimi catalog ~/.kimi-code was not found. Install or update Kimi Code CLI.';
173
+ }
174
+ else if (!installed) {
175
+ error = 'Kimi CLI binary was not found on PATH or at ~/.kimi-code/bin/kimi.exe.';
176
+ }
177
+ else if (!authenticated) {
178
+ error = 'Kimi CLI is not authenticated. Run: kimi login';
179
+ }
180
+ const version = paths.binaryPath ? await getKimiVersion(paths.binaryPath, getEnv(options)) : undefined;
181
+ return {
182
+ installed,
183
+ binPath: paths.binaryPath ?? paths.defaultBinary,
184
+ version,
185
+ authenticated,
186
+ catalogFound,
187
+ catalogPath: paths.kimiDir,
188
+ configFound,
189
+ credentialsFound,
190
+ apiConfigured,
191
+ error,
192
+ };
193
+ }