Summary
mcporter call <server>.<tool> --output json produces output that is not valid JSON in two common cases (resulting in three independent bugs), and an unrelated daemon recovery diagnostic line is logged to stdout, which corrupts the JSON output stream when the daemon needs to recover.
These bugs were surfaced during integration of openclaw's qmd memory backend with mcporter's daemon (keep-alive) mode, but they affect any MCP server using the standard structuredContent / isError shapes.
Encountered with mcporter@0.7.3 on macOS 25.4 / Node 25.5.
All three are 1-line fixes; full reproducers and proposed patches below.
Bug 1 — tryParseJson returns null for plain structuredContent objects
File: dist/result-utils.js, function tryParseJson.
When an MCP server returns a tool result like:
{
"content": [{"type": "text", "text": "..."}],
"structuredContent": {"results": [...]}
}
mcporter's Result.json() calls tryParseJson(structuredContent). The current implementation returns the value only if it has a .json or .data property; otherwise it falls through to return null. For structuredContent, both branches are typically absent — structuredContent IS the parsed value — so Result.json() returns null.
That null then defeats the case 'json' happy path in printCallOutput (dist/cli/output-utils.js), which falls through to printRaw(raw) → util.inspect(raw). The result is emitted as a JS object literal (with [Object] placeholders for nested arrays), which JSON.parse cannot parse.
Reproducer
Any MCP server returning structuredContent triggers this. Concrete example using QMD (a memory-search MCP):
mcporter call qmd.query --args '{
"searches":[{"type":"lex","query":"openclaw"}],
"limit":3,
"rerank":false
}' --output json --timeout 30000
Pre-patch output (broken):
{
content: [ [Object] ],
structuredContent: { results: [ [Object], [Object], [Object] ] }
}
Expected output (valid JSON):
{
"content": [...],
"structuredContent": {"results": [...]}
}
(or just the structuredContent body, depending on intent).
Proposed fix (1-line addition)
if ('data' in value) {
return value.data ?? null;
}
+ return value;
}
if (typeof value === 'string') {
This returns the plain object as-is when neither .json nor .data is present. It's safe because tryParseJson is only called on values mcporter already considers "JSON-like", and downstream attemptPrintJson will JSON.stringify it.
Bug 2 — printCallOutput case 'json' falls back to util.inspect for isError/text-only responses
File: dist/cli/output-utils.js, function printCallOutput, case 'json'.
Even with Bug 1 fixed, when an MCP tool returns an error envelope like:
{
"content": [{"type": "text", "text": "..."}],
"isError": true
}
(no structuredContent), wrapped.json() returns null because there's nothing JSON-shaped to extract. The case 'json' then falls through to printRaw(raw) → util.inspect again. So callers parsing --output json choke on error responses.
Reproducer
mcporter call qmd.query --args '{
"searches":[{"type":"vec","query":"verify-patches.sh"}],
"limit":1
}' --output json --timeout 30000
(QMD's vec search rejects hyphens in queries with an isError envelope.)
Pre-patch output (broken):
{ content: [ [Object] ], isError: true }
Expected output (valid JSON):
{
"content": [{"type": "text", "text": "Negation (-term) is not supported in vec/hyde queries. Use lex for exclusions."}],
"isError": true
}
Proposed fix (1-line addition + comment, using existing attemptPrintJson helper)
case 'json': {
const jsonValue = wrapped.json();
if (jsonValue !== null && attemptPrintJson(jsonValue)) {
return;
}
+ if (raw != null && attemptPrintJson(raw)) {
+ return;
+ }
printRaw(raw);
return;
}
attemptPrintJson is already an existing helper in the same file that does console.log(JSON.stringify(value, null, 2)) and returns true/false on success. This adds a JSON-stringify of the raw envelope as a fallback before the util.inspect path.
Bug 3 — logDaemonRetry writes to stdout, polluting --output json
File: dist/daemon/runtime-wrapper.js, function logDaemonRetry.
When the daemon's keep-alive child exits and a call needs to recover the server, mcporter logs:
[mcporter] Restarting '<server>' before retrying <op>: <reason>
via console.log. That goes to stdout — which is the JSON output stream. So the diagnostic line gets prepended to the JSON body, breaking JSON.parse(stdout) for callers (in our case openclaw's qmd-manager does exactly this).
Reproducer
# Start the daemon, kill the qmd-mcp child, then call:
mcporter daemon start
pkill -f 'qmd.*mcp'
mcporter call qmd.query --args '{"searches":[{"type":"lex","query":"x"}],"limit":1}' --output json --timeout 30000
Pre-patch output (broken — stdout is corrupted):
[mcporter] Restarting 'qmd' before retrying callTool: Not connected
{
"results": [...]
}
Expected: stdout = pure JSON; the diagnostic line on stderr.
Proposed fix (1-character semantic change)
function logDaemonRetry(server, operation, error) {
const reason = error instanceof Error ? error.message : String(error);
- console.log(`[mcporter] Restarting '${server}' before retrying ${operation}: ${reason}`);
+ console.error(`[mcporter] Restarting '${server}' before retrying ${operation}: ${reason}`);
}
Diagnostics belong on stderr for any tool whose stdout is structured output.
Net impact
For an MCP integration that calls mcporter call --output json and parses the stdout as JSON — including any TypeScript SDK consumer — these three bugs make the integration unusable without local patches. We currently maintain all three fixes as 1-line patches in our installed mcporter@0.7.3 to make our QMD memory-backend integration work.
Repro script (combining all three) and side-by-side pre/post patch outputs available on request — happy to attach if useful.
Adjacent issues observed (out of scope, but useful context)
These were observed during the same integration work but are separate bugs and not necessarily mcporter's responsibility.
Bug D — mcporter daemon start is not idempotent
Running mcporter daemon start while a daemon is already up appears to spawn additional foreground daemon processes rather than no-op'ing. We observed up to 11 orphan mcporter daemon start --foreground processes accumulating across a test session. Each new daemon spawned its own child MCP servers, multiplying memory cost.
Workaround: mcporter daemon stop && pkill -f 'mcporter daemon' before each start.
Suggestion: have mcporter daemon start check mcporter daemon status first and no-op if already running, and/or have it close any prior socket cleanly.
Bug E — qmd MCP server treats hyphens as negation operators in vec/hyde search
Not mcporter's bug — this is in @tobilu/qmd's MCP server. Filed here only for cross-reference. Filenames like verify-patches.sh and dates like 2026-05-04 get rejected by qmd's vec/hyde parsers with Negation (-term) is not supported in vec/hyde queries. Use lex for exclusions. Affects any MCP wrapper that forwards user queries verbatim. Should be filed against tobilu/qmd — flagging here purely to explain the isError envelopes referenced in Bug 2's reproducer.
Summary
mcporter call <server>.<tool> --output jsonproduces output that is not valid JSON in two common cases (resulting in three independent bugs), and an unrelated daemon recovery diagnostic line is logged to stdout, which corrupts the JSON output stream when the daemon needs to recover.These bugs were surfaced during integration of openclaw's
qmdmemory backend with mcporter's daemon (keep-alive) mode, but they affect any MCP server using the standardstructuredContent/isErrorshapes.Encountered with
mcporter@0.7.3on macOS 25.4 / Node 25.5.All three are 1-line fixes; full reproducers and proposed patches below.
Bug 1 —
tryParseJsonreturnsnullfor plainstructuredContentobjectsFile:
dist/result-utils.js, functiontryParseJson.When an MCP server returns a tool result like:
{ "content": [{"type": "text", "text": "..."}], "structuredContent": {"results": [...]} }mcporter's
Result.json()callstryParseJson(structuredContent). The current implementation returns the value only if it has a.jsonor.dataproperty; otherwise it falls through toreturn null. ForstructuredContent, both branches are typically absent —structuredContentIS the parsed value — soResult.json()returns null.That null then defeats the
case 'json'happy path inprintCallOutput(dist/cli/output-utils.js), which falls through toprintRaw(raw)→util.inspect(raw). The result is emitted as a JS object literal (with[Object]placeholders for nested arrays), whichJSON.parsecannot parse.Reproducer
Any MCP server returning
structuredContenttriggers this. Concrete example using QMD (a memory-search MCP):Pre-patch output (broken):
Expected output (valid JSON):
{ "content": [...], "structuredContent": {"results": [...]} }(or just the structuredContent body, depending on intent).
Proposed fix (1-line addition)
if ('data' in value) { return value.data ?? null; } + return value; } if (typeof value === 'string') {This returns the plain object as-is when neither
.jsonnor.datais present. It's safe becausetryParseJsonis only called on values mcporter already considers "JSON-like", and downstreamattemptPrintJsonwillJSON.stringifyit.Bug 2 —
printCallOutput case 'json'falls back toutil.inspectforisError/text-only responsesFile:
dist/cli/output-utils.js, functionprintCallOutput,case 'json'.Even with Bug 1 fixed, when an MCP tool returns an error envelope like:
{ "content": [{"type": "text", "text": "..."}], "isError": true }(no
structuredContent),wrapped.json()returns null because there's nothing JSON-shaped to extract. Thecase 'json'then falls through toprintRaw(raw)→util.inspectagain. So callers parsing--output jsonchoke on error responses.Reproducer
(QMD's vec search rejects hyphens in queries with an
isErrorenvelope.)Pre-patch output (broken):
Expected output (valid JSON):
{ "content": [{"type": "text", "text": "Negation (-term) is not supported in vec/hyde queries. Use lex for exclusions."}], "isError": true }Proposed fix (1-line addition + comment, using existing
attemptPrintJsonhelper)case 'json': { const jsonValue = wrapped.json(); if (jsonValue !== null && attemptPrintJson(jsonValue)) { return; } + if (raw != null && attemptPrintJson(raw)) { + return; + } printRaw(raw); return; }attemptPrintJsonis already an existing helper in the same file that doesconsole.log(JSON.stringify(value, null, 2))and returns true/false on success. This adds a JSON-stringify of the raw envelope as a fallback before theutil.inspectpath.Bug 3 —
logDaemonRetrywrites to stdout, polluting--output jsonFile:
dist/daemon/runtime-wrapper.js, functionlogDaemonRetry.When the daemon's keep-alive child exits and a call needs to recover the server, mcporter logs:
via
console.log. That goes to stdout — which is the JSON output stream. So the diagnostic line gets prepended to the JSON body, breakingJSON.parse(stdout)for callers (in our case openclaw'sqmd-managerdoes exactly this).Reproducer
Pre-patch output (broken — stdout is corrupted):
Expected: stdout = pure JSON; the diagnostic line on stderr.
Proposed fix (1-character semantic change)
function logDaemonRetry(server, operation, error) { const reason = error instanceof Error ? error.message : String(error); - console.log(`[mcporter] Restarting '${server}' before retrying ${operation}: ${reason}`); + console.error(`[mcporter] Restarting '${server}' before retrying ${operation}: ${reason}`); }Diagnostics belong on stderr for any tool whose stdout is structured output.
Net impact
For an MCP integration that calls
mcporter call --output jsonand parses the stdout as JSON — including any TypeScript SDK consumer — these three bugs make the integration unusable without local patches. We currently maintain all three fixes as 1-line patches in our installedmcporter@0.7.3to make our QMD memory-backend integration work.Repro script (combining all three) and side-by-side pre/post patch outputs available on request — happy to attach if useful.
Adjacent issues observed (out of scope, but useful context)
These were observed during the same integration work but are separate bugs and not necessarily mcporter's responsibility.
Bug D —
mcporter daemon startis not idempotentRunning
mcporter daemon startwhile a daemon is already up appears to spawn additional foreground daemon processes rather than no-op'ing. We observed up to 11 orphanmcporter daemon start --foregroundprocesses accumulating across a test session. Each new daemon spawned its own child MCP servers, multiplying memory cost.Workaround:
mcporter daemon stop && pkill -f 'mcporter daemon'before eachstart.Suggestion: have
mcporter daemon startcheckmcporter daemon statusfirst and no-op if already running, and/or have it close any prior socket cleanly.Bug E — qmd MCP server treats hyphens as negation operators in vec/hyde search
Not mcporter's bug — this is in
@tobilu/qmd's MCP server. Filed here only for cross-reference. Filenames likeverify-patches.shand dates like2026-05-04get rejected by qmd's vec/hyde parsers withNegation (-term) is not supported in vec/hyde queries. Use lex for exclusions.Affects any MCP wrapper that forwards user queries verbatim. Should be filed against tobilu/qmd — flagging here purely to explain theisErrorenvelopes referenced in Bug 2's reproducer.