UNPKG

@withpica/mcp-server

Version:

MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog

196 lines • 10.1 kB
/** * Subscription + billing tools — ADR-210 Phase 2, extended by the MPP pay * rail (spec 2026-08-24 WS-B). * * Three MCP tools that surface the explicit billing read + action layer * complementing the ambient `_meta.session_state.billing_slice` shipped * in Phase 1: * * - pica_subscription_status (read; readOnlyHint=true) * Wraps `GET /api/admin/subscription` and flattens it into the card * below (`SubscriptionStatusFlat`), composing a templated `summary` * line the agent uses verbatim. It is no longer the 9 fields ADR-210 * § Phase 2 specified: two trial fields came off on 2026-08-21, and * WS-B added `pricing` + `held`, each present only when the route * measured it. Read the interface, not a count. * * - pica_subscription_manage (action; readOnlyHint=false) * Single-verb dispatcher over the existing * `POST /api/admin/subscription` (subscribe / upgrade) and * `POST /api/admin/subscription/portal` (manage / cancel) routes. * Returns a `resource_link` content block (universal MCP floor) * and uses `elicitation/create url` only when the client * advertises `capabilities.elicitation.url` per ADR-200 Phase 1 * (capability-gated; `feedback_mcp_sdk_capability_gates_elicitation.md`). * * - pica_billing_pay (read; readOnlyHint=true) * Wraps `POST /api/admin/billing/pay-link`. Mints a link for one * offer — resident_month / unlock / settle — and never charges: an * agent POSTs the link with a Machine Payments credential, or a * person opens it and pays by card. The refusals the ROUTE itself * composes — billing disabled, nothing held, offer moved — come back * as structured results rather than thrown ApiErrors. A 4xx from the * auth wrapper in front of it is quoted, never re-interpreted; a 4xx * carrying nothing quotable, and everything from 500 up, is re-thrown * (`payRefusal` below is the single place that decides which). * * No new Stripe SDK calls — every route here is reused as-is. */ import { ApiError, PicaClient, type BillingTier, type HeldSummary, type PayOffer, type PricingSummary, type SubscriptionStatusResponse } from "@withpica/mcp-sdk"; import { ToolDefinition, ToolExecutor, ToolResult } from "./index.js"; /** * ⚠️ ADR-276 REMOVED THE TRIAL AND THE TIER LADDER. This tool described both to * agents until 2026-08-21. * * `billing_state` is the STORED value and keeps its three legacy names because * that is what `organisations.billing_state` holds — renaming it here would make * the wire disagree with the database. `plan` is the same fact in the vocabulary * that is actually true, and it is what an agent should say out loud: * * billing_state plan what it means under ADR-276 * ────────────── ─────────────── ────────────────────────────────────────── * trial free_runway walked in, never charged. no clock, no card. * active resident paying one flat fee per 30-day cycle * hibernated paused a cycle lapsed; deep-processing is off * * `trial_days_remaining` and `trial_ends_at` are GONE. They were null for 100% * of production organisations (`billing_trial_ends_at` is NULL on all 241), so * removing them drops two fields that could only ever say "null" — the cheapest * possible moment to take a concept off the wire. Codes for arguments, labels * for prose (`.claude/rules/canonical-model.md`). */ interface SubscriptionStatusFlat { billing_state: "trial" | "active" | "hibernated"; plan: "free_runway" | "resident" | "paused"; current_tier: BillingTier | null; capacity_used: number; capacity_limit: number | null; capacity_pct: number; recommended_tier: BillingTier | null; summary: string; /** * The org's price card and what is being withheld. Both are OMITTED rather * than set to null when the route has none — `null` on a flat card reads to * an agent as "pica measured this and the answer is nothing", which for a * price would mean free. Absence says "pica did not answer", which is true. */ pricing?: PricingSummary; held?: HeldSummary; } /** The stored billing state in the vocabulary ADR-276 actually uses. */ export declare function planFor(billingState: "trial" | "active" | "hibernated"): "free_runway" | "resident" | "paused"; /** * Compose `SubscriptionStatusFlat` from the GET admin route's shape: eight * fields that are always present (`billing_state`, `plan`, `current_tier`, * `capacity_used`, `capacity_limit`, `capacity_pct`, `recommended_tier`, * `summary`) plus `pricing` and `held`, each emitted ONLY when the route * measured it. * * ⚠️ This opened by quoting a FIELD COUNT until 2026-08-26, and that count had * been wrong since 08-21 — `trial_days_remaining` and `trial_ends_at` came off * then, and WS-B added the two optional keys. A count is the one thing a * docstring cannot keep true across a field change, and it rots silently * because nothing compiles against it. Do not reintroduce one: name the fields * (above) or point at the interface, which the compiler keeps honest. * * Mirrors `getOrgBillingSlice`'s "no meaningful cap" rule for `capacity_pct` * (0 whenever current_tier is null OR enterprise) and carries through * `overflow.recommendedTier` (already `inferTierFromCount`-derived) per * Stage 1 Q2. * * Exported for unit tests. */ export declare function flattenStatus(status: SubscriptionStatusResponse): SubscriptionStatusFlat; /** * The price half of the summary, built ENTIRELY from the figures the route * sent. No amount of money appears here on purpose: the money is worded once, * by the pay-link route's offer labels (`formatMoney` over the org's own * currency, whose minor-unit conventions differ from ICU's for four of the * ten supported currencies). A second wording here would be a second home for * the price — the defect FIX_LOG 2026-08-20 records as having had ten. * * Returns `null` when there is nothing measured to say, so the caller appends * nothing rather than a sentence about absence. * * Exported for unit tests. */ export declare function composePriceLine(pricing?: PricingSummary, held?: HeldSummary): string | null; /** * The sentence an agent says out loud. Server-side template (Stage 1 Q1 option * A) so wording does not vary across connectors. * * ⚠️ REWRITTEN 2026-08-21. Every branch was false under ADR-276: * * trial → "You're on a 9-day window of your trial. Today's catalogue * fits the solo tier." — there is no trial and no tier ladder * hibernated → "Your trial has ended and the catalogue is read-only. * Subscribe to restore writes." * — hibernation gates `pica_audio_analyze` and nothing else; * the catalogue is NOT read-only, and there is nothing to * subscribe to * active → "Currently on pro. 412 of 4,000 slots used." * — the capacity ladder was superseded * * 239 of 241 production organisations sit at `billing_state = 'trial'`, so the * first branch was what an agent said to almost every user pica has. The screen * had already been corrected; this had not, which is the recurring shape here — * see `.claude/rules/canonical-model.md` on `WORK_NO_WRITER`. * * Capacity is deliberately not mentioned: no cap is enforced under ADR-276, and * quoting a limit implies one. The work count is stated as a plain fact instead. * * Exported for unit tests. */ export declare function composeSummary(fields: { billing_state: "trial" | "active" | "hibernated"; capacity_used: number; /** WS-B — appended as one further sentence when the route measured them. */ pricing?: PricingSummary; held?: HeldSummary; }): string; /** * Turn a pay-link 4xx into the structured refusal an agent can act on, or * `null` when the caller must keep propagating it — 5xx, timeouts, network * failures, and any 403 whose body carries no message worth quoting. * * The 403 branch is the delicate one: exactly one 403 on this path is about * money, and the rest are about access. See its comment. * * Exported for unit tests. */ export declare function payRefusal(err: unknown, offer: PayOffer, entityId?: string): ToolResult | null; /** * Pull the route's JSON body back out of an `ApiError`. The SDK folds the * response text into the message (`"API request failed: 400 <body>"`, and * `"Authentication failed: 403 <body>"` on the auth path) rather than * carrying a structured body — the same extraction `duplicates.ts` and * `integrity.ts` do. Returns `{}` when the body is absent or not JSON, so * every caller reads a `code` of `undefined` rather than throwing twice. * * Exported for unit tests. */ export declare function parsePayErrorBody(err: ApiError): Record<string, unknown>; export declare class SubscriptionTools { private pica; constructor(pica: PicaClient); getTools(): Array<{ definition: ToolDefinition; executor: ToolExecutor; }>; /** * `pica_billing_pay` — hand the caller a link, and never a charge. * * The refusals the ROUTE composes (the flag is off; nothing is held on that * work; that offer no longer applies) become structured results rather than * thrown ApiErrors — a throw reaches the agent as UNKNOWN_ERROR, which * reads as "pica is broken" when the honest answer is "there is nothing to * pay for". `payRefusal` decides; everything it declines (5xx, timeouts, * network failures, and a 403 with nothing quotable in it) is re-thrown * unchanged, because an outage dressed as a tidy refusal is the worse lie. */ private pay; private status; private manage; } export {}; //# sourceMappingURL=subscription.d.ts.map