Context
incur renders CTAs cleanly in TTY mode (envelope-level via c.ok(data, { cta })) but strips them when commands run as MCP tools (tools/call response includes content + structuredContent — no cta field).
For agent-driven CLIs (the explicit "agents and human consumption" framing of incur), CTAs are the discovery hint that tells the agent what to invoke next. Losing them on the MCP path defeats the design intent.
Reproducer
// command with envelope-level cta
cli.command('show', {
args: z.object({ id: z.string() }),
run(c) {
return c.ok({ id: c.args.id }, {
cta: {
description: 'Next:',
commands: [
{ command: 'list', description: 'List all' }
]
}
})
},
})
# TTY: cta renders ✓
$ tool show foo
id: foo
cta:
description: Next:
commands[1]{command,description}:
tool list,List all
# MCP: cta absent from tools/call response ✗
$ (init+tools/call) | tool --mcp
{ "result": { "content": [...], "structuredContent": { "id": "foo" } } }
# no cta · agent has no way to discover the suggestion
Workaround (in production today)
Embed cta field in the output Zod schema so it appears in structuredContent:
const ctaSchema = z.object({ description: z.string(), commands: z.array(...) })
cli.command('show', {
output: z.object({ id: z.string(), cta: ctaSchema }),
run(c) {
const cta = { description: 'Next:', commands: [...] }
return c.ok({ id: c.args.id, cta }, { cta }) // both surfaces
}
})
Verified: works (29/29 tests pass · MCP responses now include cta typed under structuredContent).
Costs: typing pollution (every output schema repeats cta) + runtime duplication (same cta value passed twice).
Suggested upstream shape
Option A (clean): _meta.cta field in MCP tool response (MCP-spec compatible)
{ "result": { "content": [...], "structuredContent": {...}, "_meta": { "cta": {...} } } }
Option B: nextActions field as first-class MCP tool response extension
{ "result": { "content": [...], "structuredContent": {...}, "nextActions": [...] } }
Either way, the c.ok(data, { cta }) envelope shape stays unchanged and incur translates to the MCP-side field at serialization time. Downstream consumers of incur CLIs (Claude Code, Cursor, Amp) could opt into reading the cta surface.
Real-world use case
@0xhoneyjar/freeside-cli (built on incur · --llms/--mcp first-class) consumed via construct-freeside as the LLM lens. The CTA-in-MCP gap forced us to bake cta into every output schema (workaround above). Clean upstream support would let us remove the typing pollution.
Cycle reference: 0xHoneyJar/freeside-cli#3 (production cli #3 F-HIGH-2 path B)
Happy to PR the change if there's appetite — let me know the preferred shape.
Context
incurrenders CTAs cleanly in TTY mode (envelope-level viac.ok(data, { cta })) but strips them when commands run as MCP tools (tools/callresponse includescontent+structuredContent— no cta field).For agent-driven CLIs (the explicit "agents and human consumption" framing of incur), CTAs are the discovery hint that tells the agent what to invoke next. Losing them on the MCP path defeats the design intent.
Reproducer
Workaround (in production today)
Embed
ctafield in the output Zod schema so it appears instructuredContent:Verified: works (29/29 tests pass · MCP responses now include
ctatyped understructuredContent).Costs: typing pollution (every output schema repeats
cta) + runtime duplication (same cta value passed twice).Suggested upstream shape
Option A (clean):
_meta.ctafield in MCP tool response (MCP-spec compatible)Option B:
nextActionsfield as first-class MCP tool response extensionEither way, the
c.ok(data, { cta })envelope shape stays unchanged and incur translates to the MCP-side field at serialization time. Downstream consumers of incur CLIs (Claude Code, Cursor, Amp) could opt into reading the cta surface.Real-world use case
@0xhoneyjar/freeside-cli(built on incur ·--llms/--mcpfirst-class) consumed viaconstruct-freesideas the LLM lens. The CTA-in-MCP gap forced us to bake cta into every output schema (workaround above). Clean upstream support would let us remove the typing pollution.Cycle reference: 0xHoneyJar/freeside-cli#3 (production cli #3 F-HIGH-2 path B)
Happy to PR the change if there's appetite — let me know the preferred shape.