@diviops/mcp-server 1.5.18 → 1.5.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +4 -0
  2. package/dist/index.js +565 -0
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -84,6 +84,10 @@ Additional **conditionally-registered Pro tools** appear only on sites that have
84
84
  |----------|------------------|------------|
85
85
  | FluentCart reads (V1) | Pro plugin + FluentCart installed + module enabled | `diviops_fc_product_list`, `diviops_fc_product_get` |
86
86
  | FluentCart simple product writes (V2) | Pro plugin + FluentCart installed + module enabled | `diviops_fc_product_create`, `diviops_fc_product_update`, `diviops_fc_product_delete` |
87
+ | FluentCart variation read/write (V3) | Pro plugin + FluentCart installed + module enabled | `diviops_fc_variation_list`, `diviops_fc_variation_update` |
88
+ | FluentCart license-settings read/write (V3) | Pro plugin + FluentCart Pro installed + module enabled | `diviops_fc_license_settings_get`, `diviops_fc_license_settings_update` |
89
+ | FluentCart order readback + guarded mark-paid (V3.1) | Pro plugin + FluentCart installed + module enabled | `diviops_fc_order_list`, `diviops_fc_order_get`, `diviops_fc_order_mark_paid` |
90
+ | FluentCart license readback (V3.1) | Pro plugin + FluentCart Pro installed + module enabled | `diviops_fc_license_list`, `diviops_fc_license_get`, `diviops_fc_license_activations_list` |
87
91
 
88
92
  When the gates are not satisfied, the tools simply don't appear on the MCP surface — no error envelope, no missing-capability hint. See the `diviops-fluentcart` skill bundle for the operator-side guide.
89
93
 
package/dist/index.js CHANGED
@@ -3328,6 +3328,571 @@ function registerProTools() {
3328
3328
  ],
3329
3329
  };
3330
3330
  }, { target: "fluentcart", capabilityKey: "fluentcart_product_delete" });
3331
+ // ── V3 — variation read/write + license-settings read/write ────────
3332
+ //
3333
+ // V3 expands the FCP authoring surface so a draft simple-product catalog
3334
+ // can represent ADR-005 commercial shape: annual rows become subscription
3335
+ // products, lifetime rows stay onetime, and per-tier activation_limit
3336
+ // is writable. Activation limits live in `ProductMeta.license_settings`
3337
+ // per FluentCart Pro source (NOT variation `other_info`), so license
3338
+ // settings have their own dedicated read/write tools.
3339
+ //
3340
+ // V3 stays inside the simple-product default-variation contract:
3341
+ // no multi-variation create/delete, no signup-fee writes,
3342
+ // no license activation flow, no update-ZIP / readme / banner config.
3343
+ // diviops_fc_variation_list — POST /diviops/v1/pro/fluentcart/products/{id}/variations
3344
+ registerProTool("diviops_fc_variation_list", {
3345
+ description: "List FluentCart Pro variations for a product (Pro tier; V3; requires FluentCart Pro installed + activated). Read-only. Returns every variation row attached to the product with its subscription shape (payment_type, other_info.repeat_interval/times/trial_days/manage_setup_fee) and a license-settings projection when ProductMeta.license_settings is configured. Returns the standardized envelope { ok, data?, error: { code, message, hint? } }; success payload is { product_id, variation_type, default_variation_id, variations: VariationRow[], variations_count }. Each VariationRow carries { id, post_id, variation_title, sku, payment_type, item_price, compare_price, fulfillment_type, stock_status, manage_stock, available, other_info: { ...all stored keys... }, license: { activation_limit, validity: { unit, value } } | null }. Unit convention: item_price and compare_price are stored cents (e.g. 1900 = $19.00). compare_price is null when FCP stores the no-compare sentinel 0; sku is null when the column is SQL NULL OR an empty string. license is null when the product has no license_settings; otherwise activation_limit is null/'' (unset), 0 (unlimited per FluentCart Pro License::getActivationLimit), or a positive integer (max activations). validity.unit is one of: lifetime/day/week/month/year. Error codes: invalid_input (400) when id is not a positive integer; not_found (404) when the product does not exist; fluentcart.module_inactive (412); fluentcart.query_failed (500). Idempotency: read-only.",
3346
+ inputSchema: {
3347
+ product_id: z
3348
+ .number()
3349
+ .int()
3350
+ .positive()
3351
+ .describe("FluentCart product ID (the post ID of the fluent_products CPT entry)."),
3352
+ },
3353
+ annotations: { idempotentHint: true },
3354
+ _meta: { idempotent: "true" },
3355
+ }, async ({ product_id }) => {
3356
+ const result = await wp.requestEnveloped(`/pro/fluentcart/products/${product_id}/variations`, { method: "POST" });
3357
+ return {
3358
+ content: [
3359
+ {
3360
+ type: "text",
3361
+ text: serializeEnvelope(result, "diviops_fc_variation_list"),
3362
+ },
3363
+ ],
3364
+ };
3365
+ }, { target: "fluentcart", capabilityKey: "fluentcart_variation_list" });
3366
+ // diviops_fc_variation_update — POST /diviops/v1/pro/fluentcart/products/{product_id}/variations/{variation_id}/update
3367
+ registerProTool("diviops_fc_variation_update", {
3368
+ description: "Update the default variation of a simple FluentCart Pro product (Pro tier; V3; requires FluentCart Pro installed + activated). V3 scope: writes the product's default variation only; refuses non-simple products and non-default variations with `fluentcart.unsupported_product_shape` (HTTP 422). Multi-variation create/delete remains out of scope. Accepts partial updates on price, compare_price, sku, payment_type, and the subscription shape (repeat_interval, times, trial_days, manage_setup_fee). Switching `payment_type: \"subscription\"` requires `repeat_interval` (yearly/half_yearly/quarterly/monthly/weekly/daily) — either supplied in the same call or already stored. Switching to `onetime` strips the subscription-only keys from other_info, matching ProductVariationRequest::beforeValidation. `manage_setup_fee: \"yes\"` requires signup_fee + signup_fee_name which are out of scope for V3 (use FluentCart admin UI for setup fees); only `manage_setup_fee: \"no\"` is accepted. Returns the standardized envelope { ok, data?, error: { code, message, hint? } }; apply-mode success payload is { product_id, variation_id, changed_fields[], variation: VariationRow, product_price_range: { min_price, max_price } | null } (or { noop: true, product_id, variation_id, variation } when nothing changes). VariationRow mirrors the diviops_fc_variation_list shape — sku, item_price, compare_price, payment_type, other_info, license round-trip without a follow-up read. Unit asymmetry: write inputs (price, compare_price) are currency units (e.g. 19.00); VariationRow returns item_price + compare_price in stored cents. Error codes: invalid_input (400); not_found (404); fluentcart.unsupported_product_shape (422); fluentcart.sku_conflict (409); fluentcart.module_inactive (412); fluentcart.command_failed (500). Idempotency: conditional — identical repeat is a no-op." +
3369
+ DRY_RUN_DESC_SUFFIX,
3370
+ inputSchema: {
3371
+ product_id: z
3372
+ .number()
3373
+ .int()
3374
+ .positive()
3375
+ .describe("FluentCart product ID (the post ID of the fluent_products CPT entry)."),
3376
+ variation_id: z
3377
+ .number()
3378
+ .int()
3379
+ .positive()
3380
+ .describe("FluentCart variation ID. Must be the product's default variation (V3 constraint)."),
3381
+ price: z
3382
+ .number()
3383
+ .min(0)
3384
+ .optional()
3385
+ .describe("Variation item_price in currency units (e.g. 19.00). Non-negative."),
3386
+ compare_price: z
3387
+ .number()
3388
+ .min(0)
3389
+ .optional()
3390
+ .describe("Variation compare-at price in currency units. Must be ≥ price when both provided."),
3391
+ sku: z
3392
+ .string()
3393
+ .optional()
3394
+ .describe("Variation SKU. Must be unique across all FluentCart variations and at most 30 characters. Empty string clears the SKU (reads back as null)."),
3395
+ payment_type: z
3396
+ .enum(["onetime", "subscription"])
3397
+ .optional()
3398
+ .describe("Variation payment_type. Switching to 'subscription' requires repeat_interval. Switching to 'onetime' strips subscription-only fields from other_info."),
3399
+ repeat_interval: z
3400
+ .enum([
3401
+ "yearly",
3402
+ "half_yearly",
3403
+ "quarterly",
3404
+ "monthly",
3405
+ "weekly",
3406
+ "daily",
3407
+ ])
3408
+ .optional()
3409
+ .describe("Subscription billing interval. Required when switching to payment_type='subscription' unless already stored. For ADR-005 annual rows use 'yearly'."),
3410
+ times: z
3411
+ .number()
3412
+ .int()
3413
+ .min(0)
3414
+ .optional()
3415
+ .describe("Number of subscription billing cycles. 0 = indefinite (default for V3 subscription rows)."),
3416
+ trial_days: z
3417
+ .number()
3418
+ .int()
3419
+ .min(0)
3420
+ .max(365)
3421
+ .optional()
3422
+ .describe("Trial-period length in days (0-365)."),
3423
+ manage_setup_fee: z
3424
+ .enum(["no"])
3425
+ .optional()
3426
+ .describe("Setup-fee mode. V3 only accepts 'no' — manage_setup_fee='yes' requires signup_fee + signup_fee_name which are out of scope for V3."),
3427
+ dry_run: DRY_RUN_FIELD,
3428
+ },
3429
+ annotations: { idempotentHint: false },
3430
+ _meta: { idempotent: "conditional" },
3431
+ }, async ({ product_id, variation_id, price, compare_price, sku, payment_type, repeat_interval, times, trial_days, manage_setup_fee, dry_run, }) => {
3432
+ const body = {};
3433
+ if (price !== undefined)
3434
+ body.price = price;
3435
+ if (compare_price !== undefined)
3436
+ body.compare_price = compare_price;
3437
+ if (sku !== undefined)
3438
+ body.sku = sku;
3439
+ if (payment_type !== undefined)
3440
+ body.payment_type = payment_type;
3441
+ if (repeat_interval !== undefined)
3442
+ body.repeat_interval = repeat_interval;
3443
+ if (times !== undefined)
3444
+ body.times = times;
3445
+ if (trial_days !== undefined)
3446
+ body.trial_days = trial_days;
3447
+ if (manage_setup_fee !== undefined)
3448
+ body.manage_setup_fee = manage_setup_fee;
3449
+ if (dry_run !== undefined)
3450
+ body.dry_run = dry_run;
3451
+ const result = await wp.requestEnveloped(`/pro/fluentcart/products/${product_id}/variations/${variation_id}/update`, { method: "POST", body });
3452
+ return {
3453
+ content: [
3454
+ {
3455
+ type: "text",
3456
+ text: serializeEnvelope(result, "diviops_fc_variation_update"),
3457
+ },
3458
+ ],
3459
+ };
3460
+ }, { target: "fluentcart", capabilityKey: "fluentcart_variation_update" });
3461
+ // diviops_fc_license_settings_get — POST /diviops/v1/pro/fluentcart/products/{id}/license-settings
3462
+ registerProTool("diviops_fc_license_settings_get", {
3463
+ description: "Read the per-product FluentCart Pro license-settings projection (Pro tier; V3; requires FluentCart Pro installed + activated). FluentCart Pro stores license settings in `ProductMeta` under meta_key='license_settings'; this tool reads that meta row and joins it against the product's variations so each variation surfaces with its current activation_limit + validity. Read-only. Returns the standardized envelope { ok, data?, error: { code, message, hint? } }; success payload is { product_id, enabled: boolean, version, prefix, variations: [ { variation_id, title, activation_limit, validity: { unit, value } | null } ] }. Storage semantics: `enabled` is stored as 'yes'/'no' in FCP and projected to boolean here. `activation_limit` is null/'' (unconfigured), 0 (unlimited per FluentCart Pro License::getActivationLimit), or a positive integer. `validity.unit` is one of lifetime/day/week/month/year. Variations the product has but license_settings doesn't mention surface with `activation_limit: null` and `validity: null`. Variations license_settings mentions that no longer exist on the product are filtered out — only the live variation set is returned. Error codes: invalid_input (400) when id is not a positive integer; not_found (404) when the product does not exist; fluentcart.module_inactive (412); fluentcart.query_failed (500). Idempotency: read-only.",
3464
+ inputSchema: {
3465
+ product_id: z
3466
+ .number()
3467
+ .int()
3468
+ .positive()
3469
+ .describe("FluentCart product ID (the post ID of the fluent_products CPT entry)."),
3470
+ },
3471
+ annotations: { idempotentHint: true },
3472
+ _meta: { idempotent: "true" },
3473
+ }, async ({ product_id }) => {
3474
+ const result = await wp.requestEnveloped(`/pro/fluentcart/products/${product_id}/license-settings`, { method: "POST" });
3475
+ return {
3476
+ content: [
3477
+ {
3478
+ type: "text",
3479
+ text: serializeEnvelope(result, "diviops_fc_license_settings_get"),
3480
+ },
3481
+ ],
3482
+ };
3483
+ }, { target: "fluentcart", capabilityKey: "fluentcart_license_settings_get" });
3484
+ // diviops_fc_license_settings_update — POST /diviops/v1/pro/fluentcart/products/{id}/license-settings/update
3485
+ registerProTool("diviops_fc_license_settings_update", {
3486
+ description: "Write the per-product FluentCart Pro license-settings ProductMeta row (Pro tier; V3; requires FluentCart Pro installed + activated). Authors `enabled`, `version`, `prefix`, and per-variation `activation_limit` + `validity` — the storage shape FluentCart Pro reads via LicenseGenerationHandler when an order is placed. V3 explicitly skips configuring update-ZIP `global_update_file`, `wp` readme/banner/icon, downloadables, and the license-activation API; those fields are preserved when present but not authored. Refuses bundle products with `fluentcart.unsupported_product_shape` (HTTP 422). Inputs (all optional except product_id; partial updates supported): `enabled` (boolean — projected to FCP's 'yes'/'no' on write), `version` (required when enabling; max 50 chars), `prefix` (max 20 chars), `variations` (array of { variation_id (required), activation_limit (integer ≥ 0 or null; 0 = unlimited per FluentCart Pro License::getActivationLimit), validity (optional { unit: lifetime/day/week/month/year, value: positive integer } or null to auto-derive) }). When `validity` is omitted on a variation, the validity is derived from the variation's payment_type: subscription+yearly → { unit: 'year', value: 1 }; onetime → { unit: 'lifetime', value: 1 }. When `enabled: true` and the product carries variations, every variation must end up with a non-empty validity.unit. Returns the standardized envelope { ok, data?, error: { code, message, hint? } }; apply-mode success payload is { product_id, changed_fields[], license_settings: <same shape as diviops_fc_license_settings_get> } (or { noop: true, product_id, license_settings } on no-op). Error codes: invalid_input (400) when any field violates the constraints (unknown variation_id, negative activation_limit, missing version when enabling, missing validity.unit when enabling, bad enum value); not_found (404) when the product does not exist; fluentcart.unsupported_product_shape (422) on bundle products; fluentcart.module_inactive (412); fluentcart.command_failed (500). Idempotency: conditional — identical repeat is a no-op." +
3487
+ DRY_RUN_DESC_SUFFIX,
3488
+ inputSchema: {
3489
+ product_id: z
3490
+ .number()
3491
+ .int()
3492
+ .positive()
3493
+ .describe("FluentCart product ID (the post ID of the fluent_products CPT entry)."),
3494
+ enabled: z
3495
+ .boolean()
3496
+ .optional()
3497
+ .describe("Toggle FCP license-settings enablement. Stored as 'yes'/'no' in FCP. When true, version is required (use '1.0.0-beta' for the dogfood catalog)."),
3498
+ version: z
3499
+ .string()
3500
+ .max(50)
3501
+ .optional()
3502
+ .describe("License-settings version string (max 50 chars). Required when enabling. Recommended: '1.0.0-beta' for a beta-cohort catalog."),
3503
+ prefix: z
3504
+ .string()
3505
+ .max(20)
3506
+ .optional()
3507
+ .describe("License-key prefix (max 20 chars). Recommended: 'DOP' for DiviOps products."),
3508
+ variations: z
3509
+ .array(z.object({
3510
+ variation_id: z
3511
+ .number()
3512
+ .int()
3513
+ .positive()
3514
+ .describe("Target variation ID; must belong to this product."),
3515
+ activation_limit: z
3516
+ .number()
3517
+ .int()
3518
+ .min(0)
3519
+ .nullable()
3520
+ .optional()
3521
+ .describe("Activation limit. 0 = unlimited (per FluentCart Pro License::getActivationLimit). null clears the configured limit."),
3522
+ validity: z
3523
+ .object({
3524
+ unit: z.enum(["lifetime", "day", "week", "month", "year"]),
3525
+ value: z.number().int().positive(),
3526
+ })
3527
+ .nullable()
3528
+ .optional()
3529
+ .describe("Validity period. When omitted, derived from the variation's payment_type (subscription+yearly → year/1; onetime → lifetime/1). When null, force-rederived from current variation state."),
3530
+ }))
3531
+ .optional()
3532
+ .describe("Per-variation license configuration. Each entry's variation_id must belong to the product. Omitted variations preserve their existing license_settings."),
3533
+ dry_run: DRY_RUN_FIELD,
3534
+ },
3535
+ annotations: { idempotentHint: false },
3536
+ _meta: { idempotent: "conditional" },
3537
+ }, async ({ product_id, enabled, version, prefix, variations, dry_run, }) => {
3538
+ const body = {};
3539
+ if (enabled !== undefined)
3540
+ body.enabled = enabled;
3541
+ if (version !== undefined)
3542
+ body.version = version;
3543
+ if (prefix !== undefined)
3544
+ body.prefix = prefix;
3545
+ if (variations !== undefined)
3546
+ body.variations = variations;
3547
+ if (dry_run !== undefined)
3548
+ body.dry_run = dry_run;
3549
+ const result = await wp.requestEnveloped(`/pro/fluentcart/products/${product_id}/license-settings/update`, { method: "POST", body });
3550
+ return {
3551
+ content: [
3552
+ {
3553
+ type: "text",
3554
+ text: serializeEnvelope(result, "diviops_fc_license_settings_update"),
3555
+ },
3556
+ ],
3557
+ };
3558
+ }, {
3559
+ target: "fluentcart",
3560
+ capabilityKey: "fluentcart_license_settings_update",
3561
+ });
3562
+ // ── V3.1 — order/license/activation read + guarded mark-paid ───────
3563
+ //
3564
+ // FluentCart commerce-artifact readback surface plus a single
3565
+ // mutating tool: a guarded offline mark-paid that mirrors FCP's
3566
+ // OrderController::markAsPaid. Lifts the local checkout/license
3567
+ // smoke off of eval-file PHP probes.
3568
+ // diviops_fc_order_list — POST /diviops/v1/pro/fluentcart/orders
3569
+ registerProTool("diviops_fc_order_list", {
3570
+ description: "List FluentCart orders for commerce dogfooding / smoke baselines (Pro tier; V3.1; requires FluentCart installed + activated). Returns a paginated summary with order identity (id, status, payment_status), gateway info (payment_method + payment_method_title, mode), totals (currency, total_amount, total_paid), fulfillment_type, type (payment/subscription), customer (customer_id + customer_email), item_count, license_count, and timestamps (created_at, updated_at, completed_at). Filterable by status, payment_status, payment_method, product_id, customer_email, and mode (test/live). Read-only. Returns the standardized envelope { ok, data?, error: { code, message, hint? } }; the success payload is { orders: OrderSummary[], pagination: { page, per_page, total, total_pages }, filters: { ... } }. Error codes: invalid_input (HTTP 400) when status/payment_status/mode is out of range; fluentcart.module_inactive (HTTP 412); fluentcart.query_failed (HTTP 500). Use this alongside diviops_fc_order_get / diviops_fc_license_list to verify a smoke run without raw SQL probes.",
3571
+ inputSchema: {
3572
+ page: z
3573
+ .number()
3574
+ .int()
3575
+ .positive()
3576
+ .optional()
3577
+ .default(1)
3578
+ .describe("Page number, 1-indexed. Default 1."),
3579
+ per_page: z
3580
+ .number()
3581
+ .int()
3582
+ .positive()
3583
+ .optional()
3584
+ .default(20)
3585
+ .describe("Page size. Default 20, clamped to a max of 100."),
3586
+ status: z
3587
+ .enum([
3588
+ "on-hold",
3589
+ "pending",
3590
+ "processing",
3591
+ "completed",
3592
+ "canceled",
3593
+ "refunded",
3594
+ "failed",
3595
+ "draft",
3596
+ ])
3597
+ .optional()
3598
+ .describe("Order status filter (fct_orders.status). Match exact values such as 'completed' or 'on-hold'."),
3599
+ payment_status: z
3600
+ .enum([
3601
+ "pending",
3602
+ "paid",
3603
+ "partially_paid",
3604
+ "partially_refunded",
3605
+ "refunded",
3606
+ "failed",
3607
+ ])
3608
+ .optional()
3609
+ .describe("Payment status filter (fct_orders.payment_status)."),
3610
+ payment_method: z
3611
+ .string()
3612
+ .optional()
3613
+ .describe("Exact-match payment_method (e.g. 'offline_payment', 'stripe'). Case-sensitive — FluentCart stores gateway slugs verbatim."),
3614
+ product_id: z
3615
+ .number()
3616
+ .int()
3617
+ .positive()
3618
+ .optional()
3619
+ .describe("FluentCart product ID (post_id of the fluent_products CPT entry). Filters to orders with at least one matching order item."),
3620
+ customer_email: z
3621
+ .string()
3622
+ .optional()
3623
+ .describe("Filter to orders whose customer record has this email (exact match)."),
3624
+ mode: z
3625
+ .enum(["test", "live"])
3626
+ .optional()
3627
+ .describe("Filter to test-mode or live-mode orders. Useful for smoke runs to isolate the test gateway corpus."),
3628
+ },
3629
+ annotations: { idempotentHint: true },
3630
+ _meta: { idempotent: "true" },
3631
+ }, async ({ page, per_page, status, payment_status, payment_method, product_id, customer_email, mode, }) => {
3632
+ const body = {};
3633
+ if (page !== undefined)
3634
+ body.page = page;
3635
+ if (per_page !== undefined)
3636
+ body.per_page = per_page;
3637
+ if (status !== undefined)
3638
+ body.status = status;
3639
+ if (payment_status !== undefined)
3640
+ body.payment_status = payment_status;
3641
+ if (payment_method !== undefined)
3642
+ body.payment_method = payment_method;
3643
+ if (product_id !== undefined)
3644
+ body.product_id = product_id;
3645
+ if (customer_email !== undefined)
3646
+ body.customer_email = customer_email;
3647
+ if (mode !== undefined)
3648
+ body.mode = mode;
3649
+ const result = await wp.requestEnveloped("/pro/fluentcart/orders", {
3650
+ method: "POST",
3651
+ body,
3652
+ });
3653
+ return {
3654
+ content: [
3655
+ {
3656
+ type: "text",
3657
+ text: serializeEnvelope(result, "diviops_fc_order_list"),
3658
+ },
3659
+ ],
3660
+ };
3661
+ }, { target: "fluentcart", capabilityKey: "fluentcart_order_list" });
3662
+ // diviops_fc_order_get — POST /diviops/v1/pro/fluentcart/orders/{id}
3663
+ registerProTool("diviops_fc_order_get", {
3664
+ description: "Fetch a single FluentCart order with line items, transactions, and related license IDs (Pro tier; V3.1; requires FluentCart installed + activated). Read-only. Returns the standardized envelope { ok, data?, error: { code, message, hint? } }; the success payload is { order: OrderSummary, items: OrderItem[], transactions: Transaction[], license_ids: number[] }. OrderItem includes id, post_id (product CPT post_id), object_id (variation_id), title, quantity, unit_price, line_total, payment_type, fulfillment_type. Transaction includes id, status, payment_method, payment_mode, transaction_type, total, currency, created_at. license_ids carries the IDs of any fct_licenses rows tied to this order (use diviops_fc_license_get to fetch each row's redacted shape). Does NOT expose payment credentials, gateway secrets, or full license keys. Error codes: invalid_input (HTTP 400) when id is not a positive integer; not_found (HTTP 404) when no order matches; fluentcart.module_inactive (HTTP 412); fluentcart.query_failed (HTTP 500).",
3665
+ inputSchema: {
3666
+ id: z
3667
+ .number()
3668
+ .int()
3669
+ .positive()
3670
+ .describe("FluentCart order ID (fct_orders.id; for the local smoke runbook this is the order receipt anchor)."),
3671
+ },
3672
+ annotations: { idempotentHint: true },
3673
+ _meta: { idempotent: "true" },
3674
+ }, async ({ id }) => {
3675
+ const result = await wp.requestEnveloped(`/pro/fluentcart/orders/${id}`, { method: "POST" });
3676
+ return {
3677
+ content: [
3678
+ {
3679
+ type: "text",
3680
+ text: serializeEnvelope(result, "diviops_fc_order_get"),
3681
+ },
3682
+ ],
3683
+ };
3684
+ }, { target: "fluentcart", capabilityKey: "fluentcart_order_get" });
3685
+ // diviops_fc_order_mark_paid — POST /diviops/v1/pro/fluentcart/orders/{id}/mark-paid
3686
+ registerProTool("diviops_fc_order_mark_paid", {
3687
+ description: "Guarded local/offline mark-paid for a FluentCart order (Pro tier; V3.1; requires FluentCart installed + activated). Mirrors the canonical `FluentCart\\App\\Http\\Controllers\\OrderController::markAsPaid` sequence — updates the pending offline transaction, flips payment_status to 'paid' (unless partially_refunded), flips order status to 'processing' (and 'completed' for digital fulfillment), then dispatches `OrderPaid` (which fires the `fluent_cart/order_paid` WordPress action, the listener `LicenseGenerationHandler::maybeGenerateLicensesOnPurchaseSuccess` hangs off of) plus `OrderStatusUpdated`. Does NOT directly insert license rows — license generation is a side effect of the dispatched event, exactly like the FCP admin path. Refuses non-offline gateways (anything other than `payment_method='offline_payment'`) with `fluentcart.unsupported_payment_method` (HTTP 422) — this slice is for local/test/COD smokes only. Refuses canceled orders with `fluentcart.order_canceled` (HTTP 422). Already-paid orders are repeat-safe: returns `ok:true` with `data.already_paid: true` (no second event fires). **dry_run defaults to TRUE** for safety; apply requires `dry_run:false` PLUS `confirm_order_id` + `confirm_payment_method` + `confirm_due_amount` matching current state. Dry-run payload: { dry_run:true, plan: { summary, changes[] (payment_status, status, total_paid, transaction), warnings[] }, events: ['fluent_cart/order_paid', 'fluent_cart/order_status_updated'], order_id, licenses_before }. Apply payload: { order: OrderSummary (post-mutation), transaction: TransactionSummary, events_fired: string[], licenses_before, licenses_after, licenses_created, license_ids: number[], licenses: LicenseRedactedSummary[] (no full keys). Error codes: invalid_input (400) when id/confirmation fields are wrong; not_found (404); fluentcart.order_canceled (422); fluentcart.unsupported_payment_method (422); fluentcart.module_inactive (412); fluentcart.command_failed (500). Idempotency: repeat-safe via already_paid sentinel." +
3688
+ " Pass dry_run: false plus the confirm_* fields to apply.",
3689
+ inputSchema: {
3690
+ id: z
3691
+ .number()
3692
+ .int()
3693
+ .positive()
3694
+ .describe("FluentCart order ID (fct_orders.id) to mark paid."),
3695
+ dry_run: z
3696
+ .boolean()
3697
+ .optional()
3698
+ .default(true)
3699
+ .describe("When true (default), return the change plan without mutating state. Apply requires explicit dry_run: false + the confirm_* fields."),
3700
+ confirm_order_id: z
3701
+ .number()
3702
+ .int()
3703
+ .positive()
3704
+ .optional()
3705
+ .describe("Apply-mode confirmation: must equal `id`. Prevents accidentally marking the wrong order paid."),
3706
+ confirm_payment_method: z
3707
+ .string()
3708
+ .optional()
3709
+ .describe("Apply-mode confirmation: must equal the order's current payment_method (e.g. 'offline_payment')."),
3710
+ confirm_due_amount: z
3711
+ .number()
3712
+ .int()
3713
+ .min(0)
3714
+ .optional()
3715
+ .describe("Apply-mode confirmation: must equal (total_amount - total_paid) in stored units (cents). Inspect via diviops_fc_order_get."),
3716
+ mark_paid_note: z
3717
+ .string()
3718
+ .optional()
3719
+ .describe("Optional sanitize_text_field note to attach to the order's `note` column. Mirrors the admin mark-paid form field."),
3720
+ },
3721
+ annotations: { idempotentHint: false },
3722
+ _meta: { idempotent: "conditional" },
3723
+ }, async ({ id, dry_run, confirm_order_id, confirm_payment_method, confirm_due_amount, mark_paid_note, }) => {
3724
+ const body = {};
3725
+ // dry_run defaults to true at the MCP level — pass through whatever
3726
+ // the caller sent so the plugin can apply the same default.
3727
+ if (dry_run !== undefined)
3728
+ body.dry_run = dry_run;
3729
+ else
3730
+ body.dry_run = true;
3731
+ if (confirm_order_id !== undefined)
3732
+ body.confirm_order_id = confirm_order_id;
3733
+ if (confirm_payment_method !== undefined)
3734
+ body.confirm_payment_method = confirm_payment_method;
3735
+ if (confirm_due_amount !== undefined)
3736
+ body.confirm_due_amount = confirm_due_amount;
3737
+ if (mark_paid_note !== undefined)
3738
+ body.mark_paid_note = mark_paid_note;
3739
+ const result = await wp.requestEnveloped(`/pro/fluentcart/orders/${id}/mark-paid`, { method: "POST", body });
3740
+ return {
3741
+ content: [
3742
+ {
3743
+ type: "text",
3744
+ text: serializeEnvelope(result, "diviops_fc_order_mark_paid"),
3745
+ },
3746
+ ],
3747
+ };
3748
+ }, { target: "fluentcart", capabilityKey: "fluentcart_order_mark_paid" });
3749
+ // diviops_fc_license_list — POST /diviops/v1/pro/fluentcart/licenses
3750
+ registerProTool("diviops_fc_license_list", {
3751
+ description: "List FluentCart Pro licenses (Pro tier; V3.1; requires FluentCart Pro + Licensing module). Read-only. Filterable by product_id, variation_id, order_id, customer_id, status (active/inactive/disabled/expired/in_trial). License keys are NEVER returned in full — every row carries `redacted_key` only (first 4 + last 4 with ellipsis); use diviops_fc_license_get with explicit secret-handling opt-in if a full key is required for an authorized integration test. Returns the standardized envelope { ok, data?, error: { code, message, hint? } }; the success payload is { licenses: LicenseSummary[], pagination: { page, per_page, total, total_pages }, filters: { ... } }. LicenseSummary includes id, status, product_id, variation_id, order_id, customer_id, subscription_id, limit (0 = unlimited per License::getActivationLimit), activation_count, expiration_date, created_at, updated_at, redacted_key. Error codes: invalid_input (HTTP 400) when status is out of range; fluentcart.module_inactive (HTTP 412); fluentcart.licensing_unavailable (HTTP 412) when FluentCart Pro's Licensing module is absent; fluentcart.query_failed (HTTP 500).",
3752
+ inputSchema: {
3753
+ page: z
3754
+ .number()
3755
+ .int()
3756
+ .positive()
3757
+ .optional()
3758
+ .default(1)
3759
+ .describe("Page number, 1-indexed. Default 1."),
3760
+ per_page: z
3761
+ .number()
3762
+ .int()
3763
+ .positive()
3764
+ .optional()
3765
+ .default(20)
3766
+ .describe("Page size. Default 20, clamped to a max of 100."),
3767
+ product_id: z
3768
+ .number()
3769
+ .int()
3770
+ .positive()
3771
+ .optional()
3772
+ .describe("Filter to licenses for this FluentCart product ID."),
3773
+ variation_id: z
3774
+ .number()
3775
+ .int()
3776
+ .positive()
3777
+ .optional()
3778
+ .describe("Filter to licenses for this FluentCart variation ID."),
3779
+ order_id: z
3780
+ .number()
3781
+ .int()
3782
+ .positive()
3783
+ .optional()
3784
+ .describe("Filter to licenses issued by this order ID."),
3785
+ customer_id: z
3786
+ .number()
3787
+ .int()
3788
+ .positive()
3789
+ .optional()
3790
+ .describe("Filter to licenses owned by this customer ID."),
3791
+ status: z
3792
+ .enum(["active", "inactive", "disabled", "expired", "in_trial"])
3793
+ .optional()
3794
+ .describe("License status (fct_licenses.status). 'active' is the healthy default."),
3795
+ },
3796
+ annotations: { idempotentHint: true },
3797
+ _meta: { idempotent: "true" },
3798
+ }, async ({ page, per_page, product_id, variation_id, order_id, customer_id, status, }) => {
3799
+ const body = {};
3800
+ if (page !== undefined)
3801
+ body.page = page;
3802
+ if (per_page !== undefined)
3803
+ body.per_page = per_page;
3804
+ if (product_id !== undefined)
3805
+ body.product_id = product_id;
3806
+ if (variation_id !== undefined)
3807
+ body.variation_id = variation_id;
3808
+ if (order_id !== undefined)
3809
+ body.order_id = order_id;
3810
+ if (customer_id !== undefined)
3811
+ body.customer_id = customer_id;
3812
+ if (status !== undefined)
3813
+ body.status = status;
3814
+ const result = await wp.requestEnveloped("/pro/fluentcart/licenses", {
3815
+ method: "POST",
3816
+ body,
3817
+ });
3818
+ return {
3819
+ content: [
3820
+ {
3821
+ type: "text",
3822
+ text: serializeEnvelope(result, "diviops_fc_license_list"),
3823
+ },
3824
+ ],
3825
+ };
3826
+ }, { target: "fluentcart", capabilityKey: "fluentcart_license_list" });
3827
+ // diviops_fc_license_get — POST /diviops/v1/pro/fluentcart/licenses/{id}
3828
+ registerProTool("diviops_fc_license_get", {
3829
+ description: "Fetch a single FluentCart Pro license by ID (Pro tier; V3.1; requires FluentCart Pro + Licensing module). Read-only by default. The default response redacts the license key to `redacted_key` only (first 4 + last 4 with ellipsis). To surface the full key, pass BOTH `include_license_key: true` AND `confirm_secret_handling: true` — the response then carries `license.license_key` plus `_meta.contains_secret: true` naming the secret field. **Full license keys must never be pasted into PRs, issues, Slack, or any external surface.** Returns the standardized envelope { ok, data?, error: { code, message, hint? } }. Success payload: { license: { id, status, product_id, variation_id, order_id, customer_id, subscription_id, limit, activation_count, expiration_date, created_at, updated_at, redacted_key, license_key? } }. limit semantics: 0 = unlimited per License::getActivationLimit; positive integers are the actual activation cap. Error codes: invalid_input (HTTP 400) when id is non-positive or include_license_key was passed without confirm_secret_handling; not_found (HTTP 404); fluentcart.module_inactive (HTTP 412); fluentcart.licensing_unavailable (HTTP 412); fluentcart.query_failed (HTTP 500).",
3830
+ inputSchema: {
3831
+ id: z
3832
+ .number()
3833
+ .int()
3834
+ .positive()
3835
+ .describe("FluentCart license ID (fct_licenses.id)."),
3836
+ include_license_key: z
3837
+ .boolean()
3838
+ .optional()
3839
+ .describe("Opt-in: include the full unredacted license key in the response. Requires confirm_secret_handling: true. Default: redacted-only."),
3840
+ confirm_secret_handling: z
3841
+ .boolean()
3842
+ .optional()
3843
+ .describe("Required alongside include_license_key: true. Acknowledges that full license keys must not be pasted into reports, PRs, issues, or external chat surfaces."),
3844
+ },
3845
+ annotations: { idempotentHint: true },
3846
+ _meta: { idempotent: "true" },
3847
+ }, async ({ id, include_license_key, confirm_secret_handling, }) => {
3848
+ const body = {};
3849
+ if (include_license_key !== undefined)
3850
+ body.include_license_key = include_license_key;
3851
+ if (confirm_secret_handling !== undefined)
3852
+ body.confirm_secret_handling = confirm_secret_handling;
3853
+ const result = await wp.requestEnveloped(`/pro/fluentcart/licenses/${id}`, { method: "POST", body });
3854
+ return {
3855
+ content: [
3856
+ {
3857
+ type: "text",
3858
+ text: serializeEnvelope(result, "diviops_fc_license_get"),
3859
+ },
3860
+ ],
3861
+ };
3862
+ }, { target: "fluentcart", capabilityKey: "fluentcart_license_get" });
3863
+ // diviops_fc_license_activations_list — POST /diviops/v1/pro/fluentcart/licenses/{id}/activations
3864
+ registerProTool("diviops_fc_license_activations_list", {
3865
+ description: "List a FluentCart license's activation rows (Pro tier; V3.1; requires FluentCart Pro + Licensing module). Read-only. Returns one row per `fct_license_activations` entry for the license, including the joined `fct_license_sites.site_url` (the NORMALIZED form — scheme + trailing slash + `www.` prefix stripped per LicenseHelper::sanitizeSiteUrl — not the raw URL the consumer submitted). Filterable by status (active/inactive/deactivated). License keys are NEVER returned by this endpoint. Returns the standardized envelope { ok, data?, error: { code, message, hint? } }; success payload: { license_id, activations: Activation[], count, filters: { status } }. Activation row: id, license_id, site_id, site_url, status, is_local, product_id, variation_id, activation_method, last_update_date, last_update_version, created_at, updated_at. Error codes: invalid_input (HTTP 400); not_found (HTTP 404) when the license doesn't exist; fluentcart.module_inactive (HTTP 412); fluentcart.licensing_unavailable (HTTP 412); fluentcart.query_failed (HTTP 500). Useful for smoke verification (count + site_url presence) and for the activation-cap test described in the diviops-fluentcart skill.",
3866
+ inputSchema: {
3867
+ license_id: z
3868
+ .number()
3869
+ .int()
3870
+ .positive()
3871
+ .describe("FluentCart license ID whose activation rows to list (fct_licenses.id)."),
3872
+ status: z
3873
+ .enum(["active", "inactive", "deactivated"])
3874
+ .optional()
3875
+ .describe("Activation row status filter (fct_license_activations.status)."),
3876
+ },
3877
+ annotations: { idempotentHint: true },
3878
+ _meta: { idempotent: "true" },
3879
+ }, async ({ license_id, status, }) => {
3880
+ const body = {};
3881
+ if (status !== undefined)
3882
+ body.status = status;
3883
+ const result = await wp.requestEnveloped(`/pro/fluentcart/licenses/${license_id}/activations`, { method: "POST", body });
3884
+ return {
3885
+ content: [
3886
+ {
3887
+ type: "text",
3888
+ text: serializeEnvelope(result, "diviops_fc_license_activations_list"),
3889
+ },
3890
+ ],
3891
+ };
3892
+ }, {
3893
+ target: "fluentcart",
3894
+ capabilityKey: "fluentcart_license_activations_list",
3895
+ });
3331
3896
  }
3332
3897
  // ── Start ────────────────────────────────────────────────────────────
3333
3898
  async function main() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diviops/mcp-server",
3
- "version": "1.5.18",
3
+ "version": "1.5.20",
4
4
  "description": "MCP server exposing Divi 5 Visual Builder as tools for Claude",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",