MCP: multi-line --text goes to the CLI as --from-text <temp file> (console_verify kept only the first line on Windows); long JSON output keeps its start and names a file with the whole output (console_plan lost its start URL); test, pitfall 100
This commit is contained in:
parent
73d308164d
commit
e1634f6abe
4 changed files with 66 additions and 2 deletions
30
cli/mcp.ts
30
cli/mcp.ts
|
|
@ -20,6 +20,9 @@
|
|||
// Every tool runs the CLI as a child process in the project directory (STOREBOUND_PROJECT or the server's cwd) and
|
||||
// returns its output. stdout of this process carries protocol messages only; logs go to stderr.
|
||||
// Tool list and safety rules: mcp-tools.ts (write tools default to dry_run, nothing public or irreversible exposed).
|
||||
import { mkdtempSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import pkg from '../package.json' with { type: 'json' };
|
||||
import { runCommand, type Running } from './lib/runner';
|
||||
import { listTools, TOOLS, toArgv, validateInput } from './mcp-tools';
|
||||
|
|
@ -107,9 +110,10 @@ export async function handle(m: Msg, run: (argv: string[], id: Id) => Promise<{
|
|||
const input = (m.params?.arguments ?? {}) as Record<string, unknown>;
|
||||
const problems = validateInput(tool, input);
|
||||
if (problems.length) return ok({ content: [{ type: 'text', text: `Invalid arguments: ${problems.join('; ')}` }], isError: true });
|
||||
const { argv, dryRun } = toArgv(tool, input);
|
||||
const { argv: raw, dryRun } = toArgv(tool, input);
|
||||
const argv = textToFile(raw);
|
||||
const r = await run(argv, m.id);
|
||||
let out = r.out.length > MAX_OUTPUT ? `[... first ${r.out.length - MAX_OUTPUT} characters cut ...]\n` + r.out.slice(-MAX_OUTPUT) : r.out;
|
||||
let out = clip(r.out, argv.includes('--json'));
|
||||
if (dryRun) out += '\n\n(dry run: nothing was changed. Ask the owner before calling again with dry_run: false.)';
|
||||
const structured = { exitCode: r.code, dryRun, command: `storebound ${argv.join(' ')}`, output: out };
|
||||
return ok({ content: [{ type: 'text', text: `$ storebound ${argv.join(' ')}\n${out}\n[exit ${r.code}]` }], structuredContent: structured, isError: r.code !== 0 });
|
||||
|
|
@ -118,6 +122,28 @@ export async function handle(m: Msg, run: (argv: string[], id: Id) => Promise<{
|
|||
}
|
||||
}
|
||||
|
||||
/** A multi-line --text=... goes to the CLI as --from-text <file>: on Windows the child starts through a shell and a
|
||||
* multi-line argument kept only its first line (console_verify read every App Privacy type as missing). */
|
||||
export function textToFile(argv: string[]): string[] {
|
||||
return argv.flatMap(a => {
|
||||
if (!a.startsWith('--text=') || !/[\r\n]/.test(a)) return [a];
|
||||
const file = join(mkdtempSync(join(tmpdir(), 'storebound-mcp-')), 'page.txt');
|
||||
writeFileSync(file, a.slice('--text='.length));
|
||||
return ['--from-text', file];
|
||||
});
|
||||
}
|
||||
|
||||
/** Long output: the whole text goes to a file whose path is named; JSON keeps its start (a plan's start URL and first
|
||||
* steps were cut when the tail was kept), plain text its end (where errors are). */
|
||||
export function clip(out: string, json: boolean, max = MAX_OUTPUT): string {
|
||||
if (out.length <= max) return out;
|
||||
const file = join(mkdtempSync(join(tmpdir(), 'storebound-mcp-')), json ? 'output.json' : 'output.txt');
|
||||
writeFileSync(file, out);
|
||||
return json
|
||||
? `${out.slice(0, max)}\n[... last ${out.length - max} characters cut; the whole output: ${file}]`
|
||||
: `[... first ${out.length - max} characters cut; the whole output: ${file}]\n${out.slice(-max)}`;
|
||||
}
|
||||
|
||||
const running = new Map<Id, Running>();
|
||||
function cancel(id: Id | undefined): void { if (id !== undefined) running.get(id)?.cancel(); }
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue