ladder-mcp 1.0.1 → 1.0.2
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 +26 -0
- package/dist/environment.js +4 -1
- package/dist/index.js +8 -5
- package/dist/input-validation.js +29 -0
- package/dist/kimi-mcp-config.js +16 -1
- package/dist/kimi-runner.js +12 -2
- package/dist/transports/acp.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@ All notable changes to Ladder_mcp are documented here. Format loosely follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/); this project uses
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [1.0.2] - 2026-06-28
|
|
8
|
+
|
|
9
|
+
Security and robustness patch release addressing findings from an external
|
|
10
|
+
adversarial review of `src/`.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`isAuthenticated` false-positive on corrupt credentials** (`environment.ts`):
|
|
15
|
+
a credentials file that exists but is unreadable or invalid JSON was previously
|
|
16
|
+
reported as authenticated. It now fails closed and returns `false`, satisfying
|
|
17
|
+
NFR-5 honest diagnostics.
|
|
18
|
+
- **Unbounded `stdout`/`stderr` capture in `runKimi`** (`kimi-runner.ts`): CLI
|
|
19
|
+
output is now accumulated through `appendCapped` with a hard ceiling
|
|
20
|
+
(`MAX_CAPTURE_CHARS`), preventing memory exhaustion on runaway output.
|
|
21
|
+
- **Unvalidated `max_output_tokens` and `work_dir`** (`input-validation.ts`):
|
|
22
|
+
non-finite, zero, or negative token counts are clamped to the default budget;
|
|
23
|
+
`work_dir` must be an absolute path to an existing directory before Kimi is
|
|
24
|
+
spawned. Used by `kimi_analyze` and `kimi_resume`.
|
|
25
|
+
|
|
26
|
+
### Security
|
|
27
|
+
|
|
28
|
+
- **Arbitrary command persisted to `mcp.json`** (`kimi-mcp-config.ts`):
|
|
29
|
+
`kimi_generate_mcp_config` now validates `command` against an allow-list of
|
|
30
|
+
`node`, `npx`, or an absolute path to an existing file. Other values (e.g.
|
|
31
|
+
`powershell`) are rejected and nothing is written.
|
|
32
|
+
|
|
7
33
|
## [1.0.1] - 2026-06-28
|
|
8
34
|
|
|
9
35
|
Bug-fix release. Issues found by exercising all 20 tools live against a real
|
package/dist/environment.js
CHANGED
|
@@ -126,7 +126,10 @@ export function isAuthenticated(options) {
|
|
|
126
126
|
return Boolean(data.access_token || data.refresh_token || data.id_token || data.token);
|
|
127
127
|
}
|
|
128
128
|
catch {
|
|
129
|
-
|
|
129
|
+
// A corrupt or unreadable credentials file is NOT proof of authentication.
|
|
130
|
+
// Returning true here produced a false-positive "authenticated" status that
|
|
131
|
+
// misled diagnostics (NFR-5). Fail closed.
|
|
132
|
+
return false;
|
|
130
133
|
}
|
|
131
134
|
}
|
|
132
135
|
export function isKimiInstalled(options) {
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
+
import { maxChars, validateWorkDir } from './input-validation.js';
|
|
5
6
|
import { getKimiStatus } from './environment.js';
|
|
6
7
|
import { runKimiApi, isApiConfigured } from './kimi-api.js';
|
|
7
8
|
import { runKimi } from './kimi-runner.js';
|
|
@@ -13,9 +14,8 @@ import { generateMcpConfig } from './kimi-mcp-config.js';
|
|
|
13
14
|
import { buildBudgetProbeGuide, getDesktopStatus } from './desktop-work.js';
|
|
14
15
|
const server = new McpServer({
|
|
15
16
|
name: 'kimi-code',
|
|
16
|
-
version: '1.0.
|
|
17
|
+
version: '1.0.2',
|
|
17
18
|
});
|
|
18
|
-
const DEFAULT_MAX_OUTPUT_CHARS = 60_000;
|
|
19
19
|
const FORMAT_INSTRUCTIONS = {
|
|
20
20
|
summary: `
|
|
21
21
|
OUTPUT FORMAT CONSTRAINTS:
|
|
@@ -42,9 +42,6 @@ IMPORTANT: Your response will be consumed by another AI model with limited conte
|
|
|
42
42
|
function wrapPrompt(prompt, detailLevel) {
|
|
43
43
|
return `${prompt}\n${FORMAT_INSTRUCTIONS[detailLevel]}\n${AI_CONSUMER_NOTICE}`;
|
|
44
44
|
}
|
|
45
|
-
function maxChars(maxOutputTokens) {
|
|
46
|
-
return maxOutputTokens ? maxOutputTokens * 4 : DEFAULT_MAX_OUTPUT_CHARS;
|
|
47
|
-
}
|
|
48
45
|
function textResponse(text, isError = false) {
|
|
49
46
|
return { content: [{ type: 'text', text }], isError };
|
|
50
47
|
}
|
|
@@ -60,6 +57,9 @@ server.tool('kimi_analyze', 'Send a prompt to Kimi Code for codebase analysis. D
|
|
|
60
57
|
max_output_tokens: z.number().optional(),
|
|
61
58
|
include_thinking: z.boolean().optional(),
|
|
62
59
|
}, async ({ prompt, work_dir, session_id, new_session, detail_level, max_output_tokens, include_thinking }) => {
|
|
60
|
+
const workDirError = validateWorkDir(work_dir);
|
|
61
|
+
if (workDirError)
|
|
62
|
+
return textResponse(`Error: ${workDirError}`, true);
|
|
63
63
|
const result = await runKimi({
|
|
64
64
|
prompt: wrapPrompt(prompt, detail_level ?? 'normal'),
|
|
65
65
|
workDir: work_dir,
|
|
@@ -107,6 +107,9 @@ server.tool('kimi_resume', 'Resume an explicit Kimi Code session with a new prom
|
|
|
107
107
|
max_output_tokens: z.number().optional(),
|
|
108
108
|
include_thinking: z.boolean().optional(),
|
|
109
109
|
}, async ({ session_id, prompt, work_dir, detail_level, max_output_tokens, include_thinking }) => {
|
|
110
|
+
const workDirError = validateWorkDir(work_dir);
|
|
111
|
+
if (workDirError)
|
|
112
|
+
return textResponse(`Error: ${workDirError}`, true);
|
|
110
113
|
const result = await runKimi({
|
|
111
114
|
prompt: wrapPrompt(prompt, detail_level ?? 'normal'),
|
|
112
115
|
workDir: work_dir,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
export const DEFAULT_MAX_OUTPUT_CHARS = 60_000;
|
|
4
|
+
// Convert an untrusted max_output_tokens into a character budget. NaN/Infinity/zero/
|
|
5
|
+
// negative/non-integer would otherwise produce a nonsensical budget (e.g. NaN) handed
|
|
6
|
+
// to truncation, so clamp those to the default.
|
|
7
|
+
export function maxChars(maxOutputTokens) {
|
|
8
|
+
if (typeof maxOutputTokens !== 'number' || !Number.isFinite(maxOutputTokens) || maxOutputTokens <= 0) {
|
|
9
|
+
return DEFAULT_MAX_OUTPUT_CHARS;
|
|
10
|
+
}
|
|
11
|
+
return Math.floor(maxOutputTokens) * 4;
|
|
12
|
+
}
|
|
13
|
+
// Reject a work_dir before spawning Kimi: a relative or non-existent directory would
|
|
14
|
+
// otherwise be passed to the CLI as cwd and fail opaquely. Returns an error string when
|
|
15
|
+
// invalid, or undefined when the path is an existing absolute directory.
|
|
16
|
+
export function validateWorkDir(workDir) {
|
|
17
|
+
if (!path.isAbsolute(workDir))
|
|
18
|
+
return 'work_dir must be an absolute path.';
|
|
19
|
+
let stat;
|
|
20
|
+
try {
|
|
21
|
+
stat = fs.statSync(workDir);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return `work_dir does not exist: ${workDir}`;
|
|
25
|
+
}
|
|
26
|
+
if (!stat.isDirectory())
|
|
27
|
+
return `work_dir is not a directory: ${workDir}`;
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
package/dist/kimi-mcp-config.js
CHANGED
|
@@ -44,6 +44,19 @@ function assertWritableProjectTarget(projectDir) {
|
|
|
44
44
|
throw new Error('Refusing to write MCP config under read-only kimi-code-mcp reference tree.');
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
+
const ALLOWED_BARE_COMMANDS = new Set(['node', 'npx']);
|
|
48
|
+
// Constrain the launcher written into mcp.json. Without this, an arbitrary `command`
|
|
49
|
+
// (e.g. "powershell") would be persisted and later executed when Kimi loads the
|
|
50
|
+
// server config. Allow only the known Node launchers by bare name, or an absolute
|
|
51
|
+
// path to an existing file (a vetted local binary).
|
|
52
|
+
function assertSafeCommand(command) {
|
|
53
|
+
const value = command.trim();
|
|
54
|
+
if (ALLOWED_BARE_COMMANDS.has(value))
|
|
55
|
+
return;
|
|
56
|
+
if (path.isAbsolute(value) && fs.existsSync(value) && fs.statSync(value).isFile())
|
|
57
|
+
return;
|
|
58
|
+
throw new Error(`command must be one of [${[...ALLOWED_BARE_COMMANDS].join(', ')}] or an absolute path to an existing file; got "${command}".`);
|
|
59
|
+
}
|
|
47
60
|
function readMcpServers(existing) {
|
|
48
61
|
const value = existing.mcpServers;
|
|
49
62
|
if (value === undefined || value === null)
|
|
@@ -66,8 +79,10 @@ export function generateMcpConfig(options = {}) {
|
|
|
66
79
|
throw new Error('server_name must contain only letters, digits, underscores, and hyphens.');
|
|
67
80
|
}
|
|
68
81
|
const defaults = defaultServerCommand();
|
|
82
|
+
const command = options.command ?? defaults.command;
|
|
83
|
+
assertSafeCommand(command);
|
|
69
84
|
const serverConfig = {
|
|
70
|
-
command
|
|
85
|
+
command,
|
|
71
86
|
args: options.args ?? defaults.args,
|
|
72
87
|
env: {},
|
|
73
88
|
};
|
package/dist/kimi-runner.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { buildKimiEnv, resolveKimiPaths } from './environment.js';
|
|
4
|
+
const MAX_CAPTURE_CHARS = 16 * 1024 * 1024;
|
|
5
|
+
// Append to a captured stream while enforcing a hard ceiling so a runaway CLI cannot
|
|
6
|
+
// grow the buffer without bound. Once the cap is reached, further data is dropped (the
|
|
7
|
+
// stream is still drained by Node, just not stored).
|
|
8
|
+
export function appendCapped(current, addition, max) {
|
|
9
|
+
if (current.length >= max)
|
|
10
|
+
return current;
|
|
11
|
+
const next = current + addition;
|
|
12
|
+
return next.length > max ? next.slice(0, max) : next;
|
|
13
|
+
}
|
|
4
14
|
export function buildKimiArgs(config) {
|
|
5
15
|
const args = ['-p', config.prompt, '--output-format', 'stream-json'];
|
|
6
16
|
if (config.sessionId) {
|
|
@@ -120,8 +130,8 @@ export function runKimi(config) {
|
|
|
120
130
|
killProcessTree(proc.pid);
|
|
121
131
|
finish({ ok: false, text: '', error: `Kimi timed out after ${Math.round(timeoutMs / 1000)}s` });
|
|
122
132
|
}, timeoutMs);
|
|
123
|
-
proc.stdout?.on('data', (chunk) => { stdout
|
|
124
|
-
proc.stderr?.on('data', (chunk) => { stderr
|
|
133
|
+
proc.stdout?.on('data', (chunk) => { stdout = appendCapped(stdout, chunk.toString('utf-8'), MAX_CAPTURE_CHARS); });
|
|
134
|
+
proc.stderr?.on('data', (chunk) => { stderr = appendCapped(stderr, chunk.toString('utf-8'), MAX_CAPTURE_CHARS); });
|
|
125
135
|
proc.on('error', (err) => {
|
|
126
136
|
finish({ ok: false, text: '', error: err instanceof Error ? err.message : String(err) });
|
|
127
137
|
});
|
package/dist/transports/acp.js
CHANGED
|
@@ -221,7 +221,7 @@ export class AcpClient extends EventEmitter {
|
|
|
221
221
|
async initialize() {
|
|
222
222
|
return this.request('initialize', {
|
|
223
223
|
protocolVersion: 1,
|
|
224
|
-
clientInfo: { name: 'ladder-mcp', version: '1.0.
|
|
224
|
+
clientInfo: { name: 'ladder-mcp', version: '1.0.2' },
|
|
225
225
|
capabilities: {},
|
|
226
226
|
}, 30_000);
|
|
227
227
|
}
|