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
|
|
@ -2,6 +2,9 @@
|
|||
|
||||
## Unreleased
|
||||
|
||||
- **MCP `console_verify` reads multi-line page text whole** (it kept only the first line on Windows), and long JSON
|
||||
output such as `console_plan` keeps its start and names a file with the whole output. Pitfall 100.
|
||||
|
||||
- **Windows builds.** `play build` runs `gradlew.bat` through `cmd /c` (it was looked up on PATH only) and takes
|
||||
`C:/...` keystore paths as absolute. Pitfall 98.
|
||||
- **`check` verifies credential files.** Every credential path that does not exist on this machine and an upload keystore
|
||||
|
|
|
|||
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(); }
|
||||
|
||||
|
|
|
|||
|
|
@ -1209,3 +1209,14 @@ that fails without the fix (see CONTRIBUTING.md).
|
|||
exist and runs `keytool -list` with the stored password and alias (when keytool is on PATH or in JAVA_HOME), naming
|
||||
the bad field (`tests/build-sign.test.ts`). `play build` / `apple build` also write `<out>/latest.json` so CI
|
||||
needs no glob for the AAB / IPA.
|
||||
|
||||
## 100. `console_verify` reported every App Privacy type missing (multi-line page text over MCP on Windows)
|
||||
- **Symptom:** MCP `console_verify` with the App Privacy page text said "not on the page" for every data type and "no
|
||||
Published line"; the command line it echoed held only the text's first line. The same text from a file matched.
|
||||
- **Cause:** the text went to the CLI as one `--text=...` argument; on Windows the child starts through a shell, which
|
||||
kept only the first line. Separately, `console_plan` output over 60 KB was cut at the start, losing the start URL
|
||||
and the first steps.
|
||||
- **Do:** update storebound; meanwhile save the page text to a file and run `storebound console verify <form>
|
||||
--from-text <file>`.
|
||||
- **Guard:** `textToFile` in `cli/mcp.ts` sends multi-line text as `--from-text <temp file>`; `clip` keeps the start of
|
||||
long JSON output (the end of plain text) and names a file with the whole output (`tests/mcp-text-file.test.ts`).
|
||||
|
|
|
|||
24
tests/mcp-text-file.test.ts
Normal file
24
tests/mcp-text-file.test.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// MCP calls: multi-line page text reaches the CLI whole (pitfall 100), long JSON output keeps its start.
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { clip, textToFile } from '../cli/mcp';
|
||||
|
||||
describe('mcp argv and output', () => {
|
||||
test('a multi-line --text= becomes --from-text <file> with the whole text', () => {
|
||||
const page = 'App Privacy\nData Types\nCrash Data\r\nUsed for App Functionality\nPublished a few seconds ago';
|
||||
const argv = textToFile(['console', 'verify', 'apple-app-privacy', `--text=${page}`, '--json']);
|
||||
expect(argv.slice(0, 4)).toEqual(['console', 'verify', 'apple-app-privacy', '--from-text']);
|
||||
expect(argv.some(a => /[\r\n]/.test(a))).toBe(false);
|
||||
expect(readFileSync(argv[4]!, 'utf8')).toBe(page);
|
||||
expect(textToFile(['console', 'verify', 'x', '--text=one line'])).toEqual(['console', 'verify', 'x', '--text=one line']);
|
||||
});
|
||||
test('long JSON keeps its start, long text its end; both name the full file', () => {
|
||||
const json = `{"startUrl":"https://example.com/start",${'"x":1,'.repeat(50)}"end":true}`;
|
||||
const j = clip(json, true, 40);
|
||||
expect(j.startsWith('{"startUrl":"https://example.com/start"')).toBe(true);
|
||||
const file = /the whole output: (.+)\]$/.exec(j)![1]!;
|
||||
expect(readFileSync(file, 'utf8')).toBe(json);
|
||||
expect(clip('a'.repeat(100) + 'ERROR at the end', false, 20).endsWith('ERROR at the end')).toBe(true);
|
||||
expect(clip('short', true, 40)).toBe('short');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue