Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/ninety-groups-start.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@stripe/link-cli": minor
---

Adds local stdio mcp server and agent-friendly formatting
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ dist/
.claude/skills/link-cli
CLAUDE.local.md
docs/
packages/cli/README.md
packages/cli/README.md
.claude/
.codex/
8 changes: 8 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"mcpServers": {
"link-cli": {
"command": "pnpx",
"args": ["link-cli", "--mcp"]
}
}
}
26 changes: 10 additions & 16 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,44 +43,38 @@ Defined in `packages/sdk/src/resources/interfaces.ts`:

### CLI Command Structure

Commands in `packages/cli/src/cli.tsx` (Commander.js). Each has two output modes:
Commands in `packages/cli/src/cli.tsx` (incur framework). Each has two output modes:
- **Interactive** (default): Ink/React components from `packages/cli/src/commands/`
- **JSON** (`--output-json`): JSON to stdout, errors as JSON to stderr with exit code 1
- **JSON** (`--format json`): JSON to stdout, errors as JSON with `code` and `message` fields with exit code 1

Commands: `auth login|logout|status`, `spend-request create|update|retrieve|request-approval`, `payment-methods list`, `mpp pay`, `skill`.
Commands: `auth login|logout|status`, `spend-request create|update|retrieve|request-approval`, `payment-methods list`, `mpp pay|decode`.

**When adding a new command, always update `configureRootHelp` in `packages/cli/src/utils/configure-root-help.ts`** to include it in the root help output. Pass the command as a parameter and add it to the appropriate section (or a new one).
The CLI also runs as an MCP server (`--mcp`) and serves skill files via `skills` subcommand, both provided by incur.

**When changing commands, flags, or schema descriptions, always update all four together:** `README.md`, `skills/link-cli/SKILL.md`, the schema description strings in the relevant `schema.ts` file, and `CLAUDE.md`. These can easily drift apart.
**When changing commands, flags, or schema descriptions, always update all three together:** `README.md`, `skills/create-payment-credential/SKILL.md`, the schema description strings in the relevant `schema.ts` file, and `CLAUDE.md`. These can easily drift apart.

Input: flags OR `--json` (mutually exclusive) via `resolveInput` in `packages/cli/src/utils/json-options.ts`.

**`InputSchema` and `.strict()` gotcha:** `resolveInput` validates input with `z.object(...).strict()`, which rejects any key not defined in the schema. This means every field that can be passed via `--json` must be defined in the command's `InputSchema` — including boolean flags like `request_approval`. If a field is only registered as a standalone `.option()` call, it will be rejected when using `--json`.

**Always add new flags/options via `InputSchema`, never via standalone `.option()` calls.** Define the field in the relevant `InputSchema` with its `flag`, `schema`, and `description` — `registerSchemaOptions` will register the Commander option automatically. Standalone `.option()` calls bypass schema validation and break `--json` input.
Input is passed via flags. Define options in the command's zod schema — incur registers CLI flags automatically from the schema.

### auth login

- `auth login --client-name <name>` — optional flag to identify the agent or app; shown in the user's Link app as `<name> on <hostname>`. Defined in `LOGIN_INPUT_SCHEMA` in `packages/cli/src/commands/auth/schema.ts`.
- `auth login --client-name <name>` — optional flag to identify the agent or app; shown in the user's Link app as `<name> on <hostname>`. Defined in `loginOptions` in `packages/cli/src/commands/auth/schema.ts`.

### spend-request command

CLI command is `spend-request` (user-facing). Implemented in `packages/cli/src/commands/spend-request/`. The SDK interfaces (`ISpendIntentRepository`, `CreateSpendIntentParams`, `UpdateSpendIntentParams`) and API endpoints (`/spend-intents`) retain their original names.

Key input field notes:
- CLI input uses `payment_method_id`; mapped to `payment_details` when calling the SDK
- `request_approval` is part of `CREATE_INPUT_SCHEMA` (not a separate Commander flag) so it works via both `--json` and `--request-approval` flag
- `test` is part of `CREATE_INPUT_SCHEMA` — pass `--test` or `"test": true` in JSON to create testmode credentials (real testmode SPT from test card data) instead of livemode ones
- `context` requires min 100 characters; `amount` is in cents with max 50000
- `create --request-approval` and `request-approval` both show an approval URL in interactive mode and poll until approved/denied/expired/failed. In JSON mode (`--output-json`), they block silently and return the final `SpendRequest` when complete.
- The `request-approval` command now returns `SpendRequest` (not `RequestApprovalResponse`) — output schema updated to `SPEND_REQUEST_OUTPUT_SCHEMA`
- `--test` flag creates testmode credentials (real testmode SPT from test card data) instead of livemode ones
- `create --request-approval` and `request-approval` both show an approval URL in interactive mode and poll until approved/denied/expired/failed. In JSON mode (`--format json`), they block silently and return the final `SpendRequest` when complete.
- `card` credentials include `billing_address` (name, line1, line2, city, state, postal_code, country) and `valid_until` (unix timestamp — when the card expires/stops working)

### mpp pay

- `mpp pay <url> --spend-request-id <id> [--method <method>] [--data <body>] [--header <header>]...` — completes the 402 flow: retrieves the spend request with `include: ['shared_payment_token']`, probes the URL, parses the `www-authenticate` stripe challenge, builds the `Authorization: Payment` credential, and retries. `--header` is repeatable and uses `"Name: Value"` format. `Content-Type: application/json` is auto-applied when `--data` is provided; user-provided headers take precedence.
- Requires an approved spend request with `credential_type: "shared_payment_token"`. The SPT is one-time-use — a failed payment requires a new spend request.
- Implemented in `packages/cli/src/commands/mpp/` — pay.tsx (logic), schema.ts (input/output schema), index.tsx (Commander registration).
- Implemented in `packages/cli/src/commands/mpp/` — pay.tsx (logic), schema.ts (input/output schema), index.tsx (incur registration).


## Code Conventions
Expand Down
40 changes: 13 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ By default, a spend request provisions a virtual card. For merchants that suppor
The approved spend request includes a `card` object with `number`, `cvc`, `exp_month`, `exp_year`, `billing_address`, and `valid_until`. Enter these into the merchant's checkout form.

```bash
link-cli spend-request retrieve lsrq_001 --output-json
link-cli spend-request retrieve lsrq_001 --format json
```
By default, retrieving a spend request will not include card details. Use the `--include=card` to see unmasked card details.

Expand All @@ -76,22 +76,22 @@ link-cli mpp pay https://climate.stripe.dev/api/contribute \
--spend-request-id lsrq_001 \
--method POST \
--data '{"amount":100}' \
--output-json
--format json
```

## Advanced

### Authentication

```bash
link-cli auth login --client-name "Claude Code" --output-json # identify the connecting agent
link-cli auth status --output-json # check auth status
link-cli auth logout --output-json # disconnect
link-cli auth login --client-name "Claude Code" --format json # identify the connecting agent
link-cli auth status --format json # check auth status
link-cli auth logout --format json # disconnect
```

When `--client-name` is provided, the name is shown in the Link app when the user approves the connection — e.g. `Claude Code on my-macbook` instead of `link-cli on my-macbook`.

`auth status --output-json` includes an `update` field when a newer version is available:
`auth status --format json` includes an `update` field when a newer version is available:

```json
{
Expand Down Expand Up @@ -119,32 +119,18 @@ A spend request moves through: **create** → **request approval** → **approve
# Update before approval
link-cli spend-request update lsrq_001 \
--merchant-url https://press.stripe.com/working-in-public \
--output-json
--format json

# Request approval separately (alternative to create --request-approval)
link-cli spend-request request-approval lsrq_001 --output-json
link-cli spend-request request-approval lsrq_001 --format json

# Retrieve at any time (includes card credentials once approved)
link-cli spend-request retrieve lsrq_001 --output-json
link-cli spend-request retrieve lsrq_001 --format json
```

### JSON
### Output formats

All commands accept `--json` for structured input (mutually exclusive with flags):

```bash
link-cli spend-request create --json '{
"payment_method_id": "csmrpd_xxx",
"merchant_name": "Stripe Press",
"merchant_url": "https://press.stripe.com/working-in-public",
"context": "Purchasing '\''Working in Public'\'' from press.stripe.com. The user initiated this purchase through the shopping assistant.",
"amount": 3500,
"line_items": [{ "name": "Working in Public", "unit_amount": 3500, "quantity": 1 }],
"totals": [{ "type": "total", "display_text": "Total", "amount": 3500 }]
}' --output-json
```

All commands also accept `--output-json` for structured JSON output. Errors are returned as JSON to stderr with exit code 1.
All commands accept `--format json` for structured JSON output. Other formats: `yaml`, `md`, `jsonl`, `toon` (default). Errors are returned as JSON with `code` and `message` fields, with exit code 1.

### MPP

Expand All @@ -156,15 +142,15 @@ link-cli mpp pay https://climate.stripe.dev/api/contribute \
--method POST \
--data '{"amount":100}' \
--header "X-Custom: value" \
--output-json
--format json
```

Use `mpp decode` to validate a raw `WWW-Authenticate` header and extract the `network_id` needed for `shared_payment_token` spend requests:

```bash
link-cli mpp decode \
--challenge 'Payment id="ch_001", realm="merchant.example", method="stripe", intent="charge", request="..."' \
--output-json
--format json
```

### Environment variables
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"dev": "tsx src/cli.tsx"
},
"dependencies": {
"commander": "^12.1.0",
"incur": "^0.4.1",
"ink": "^5.2.1",
"ink-spinner": "^5.0.0",
"mppx": "^0.5.7",
Expand Down
Loading
Loading