mcp-zenskar 1.1.11 → 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/package.json +1 -1
- package/src/mcp-config.json +40 -56
- package/src/server.js +176 -43
package/package.json
CHANGED
package/src/mcp-config.json
CHANGED
|
@@ -586,7 +586,7 @@
|
|
|
586
586
|
},
|
|
587
587
|
{
|
|
588
588
|
"name": "payInvoice",
|
|
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.",
|
|
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
590
|
"needsApproval": true,
|
|
591
591
|
"approvalConfig": {
|
|
592
592
|
"title": "Pay Invoice",
|
|
@@ -976,7 +976,7 @@
|
|
|
976
976
|
},
|
|
977
977
|
{
|
|
978
978
|
"name": "createRawMetric",
|
|
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.",
|
|
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.",
|
|
980
980
|
"needsApproval": true,
|
|
981
981
|
"approvalConfig": {
|
|
982
982
|
"title": "Create Usage Event Schema",
|
|
@@ -1840,7 +1840,7 @@
|
|
|
1840
1840
|
},
|
|
1841
1841
|
{
|
|
1842
1842
|
"name": "createEntitlement",
|
|
1843
|
-
"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.",
|
|
1844
1844
|
"needsApproval": true,
|
|
1845
1845
|
"approvalConfig": {
|
|
1846
1846
|
"title": "Create New Entitlement",
|
|
@@ -1917,7 +1917,7 @@
|
|
|
1917
1917
|
},
|
|
1918
1918
|
{
|
|
1919
1919
|
"name": "approveInvoice",
|
|
1920
|
-
"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.",
|
|
1921
1921
|
"needsApproval": true,
|
|
1922
1922
|
"approvalConfig": {
|
|
1923
1923
|
"title": "Approve Invoice",
|
|
@@ -1969,7 +1969,7 @@
|
|
|
1969
1969
|
},
|
|
1970
1970
|
{
|
|
1971
1971
|
"name": "recogniseRevenue",
|
|
1972
|
-
"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.",
|
|
1973
1973
|
"needsApproval": true,
|
|
1974
1974
|
"approvalConfig": {
|
|
1975
1975
|
"title": "Recognise Revenue",
|
|
@@ -2003,7 +2003,7 @@
|
|
|
2003
2003
|
},
|
|
2004
2004
|
{
|
|
2005
2005
|
"name": "createBusinessEntity",
|
|
2006
|
-
"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.",
|
|
2007
2007
|
"needsApproval": true,
|
|
2008
2008
|
"approvalConfig": {
|
|
2009
2009
|
"title": "Create New Business Entity",
|
|
@@ -2316,7 +2316,7 @@
|
|
|
2316
2316
|
},
|
|
2317
2317
|
{
|
|
2318
2318
|
"name": "ingestRawMetricEvent",
|
|
2319
|
-
"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.",
|
|
2320
2320
|
"needsApproval": true,
|
|
2321
2321
|
"approvalConfig": {
|
|
2322
2322
|
"title": "Ingest Usage Event",
|
|
@@ -2730,7 +2730,7 @@
|
|
|
2730
2730
|
},
|
|
2731
2731
|
{
|
|
2732
2732
|
"name": "deleteContract",
|
|
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.",
|
|
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
2734
|
"needsApproval": true,
|
|
2735
2735
|
"approvalConfig": {
|
|
2736
2736
|
"title": "Delete Draft Contract",
|
|
@@ -2978,7 +2978,7 @@
|
|
|
2978
2978
|
},
|
|
2979
2979
|
{
|
|
2980
2980
|
"name": "expireContract",
|
|
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.",
|
|
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
2982
|
"needsApproval": true,
|
|
2983
2983
|
"approvalConfig": {
|
|
2984
2984
|
"title": "Expire Contract",
|
|
@@ -3020,7 +3020,7 @@
|
|
|
3020
3020
|
},
|
|
3021
3021
|
{
|
|
3022
3022
|
"name": "voidInvoice",
|
|
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.",
|
|
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
3024
|
"needsApproval": true,
|
|
3025
3025
|
"approvalConfig": {
|
|
3026
3026
|
"title": "Void Invoice",
|
|
@@ -3054,7 +3054,7 @@
|
|
|
3054
3054
|
},
|
|
3055
3055
|
{
|
|
3056
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.",
|
|
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
3058
|
"needsApproval": true,
|
|
3059
3059
|
"approvalConfig": {
|
|
3060
3060
|
"title": "Delete Invoice",
|
|
@@ -3125,7 +3125,7 @@
|
|
|
3125
3125
|
},
|
|
3126
3126
|
{
|
|
3127
3127
|
"name": "generateInvoice",
|
|
3128
|
-
"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.",
|
|
3129
3129
|
"args": [
|
|
3130
3130
|
{
|
|
3131
3131
|
"name": "contract_id",
|
|
@@ -3381,7 +3381,7 @@
|
|
|
3381
3381
|
},
|
|
3382
3382
|
{
|
|
3383
3383
|
"name": "deleteManualPayment",
|
|
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.",
|
|
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
3385
|
"needsApproval": true,
|
|
3386
3386
|
"approvalConfig": {
|
|
3387
3387
|
"title": "Delete Manual Payment",
|
|
@@ -4216,7 +4216,7 @@
|
|
|
4216
4216
|
},
|
|
4217
4217
|
{
|
|
4218
4218
|
"name": "createProductPricing",
|
|
4219
|
-
"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.",
|
|
4220
4220
|
"args": [
|
|
4221
4221
|
{
|
|
4222
4222
|
"name": "productId",
|
|
@@ -4248,16 +4248,16 @@
|
|
|
4248
4248
|
},
|
|
4249
4249
|
{
|
|
4250
4250
|
"name": "quantity",
|
|
4251
|
-
"description": "Quantity configuration (REQUIRED
|
|
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.",
|
|
4252
4252
|
"type": "object",
|
|
4253
|
-
"required":
|
|
4253
|
+
"required": true,
|
|
4254
4254
|
"position": "body"
|
|
4255
4255
|
},
|
|
4256
4256
|
{
|
|
4257
4257
|
"name": "billing_period",
|
|
4258
|
-
"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'.",
|
|
4259
4259
|
"type": "object",
|
|
4260
|
-
"required":
|
|
4260
|
+
"required": true,
|
|
4261
4261
|
"position": "body"
|
|
4262
4262
|
},
|
|
4263
4263
|
{
|
|
@@ -4355,62 +4355,46 @@
|
|
|
4355
4355
|
},
|
|
4356
4356
|
{
|
|
4357
4357
|
"name": "createPlan",
|
|
4358
|
-
"description": "Create a new plan — a reusable contract template with
|
|
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.",
|
|
4359
4359
|
"args": [
|
|
4360
4360
|
{
|
|
4361
4361
|
"name": "name",
|
|
4362
|
-
"description": "Name of the plan.",
|
|
4362
|
+
"description": "Name of the plan (required).",
|
|
4363
4363
|
"type": "string",
|
|
4364
4364
|
"required": true,
|
|
4365
4365
|
"position": "body"
|
|
4366
4366
|
},
|
|
4367
4367
|
{
|
|
4368
|
-
"name": "
|
|
4369
|
-
"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.",
|
|
4370
4370
|
"type": "string",
|
|
4371
4371
|
"required": true,
|
|
4372
|
-
"position": "body"
|
|
4372
|
+
"position": "body",
|
|
4373
|
+
"enum": [
|
|
4374
|
+
"draft",
|
|
4375
|
+
"active",
|
|
4376
|
+
"archived"
|
|
4377
|
+
]
|
|
4373
4378
|
},
|
|
4374
4379
|
{
|
|
4375
|
-
"name": "
|
|
4376
|
-
"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\"}.",
|
|
4377
4382
|
"type": "object",
|
|
4378
|
-
"required":
|
|
4383
|
+
"required": true,
|
|
4379
4384
|
"position": "body"
|
|
4380
4385
|
},
|
|
4381
4386
|
{
|
|
4382
|
-
"name": "
|
|
4383
|
-
"description": "
|
|
4387
|
+
"name": "description",
|
|
4388
|
+
"description": "Optional plan description.",
|
|
4384
4389
|
"type": "string",
|
|
4385
4390
|
"required": false,
|
|
4386
4391
|
"position": "body"
|
|
4387
4392
|
},
|
|
4388
4393
|
{
|
|
4389
|
-
"name": "
|
|
4390
|
-
"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\"}}}].",
|
|
4391
4396
|
"type": "array",
|
|
4392
|
-
"required":
|
|
4393
|
-
"position": "body"
|
|
4394
|
-
},
|
|
4395
|
-
{
|
|
4396
|
-
"name": "custom_attributes",
|
|
4397
|
-
"description": "Custom attributes as key-value pairs.",
|
|
4398
|
-
"type": "object",
|
|
4399
|
-
"required": false,
|
|
4400
|
-
"position": "body"
|
|
4401
|
-
},
|
|
4402
|
-
{
|
|
4403
|
-
"name": "status",
|
|
4404
|
-
"description": "Plan status. Default: 'draft'. Options: 'draft', 'active', 'archived'.",
|
|
4405
|
-
"type": "string",
|
|
4406
|
-
"required": false,
|
|
4407
|
-
"position": "body"
|
|
4408
|
-
},
|
|
4409
|
-
{
|
|
4410
|
-
"name": "business_entity_id",
|
|
4411
|
-
"description": "Business entity ID for invoicing.",
|
|
4412
|
-
"type": "string",
|
|
4413
|
-
"required": false,
|
|
4397
|
+
"required": true,
|
|
4414
4398
|
"position": "body"
|
|
4415
4399
|
}
|
|
4416
4400
|
],
|
|
@@ -5137,7 +5121,7 @@
|
|
|
5137
5121
|
},
|
|
5138
5122
|
{
|
|
5139
5123
|
"name": "deleteCustomer",
|
|
5140
|
-
"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.",
|
|
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.",
|
|
5141
5125
|
"needsApproval": true,
|
|
5142
5126
|
"approvalConfig": {
|
|
5143
5127
|
"title": "Delete Customer",
|
|
@@ -5171,7 +5155,7 @@
|
|
|
5171
5155
|
},
|
|
5172
5156
|
{
|
|
5173
5157
|
"name": "deleteContact",
|
|
5174
|
-
"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.",
|
|
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.",
|
|
5175
5159
|
"needsApproval": true,
|
|
5176
5160
|
"approvalConfig": {
|
|
5177
5161
|
"title": "Delete Contact",
|
|
@@ -5205,7 +5189,7 @@
|
|
|
5205
5189
|
},
|
|
5206
5190
|
{
|
|
5207
5191
|
"name": "deletePaymentMethod",
|
|
5208
|
-
"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.",
|
|
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.",
|
|
5209
5193
|
"needsApproval": true,
|
|
5210
5194
|
"approvalConfig": {
|
|
5211
5195
|
"title": "Delete Payment Method",
|
|
@@ -5343,7 +5327,7 @@
|
|
|
5343
5327
|
},
|
|
5344
5328
|
{
|
|
5345
5329
|
"name": "resumeContract",
|
|
5346
|
-
"description": "Resume an actively-running pause
|
|
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.",
|
|
5347
5331
|
"args": [
|
|
5348
5332
|
{
|
|
5349
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
|
|