Skip to content

[Feature request] Surface CTAs in MCP response (_meta or nextActions) #140

@zkSoju

Description

@zkSoju

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions