isc-transforms-mcp 1.0.9 → 1.0.10

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.
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "sailpoint.isc.transforms.dateCompare.schema.json",
4
4
  "title": "SailPoint ISC Transform Schema - dateCompare",
5
- "description": "Strict schema derived from the SailPoint official Date Compare operation documentation. Compares firstDate and secondDate (ISO8601 date-time strings or the keyword 'now', or nested transforms producing those values) using operator LT/LTE/GT/GTE, and returns positiveCondition or negativeCondition.",
5
+ "description": "Strict schema for the SailPoint ISC Date Compare transform. Compares firstDate against secondDate using the specified operator and returns positiveCondition (true) or negativeCondition (false). Both date operands must be ISO8601 datetime strings, the keyword 'now', or nested transforms whose output is ISO8601.",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
8
  "required": [
@@ -12,16 +12,18 @@
12
12
  ],
13
13
  "properties": {
14
14
  "type": {
15
- "const": "dateCompare"
15
+ "const": "dateCompare",
16
+ "description": "Transform operation type. Must be exactly 'dateCompare'."
16
17
  },
17
18
  "name": {
18
19
  "type": "string",
19
- "minLength": 1
20
+ "minLength": 1,
21
+ "description": "Display name for this transform, shown in UI dropdowns and identity profile mappings."
20
22
  },
21
23
  "requiresPeriodicRefresh": {
22
24
  "type": "boolean",
23
25
  "default": false,
24
- "description": "Whether the transform logic should be reevaluated nightly as part of identity refresh. Default false."
26
+ "description": "If true, re-evaluates this transform during the nightly identity refresh cycle. RECOMMENDED when either date operand uses 'now', so time-based comparisons stay current. Default is false."
25
27
  },
26
28
  "attributes": {
27
29
  "type": "object",
@@ -35,47 +37,42 @@
35
37
  ],
36
38
  "properties": {
37
39
  "firstDate": {
38
- "$ref": "#/$defs/DateOperand"
40
+ "$ref": "#/$defs/DateOperand",
41
+ "description": "Left-hand side of the comparison. Must be an ISO8601 datetime string (with time and timezone), the keyword 'now', or a nested transform whose output is ISO8601."
39
42
  },
40
43
  "secondDate": {
41
- "$ref": "#/$defs/DateOperand"
44
+ "$ref": "#/$defs/DateOperand",
45
+ "description": "Right-hand side of the comparison. Must be an ISO8601 datetime string (with time and timezone), the keyword 'now', or a nested transform whose output is ISO8601."
42
46
  },
43
47
  "operator": {
44
48
  "type": "string",
45
- "description": "Comparison operator: LT, LTE, GT, GTE (docs list uppercase; examples show lowercase).",
46
- "enum": [
47
- "LT",
48
- "LTE",
49
- "GT",
50
- "GTE",
51
- "lt",
52
- "lte",
53
- "gt",
54
- "gte"
55
- ]
49
+ "description": "Comparison operator (SailPoint docs specify uppercase). LT = firstDate < secondDate, LTE = firstDate ≤ secondDate, GT = firstDate > secondDate, GTE = firstDate ≥ secondDate. Case-insensitive at runtime but uppercase is recommended.",
50
+ "enum": ["LT", "LTE", "GT", "GTE", "lt", "lte", "gt", "gte"]
56
51
  },
57
52
  "positiveCondition": {
58
53
  "type": "string",
59
- "description": "Value to return if the comparison evaluates to true."
54
+ "description": "String value returned when the comparison evaluates to true. Can be any string (e.g., 'active', 'true', 'legacy'). Cannot be null."
60
55
  },
61
56
  "negativeCondition": {
62
57
  "type": "string",
63
- "description": "Value to return if the comparison evaluates to false."
58
+ "description": "String value returned when the comparison evaluates to false. Can be any string (e.g., 'inactive', 'false', 'regular'). Cannot be null."
64
59
  }
65
60
  }
66
61
  }
67
62
  },
68
63
  "$defs": {
69
64
  "DateOperand": {
70
- "description": "A date operand: ISO8601 date-time string, the keyword 'now', or a nested transform returning an ISO8601 value.",
65
+ "description": "A date operand: the keyword 'now' (evaluated at runtime), a full ISO8601 datetime string (must include time and timezone), or a nested transform object whose output is an ISO8601 datetime string.",
71
66
  "oneOf": [
72
67
  {
73
68
  "type": "string",
74
- "const": "now"
69
+ "const": "now",
70
+ "description": "The special keyword 'now', evaluated to the current datetime at transform execution. Must be lowercase."
75
71
  },
76
72
  {
77
73
  "type": "string",
78
- "format": "date-time"
74
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}(:\\d{2}(\\.\\d{1,9})?)?(Z|[+-]\\d{2}:\\d{2})$",
75
+ "description": "ISO8601 datetime string with time and timezone. Examples: '2025-01-15T00:00:00Z', '1995-12-31T00:00:00-05:00'."
79
76
  },
80
77
  {
81
78
  "$ref": "#/$defs/NestedTransform"
@@ -84,7 +81,7 @@
84
81
  },
85
82
  "NestedTransform": {
86
83
  "type": "object",
87
- "description": "Nested transform object. In SailPoint docs, nested transforms commonly omit 'name'. Operation-specific validation should be applied by the nested transform's own schema.",
84
+ "description": "A nested transform object that produces a date value. The output must be an ISO8601 datetime string. Use a dateFormat transform with outputFormat: 'ISO8601' if the source attribute is not already in ISO8601 format. The 'name' field is optional in nested transforms per SailPoint docs.",
88
85
  "additionalProperties": false,
89
86
  "required": [
90
87
  "type",
@@ -92,18 +89,22 @@
92
89
  ],
93
90
  "properties": {
94
91
  "id": {
95
- "type": "string"
92
+ "type": "string",
93
+ "description": "Optional ID when referencing an existing saved transform."
96
94
  },
97
95
  "name": {
98
96
  "type": "string",
99
- "minLength": 1
97
+ "minLength": 1,
98
+ "description": "Optional display name for the nested transform."
100
99
  },
101
100
  "type": {
102
101
  "type": "string",
103
- "minLength": 1
102
+ "minLength": 1,
103
+ "description": "The operation type of the nested transform (e.g., 'accountAttribute', 'dateFormat', 'dateMath')."
104
104
  },
105
105
  "requiresPeriodicRefresh": {
106
- "type": "boolean"
106
+ "type": "boolean",
107
+ "description": "Whether this nested transform re-evaluates during nightly refresh."
107
108
  },
108
109
  "attributes": {
109
110
  "type": "object",
@@ -115,8 +116,9 @@
115
116
  },
116
117
  "examples": [
117
118
  {
119
+ "name": "Is Termination Date in the Future",
118
120
  "type": "dateCompare",
119
- "name": "Date Compare Transform",
121
+ "requiresPeriodicRefresh": true,
120
122
  "attributes": {
121
123
  "firstDate": {
122
124
  "type": "accountAttribute",
@@ -126,14 +128,14 @@
126
128
  }
127
129
  },
128
130
  "secondDate": "now",
129
- "operator": "gt",
131
+ "operator": "GT",
130
132
  "positiveCondition": "active",
131
133
  "negativeCondition": "terminated"
132
134
  }
133
135
  },
134
136
  {
137
+ "name": "Legacy vs Regular Employee Cutover",
135
138
  "type": "dateCompare",
136
- "name": "Date Compare Transform (Legacy Cutover)",
137
139
  "attributes": {
138
140
  "firstDate": {
139
141
  "type": "accountAttribute",
@@ -150,10 +152,28 @@
150
152
  "outputFormat": "ISO8601"
151
153
  }
152
154
  },
153
- "operator": "lte",
155
+ "operator": "LTE",
154
156
  "positiveCondition": "legacy",
155
157
  "negativeCondition": "regular"
156
158
  }
159
+ },
160
+ {
161
+ "name": "Hire Date Before Cutover Check",
162
+ "type": "dateCompare",
163
+ "requiresPeriodicRefresh": false,
164
+ "attributes": {
165
+ "firstDate": "2020-01-01T00:00:00Z",
166
+ "secondDate": {
167
+ "type": "accountAttribute",
168
+ "attributes": {
169
+ "applicationName": "corp-hr-system",
170
+ "attributeName": "HIREDATE"
171
+ }
172
+ },
173
+ "operator": "LT",
174
+ "positiveCondition": "pre-2020",
175
+ "negativeCondition": "post-2020"
176
+ }
157
177
  }
158
178
  ]
159
179
  }
@@ -20,7 +20,7 @@ const ALLOWED_ATTRS = {
20
20
  base64Encode: new Set(["input"]),
21
21
  concat: new Set(["values", "input"]),
22
22
  conditional: "open", // dynamic variable keys allowed per docs
23
- dateCompare: new Set(["firstDate", "secondDate", "operator", "positiveCondition", "negativeCondition", "input"]),
23
+ dateCompare: new Set(["firstDate", "secondDate", "operator", "positiveCondition", "negativeCondition"]),
24
24
  dateFormat: new Set(["input", "inputFormat", "outputFormat"]),
25
25
  dateMath: new Set(["expression", "input", "roundUp"]),
26
26
  decomposeDiacriticalMarks: new Set(["input"]),
@@ -523,6 +523,7 @@ function lintDateCompare(attrs) {
523
523
  const msgs = [];
524
524
  const ISO8601_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d{1,9})?)?(Z|[+-]\d{2}:\d{2})$/;
525
525
  const isNestedTransform = (v) => !!(v && typeof v === "object" && typeof v.type === "string");
526
+ let usesNow = false;
526
527
  const checkDateOperand = (field) => {
527
528
  const v = attrs?.[field];
528
529
  if (v === undefined || v === null) {
@@ -531,41 +532,83 @@ function lintDateCompare(attrs) {
531
532
  }
532
533
  if (typeof v === "string") {
533
534
  const t = v.trim();
534
- if (t.toLowerCase() === "now")
535
+ // "now" keyword — SailPoint docs show lowercase; warn if casing differs
536
+ if (t.toLowerCase() === "now") {
537
+ usesNow = true;
538
+ if (t !== "now") {
539
+ push(msgs, "warn", `${field} value '${t}' should be the lowercase keyword 'now'. ISC evaluates it case-sensitively.`, `attributes.${field}`);
540
+ }
535
541
  return;
542
+ }
543
+ // Must be a full ISO8601 datetime with time and timezone
536
544
  if (!ISO8601_RE.test(t)) {
537
- push(msgs, "error", `${field} must be an ISO8601 datetime (e.g., 2025-01-01T00:00:00Z), 'now', or a nested transform.`, `attributes.${field}`);
545
+ push(msgs, "error", `${field} string '${t}' is not valid. Must be an ISO8601 datetime with time and timezone ` +
546
+ "(e.g., '2025-01-15T00:00:00Z' or '2025-01-15T00:00:00+05:30'), the keyword 'now', or a nested transform object.", `attributes.${field}`);
538
547
  }
539
548
  return;
540
549
  }
541
- if (isNestedTransform(v))
550
+ if (isNestedTransform(v)) {
551
+ // Nested transforms must ultimately output an ISO8601 string for the comparison to work
552
+ push(msgs, "info", `${field} uses a nested '${v.type}' transform. Ensure its output is an ISO8601 datetime string. ` +
553
+ "If the source attribute is not ISO8601, wrap it with a dateFormat transform using outputFormat: 'ISO8601'.", `attributes.${field}`);
542
554
  return;
543
- push(msgs, "error", `${field} must be a string (ISO8601/'now') or a nested transform object.`, `attributes.${field}`);
555
+ }
556
+ push(msgs, "error", `${field} must be an ISO8601 datetime string, the keyword 'now', or a nested transform object with a 'type' field.`, `attributes.${field}`);
544
557
  };
545
558
  checkDateOperand("firstDate");
546
559
  checkDateOperand("secondDate");
560
+ // --- operator: required, LT / LTE / GT / GTE ---
561
+ const VALID_OPS = new Set(["LT", "LTE", "GT", "GTE"]);
562
+ const OP_SEMANTICS = {
563
+ LT: "firstDate < secondDate",
564
+ LTE: "firstDate ≤ secondDate",
565
+ GT: "firstDate > secondDate",
566
+ GTE: "firstDate ≥ secondDate",
567
+ };
547
568
  const op = attrs?.operator;
548
569
  if (!op || String(op).trim() === "") {
549
- push(msgs, "error", "operator is required.", "attributes.operator");
570
+ push(msgs, "error", "operator is required. Must be one of: LT (less than), LTE (less than or equal), GT (greater than), GTE (greater than or equal).", "attributes.operator");
550
571
  }
551
572
  else {
552
- const v = String(op).trim().toUpperCase();
553
- if (!new Set(["LT", "LTE", "GT", "GTE"]).has(v)) {
554
- push(msgs, "error", "operator must be one of: LT, LTE, GT, GTE (case-insensitive).", "attributes.operator");
573
+ const opUpper = String(op).trim().toUpperCase();
574
+ if (!VALID_OPS.has(opUpper)) {
575
+ push(msgs, "error", `operator '${op}' is not valid. Allowed values: LT, LTE, GT, GTE (case-insensitive). ` +
576
+ "LT = firstDate < secondDate, LTE = ≤, GT = >, GTE = ≥.", "attributes.operator");
577
+ }
578
+ else {
579
+ if (op !== opUpper) {
580
+ push(msgs, "warn", `operator '${op}' is accepted but SailPoint docs specify uppercase. Use '${opUpper}' for consistency.`, "attributes.operator");
581
+ }
582
+ push(msgs, "info", `operator '${opUpper}': ${OP_SEMANTICS[opUpper]}. ` +
583
+ "Returns positiveCondition when true, negativeCondition when false.", "attributes.operator");
555
584
  }
556
585
  }
586
+ // --- positiveCondition: required string ---
557
587
  if (attrs?.positiveCondition === undefined || attrs.positiveCondition === null) {
558
- push(msgs, "error", "positiveCondition is required for dateCompare.", "attributes.positiveCondition");
588
+ push(msgs, "error", "positiveCondition is required the string value returned when the date comparison evaluates to true.", "attributes.positiveCondition");
559
589
  }
560
590
  else if (typeof attrs.positiveCondition !== "string") {
561
591
  push(msgs, "error", "positiveCondition must be a string.", "attributes.positiveCondition");
562
592
  }
593
+ // --- negativeCondition: required string ---
563
594
  if (attrs?.negativeCondition === undefined || attrs.negativeCondition === null) {
564
- push(msgs, "error", "negativeCondition is required for dateCompare.", "attributes.negativeCondition");
595
+ push(msgs, "error", "negativeCondition is required the string value returned when the date comparison evaluates to false.", "attributes.negativeCondition");
565
596
  }
566
597
  else if (typeof attrs.negativeCondition !== "string") {
567
598
  push(msgs, "error", "negativeCondition must be a string.", "attributes.negativeCondition");
568
599
  }
600
+ // --- Warn if positiveCondition === negativeCondition (comparison has no effect) ---
601
+ if (typeof attrs?.positiveCondition === "string" &&
602
+ typeof attrs?.negativeCondition === "string" &&
603
+ attrs.positiveCondition === attrs.negativeCondition) {
604
+ push(msgs, "warn", `positiveCondition and negativeCondition are both '${attrs.positiveCondition}'. ` +
605
+ "The comparison result has no effect since both branches return the same value.", "attributes");
606
+ }
607
+ // --- Recommend requiresPeriodicRefresh when 'now' is used ---
608
+ if (usesNow) {
609
+ push(msgs, "info", "One or both date operands use 'now'. Set requiresPeriodicRefresh: true at the transform root level " +
610
+ "so the comparison re-evaluates during nightly identity refresh — otherwise results may become stale for active identities.", "attributes");
611
+ }
569
612
  return msgs;
570
613
  }
571
614
  // ---------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "isc-transforms-mcp",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "type": "module",
5
5
  "description": "MCP server for SailPoint Identity Security Cloud (ISC) Transform authoring — scaffold, strict lint, catalog, and safe upsert to live tenants.",
6
6
  "author": {