Check existing issues
Describe the bug
Summary
With the process.exit(0) race from Issue #1 worked around, mppx --mcp connects but every tool call returns null AND writes extra non-JSON-RPC lines to stdout, which corrupts the stdio transport and causes the MCP client to disconnect. Example reproduction with sign:
MCP tool result: {"content":[{"type":"text","text":"null"}]}
Extra stdout: {"authorization":"Payment eyJ..."}
The second line (the actual payload) is what the user wants, but it's written as raw console.log output on the same file descriptor that the JSON-RPC transport is using — so the client reads it as an invalid frame and drops the connection.
Root cause
incur's MCP handler (incur/dist/Mcp.js) builds tool responses from result.data:
const result = await Command.execute(tool.command, { ... });
...
const data = result.data ?? null;
return {
content: [{ type: 'text', text: JSON.stringify(data) }],
...(data !== null && tool.outputSchema ? { structuredContent: data } : {}),
};
But mppx's CLI command handlers never set result.data — they write their output with console.log(...) instead. Examples from dist/cli/cli.js:
| Line |
Code |
Command |
| 874 |
console.log(JSON.stringify({ authorization: credential })); |
sign --format json |
| 877 |
console.log(credential); |
sign |
| 500 |
console.log('Account "..." saved to keychain.'); |
account create |
| 530 |
console.log('Default account set to ...'); |
account default |
| 542 |
console.log('Default account set to ...'); |
same |
| 592 |
console.log('Account ... deleted'); |
account delete |
| 674 |
console.log(... address ... balance ...) |
account list |
| 704 |
console.log(key); |
account export |
| 732+ |
console.log(...) (address/balance/name/type lines) |
account view |
| 979 |
console.log(json); |
discover generate |
| 1059 |
console.log(...) per validation issue |
discover validate |
None of these propagate a structured payload back through the c.* context, so (a) the MCP tool response is always {text: "null"}, and (b) the console.log output bleeds onto stdio, corrupting the JSON-RPC channel.
Link to Minimal Reproducible Example
https://github.com/woven-record-media/mppx-mcp-repro
Steps To Reproduce
Reproduction
Minimal repo: https://github.com/woven-record-media/mppx-mcp-repro (same repo as Issue #386).
git clone https://github.com/woven-record-media/mppx-mcp-repro
cd mppx-mcp-repro && npm install
npm run bug2
The reproducer works around Issue #1 by importing dist/cli/cli.js directly and calling cli.serve(['--mcp']) without the process.exit(0) wrapper, then acts as a minimal JSON-RPC 2.0 client over stdin/stdout against that subprocess. It calls tools/call name="account_list" (chosen because it needs no args and hits a per-command handler).
Expected output:
[bug2] tool call result:
{
"result": { "content": [ { "type": "text", "text": "null" } ] },
"jsonrpc": "2.0",
"id": 3
}
[bug2] non-JSON-RPC lines observed on stdout (these are the leak):
>> <one or more lines of human-formatted account_list output>
[bug2] BUG CONFIRMED: tool result is "null" AND stdout was polluted with N non-JSON-RPC line(s).
(The exact leaked content depends on what's in the local mppx keychain — either account rows, or No accounts found. on a clean machine. Either way: the tool result is null, AND there is stray stdout outside the JSON-RPC framing. A real MCP client drops the connection on the first unparseable frame.)
The same pattern reproduces with sign (line 874/877), account_view (line 732+), and every other per-command handler listed above.
Package Version
[email protected]
Anything else?
Fix
Two parts:
-
Return data through the runtime, not console.log. Each command handler should populate whatever incur exposes as the structured result (likely c.ok({...}) / return {data: ...} — whichever the incur API uses). Example for sign:
- if (c.format === 'json') {
- console.log(JSON.stringify({ authorization: credential }));
- } else {
- console.log(credential);
- }
+ return c.ok({ authorization: credential });
-
Render for humans only when not in agent/MCP mode. For the CLI mppx sign case, keep human-friendly stdout, but gate it on c.agent !== true (or however incur signals the MCP runtime). Alternatively, let incur's default text renderer format result.data for CLI mode — that way the handler stays runtime-agnostic.
+ if (c.agent) {
+ return c.ok({ authorization: credential });
+ }
+ if (c.format === 'json') {
+ console.log(JSON.stringify({ authorization: credential }));
+ } else {
+ console.log(credential);
+ }
This pattern needs to be applied to every console.log in dist/cli/cli.js that represents tool output (not user-facing progress). The tool definitions should also declare outputSchema so the MCP runtime emits structuredContent alongside text.
Impact
Every mppx MCP tool is currently non-functional. For agent use cases that need programmatic signing (Weftly, other MPP-speaking APIs with unattended payment flows), --mcp cannot be used without an external proxy. I've worked around this locally with a ~180-line stdio proxy that shells out to mppx sign per call, but the right fix is upstream.
Check existing issues
Describe the bug
Summary
With the
process.exit(0)race from Issue #1 worked around,mppx --mcpconnects but every tool call returnsnullAND writes extra non-JSON-RPC lines to stdout, which corrupts the stdio transport and causes the MCP client to disconnect. Example reproduction withsign:The second line (the actual payload) is what the user wants, but it's written as raw
console.logoutput on the same file descriptor that the JSON-RPC transport is using — so the client reads it as an invalid frame and drops the connection.Root cause
incur's MCP handler (incur/dist/Mcp.js) builds tool responses fromresult.data:But mppx's CLI command handlers never set
result.data— they write their output withconsole.log(...)instead. Examples fromdist/cli/cli.js:console.log(JSON.stringify({ authorization: credential }));sign --format jsonconsole.log(credential);signconsole.log('Account "..." saved to keychain.');account createconsole.log('Default account set to ...');account defaultconsole.log('Default account set to ...');console.log('Account ... deleted');account deleteconsole.log(... address ... balance ...)account listconsole.log(key);account exportconsole.log(...)(address/balance/name/type lines)account viewconsole.log(json);discover generateconsole.log(...)per validation issuediscover validateNone of these propagate a structured payload back through the
c.*context, so (a) the MCP tool response is always{text: "null"}, and (b) theconsole.logoutput bleeds onto stdio, corrupting the JSON-RPC channel.Link to Minimal Reproducible Example
https://github.com/woven-record-media/mppx-mcp-repro
Steps To Reproduce
Reproduction
Minimal repo: https://github.com/woven-record-media/mppx-mcp-repro (same repo as Issue #386).
The reproducer works around Issue #1 by importing
dist/cli/cli.jsdirectly and callingcli.serve(['--mcp'])without theprocess.exit(0)wrapper, then acts as a minimal JSON-RPC 2.0 client over stdin/stdout against that subprocess. It callstools/call name="account_list"(chosen because it needs no args and hits a per-command handler).Expected output:
(The exact leaked content depends on what's in the local mppx keychain — either account rows, or
No accounts found.on a clean machine. Either way: the tool result isnull, AND there is stray stdout outside the JSON-RPC framing. A real MCP client drops the connection on the first unparseable frame.)The same pattern reproduces with
sign(line 874/877),account_view(line 732+), and every other per-command handler listed above.Package Version
[email protected]
Anything else?
Fix
Two parts:
Return data through the runtime, not
console.log. Each command handler should populate whatever incur exposes as the structured result (likelyc.ok({...})/return {data: ...}— whichever the incur API uses). Example forsign:Render for humans only when not in agent/MCP mode. For the CLI
mppx signcase, keep human-friendly stdout, but gate it onc.agent !== true(or however incur signals the MCP runtime). Alternatively, let incur's default text renderer formatresult.datafor CLI mode — that way the handler stays runtime-agnostic.This pattern needs to be applied to every
console.logindist/cli/cli.jsthat represents tool output (not user-facing progress). The tool definitions should also declareoutputSchemaso the MCP runtime emitsstructuredContentalongside text.Impact
Every mppx MCP tool is currently non-functional. For agent use cases that need programmatic signing (Weftly, other MPP-speaking APIs with unattended payment flows),
--mcpcannot be used without an external proxy. I've worked around this locally with a ~180-line stdio proxy that shells out tomppx signper call, but the right fix is upstream.