mcp-zenskar 1.1.13 → 1.1.15
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 +50 -5
- package/src/server.js +179 -45
- package/src/ui/shapes/app.html +12 -0
package/package.json
CHANGED
package/src/mcp-config.json
CHANGED
|
@@ -2921,6 +2921,37 @@
|
|
|
2921
2921
|
"prependBody": "## Contract Amendments\n\n"
|
|
2922
2922
|
}
|
|
2923
2923
|
},
|
|
2924
|
+
{
|
|
2925
|
+
"name": "getContractBillingCycles",
|
|
2926
|
+
"description": "Returns the contract's pre-computed billing cycles as a list of {name, start_date, end_date, bill_for_date, billing_cycle_start_day}. REQUIRED before calling generateInvoice. Workflow: (1) call this tool, (2) pick the cycle entry whose date range covers the period you want to invoice, (3) pass that entry's `start_date` → `from_date`, `end_date` → `to_date`, `bill_for_date`, `billing_cycle_start_day` to generateInvoice — copy the values verbatim, no math. The backend matches `bill_for_date` exactly against this list; arbitrary dates produce empty invoice arrays.",
|
|
2927
|
+
"args": [
|
|
2928
|
+
{
|
|
2929
|
+
"name": "contractId",
|
|
2930
|
+
"description": "The unique identifier (UUID) of the contract.",
|
|
2931
|
+
"type": "string",
|
|
2932
|
+
"required": true,
|
|
2933
|
+
"position": "path"
|
|
2934
|
+
},
|
|
2935
|
+
{
|
|
2936
|
+
"name": "limit",
|
|
2937
|
+
"description": "Maximum number of cycles to return (default 10). Increase if the contract spans many years.",
|
|
2938
|
+
"type": "integer",
|
|
2939
|
+
"required": false,
|
|
2940
|
+
"default": 10,
|
|
2941
|
+
"position": "query"
|
|
2942
|
+
}
|
|
2943
|
+
],
|
|
2944
|
+
"requestTemplate": {
|
|
2945
|
+
"url": "/contracts/{contractId}/billingCycles",
|
|
2946
|
+
"method": "GET",
|
|
2947
|
+
"headers": {
|
|
2948
|
+
"Content-Type": "application/json"
|
|
2949
|
+
}
|
|
2950
|
+
},
|
|
2951
|
+
"responseTemplate": {
|
|
2952
|
+
"prependBody": "## Contract Billing Cycles\n\n"
|
|
2953
|
+
}
|
|
2954
|
+
},
|
|
2924
2955
|
{
|
|
2925
2956
|
"name": "createContractPhase",
|
|
2926
2957
|
"description": "Add a new phase to an existing contract. Use this for add-on expansions, mid-contract changes, or creating distinct billing periods with different pricing. Phase type enum: active, pause, trial.",
|
|
@@ -3261,7 +3292,7 @@
|
|
|
3261
3292
|
},
|
|
3262
3293
|
{
|
|
3263
3294
|
"name": "generateInvoice",
|
|
3264
|
-
"description": "
|
|
3295
|
+
"description": "Generate an invoice for a single contract+customer over a billing window.\n\nGranularity 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.\n\nMANDATORY PRE-STEP: call `getContractBillingCycles` first to fetch the list of valid {start_date, end_date, bill_for_date, billing_cycle_start_day} entries for this contract. Pick ONE entry. Copy its `start_date` → `from_date`, `end_date` → `to_date`, `bill_for_date`, `billing_cycle_start_day` verbatim. DO NOT compute these from contract.start_date, pricing.billing_period, or human intuition — the backend matches `bill_for_date` exactly against the pre-computed list.\n\nDate fields accept ISO-8601 strings (e.g. `\"2026-04-28T00:00:00Z\"`) or integer UNIX seconds. Prefer pasting ISO strings verbatim from getContractBillingCycles output — the server converts to unix internally before sending to the API.\n\nKnown failure modes:\n • `{invoice_ids: []}` → `bill_for_date` did not match any cycle. Re-fetch billingCycles and copy the exact value.\n • $0 invoice → bill_for_date/billing_cycle_start_day omitted entirely.\n • InvoiceAlreadyGeneratedForThisBillingPeriod → an approved invoice already exists for this cycle.",
|
|
3265
3296
|
"args": [
|
|
3266
3297
|
{
|
|
3267
3298
|
"name": "contract_id",
|
|
@@ -3279,15 +3310,29 @@
|
|
|
3279
3310
|
},
|
|
3280
3311
|
{
|
|
3281
3312
|
"name": "from_date",
|
|
3282
|
-
"description": "Start
|
|
3283
|
-
"type": "
|
|
3313
|
+
"description": "Start of billing period. Accepts ISO-8601 string (e.g. `2026-04-28T00:00:00Z`) or UNIX seconds integer. PREFER ISO string copied verbatim from getContractBillingCycles.start_date — avoids timezone bugs.",
|
|
3314
|
+
"type": "datetime",
|
|
3284
3315
|
"required": true,
|
|
3285
3316
|
"position": "body"
|
|
3286
3317
|
},
|
|
3287
3318
|
{
|
|
3288
3319
|
"name": "to_date",
|
|
3289
|
-
"description": "End
|
|
3290
|
-
"type": "
|
|
3320
|
+
"description": "End of billing period. Accepts ISO-8601 string or UNIX seconds integer. PREFER ISO string copied verbatim from getContractBillingCycles.end_date.",
|
|
3321
|
+
"type": "datetime",
|
|
3322
|
+
"required": true,
|
|
3323
|
+
"position": "body"
|
|
3324
|
+
},
|
|
3325
|
+
{
|
|
3326
|
+
"name": "bill_for_date",
|
|
3327
|
+
"description": "REQUIRED. Anchors which billing cycle this invoice belongs to. Accepts ISO-8601 string or UNIX seconds. MUST match exactly one of the `bill_for_date` values returned by getContractBillingCycles — pasting the ISO string verbatim is the safest path. Arbitrary dates produce `invoice_ids: []`.",
|
|
3328
|
+
"type": "datetime",
|
|
3329
|
+
"required": true,
|
|
3330
|
+
"position": "body"
|
|
3331
|
+
},
|
|
3332
|
+
{
|
|
3333
|
+
"name": "billing_cycle_start_day",
|
|
3334
|
+
"description": "REQUIRED. Day of month (1-31) anchoring the recurring billing cycle. Fetch from the contract or plan's billing_period; if unsure, use the day-of-month of contract.start_date. Omitting this causes the API to return a $0 invoice because no billing cycle matches.",
|
|
3335
|
+
"type": "integer",
|
|
3291
3336
|
"required": true,
|
|
3292
3337
|
"position": "body"
|
|
3293
3338
|
}
|
package/src/server.js
CHANGED
|
@@ -683,6 +683,11 @@ function convertArgsToZodSchema(args) {
|
|
|
683
683
|
zodType = z.record(z.any());
|
|
684
684
|
} else if (arg.type === 'array') {
|
|
685
685
|
zodType = z.array(z.any());
|
|
686
|
+
} else if (arg.type === 'datetime') {
|
|
687
|
+
// Accept ISO-8601 string or numeric unix seconds. Backend Pydantic
|
|
688
|
+
// datetime fields parse both. Lets the LLM copy ISO strings directly
|
|
689
|
+
// from getContractBillingCycles output instead of converting to unix.
|
|
690
|
+
zodType = z.union([z.string(), z.number()]);
|
|
686
691
|
} else {
|
|
687
692
|
zodType = z.string();
|
|
688
693
|
}
|
|
@@ -713,6 +718,10 @@ function convertArgsToZodSchema(args) {
|
|
|
713
718
|
// Add approval support for human-in-the-loop workflow
|
|
714
719
|
approval: z.object({
|
|
715
720
|
approved: z.boolean(),
|
|
721
|
+
// Single-use token issued by server in the approval_required response.
|
|
722
|
+
// Without this in the schema, Zod strips it from __userContext.approval
|
|
723
|
+
// and consumeApprovalToken() always sees undefined → infinite re-approval loop.
|
|
724
|
+
token: z.string().optional(),
|
|
716
725
|
modifiedArguments: z.record(z.any()).optional(),
|
|
717
726
|
originalArguments: z.record(z.any()).optional(),
|
|
718
727
|
toolName: z.string().optional()
|
|
@@ -829,9 +838,26 @@ async function executeAPICall(tool, args) {
|
|
|
829
838
|
// Build request body
|
|
830
839
|
let body = null;
|
|
831
840
|
if (method !== 'GET' && method !== 'DELETE' && tool.args) {
|
|
841
|
+
// Pydantic datetime fields on the Zenskar backend accept both ISO strings
|
|
842
|
+
// and unix int — but downstream code paths assume naive UTC datetimes
|
|
843
|
+
// (.astimezone() on aware datetimes raises). The UI normalises everything
|
|
844
|
+
// to unix int via dayjs.utc(...).unix(); we do the same here so the LLM
|
|
845
|
+
// can pass either form without producing a 500.
|
|
846
|
+
const coerceDatetime = (v) => {
|
|
847
|
+
if (typeof v === 'number') return Number.isFinite(v) ? Math.floor(v) : v;
|
|
848
|
+
if (typeof v === 'string') {
|
|
849
|
+
const t = Date.parse(v);
|
|
850
|
+
return Number.isFinite(t) ? Math.floor(t / 1000) : v;
|
|
851
|
+
}
|
|
852
|
+
return v;
|
|
853
|
+
};
|
|
832
854
|
const bodyParams = {};
|
|
833
855
|
tool.args.forEach(arg => {
|
|
834
856
|
if (arg.position === 'body' && cleanArgs[arg.name] !== undefined) {
|
|
857
|
+
if (arg.type === 'datetime') {
|
|
858
|
+
bodyParams[arg.name] = coerceDatetime(cleanArgs[arg.name]);
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
835
861
|
if (tool.name === 'ingestRawMetricEvent' && arg.name === 'event') {
|
|
836
862
|
const eventPayload = normalizeUsageEventPayload(cleanArgs[arg.name]);
|
|
837
863
|
if (eventPayload && typeof eventPayload === 'object' && !Array.isArray(eventPayload)) {
|
|
@@ -992,10 +1018,19 @@ async function executeAPICall(tool, args) {
|
|
|
992
1018
|
throw new Error('Authorization is required. Set ZENSKAR_AUTH_TOKEN (JWT) or ZENSKAR_API_KEY env var.');
|
|
993
1019
|
}
|
|
994
1020
|
|
|
995
|
-
// Add any other headers from user context
|
|
1021
|
+
// Add any other headers from user context (case-insensitive collision check).
|
|
1022
|
+
// Object keys cannot duplicate across casings within the same object, so the
|
|
1023
|
+
// Set only needs to be built once from the existing `headers`.
|
|
1024
|
+
// Filter prototype-pollution keys: `userContext.headers` may originate from
|
|
1025
|
+
// a JSON payload where an attacker could set `__proto__` to mutate the
|
|
1026
|
+
// local `headers` prototype.
|
|
996
1027
|
if (userContext?.headers) {
|
|
1028
|
+
const existingLower = new Set(Object.keys(headers).map(k => k.toLowerCase()));
|
|
1029
|
+
const POISON_KEYS = new Set(['__proto__', 'prototype', 'constructor']);
|
|
997
1030
|
Object.keys(userContext.headers).forEach(key => {
|
|
998
|
-
if (
|
|
1031
|
+
if (POISON_KEYS.has(key)) return;
|
|
1032
|
+
if (!Object.prototype.hasOwnProperty.call(userContext.headers, key)) return;
|
|
1033
|
+
if (userContext.headers[key] && !existingLower.has(key.toLowerCase())) {
|
|
999
1034
|
headers[key] = userContext.headers[key];
|
|
1000
1035
|
}
|
|
1001
1036
|
});
|
|
@@ -1118,73 +1153,131 @@ async function executeAPICall(tool, args) {
|
|
|
1118
1153
|
// scenarios where an LLM fabricates `approval.approved=true` to bypass the dialog.
|
|
1119
1154
|
// Tokens are single-use and expire after 5 minutes.
|
|
1120
1155
|
const APPROVAL_TOKEN_TTL_MS = 5 * 60 * 1000;
|
|
1121
|
-
const
|
|
1156
|
+
const APPROVAL_TOKEN_CLEANUP_MS = 60 * 1000;
|
|
1157
|
+
const approvalTokens = new Map(); // token -> {toolName, issuedAt, expiresAt, args}
|
|
1158
|
+
|
|
1159
|
+
// Deep clone via structuredClone when available, JSON fallback otherwise.
|
|
1160
|
+
// Approval args may include nested objects (addresses, pricing_data) that
|
|
1161
|
+
// executeAPICall mutates downstream; a shallow spread would share refs.
|
|
1162
|
+
function deepCloneArgs(value) {
|
|
1163
|
+
if (value == null) return value;
|
|
1164
|
+
if (typeof structuredClone === 'function') {
|
|
1165
|
+
try { return structuredClone(value); } catch (_) { /* fall through to JSON */ }
|
|
1166
|
+
}
|
|
1167
|
+
return JSON.parse(JSON.stringify(value));
|
|
1168
|
+
}
|
|
1122
1169
|
|
|
1123
|
-
function issueApprovalToken(toolName) {
|
|
1170
|
+
function issueApprovalToken(toolName, args) {
|
|
1124
1171
|
const token = uuidv4();
|
|
1125
1172
|
const issuedAt = Date.now();
|
|
1173
|
+
// Snapshot args at issue time so the second invocation can restore them
|
|
1174
|
+
// even when the host does not echo them at the top level (e.g. Claude
|
|
1175
|
+
// Desktop's approval dialog only echoes __userContext.approval).
|
|
1176
|
+
// Note: `args` here is the initial tool invocation. generateApprovalRequest
|
|
1177
|
+
// only runs when checkNeedsApproval returned true, which means there was no
|
|
1178
|
+
// valid approval block. We still strip __userContext defensively in case a
|
|
1179
|
+
// host replays a stale approval block alongside fresh top-level args.
|
|
1180
|
+
const snapshot = args ? deepCloneArgs(args) : {};
|
|
1181
|
+
delete snapshot.__userContext;
|
|
1126
1182
|
approvalTokens.set(token, {
|
|
1127
1183
|
toolName,
|
|
1128
1184
|
issuedAt,
|
|
1129
|
-
expiresAt: issuedAt + APPROVAL_TOKEN_TTL_MS
|
|
1185
|
+
expiresAt: issuedAt + APPROVAL_TOKEN_TTL_MS,
|
|
1186
|
+
args: snapshot
|
|
1130
1187
|
});
|
|
1131
|
-
// Opportunistic cleanup of expired tokens.
|
|
1132
|
-
for (const [t, entry] of approvalTokens) {
|
|
1133
|
-
if (entry.expiresAt < issuedAt) approvalTokens.delete(t);
|
|
1134
|
-
}
|
|
1135
1188
|
return token;
|
|
1136
1189
|
}
|
|
1137
1190
|
|
|
1138
1191
|
function consumeApprovalToken(token, toolName) {
|
|
1139
|
-
if (!token || typeof token !== 'string') return
|
|
1192
|
+
if (!token || typeof token !== 'string') return null;
|
|
1140
1193
|
const entry = approvalTokens.get(token);
|
|
1141
|
-
if (!entry) return
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1194
|
+
if (!entry) return null;
|
|
1195
|
+
// Single-use: delete on first lookup regardless of toolName/expiry. A wrong
|
|
1196
|
+
// toolName here means either a replay attack or a host bug; either way the
|
|
1197
|
+
// token should not survive. Legitimate users get a fresh token via re-approval.
|
|
1198
|
+
approvalTokens.delete(token);
|
|
1199
|
+
if (entry.toolName !== toolName) return null;
|
|
1200
|
+
if (entry.expiresAt < Date.now()) return null;
|
|
1201
|
+
return entry;
|
|
1146
1202
|
}
|
|
1147
1203
|
|
|
1148
|
-
//
|
|
1204
|
+
// Periodic sweep so expired tokens (and the args they snapshot) don't linger
|
|
1205
|
+
// in memory past their TTL even when no new approval is issued.
|
|
1206
|
+
setInterval(() => {
|
|
1207
|
+
const now = Date.now();
|
|
1208
|
+
for (const [t, entry] of approvalTokens) {
|
|
1209
|
+
if (entry.expiresAt < now) approvalTokens.delete(t);
|
|
1210
|
+
}
|
|
1211
|
+
}, APPROVAL_TOKEN_CLEANUP_MS).unref();
|
|
1212
|
+
|
|
1213
|
+
// Pure resolver: decides which arg source wins for an approved re-invoke.
|
|
1214
|
+
// Returns { args, source } where args is the object to apply (always non-null
|
|
1215
|
+
// for valid tokens), and source names the tier that won.
|
|
1216
|
+
//
|
|
1217
|
+
// SECURITY MODEL: the token snapshot stored at issue time is the canonical
|
|
1218
|
+
// record of what the user approved. We DO NOT trust top-level args on a
|
|
1219
|
+
// re-invoke — a malicious LLM holding a valid token could otherwise swap
|
|
1220
|
+
// in arbitrary path/body params (e.g. a different invoiceId) and still pass
|
|
1221
|
+
// the gate. The only host-supplied override accepted is
|
|
1222
|
+
// `approval.modifiedArguments`, which represents user edits performed inside
|
|
1223
|
+
// the approval dialog. Those edits MUST be re-validated by the caller
|
|
1224
|
+
// against the tool schema before the API call is executed.
|
|
1225
|
+
function resolveApprovedArgs(approval, tokenEntry) {
|
|
1226
|
+
const nonEmptyObj = (obj) =>
|
|
1227
|
+
obj && typeof obj === 'object' && !Array.isArray(obj) && Object.keys(obj).length > 0;
|
|
1228
|
+
|
|
1229
|
+
if (nonEmptyObj(approval.modifiedArguments)) {
|
|
1230
|
+
return { args: approval.modifiedArguments, source: 'modifiedArguments' };
|
|
1231
|
+
}
|
|
1232
|
+
return { args: tokenEntry.args, source: 'tokenSnapshot' };
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// Replace `args` contents with `resolvedArgs` while preserving __userContext.
|
|
1236
|
+
// Kept separate from the resolver so the decision logic stays pure/testable.
|
|
1237
|
+
function applyResolvedArgs(args, resolvedArgs) {
|
|
1238
|
+
const savedUserContext = args.__userContext;
|
|
1239
|
+
Object.keys(args).forEach(key => {
|
|
1240
|
+
if (key !== '__userContext') delete args[key];
|
|
1241
|
+
});
|
|
1242
|
+
Object.assign(args, resolvedArgs);
|
|
1243
|
+
args.__userContext = savedUserContext;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// Decides whether `tool` still needs approval given the current `args`.
|
|
1247
|
+
// As a side effect, when a valid approval token is present this function
|
|
1248
|
+
// also restores path/body params onto `args` from the resolver's chosen
|
|
1249
|
+
// tier. The mutation is deliberate — the MCP SDK passes `args` by
|
|
1250
|
+
// reference, and downstream callers (validateToolLimits, executeAPICall)
|
|
1251
|
+
// read from the same object. Argument resolution is itself pure, see
|
|
1252
|
+
// resolveApprovedArgs above.
|
|
1149
1253
|
function checkNeedsApproval(tool, args) {
|
|
1150
1254
|
if (!tool.needsApproval) {
|
|
1151
1255
|
return false;
|
|
1152
1256
|
}
|
|
1153
1257
|
|
|
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.
|
|
1156
1258
|
const userContext = args.__userContext;
|
|
1157
1259
|
const approval = userContext && userContext.approval;
|
|
1158
1260
|
if (approval && approval.approved === true) {
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
//
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
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
|
|
1261
|
+
const tokenEntry = consumeApprovalToken(approval.token, tool.name);
|
|
1262
|
+
if (tokenEntry) {
|
|
1263
|
+
// Snapshot is canonical (see resolveApprovedArgs). The only host-supplied
|
|
1264
|
+
// override accepted is `approval.modifiedArguments` (user edits from the
|
|
1265
|
+
// approval dialog); those are re-validated against the tool schema in
|
|
1266
|
+
// the caller. Top-level args on the re-invoke are ignored to prevent a
|
|
1267
|
+
// valid token from acting as a blank check on path/body params.
|
|
1268
|
+
const { args: resolvedArgs, source } = resolveApprovedArgs(approval, tokenEntry);
|
|
1269
|
+
logger.info(`[${tool.name}] Approval token verified; arg source=${source}`);
|
|
1270
|
+
applyResolvedArgs(args, resolvedArgs);
|
|
1271
|
+
logger.info(`[${tool.name}] Restored arguments for approved execution:`, Object.keys(resolvedArgs));
|
|
1272
|
+
return false;
|
|
1177
1273
|
}
|
|
1178
1274
|
logger.warn(`[${tool.name}] Approval received without valid token (got: ${approval.token ? 'expired/mismatched' : 'missing'}); requiring re-approval`);
|
|
1179
1275
|
// Fall through to issue a fresh approval_required response.
|
|
1180
1276
|
}
|
|
1181
1277
|
|
|
1182
|
-
// If needsApproval is a function, evaluate it
|
|
1183
1278
|
if (typeof tool.needsApproval === 'function') {
|
|
1184
1279
|
return tool.needsApproval(args);
|
|
1185
1280
|
}
|
|
1186
|
-
|
|
1187
|
-
// If it's a boolean true, always needs approval
|
|
1188
1281
|
return tool.needsApproval === true;
|
|
1189
1282
|
}
|
|
1190
1283
|
|
|
@@ -1193,7 +1286,7 @@ function generateApprovalRequest(tool, args) {
|
|
|
1193
1286
|
const userContext = args.__userContext;
|
|
1194
1287
|
const cleanArgs = { ...args };
|
|
1195
1288
|
delete cleanArgs.__userContext;
|
|
1196
|
-
const approvalToken = issueApprovalToken(tool.name);
|
|
1289
|
+
const approvalToken = issueApprovalToken(tool.name, args);
|
|
1197
1290
|
|
|
1198
1291
|
return {
|
|
1199
1292
|
type: 'approval_required',
|
|
@@ -1276,11 +1369,27 @@ function validateToolArgs(toolName, args) {
|
|
|
1276
1369
|
}
|
|
1277
1370
|
|
|
1278
1371
|
if (toolName === 'generateInvoice') {
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1372
|
+
const toUnix = (v) => {
|
|
1373
|
+
if (typeof v === 'number') return Number.isInteger(v) ? v : null;
|
|
1374
|
+
if (typeof v === 'string') {
|
|
1375
|
+
const t = Date.parse(v);
|
|
1376
|
+
return Number.isFinite(t) ? Math.floor(t / 1000) : null;
|
|
1377
|
+
}
|
|
1378
|
+
return null;
|
|
1379
|
+
};
|
|
1380
|
+
const from = toUnix(args.from_date);
|
|
1381
|
+
const to = toUnix(args.to_date);
|
|
1382
|
+
if (from === null) errors.push("'from_date' must be either an integer UNIX timestamp (seconds) or an ISO-8601 datetime string. Copy from getContractBillingCycles.start_date.");
|
|
1383
|
+
if (to === null) errors.push("'to_date' must be either an integer UNIX timestamp (seconds) or an ISO-8601 datetime string. Copy from getContractBillingCycles.end_date.");
|
|
1384
|
+
if (from !== null && to !== null && to <= from) {
|
|
1282
1385
|
errors.push("'to_date' must be strictly greater than 'from_date'.");
|
|
1283
1386
|
}
|
|
1387
|
+
if (toUnix(args.bill_for_date) === null) {
|
|
1388
|
+
errors.push("'bill_for_date' is REQUIRED (UNIX seconds or ISO-8601 string). Without it the API returns a $0 invoice. Copy from getContractBillingCycles.bill_for_date — do NOT compute.");
|
|
1389
|
+
}
|
|
1390
|
+
if (!Number.isInteger(args.billing_cycle_start_day) || args.billing_cycle_start_day < 1 || args.billing_cycle_start_day > 31) {
|
|
1391
|
+
errors.push("'billing_cycle_start_day' is REQUIRED and must be an integer 1-31. Copy from getContractBillingCycles.billing_cycle_start_day.");
|
|
1392
|
+
}
|
|
1284
1393
|
}
|
|
1285
1394
|
|
|
1286
1395
|
if (toolName === 'pauseContract') {
|
|
@@ -1391,7 +1500,31 @@ if (mcpConfig.tools && mcpConfig.tools.length > 0) {
|
|
|
1391
1500
|
approvalRequest: approvalRequest
|
|
1392
1501
|
};
|
|
1393
1502
|
}
|
|
1394
|
-
|
|
1503
|
+
|
|
1504
|
+
// Re-validate after the approval gate. checkNeedsApproval may have
|
|
1505
|
+
// swapped path/body params with user-supplied `modifiedArguments`
|
|
1506
|
+
// (host dialog edits), which were not part of the pre-approval
|
|
1507
|
+
// validation pass. Bypass attempts via crafted modifiedArguments
|
|
1508
|
+
// get caught here.
|
|
1509
|
+
const postApprovalValidation = validateToolArgs(tool.name, args);
|
|
1510
|
+
if (!postApprovalValidation.valid) {
|
|
1511
|
+
logger.error(`[${tool.name}] Approved arguments failed validation:`, postApprovalValidation.errors);
|
|
1512
|
+
tokenUsageStatus = 'blocked';
|
|
1513
|
+
tokenUsageReason = `invalid_args_post_approval: ${postApprovalValidation.errors.join('; ')}`;
|
|
1514
|
+
return {
|
|
1515
|
+
content: [
|
|
1516
|
+
{ type: "text", text: JSON.stringify({ type: 'invalid_arguments', toolName: tool.name, errors: postApprovalValidation.errors }, null, 2) },
|
|
1517
|
+
{
|
|
1518
|
+
type: "text",
|
|
1519
|
+
text: `ACTION_NOT_EXECUTED — INVALID_ARGUMENTS\n\nApproved arguments failed validation. The user-edited values from the approval dialog do not satisfy the tool schema:\n\n` +
|
|
1520
|
+
postApprovalValidation.errors.map((e, i) => `${i + 1}. ${e}`).join('\n') +
|
|
1521
|
+
`\n\nRe-issue the call with corrected arguments; a new approval gate will be required.`
|
|
1522
|
+
}
|
|
1523
|
+
],
|
|
1524
|
+
isError: true
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1395
1528
|
// Extract user context for token usage tracking
|
|
1396
1529
|
const userId = userContext?.userId || 'unknown';
|
|
1397
1530
|
const chatId = userContext?.chatId || null; // Use NULL for direct MCP calls
|
|
@@ -1442,7 +1575,8 @@ if (mcpConfig.tools && mcpConfig.tools.length > 0) {
|
|
|
1442
1575
|
};
|
|
1443
1576
|
}
|
|
1444
1577
|
|
|
1445
|
-
// (validateToolArgs
|
|
1578
|
+
// (validateToolArgs ran pre-approval; post-approval re-validation
|
|
1579
|
+
// ran immediately after the approval gate. No third check here.)
|
|
1446
1580
|
|
|
1447
1581
|
// Use adjusted args with enforced limits
|
|
1448
1582
|
const adjustedArgs = limitsValidation.adjustedArgs;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>app</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="./app.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|