mcp-zenskar 1.1.10 → 1.1.12
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.
- package/README.md +11 -15
- package/package.json +1 -1
- package/src/mcp-config.json +225 -129
- package/src/server.js +176 -43
package/README.md
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
# Zenskar MCP Server
|
|
2
2
|
|
|
3
|
-
MCP server for the Zenskar API. 103 tools covering customers, contracts, invoices, payments, credit notes, accounting, products, plans,
|
|
3
|
+
MCP server for the Zenskar API. 103 tools covering customers, contracts, invoices, payments, credit notes, accounting, products, plans, and more.
|
|
4
4
|
|
|
5
5
|
## What it does
|
|
6
6
|
|
|
7
|
-
- Customers: list, search, create, update, addresses, contacts, payment methods
|
|
8
|
-
- Contracts: create, read, update, delete, amend, add phases and pricing, expire
|
|
7
|
+
- Customers: list, search, create, update, delete, addresses, contacts, payment methods
|
|
8
|
+
- Contracts: create, read, update, delete, amend, add phases and pricing, pause/resume, expire
|
|
9
9
|
- Invoices: list, get, approve, void, generate, credit notes, download
|
|
10
10
|
- Payments: create, edit, refund, delete, auto-charge
|
|
11
11
|
- Credit notes: list, create against invoice, get by ID
|
|
12
12
|
- Accounting: chart of accounts, journal entries and lines, balance sheet, income statement, account balances
|
|
13
13
|
- Products: CRUD, pricing configurations
|
|
14
14
|
- Plans: list, create, add products, preview estimates
|
|
15
|
-
- Quotes: create, preview, accept (converts to contract)
|
|
16
15
|
- Business entities: list, get, create, update
|
|
17
16
|
- Jobs: monitor async operations
|
|
18
17
|
- Custom attributes and tax categories
|
|
@@ -91,6 +90,7 @@ Once configured, you can ask Claude to interact with your Zenskar data:
|
|
|
91
90
|
| `getCustomerById` | Get a customer by ID |
|
|
92
91
|
| `createCustomer` | Create a customer with address and tax info |
|
|
93
92
|
| `updateCustomer` | Update customer details (partial update) |
|
|
93
|
+
| `deleteCustomer` | Permanently delete a customer by ID (only allowed when they have no active contracts or unpaid invoices) |
|
|
94
94
|
|
|
95
95
|
### Contacts
|
|
96
96
|
| Tool | Description |
|
|
@@ -99,6 +99,7 @@ Once configured, you can ask Claude to interact with your Zenskar data:
|
|
|
99
99
|
| `getContactById` | Get a contact by ID |
|
|
100
100
|
| `createContact` | Create a contact for a customer |
|
|
101
101
|
| `updateContact` | Update a contact's details |
|
|
102
|
+
| `deleteContact` | Delete a contact by ID |
|
|
102
103
|
|
|
103
104
|
### Contracts
|
|
104
105
|
| Tool | Description |
|
|
@@ -112,6 +113,9 @@ Once configured, you can ask Claude to interact with your Zenskar data:
|
|
|
112
113
|
| `createContractPhase` | Add a phase to a contract (add-ons, expansions) |
|
|
113
114
|
| `createContractPhasePricing` | Add pricing to a contract phase |
|
|
114
115
|
| `expireContract` | Expire an active contract |
|
|
116
|
+
| `pauseContract` | Pause an active contract from a given start date, with an unpause-extension policy (`extend` or `overlap`) and optional end date for auto-resume |
|
|
117
|
+
| `editPauseContract` | Edit an existing pause phase — set or change the resume date, shift the start, or change the unpause policy |
|
|
118
|
+
| `resumeContract` | Resume a paused contract |
|
|
115
119
|
| `createContractPrompt` | Create a contract prompt |
|
|
116
120
|
| `extractContractFromRaw` | Extract contract data from raw text using AI |
|
|
117
121
|
|
|
@@ -133,7 +137,8 @@ Once configured, you can ask Claude to interact with your Zenskar data:
|
|
|
133
137
|
| `generateInvoicePaymentLink` | Generate a payment link for an invoice |
|
|
134
138
|
| `payInvoice` | Initiate payment for an invoice |
|
|
135
139
|
| `approveInvoice` | Approve an invoice for billing |
|
|
136
|
-
| `voidInvoice` | Void an
|
|
140
|
+
| `voidInvoice` | Void an invoice |
|
|
141
|
+
| `deleteInvoice` | Delete a draft invoice |
|
|
137
142
|
| `generateInvoice` | Generate an invoice for a contract and date range |
|
|
138
143
|
| `createInvoiceCreditNote` | Create a credit note against an invoice |
|
|
139
144
|
| `createInvoiceCharge` | Auto-charge an invoice via payment gateway |
|
|
@@ -171,8 +176,6 @@ Once configured, you can ask Claude to interact with your Zenskar data:
|
|
|
171
176
|
| `listPlans` | List plan templates |
|
|
172
177
|
| `getPlanById` | Get a plan by ID with phases and pricing |
|
|
173
178
|
| `createPlan` | Create a plan template |
|
|
174
|
-
| `addProductsToPlan` | Add products to an existing plan |
|
|
175
|
-
| `previewPlanEstimate` | Preview estimated billing for a plan |
|
|
176
179
|
|
|
177
180
|
### Accounting
|
|
178
181
|
| Tool | Description |
|
|
@@ -189,14 +192,6 @@ Once configured, you can ask Claude to interact with your Zenskar data:
|
|
|
189
192
|
| `getAccountBalance` | Get balance for a specific GL account |
|
|
190
193
|
| `recogniseRevenue` | Trigger revenue recognition up to a date |
|
|
191
194
|
|
|
192
|
-
### Quotes
|
|
193
|
-
| Tool | Description |
|
|
194
|
-
|---|---|
|
|
195
|
-
| `createQuote` | Create a quote/proposal |
|
|
196
|
-
| `previewQuoteEstimate` | Preview estimated billing for a quote |
|
|
197
|
-
| `getQuoteById` | Get a quote by ID |
|
|
198
|
-
| `acceptQuote` | Accept a quote, converting to a contract |
|
|
199
|
-
|
|
200
195
|
### Custom Attributes and Tax
|
|
201
196
|
| Tool | Description |
|
|
202
197
|
|---|---|
|
|
@@ -227,6 +222,7 @@ Once configured, you can ask Claude to interact with your Zenskar data:
|
|
|
227
222
|
| `updateCustomerAddress` | Update a customer address |
|
|
228
223
|
| `listPaymentMethods` | List payment methods for a customer |
|
|
229
224
|
| `attachPaymentMethod` | Attach a payment method to a customer |
|
|
225
|
+
| `deletePaymentMethod` | Delete a payment method from a customer |
|
|
230
226
|
|
|
231
227
|
### Metrics and Usage Events
|
|
232
228
|
| Tool | Description |
|
package/package.json
CHANGED
package/src/mcp-config.json
CHANGED
|
@@ -586,7 +586,18 @@
|
|
|
586
586
|
},
|
|
587
587
|
{
|
|
588
588
|
"name": "payInvoice",
|
|
589
|
-
"description": "Initiate payment for an invoice using a payload.",
|
|
589
|
+
"description": "Initiate payment for an invoice using a payload. MONEY-MOVING ACTION — only call when the user explicitly authorizes payment for this specific invoice. Host enforces user confirmation via the approval gate; do NOT ask the user to re-confirm before calling.",
|
|
590
|
+
"needsApproval": true,
|
|
591
|
+
"approvalConfig": {
|
|
592
|
+
"title": "Pay Invoice",
|
|
593
|
+
"description": "This will initiate a payment against the invoice using the provided payload.",
|
|
594
|
+
"warningText": "Money-moving action — funds will be charged. Verify the invoice and amount before confirming.",
|
|
595
|
+
"confirmText": "Pay Invoice",
|
|
596
|
+
"cancelText": "Cancel",
|
|
597
|
+
"sensitiveFields": [
|
|
598
|
+
"payload"
|
|
599
|
+
]
|
|
600
|
+
},
|
|
590
601
|
"args": [
|
|
591
602
|
{
|
|
592
603
|
"name": "payload",
|
|
@@ -965,7 +976,7 @@
|
|
|
965
976
|
},
|
|
966
977
|
{
|
|
967
978
|
"name": "createRawMetric",
|
|
968
|
-
"description": "Create a new Usage Event schema. In backend/API terminology this resource is called a raw metric. This defines the schema used for ingesting usage events.",
|
|
979
|
+
"description": "Create a new Usage Event schema. In backend/API terminology this resource is called a raw metric. This defines the schema used for ingesting usage events. Host enforces user confirmation via the approval gate; do NOT ask the user to re-confirm before calling.",
|
|
969
980
|
"needsApproval": true,
|
|
970
981
|
"approvalConfig": {
|
|
971
982
|
"title": "Create Usage Event Schema",
|
|
@@ -1829,7 +1840,7 @@
|
|
|
1829
1840
|
},
|
|
1830
1841
|
{
|
|
1831
1842
|
"name": "createEntitlement",
|
|
1832
|
-
"description": "Creates a new entitlement in the system with various attributes including name, description, type, units, and product association.",
|
|
1843
|
+
"description": "Creates a new entitlement in the system with various attributes including name, description, type, units, and product association. Host enforces user confirmation via the approval gate; do NOT ask the user to re-confirm before calling.",
|
|
1833
1844
|
"needsApproval": true,
|
|
1834
1845
|
"approvalConfig": {
|
|
1835
1846
|
"title": "Create New Entitlement",
|
|
@@ -1906,7 +1917,7 @@
|
|
|
1906
1917
|
},
|
|
1907
1918
|
{
|
|
1908
1919
|
"name": "approveInvoice",
|
|
1909
|
-
"description": "
|
|
1920
|
+
"description": "Approve an invoice; sets status to approved. Host enforces user confirmation via the approval gate; do NOT ask the user to re-confirm before calling.",
|
|
1910
1921
|
"needsApproval": true,
|
|
1911
1922
|
"approvalConfig": {
|
|
1912
1923
|
"title": "Approve Invoice",
|
|
@@ -1958,7 +1969,7 @@
|
|
|
1958
1969
|
},
|
|
1959
1970
|
{
|
|
1960
1971
|
"name": "recogniseRevenue",
|
|
1961
|
-
"description": "
|
|
1972
|
+
"description": "Recognize revenue for the organization up to a specified end_date. Host enforces user confirmation via the approval gate; do NOT ask the user to re-confirm before calling.",
|
|
1962
1973
|
"needsApproval": true,
|
|
1963
1974
|
"approvalConfig": {
|
|
1964
1975
|
"title": "Recognise Revenue",
|
|
@@ -1992,7 +2003,7 @@
|
|
|
1992
2003
|
},
|
|
1993
2004
|
{
|
|
1994
2005
|
"name": "createBusinessEntity",
|
|
1995
|
-
"description": "Creates a new business entity in the system with various attributes including name, address, contact details, tax configuration, and logo.",
|
|
2006
|
+
"description": "Creates a new business entity in the system with various attributes including name, address, contact details, tax configuration, and logo. Host enforces user confirmation via the approval gate; do NOT ask the user to re-confirm before calling.",
|
|
1996
2007
|
"needsApproval": true,
|
|
1997
2008
|
"approvalConfig": {
|
|
1998
2009
|
"title": "Create New Business Entity",
|
|
@@ -2305,7 +2316,7 @@
|
|
|
2305
2316
|
},
|
|
2306
2317
|
{
|
|
2307
2318
|
"name": "ingestRawMetricEvent",
|
|
2308
|
-
"description": "Ingest a Usage Event for the specified resource slug. Backend/API terminology may also call the target resource a raw metric.",
|
|
2319
|
+
"description": "Ingest a Usage Event for the specified resource slug. Backend/API terminology may also call the target resource a raw metric. Host enforces user confirmation via the approval gate; do NOT ask the user to re-confirm before calling.",
|
|
2309
2320
|
"needsApproval": true,
|
|
2310
2321
|
"approvalConfig": {
|
|
2311
2322
|
"title": "Ingest Usage Event",
|
|
@@ -2719,7 +2730,18 @@
|
|
|
2719
2730
|
},
|
|
2720
2731
|
{
|
|
2721
2732
|
"name": "deleteContract",
|
|
2722
|
-
"description": "
|
|
2733
|
+
"description": "Permanently delete a DRAFT contract. ONLY works on contracts with status='draft'. Do NOT call on ACTIVE, EXPIRED, or PAUSED contracts. DESTRUCTIVE: removes all phases, products, and pricing associations. NEVER call as automatic recovery from another failed operation (e.g., do not retry as a fallback after expireContract or voidInvoice fails). ONLY call when the user explicitly says 'delete' or 'remove' for THIS draft contract. If a non-destructive action fails, surface the error verbatim and stop. Host enforces user confirmation via the approval gate; do NOT ask the user to re-confirm before calling.",
|
|
2734
|
+
"needsApproval": true,
|
|
2735
|
+
"approvalConfig": {
|
|
2736
|
+
"title": "Delete Draft Contract",
|
|
2737
|
+
"description": "This will permanently delete the draft contract along with all its phases, products, and pricing associations.",
|
|
2738
|
+
"warningText": "DESTRUCTIVE — this cannot be undone. Only draft contracts may be deleted; verify status before confirming.",
|
|
2739
|
+
"confirmText": "Delete Contract",
|
|
2740
|
+
"cancelText": "Cancel",
|
|
2741
|
+
"sensitiveFields": [
|
|
2742
|
+
"contractId"
|
|
2743
|
+
]
|
|
2744
|
+
},
|
|
2723
2745
|
"args": [
|
|
2724
2746
|
{
|
|
2725
2747
|
"name": "contractId",
|
|
@@ -2956,7 +2978,19 @@
|
|
|
2956
2978
|
},
|
|
2957
2979
|
{
|
|
2958
2980
|
"name": "expireContract",
|
|
2959
|
-
"description": "Expire an
|
|
2981
|
+
"description": "Expire an ACTIVE or PAUSED contract by adjusting its end_date. Allowed status transitions: ACTIVE→EXPIRED, PAUSED→EXPIRED. EXPIRED is terminal — calling on an already-expired contract returns 400; do NOT retry, do NOT escalate to deleteContract or any other destructive tool as a fallback. Verify contract.status via getContractById before calling. Idempotency: NO. The contract expires at end of day 23:59:59.999999. Host enforces user confirmation via the approval gate; do NOT ask the user to re-confirm before calling.",
|
|
2982
|
+
"needsApproval": true,
|
|
2983
|
+
"approvalConfig": {
|
|
2984
|
+
"title": "Expire Contract",
|
|
2985
|
+
"description": "This will set the contract's end_date and transition status to EXPIRED. EXPIRED is terminal.",
|
|
2986
|
+
"warningText": "Once expired, the contract cannot be re-activated through this endpoint. Future-dated phases beyond the expiry will be removed.",
|
|
2987
|
+
"confirmText": "Expire Contract",
|
|
2988
|
+
"cancelText": "Cancel",
|
|
2989
|
+
"sensitiveFields": [
|
|
2990
|
+
"contractId",
|
|
2991
|
+
"expiry_date"
|
|
2992
|
+
]
|
|
2993
|
+
},
|
|
2960
2994
|
"args": [
|
|
2961
2995
|
{
|
|
2962
2996
|
"name": "contractId",
|
|
@@ -2967,7 +3001,7 @@
|
|
|
2967
3001
|
},
|
|
2968
3002
|
{
|
|
2969
3003
|
"name": "expiry_date",
|
|
2970
|
-
"description": "Date to expire the contract (e.g. 2026-12-31). Defaults to today if not provided.",
|
|
3004
|
+
"description": "Date to expire the contract (e.g. 2026-12-31). Defaults to today if not provided. Must not be earlier than contract.start_date.",
|
|
2971
3005
|
"type": "string",
|
|
2972
3006
|
"required": false,
|
|
2973
3007
|
"position": "body"
|
|
@@ -2986,7 +3020,18 @@
|
|
|
2986
3020
|
},
|
|
2987
3021
|
{
|
|
2988
3022
|
"name": "voidInvoice",
|
|
2989
|
-
"description": "Void an invoice
|
|
3023
|
+
"description": "Void an invoice. ONLY works on approved or paid invoices with invoice_total == 0 (zero-balance, typically after a full credit-note offset). For non-zero approved invoices, issue a credit note via createInvoiceCreditNote first to bring the balance to zero, then void. If the call returns INVOICE_CANNOT_BE_VOIDED, surface that error verbatim — do NOT retry, do NOT escalate to deleteInvoice. Host enforces user confirmation via the approval gate; do NOT ask the user to re-confirm before calling.",
|
|
3024
|
+
"needsApproval": true,
|
|
3025
|
+
"approvalConfig": {
|
|
3026
|
+
"title": "Void Invoice",
|
|
3027
|
+
"description": "This will set the invoice status to 'void' and post the corresponding accounting entries. Only zero-balance approved/paid invoices are eligible.",
|
|
3028
|
+
"warningText": "Voiding affects accounting records and cannot be easily undone.",
|
|
3029
|
+
"confirmText": "Void Invoice",
|
|
3030
|
+
"cancelText": "Cancel",
|
|
3031
|
+
"sensitiveFields": [
|
|
3032
|
+
"invoiceId"
|
|
3033
|
+
]
|
|
3034
|
+
},
|
|
2990
3035
|
"args": [
|
|
2991
3036
|
{
|
|
2992
3037
|
"name": "invoiceId",
|
|
@@ -3007,6 +3052,40 @@
|
|
|
3007
3052
|
"prependBody": "## Invoice Voided\n\n"
|
|
3008
3053
|
}
|
|
3009
3054
|
},
|
|
3055
|
+
{
|
|
3056
|
+
"name": "deleteInvoice",
|
|
3057
|
+
"description": "Permanently delete a draft invoice. ONLY works on status='draft'. For approved invoices, use voidInvoice (with caveats) or createInvoiceCreditNote — do NOT call deleteInvoice on approved invoices. DESTRUCTIVE: record removed permanently. ONLY call when the user explicitly says 'delete' for THIS invoice; never call as automatic recovery from another failed operation. Host enforces user confirmation via the approval gate; do NOT ask the user to re-confirm before calling.",
|
|
3058
|
+
"needsApproval": true,
|
|
3059
|
+
"approvalConfig": {
|
|
3060
|
+
"title": "Delete Invoice",
|
|
3061
|
+
"description": "This will permanently delete the draft invoice.",
|
|
3062
|
+
"warningText": "DESTRUCTIVE — invoice record will be removed and cannot be recovered.",
|
|
3063
|
+
"confirmText": "Delete Invoice",
|
|
3064
|
+
"cancelText": "Cancel",
|
|
3065
|
+
"sensitiveFields": [
|
|
3066
|
+
"invoiceId"
|
|
3067
|
+
]
|
|
3068
|
+
},
|
|
3069
|
+
"args": [
|
|
3070
|
+
{
|
|
3071
|
+
"name": "invoiceId",
|
|
3072
|
+
"description": "The unique identifier (UUID) of the draft or upcoming invoice to delete.",
|
|
3073
|
+
"type": "string",
|
|
3074
|
+
"required": true,
|
|
3075
|
+
"position": "path"
|
|
3076
|
+
}
|
|
3077
|
+
],
|
|
3078
|
+
"requestTemplate": {
|
|
3079
|
+
"url": "/invoices/{invoiceId}",
|
|
3080
|
+
"method": "DELETE",
|
|
3081
|
+
"headers": {
|
|
3082
|
+
"Content-Type": "application/json"
|
|
3083
|
+
}
|
|
3084
|
+
},
|
|
3085
|
+
"responseTemplate": {
|
|
3086
|
+
"prependBody": "## Invoice Deleted\n\n"
|
|
3087
|
+
}
|
|
3088
|
+
},
|
|
3010
3089
|
{
|
|
3011
3090
|
"name": "createInvoiceCreditNote",
|
|
3012
3091
|
"description": "Create a credit note against a specific invoice. The credit_note_amount is in the invoice's currency (e.g. 25 for $25). The API stores amounts in cents internally. Returns the created credit note.",
|
|
@@ -3046,7 +3125,7 @@
|
|
|
3046
3125
|
},
|
|
3047
3126
|
{
|
|
3048
3127
|
"name": "generateInvoice",
|
|
3049
|
-
"description": "
|
|
3128
|
+
"description": "STOP — read this before calling. Granularity is PER PHASE, NOT per product. ONE call generates ONE invoice covering ALL products active in the (contract, customer, from_date, to_date) window — every product becomes a line item on the same invoice. NEVER iterate over products. Iterate over PHASES only. CORRECT: a 2-phase contract with 2 products per phase → exactly 2 calls (one per phase), producing 2 invoices each with 2 line items. WRONG: 4 calls (one per product per phase) → produces 4 fragmented duplicate invoices, which is the #1 bug reported on this tool. Before calling, fetch the contract's phases via getContractById and use each phase's start_date / end_date as from_date / to_date. Use for billing replay or on-demand invoice generation.",
|
|
3050
3129
|
"args": [
|
|
3051
3130
|
{
|
|
3052
3131
|
"name": "contract_id",
|
|
@@ -3302,7 +3381,18 @@
|
|
|
3302
3381
|
},
|
|
3303
3382
|
{
|
|
3304
3383
|
"name": "deleteManualPayment",
|
|
3305
|
-
"description": "
|
|
3384
|
+
"description": "Permanently delete a manual payment record. Only works on payments in an eligible status for deletion. DESTRUCTIVE — only call when the user explicitly says 'delete' for THIS payment; never as automatic recovery from another failed operation. Host enforces user confirmation via the approval gate; do NOT ask the user to re-confirm before calling.",
|
|
3385
|
+
"needsApproval": true,
|
|
3386
|
+
"approvalConfig": {
|
|
3387
|
+
"title": "Delete Manual Payment",
|
|
3388
|
+
"description": "This will permanently remove the manual payment record.",
|
|
3389
|
+
"warningText": "DESTRUCTIVE — payment record will be removed and cannot be recovered. Linked invoice balance may shift as a result.",
|
|
3390
|
+
"confirmText": "Delete Payment",
|
|
3391
|
+
"cancelText": "Cancel",
|
|
3392
|
+
"sensitiveFields": [
|
|
3393
|
+
"paymentId"
|
|
3394
|
+
]
|
|
3395
|
+
},
|
|
3306
3396
|
"args": [
|
|
3307
3397
|
{
|
|
3308
3398
|
"name": "paymentId",
|
|
@@ -4126,7 +4216,7 @@
|
|
|
4126
4216
|
},
|
|
4127
4217
|
{
|
|
4128
4218
|
"name": "createProductPricing",
|
|
4129
|
-
"description": "Create a new pricing configuration for a product.
|
|
4219
|
+
"description": "Create a new pricing configuration for a product. MANDATORY pre-call checklist — ask the user for ALL of these and do NOT default any silently: (1) currency (ISO 4217), (2) pricing_type (per_unit|flat_fee|tiered|volume|percent|package|step|matrix), (3) unit_amount in MAJOR currency units (float — 3 means $3, NOT 300 cents), (4) quantity object with type (fixed|metered) and unit label (e.g. 'user', 'request') and either quantity (for fixed) or aggregate_id (for metered), (5) billing_period.cadence (ISO 8601 — 'P1M' monthly, 'P3M' quarterly, 'P1Y' yearly) and billing_period.offset. SKIPPING quantity OR billing_period causes the Zenskar UI to render 'Undefined- Every Undefined Undefined' for billing cadence and 0 for billing metric — that is the #1 bug reported on this tool.",
|
|
4130
4220
|
"args": [
|
|
4131
4221
|
{
|
|
4132
4222
|
"name": "productId",
|
|
@@ -4151,16 +4241,23 @@
|
|
|
4151
4241
|
},
|
|
4152
4242
|
{
|
|
4153
4243
|
"name": "pricing_data",
|
|
4154
|
-
"description": "Pricing data object (required).
|
|
4244
|
+
"description": "Pricing data object (required). MUST include 'pricing_type' discriminator AND 'currency' (ISO 4217, e.g. 'USD'). 'unit_amount' is in MAJOR currency units (float) — 3 means $3, NOT 300 cents. Do NOT convert to cents. Supported pricing_type values with examples: flat_fee: {pricing_type:'flat_fee', unit_amount:100, currency:'USD'}. per_unit: {pricing_type:'per_unit', unit_amount:3, currency:'USD'}. tiered: {pricing_type:'tiered', unit_amount:[10,5], up_to:[100,null], currency:'USD'}. volume: {pricing_type:'volume', unit_amount:[10,5], up_to:[100,null], currency:'USD'}. percent: {pricing_type:'percent', percentage:5.0, currency:'USD'}. package: {pricing_type:'package', package_size:10, unit_amount:50, currency:'USD'}. step: {pricing_type:'step', unit_amount:[...], up_to:[...], currency:'USD'}. matrix: {pricing_type:'matrix', dimensions:[...], values:[...], currency:'USD'}. Optional inside pricing_data for per_unit: 'proration_type' ('day_based'|'cadence_based'), 'charge_full_amount' (bool).",
|
|
4245
|
+
"type": "object",
|
|
4246
|
+
"required": true,
|
|
4247
|
+
"position": "body"
|
|
4248
|
+
},
|
|
4249
|
+
{
|
|
4250
|
+
"name": "quantity",
|
|
4251
|
+
"description": "Quantity configuration (REQUIRED — ask the user; do not omit). Top-level object, NOT inside pricing_data. Shape: {type: 'fixed'|'metered', quantity?: number, unit?: string, aggregate_id?: UUID}. 'fixed' = static quantity (e.g. seats); also set 'unit' (label like 'user') and 'quantity' (number). 'metered' = consumption tracked via a billable metric; set aggregate_id to the billable-metric UUID. Omitting this object causes the UI to show 0 for billing metric.",
|
|
4155
4252
|
"type": "object",
|
|
4156
4253
|
"required": true,
|
|
4157
4254
|
"position": "body"
|
|
4158
4255
|
},
|
|
4159
4256
|
{
|
|
4160
4257
|
"name": "billing_period",
|
|
4161
|
-
"description": "Billing cadence
|
|
4258
|
+
"description": "Billing cadence (REQUIRED — ask the user; do not omit). Object: {cadence: ISO-8601 duration ('P1M'=monthly, 'P3M'=quarterly, 'P1Y'=annually), offset: ISO-8601 duration ('P0D' for no offset, 'P1M' to bill 1 month after period start)}. Both fields needed. Example: {\"cadence\":\"P1M\",\"offset\":\"P0D\"}. Omitting this causes the UI to render 'Undefined- Every Undefined Undefined'.",
|
|
4162
4259
|
"type": "object",
|
|
4163
|
-
"required":
|
|
4260
|
+
"required": true,
|
|
4164
4261
|
"position": "body"
|
|
4165
4262
|
},
|
|
4166
4263
|
{
|
|
@@ -4191,7 +4288,7 @@
|
|
|
4191
4288
|
},
|
|
4192
4289
|
{
|
|
4193
4290
|
"name": "listPlans",
|
|
4194
|
-
"description": "Retrieve a paginated list of plans
|
|
4291
|
+
"description": "Retrieve a paginated list of plans that define reusable contract structures. Hits /plans — the same data the Zenskar app's Plans page reads.",
|
|
4195
4292
|
"args": [
|
|
4196
4293
|
{
|
|
4197
4294
|
"name": "cursor",
|
|
@@ -4223,7 +4320,7 @@
|
|
|
4223
4320
|
}
|
|
4224
4321
|
],
|
|
4225
4322
|
"requestTemplate": {
|
|
4226
|
-
"url": "/
|
|
4323
|
+
"url": "/plans",
|
|
4227
4324
|
"method": "GET",
|
|
4228
4325
|
"headers": {
|
|
4229
4326
|
"Content-Type": "application/json"
|
|
@@ -4246,7 +4343,7 @@
|
|
|
4246
4343
|
}
|
|
4247
4344
|
],
|
|
4248
4345
|
"requestTemplate": {
|
|
4249
|
-
"url": "/
|
|
4346
|
+
"url": "/plans/{planId}",
|
|
4250
4347
|
"method": "GET",
|
|
4251
4348
|
"headers": {
|
|
4252
4349
|
"Content-Type": "application/json"
|
|
@@ -4258,141 +4355,58 @@
|
|
|
4258
4355
|
},
|
|
4259
4356
|
{
|
|
4260
4357
|
"name": "createPlan",
|
|
4261
|
-
"description": "Create a new plan
|
|
4358
|
+
"description": "Create a new plan — a reusable contract template with phased pricing. Hits POST /plans (same Plan table the Zenskar app's PlansV2 page reads). The request body shape is NESTED, not flat: top-level fields are name, status, schedule, optional description, optional phases[]. Currency is set per-pricing inside phase.pricings[].pricing.pricing_data.currency, NOT at the top level. A plan with no phases is unusable in the UI — always include at least one phase. Plan starts as 'draft'; the user must publish it to make it active.",
|
|
4262
4359
|
"args": [
|
|
4263
4360
|
{
|
|
4264
4361
|
"name": "name",
|
|
4265
|
-
"description": "Name of the plan.",
|
|
4362
|
+
"description": "Name of the plan (required).",
|
|
4266
4363
|
"type": "string",
|
|
4267
4364
|
"required": true,
|
|
4268
4365
|
"position": "body"
|
|
4269
4366
|
},
|
|
4270
4367
|
{
|
|
4271
|
-
"name": "
|
|
4272
|
-
"description": "
|
|
4368
|
+
"name": "status",
|
|
4369
|
+
"description": "Plan status (required). Valid values: 'draft', 'active', 'archived'. New plans almost always start as 'draft' — the user publishes via the UI to activate.",
|
|
4273
4370
|
"type": "string",
|
|
4274
4371
|
"required": true,
|
|
4275
|
-
"position": "body"
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
"required": false,
|
|
4282
|
-
"position": "body"
|
|
4283
|
-
},
|
|
4284
|
-
{
|
|
4285
|
-
"name": "duration",
|
|
4286
|
-
"description": "Billing period in ISO 8601 duration format (e.g., 'P1M' for monthly, 'P1Y' for yearly).",
|
|
4287
|
-
"type": "string",
|
|
4288
|
-
"required": false,
|
|
4289
|
-
"position": "body"
|
|
4290
|
-
},
|
|
4291
|
-
{
|
|
4292
|
-
"name": "products",
|
|
4293
|
-
"description": "Array of products to add to plan, each with product_id and pricing configuration.",
|
|
4294
|
-
"type": "array",
|
|
4295
|
-
"required": false,
|
|
4296
|
-
"position": "body"
|
|
4372
|
+
"position": "body",
|
|
4373
|
+
"enum": [
|
|
4374
|
+
"draft",
|
|
4375
|
+
"active",
|
|
4376
|
+
"archived"
|
|
4377
|
+
]
|
|
4297
4378
|
},
|
|
4298
4379
|
{
|
|
4299
|
-
"name": "
|
|
4300
|
-
"description": "
|
|
4380
|
+
"name": "schedule",
|
|
4381
|
+
"description": "Plan-level schedule (required). Object: {duration: ISO-8601 e.g. 'P1Y'|'P1M', start_offset?: ISO-8601 e.g. 'P0D', trigger_type?: 'time_based'}. Example: {\"duration\":\"P1Y\",\"start_offset\":\"P0D\"}.",
|
|
4301
4382
|
"type": "object",
|
|
4302
|
-
"required":
|
|
4303
|
-
"position": "body"
|
|
4304
|
-
},
|
|
4305
|
-
{
|
|
4306
|
-
"name": "status",
|
|
4307
|
-
"description": "Plan status. Default: 'draft'. Options: 'draft', 'active', 'archived'.",
|
|
4308
|
-
"type": "string",
|
|
4309
|
-
"required": false,
|
|
4383
|
+
"required": true,
|
|
4310
4384
|
"position": "body"
|
|
4311
4385
|
},
|
|
4312
4386
|
{
|
|
4313
|
-
"name": "
|
|
4314
|
-
"description": "
|
|
4387
|
+
"name": "description",
|
|
4388
|
+
"description": "Optional plan description.",
|
|
4315
4389
|
"type": "string",
|
|
4316
4390
|
"required": false,
|
|
4317
4391
|
"position": "body"
|
|
4318
|
-
}
|
|
4319
|
-
],
|
|
4320
|
-
"requestTemplate": {
|
|
4321
|
-
"url": "/templates/plan",
|
|
4322
|
-
"method": "POST",
|
|
4323
|
-
"headers": {
|
|
4324
|
-
"Content-Type": "application/json"
|
|
4325
|
-
}
|
|
4326
|
-
},
|
|
4327
|
-
"responseTemplate": {
|
|
4328
|
-
"prependBody": "## Created Plan\n\n"
|
|
4329
|
-
}
|
|
4330
|
-
},
|
|
4331
|
-
{
|
|
4332
|
-
"name": "addProductsToPlan",
|
|
4333
|
-
"description": "Add products with pricing to an existing plan.",
|
|
4334
|
-
"args": [
|
|
4335
|
-
{
|
|
4336
|
-
"name": "planId",
|
|
4337
|
-
"description": "The unique identifier of the plan.",
|
|
4338
|
-
"type": "string",
|
|
4339
|
-
"required": true,
|
|
4340
|
-
"position": "path"
|
|
4341
4392
|
},
|
|
4342
4393
|
{
|
|
4343
|
-
"name": "
|
|
4344
|
-
"description": "Array of
|
|
4394
|
+
"name": "phases",
|
|
4395
|
+
"description": "Array of plan phases (REQUIRED — must be non-empty). Each phase: {name: string (required), schedule: {duration, start_offset?, trigger_type?} (required), order: int (required, 0-indexed), description?: string, features?: CreateProductPricingRequestSchema (one-off phase-level pricing/features), pricings?: [{schedule, pricing_id?, product_id?, pricing?: CreateProductPricingRequestSchema, product?: CreateProductRequestSchema}] (per-product pricings)}. Each phase must have features OR a non-empty pricings array. Minimal example: [{\"name\":\"Phase 1\",\"schedule\":{\"duration\":\"P1Y\"},\"order\":0,\"features\":{\"pricing_data\":{\"pricing_type\":\"features\"}}}].",
|
|
4345
4396
|
"type": "array",
|
|
4346
4397
|
"required": true,
|
|
4347
4398
|
"position": "body"
|
|
4348
4399
|
}
|
|
4349
4400
|
],
|
|
4350
4401
|
"requestTemplate": {
|
|
4351
|
-
"url": "/
|
|
4402
|
+
"url": "/plans",
|
|
4352
4403
|
"method": "POST",
|
|
4353
4404
|
"headers": {
|
|
4354
4405
|
"Content-Type": "application/json"
|
|
4355
4406
|
}
|
|
4356
4407
|
},
|
|
4357
4408
|
"responseTemplate": {
|
|
4358
|
-
"prependBody": "##
|
|
4359
|
-
}
|
|
4360
|
-
},
|
|
4361
|
-
{
|
|
4362
|
-
"name": "previewPlanEstimate",
|
|
4363
|
-
"description": "Preview the estimated billing for a plan, showing projected charges per phase.",
|
|
4364
|
-
"args": [
|
|
4365
|
-
{
|
|
4366
|
-
"name": "planId",
|
|
4367
|
-
"description": "The unique identifier of the plan.",
|
|
4368
|
-
"type": "string",
|
|
4369
|
-
"required": true,
|
|
4370
|
-
"position": "path"
|
|
4371
|
-
},
|
|
4372
|
-
{
|
|
4373
|
-
"name": "start_date",
|
|
4374
|
-
"description": "Start date for the estimate (YYYY-MM-DD).",
|
|
4375
|
-
"type": "string",
|
|
4376
|
-
"required": true,
|
|
4377
|
-
"position": "body"
|
|
4378
|
-
},
|
|
4379
|
-
{
|
|
4380
|
-
"name": "end_date",
|
|
4381
|
-
"description": "End date for the estimate (YYYY-MM-DD). Optional — calculated from plan duration if omitted.",
|
|
4382
|
-
"type": "string",
|
|
4383
|
-
"required": false,
|
|
4384
|
-
"position": "body"
|
|
4385
|
-
}
|
|
4386
|
-
],
|
|
4387
|
-
"requestTemplate": {
|
|
4388
|
-
"url": "/templates/plan/{planId}/preview",
|
|
4389
|
-
"method": "POST",
|
|
4390
|
-
"headers": {
|
|
4391
|
-
"Content-Type": "application/json"
|
|
4392
|
-
}
|
|
4393
|
-
},
|
|
4394
|
-
"responseTemplate": {
|
|
4395
|
-
"prependBody": "## Plan Estimate Preview\n\n"
|
|
4409
|
+
"prependBody": "## Created Plan\n\n"
|
|
4396
4410
|
}
|
|
4397
4411
|
},
|
|
4398
4412
|
{
|
|
@@ -5107,7 +5121,18 @@
|
|
|
5107
5121
|
},
|
|
5108
5122
|
{
|
|
5109
5123
|
"name": "deleteCustomer",
|
|
5110
|
-
"description": "
|
|
5124
|
+
"description": "Permanently delete a customer by ID. DESTRUCTIVE and cannot be undone. The customer must not have active contracts or unpaid invoices. ONLY call when the user explicitly says 'delete' for THIS customer; never as automatic recovery from another failed operation. Host enforces user confirmation via the approval gate; do NOT ask the user to re-confirm before calling.",
|
|
5125
|
+
"needsApproval": true,
|
|
5126
|
+
"approvalConfig": {
|
|
5127
|
+
"title": "Delete Customer",
|
|
5128
|
+
"description": "This will permanently delete the customer and all associated records.",
|
|
5129
|
+
"warningText": "DESTRUCTIVE — customer record cannot be recovered. Verify there are no active contracts or unpaid invoices first.",
|
|
5130
|
+
"confirmText": "Delete Customer",
|
|
5131
|
+
"cancelText": "Cancel",
|
|
5132
|
+
"sensitiveFields": [
|
|
5133
|
+
"customerId"
|
|
5134
|
+
]
|
|
5135
|
+
},
|
|
5111
5136
|
"args": [
|
|
5112
5137
|
{
|
|
5113
5138
|
"name": "customerId",
|
|
@@ -5130,7 +5155,18 @@
|
|
|
5130
5155
|
},
|
|
5131
5156
|
{
|
|
5132
5157
|
"name": "deleteContact",
|
|
5133
|
-
"description": "
|
|
5158
|
+
"description": "Permanently delete a contact by ID. DESTRUCTIVE — only call when the user explicitly says 'delete' for THIS contact; never as automatic recovery from another failed operation. Host enforces user confirmation via the approval gate; do NOT ask the user to re-confirm before calling.",
|
|
5159
|
+
"needsApproval": true,
|
|
5160
|
+
"approvalConfig": {
|
|
5161
|
+
"title": "Delete Contact",
|
|
5162
|
+
"description": "This will permanently delete the contact record.",
|
|
5163
|
+
"warningText": "DESTRUCTIVE — contact record will be removed and cannot be recovered.",
|
|
5164
|
+
"confirmText": "Delete Contact",
|
|
5165
|
+
"cancelText": "Cancel",
|
|
5166
|
+
"sensitiveFields": [
|
|
5167
|
+
"contactId"
|
|
5168
|
+
]
|
|
5169
|
+
},
|
|
5134
5170
|
"args": [
|
|
5135
5171
|
{
|
|
5136
5172
|
"name": "contactId",
|
|
@@ -5153,7 +5189,19 @@
|
|
|
5153
5189
|
},
|
|
5154
5190
|
{
|
|
5155
5191
|
"name": "deletePaymentMethod",
|
|
5156
|
-
"description": "
|
|
5192
|
+
"description": "Permanently delete a payment method from a customer. DESTRUCTIVE — saved card/bank details will be removed; recurring auto-charges using this method will fail. Only call when the user explicitly says 'delete' for THIS payment method. Host enforces user confirmation via the approval gate; do NOT ask the user to re-confirm before calling.",
|
|
5193
|
+
"needsApproval": true,
|
|
5194
|
+
"approvalConfig": {
|
|
5195
|
+
"title": "Delete Payment Method",
|
|
5196
|
+
"description": "This will permanently delete the payment method from the customer's profile.",
|
|
5197
|
+
"warningText": "DESTRUCTIVE — saved payment details will be removed. Any auto-charge or scheduled charge using this method will fail.",
|
|
5198
|
+
"confirmText": "Delete Payment Method",
|
|
5199
|
+
"cancelText": "Cancel",
|
|
5200
|
+
"sensitiveFields": [
|
|
5201
|
+
"customerId",
|
|
5202
|
+
"paymentMethodId"
|
|
5203
|
+
]
|
|
5204
|
+
},
|
|
5157
5205
|
"args": [
|
|
5158
5206
|
{
|
|
5159
5207
|
"name": "customerId",
|
|
@@ -5183,7 +5231,7 @@
|
|
|
5183
5231
|
},
|
|
5184
5232
|
{
|
|
5185
5233
|
"name": "pauseContract",
|
|
5186
|
-
"description": "Pause an active contract.
|
|
5234
|
+
"description": "Pause an active contract. ALWAYS ask the user explicitly for both 'start_date' and 'unpause_extension_policy' before calling — do NOT silently default. A future-dated start_date will create a scheduled pause that has not yet begun; the contract's top-level status stays 'active' until start_date passes. Use 'editPauseContract' to adjust the pause window or set a resume date afterward.",
|
|
5187
5235
|
"args": [
|
|
5188
5236
|
{
|
|
5189
5237
|
"name": "contractId",
|
|
@@ -5194,14 +5242,14 @@
|
|
|
5194
5242
|
},
|
|
5195
5243
|
{
|
|
5196
5244
|
"name": "start_date",
|
|
5197
|
-
"description": "Date when the pause begins (ISO 8601 format, e.g. 2026-04-01T00:00:00).",
|
|
5245
|
+
"description": "Date when the pause begins (ISO 8601 format, e.g. 2026-04-01T00:00:00). REQUIRED — ask the user; never default to today or a future date silently.",
|
|
5198
5246
|
"type": "string",
|
|
5199
5247
|
"required": true,
|
|
5200
5248
|
"position": "body"
|
|
5201
5249
|
},
|
|
5202
5250
|
{
|
|
5203
5251
|
"name": "unpause_extension_policy",
|
|
5204
|
-
"description": "How to handle the contract end date when unpaused.",
|
|
5252
|
+
"description": "How to handle the contract end date when unpaused. 'extend' pushes the end_date out by the pause duration; 'overlap' keeps end_date fixed. ASK the user.",
|
|
5205
5253
|
"type": "string",
|
|
5206
5254
|
"required": true,
|
|
5207
5255
|
"position": "body",
|
|
@@ -5229,9 +5277,57 @@
|
|
|
5229
5277
|
"prependBody": "## Contract Paused\n\n"
|
|
5230
5278
|
}
|
|
5231
5279
|
},
|
|
5280
|
+
{
|
|
5281
|
+
"name": "editPauseContract",
|
|
5282
|
+
"description": "Edit an existing pause phase on a contract. Use this when 'resumeContract' returns 'pause phase not found' for a future-dated pause, or when the user wants to set a resume date (pause end_date), shift the pause start, or change the unpause_extension_policy. Hits PATCH /contract_v2/{contractId}/pause.",
|
|
5283
|
+
"args": [
|
|
5284
|
+
{
|
|
5285
|
+
"name": "contractId",
|
|
5286
|
+
"description": "The unique identifier (UUID) of the contract whose pause phase to edit.",
|
|
5287
|
+
"type": "string",
|
|
5288
|
+
"required": true,
|
|
5289
|
+
"position": "path"
|
|
5290
|
+
},
|
|
5291
|
+
{
|
|
5292
|
+
"name": "start_date",
|
|
5293
|
+
"description": "New pause start date (ISO 8601). Optional.",
|
|
5294
|
+
"type": "string",
|
|
5295
|
+
"required": false,
|
|
5296
|
+
"position": "body"
|
|
5297
|
+
},
|
|
5298
|
+
{
|
|
5299
|
+
"name": "end_date",
|
|
5300
|
+
"description": "Resume date — when the pause ends (ISO 8601). Set this to schedule a future resume.",
|
|
5301
|
+
"type": "string",
|
|
5302
|
+
"required": false,
|
|
5303
|
+
"position": "body"
|
|
5304
|
+
},
|
|
5305
|
+
{
|
|
5306
|
+
"name": "unpause_extension_policy",
|
|
5307
|
+
"description": "How to handle the contract end date when unpaused. 'extend' or 'overlap'.",
|
|
5308
|
+
"type": "string",
|
|
5309
|
+
"required": false,
|
|
5310
|
+
"position": "body",
|
|
5311
|
+
"enum": [
|
|
5312
|
+
"extend",
|
|
5313
|
+
"overlap"
|
|
5314
|
+
]
|
|
5315
|
+
}
|
|
5316
|
+
],
|
|
5317
|
+
"requestTemplate": {
|
|
5318
|
+
"url": "/contract_v2/{contractId}/pause",
|
|
5319
|
+
"method": "PATCH",
|
|
5320
|
+
"headers": {
|
|
5321
|
+
"Content-Type": "application/json"
|
|
5322
|
+
}
|
|
5323
|
+
},
|
|
5324
|
+
"responseTemplate": {
|
|
5325
|
+
"prependBody": "## Pause Phase Updated\n\n"
|
|
5326
|
+
}
|
|
5327
|
+
},
|
|
5232
5328
|
{
|
|
5233
5329
|
"name": "resumeContract",
|
|
5234
|
-
"description": "Resume a
|
|
5330
|
+
"description": "Resume a contract that is currently in an actively-running pause. MANDATORY FLOW: (1) ASK the user 'What date should the contract resume from?' — never call without a known resume date even though this endpoint takes no body. (2) Call getContractById and check the pause phase's start_date. (3a) If pause start_date is strictly in the past (pause currently active) AND the user wants to resume immediately/today → call this endpoint. (3b) If pause start_date is today OR in the future, OR the user wants a specific future resume date → call 'editPauseContract' with end_date=<resume_date> instead. The error 'pause phase not found' does NOT mean no pause exists — it means no pause has yet started; on this error, automatically pivot to 'editPauseContract' with the resume date. Never escalate to deleteContract or any destructive fallback.",
|
|
5235
5331
|
"args": [
|
|
5236
5332
|
{
|
|
5237
5333
|
"name": "contractId",
|
package/src/server.js
CHANGED
|
@@ -1113,43 +1113,77 @@ async function executeAPICall(tool, args) {
|
|
|
1113
1113
|
}
|
|
1114
1114
|
}
|
|
1115
1115
|
|
|
1116
|
+
// One-time approval tokens. Server issues a token on the approval_required response;
|
|
1117
|
+
// the host must echo it back on the second invocation. Prevents prompt-injection
|
|
1118
|
+
// scenarios where an LLM fabricates `approval.approved=true` to bypass the dialog.
|
|
1119
|
+
// Tokens are single-use and expire after 5 minutes.
|
|
1120
|
+
const APPROVAL_TOKEN_TTL_MS = 5 * 60 * 1000;
|
|
1121
|
+
const approvalTokens = new Map(); // token -> {toolName, issuedAt, expiresAt}
|
|
1122
|
+
|
|
1123
|
+
function issueApprovalToken(toolName) {
|
|
1124
|
+
const token = uuidv4();
|
|
1125
|
+
const issuedAt = Date.now();
|
|
1126
|
+
approvalTokens.set(token, {
|
|
1127
|
+
toolName,
|
|
1128
|
+
issuedAt,
|
|
1129
|
+
expiresAt: issuedAt + APPROVAL_TOKEN_TTL_MS
|
|
1130
|
+
});
|
|
1131
|
+
// Opportunistic cleanup of expired tokens.
|
|
1132
|
+
for (const [t, entry] of approvalTokens) {
|
|
1133
|
+
if (entry.expiresAt < issuedAt) approvalTokens.delete(t);
|
|
1134
|
+
}
|
|
1135
|
+
return token;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
function consumeApprovalToken(token, toolName) {
|
|
1139
|
+
if (!token || typeof token !== 'string') return false;
|
|
1140
|
+
const entry = approvalTokens.get(token);
|
|
1141
|
+
if (!entry) return false;
|
|
1142
|
+
approvalTokens.delete(token); // single-use regardless of validity
|
|
1143
|
+
if (entry.toolName !== toolName) return false;
|
|
1144
|
+
if (entry.expiresAt < Date.now()) return false;
|
|
1145
|
+
return true;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1116
1148
|
// Function to check if tool needs approval
|
|
1117
1149
|
function checkNeedsApproval(tool, args) {
|
|
1118
1150
|
if (!tool.needsApproval) {
|
|
1119
1151
|
return false;
|
|
1120
1152
|
}
|
|
1121
|
-
|
|
1122
|
-
// Check if this is a re-execution after approval
|
|
1153
|
+
|
|
1154
|
+
// Check if this is a re-execution after approval. The host must echo the
|
|
1155
|
+
// server-issued one-time token; bare `approved: true` is no longer trusted.
|
|
1123
1156
|
const userContext = args.__userContext;
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1157
|
+
const approval = userContext && userContext.approval;
|
|
1158
|
+
if (approval && approval.approved === true) {
|
|
1159
|
+
if (consumeApprovalToken(approval.token, tool.name)) {
|
|
1160
|
+
logger.info(`[${tool.name}] Approval token verified, executing with approved arguments`);
|
|
1161
|
+
|
|
1162
|
+
// Replace current args with user-approved/modified arguments
|
|
1163
|
+
const modifiedArgs = approval.modifiedArguments;
|
|
1164
|
+
if (modifiedArgs) {
|
|
1165
|
+
const savedUserContext = args.__userContext;
|
|
1166
|
+
Object.keys(args).forEach(key => {
|
|
1167
|
+
if (key !== '__userContext') {
|
|
1168
|
+
delete args[key];
|
|
1169
|
+
}
|
|
1170
|
+
});
|
|
1171
|
+
Object.assign(args, modifiedArgs);
|
|
1172
|
+
args.__userContext = savedUserContext;
|
|
1173
|
+
logger.info(`[${tool.name}] Using user-modified arguments:`, modifiedArgs);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
return false; // Skip approval, execute with approved args
|
|
1143
1177
|
}
|
|
1144
|
-
|
|
1145
|
-
|
|
1178
|
+
logger.warn(`[${tool.name}] Approval received without valid token (got: ${approval.token ? 'expired/mismatched' : 'missing'}); requiring re-approval`);
|
|
1179
|
+
// Fall through to issue a fresh approval_required response.
|
|
1146
1180
|
}
|
|
1147
|
-
|
|
1181
|
+
|
|
1148
1182
|
// If needsApproval is a function, evaluate it
|
|
1149
1183
|
if (typeof tool.needsApproval === 'function') {
|
|
1150
1184
|
return tool.needsApproval(args);
|
|
1151
1185
|
}
|
|
1152
|
-
|
|
1186
|
+
|
|
1153
1187
|
// If it's a boolean true, always needs approval
|
|
1154
1188
|
return tool.needsApproval === true;
|
|
1155
1189
|
}
|
|
@@ -1159,12 +1193,15 @@ function generateApprovalRequest(tool, args) {
|
|
|
1159
1193
|
const userContext = args.__userContext;
|
|
1160
1194
|
const cleanArgs = { ...args };
|
|
1161
1195
|
delete cleanArgs.__userContext;
|
|
1162
|
-
|
|
1196
|
+
const approvalToken = issueApprovalToken(tool.name);
|
|
1197
|
+
|
|
1163
1198
|
return {
|
|
1164
1199
|
type: 'approval_required',
|
|
1165
1200
|
toolName: tool.name,
|
|
1166
1201
|
toolDescription: tool.description,
|
|
1167
1202
|
arguments: cleanArgs,
|
|
1203
|
+
approvalToken,
|
|
1204
|
+
approvalTokenExpiresInSeconds: Math.floor(APPROVAL_TOKEN_TTL_MS / 1000),
|
|
1168
1205
|
approvalConfig: tool.approvalConfig || {
|
|
1169
1206
|
title: `Approve ${tool.name}`,
|
|
1170
1207
|
description: `This action requires your approval: ${tool.description}`,
|
|
@@ -1184,6 +1221,76 @@ function generateApprovalRequest(tool, args) {
|
|
|
1184
1221
|
};
|
|
1185
1222
|
}
|
|
1186
1223
|
|
|
1224
|
+
// Tool-specific deep argument validation. Catches semantic gaps that JSON Schema
|
|
1225
|
+
// `required` cannot express (e.g. nested fields inside an `object` arg).
|
|
1226
|
+
// Return shape matches validateToolLimits: {valid, errors[]}.
|
|
1227
|
+
function validateToolArgs(toolName, args) {
|
|
1228
|
+
const errors = [];
|
|
1229
|
+
|
|
1230
|
+
if (toolName === 'createProductPricing') {
|
|
1231
|
+
const pd = args.pricing_data;
|
|
1232
|
+
if (!pd || typeof pd !== 'object') {
|
|
1233
|
+
errors.push("'pricing_data' is required and must be an object containing 'pricing_type' and 'currency'.");
|
|
1234
|
+
} else {
|
|
1235
|
+
if (!pd.pricing_type) errors.push("'pricing_data.pricing_type' is required (e.g. 'per_unit', 'flat_fee', 'tiered', 'volume', 'percent', 'package').");
|
|
1236
|
+
if (!pd.currency) errors.push("'pricing_data.currency' is required (ISO 4217, e.g. 'USD').");
|
|
1237
|
+
}
|
|
1238
|
+
const q = args.quantity;
|
|
1239
|
+
if (!q || typeof q !== 'object') {
|
|
1240
|
+
errors.push("'quantity' is required and must be an object: {type:'fixed'|'metered', quantity?, unit?, aggregate_id?}. Without it the UI shows 0 for billing_metric.");
|
|
1241
|
+
} else {
|
|
1242
|
+
if (q.type !== 'fixed' && q.type !== 'metered') errors.push("'quantity.type' must be exactly 'fixed' or 'metered'.");
|
|
1243
|
+
if (q.type === 'fixed' && (q.quantity == null || !q.unit)) errors.push("'quantity.type'='fixed' requires both 'quantity' (number) and 'unit' (label string).");
|
|
1244
|
+
if (q.type === 'metered' && !q.aggregate_id) errors.push("'quantity.type'='metered' requires 'aggregate_id' (UUID of the billable metric).");
|
|
1245
|
+
}
|
|
1246
|
+
const bp = args.billing_period;
|
|
1247
|
+
if (!bp || typeof bp !== 'object') {
|
|
1248
|
+
errors.push("'billing_period' is required and must be an object: {cadence:'P1M'|'P3M'|'P1Y'|..., offset:'P0D'|...}. Without it the UI renders 'Undefined- Every Undefined Undefined'.");
|
|
1249
|
+
} else {
|
|
1250
|
+
if (!bp.cadence) errors.push("'billing_period.cadence' is required (ISO-8601 duration: 'P1M'=monthly, 'P3M'=quarterly, 'P1Y'=yearly).");
|
|
1251
|
+
if (!bp.offset) errors.push("'billing_period.offset' is required (ISO-8601 duration, 'P0D' for no offset).");
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
if (toolName === 'createPlan') {
|
|
1256
|
+
if (!args.schedule || typeof args.schedule !== 'object' || !args.schedule.duration) {
|
|
1257
|
+
errors.push("'schedule' is required and must include 'duration' (ISO-8601, e.g. 'P1Y').");
|
|
1258
|
+
}
|
|
1259
|
+
if (!args.status) {
|
|
1260
|
+
errors.push("'status' is required: 'draft' | 'active' | 'archived'.");
|
|
1261
|
+
}
|
|
1262
|
+
const phases = args.phases;
|
|
1263
|
+
if (!Array.isArray(phases) || phases.length === 0) {
|
|
1264
|
+
errors.push("'phases' must be a non-empty array. A plan with no phases is unusable in PlansV2 — include at least one phase with name, schedule, order, and either features or pricings.");
|
|
1265
|
+
} else {
|
|
1266
|
+
phases.forEach((p, i) => {
|
|
1267
|
+
if (!p || typeof p !== 'object') { errors.push(`'phases[${i}]' must be an object.`); return; }
|
|
1268
|
+
if (!p.name) errors.push(`'phases[${i}].name' is required.`);
|
|
1269
|
+
if (!p.schedule || !p.schedule.duration) errors.push(`'phases[${i}].schedule.duration' is required (ISO-8601).`);
|
|
1270
|
+
if (typeof p.order !== 'number') errors.push(`'phases[${i}].order' is required (integer, 0-indexed).`);
|
|
1271
|
+
if (!p.features && (!Array.isArray(p.pricings) || p.pricings.length === 0)) {
|
|
1272
|
+
errors.push(`'phases[${i}]' has neither 'features' nor non-empty 'pricings' — phase will be empty in the UI.`);
|
|
1273
|
+
}
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
if (toolName === 'generateInvoice') {
|
|
1279
|
+
if (!Number.isInteger(args.from_date)) errors.push("'from_date' must be an INTEGER UNIX timestamp (seconds), not a date string.");
|
|
1280
|
+
if (!Number.isInteger(args.to_date)) errors.push("'to_date' must be an INTEGER UNIX timestamp (seconds), not a date string.");
|
|
1281
|
+
if (Number.isInteger(args.from_date) && Number.isInteger(args.to_date) && args.to_date <= args.from_date) {
|
|
1282
|
+
errors.push("'to_date' must be strictly greater than 'from_date'.");
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
if (toolName === 'pauseContract') {
|
|
1287
|
+
if (!args.start_date) errors.push("'start_date' is required (ISO 8601).");
|
|
1288
|
+
if (!args.unpause_extension_policy) errors.push("'unpause_extension_policy' is required: 'extend' or 'overlap'.");
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
return { valid: errors.length === 0, errors };
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1187
1294
|
// Helper to map API types to form field types
|
|
1188
1295
|
function getFieldType(apiType) {
|
|
1189
1296
|
switch (apiType) {
|
|
@@ -1229,38 +1336,62 @@ if (mcpConfig.tools && mcpConfig.tools.length > 0) {
|
|
|
1229
1336
|
approvedInArgs: args.__userContext?.approved
|
|
1230
1337
|
});
|
|
1231
1338
|
|
|
1232
|
-
//
|
|
1339
|
+
// Validate args BEFORE the approval gate so users do not approve broken calls.
|
|
1340
|
+
const earlyArgValidation = validateToolArgs(tool.name, args);
|
|
1341
|
+
if (!earlyArgValidation.valid) {
|
|
1342
|
+
logger.error(`[${tool.name}] Tool execution blocked due to invalid arguments (pre-approval):`, earlyArgValidation.errors);
|
|
1343
|
+
tokenUsageStatus = 'blocked';
|
|
1344
|
+
tokenUsageReason = `invalid_args: ${earlyArgValidation.errors.join('; ')}`;
|
|
1345
|
+
const errorJson = {
|
|
1346
|
+
type: 'invalid_arguments',
|
|
1347
|
+
toolName: tool.name,
|
|
1348
|
+
errors: earlyArgValidation.errors
|
|
1349
|
+
};
|
|
1350
|
+
return {
|
|
1351
|
+
content: [
|
|
1352
|
+
{ type: "text", text: JSON.stringify(errorJson, null, 2) },
|
|
1353
|
+
{
|
|
1354
|
+
type: "text",
|
|
1355
|
+
text: `ACTION_NOT_EXECUTED — INVALID_ARGUMENTS\n\nTool '${tool.name}' was NOT executed because the supplied arguments are invalid or incomplete:\n\n` +
|
|
1356
|
+
earlyArgValidation.errors.map((e, i) => `${i + 1}. ${e}`).join('\n') +
|
|
1357
|
+
`\n\nFix the arguments and call again. Do NOT report success to the user.`
|
|
1358
|
+
}
|
|
1359
|
+
],
|
|
1360
|
+
isError: true
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// Check if this tool needs approval and hasn't been approved yet.
|
|
1365
|
+
// checkNeedsApproval() validates the one-time token from
|
|
1366
|
+
// __userContext.approval.token; bare `approved: true` is not trusted.
|
|
1233
1367
|
const needsApproval = checkNeedsApproval(tool, args);
|
|
1234
1368
|
const userContext = args.__userContext;
|
|
1235
|
-
|
|
1236
|
-
|
|
1369
|
+
|
|
1237
1370
|
logger.info(`[${tool.name}] Approval check:`, {
|
|
1238
1371
|
needsApproval,
|
|
1239
|
-
isApproved,
|
|
1240
1372
|
hasUserContext: !!userContext,
|
|
1241
1373
|
userContextKeys: userContext ? Object.keys(userContext) : [],
|
|
1242
|
-
|
|
1243
|
-
|
|
1374
|
+
hasApprovalBlock: !!(userContext && userContext.approval),
|
|
1375
|
+
hasToken: !!(userContext && userContext.approval && userContext.approval.token)
|
|
1244
1376
|
});
|
|
1245
|
-
|
|
1246
|
-
if (needsApproval
|
|
1377
|
+
|
|
1378
|
+
if (needsApproval) {
|
|
1247
1379
|
logger.info(`[${tool.name}] Tool requires approval, generating approval request`);
|
|
1248
1380
|
const approvalRequest = generateApprovalRequest(tool, args);
|
|
1249
|
-
|
|
1250
1381
|
return {
|
|
1251
|
-
content: [
|
|
1252
|
-
type: "text",
|
|
1253
|
-
|
|
1254
|
-
|
|
1382
|
+
content: [
|
|
1383
|
+
{ type: "text", text: JSON.stringify(approvalRequest, null, 2) },
|
|
1384
|
+
{
|
|
1385
|
+
type: "text",
|
|
1386
|
+
text: `ACTION_NOT_EXECUTED — APPROVAL_REQUIRED\n\n` +
|
|
1387
|
+
`Tool '${tool.name}' was NOT executed. The host must render an approval dialog from the JSON payload above and re-invoke this tool with __userContext.approval = {approved: true, token: '<approvalToken from payload>'} to actually perform the action. The token is single-use and expires in ${Math.floor(APPROVAL_TOKEN_TTL_MS / 1000)}s. Do NOT fabricate the token. Do NOT report success to the user; surface the dialog instead.`
|
|
1388
|
+
}
|
|
1389
|
+
],
|
|
1255
1390
|
isApprovalRequired: true,
|
|
1256
1391
|
approvalRequest: approvalRequest
|
|
1257
1392
|
};
|
|
1258
1393
|
}
|
|
1259
1394
|
|
|
1260
|
-
if (needsApproval && isApproved) {
|
|
1261
|
-
logger.info(`[${tool.name}] Tool was approved, executing actual API call`);
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
1395
|
// Extract user context for token usage tracking
|
|
1265
1396
|
const userId = userContext?.userId || 'unknown';
|
|
1266
1397
|
const chatId = userContext?.chatId || null; // Use NULL for direct MCP calls
|
|
@@ -1311,6 +1442,8 @@ if (mcpConfig.tools && mcpConfig.tools.length > 0) {
|
|
|
1311
1442
|
};
|
|
1312
1443
|
}
|
|
1313
1444
|
|
|
1445
|
+
// (validateToolArgs already ran pre-approval; no duplicate check here.)
|
|
1446
|
+
|
|
1314
1447
|
// Use adjusted args with enforced limits
|
|
1315
1448
|
const adjustedArgs = limitsValidation.adjustedArgs;
|
|
1316
1449
|
|