isc-transforms-mcp 1.0.2 → 1.0.4

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/dist/index.js CHANGED
@@ -338,6 +338,58 @@ export async function buildMcpServer(cfg) {
338
338
  });
339
339
  return { content: [{ type: "text", text: asText({ count: items.length, types: listTransformTypes(), items }) }] };
340
340
  });
341
+ // ── 7b. Full operation catalog (all ops + schemas in one call) ───────────
342
+ server.registerTool(tn("isc.transforms.operationCatalog"), {
343
+ title: "Full Transform Operation Catalog",
344
+ description: "Returns EVERYTHING the LLM needs to build any SailPoint ISC transform — all 39 operation types " +
345
+ "in a single response. For each operation: type key, title, required attributes (with types), " +
346
+ "optional attributes, attribute constraints, doc URL, scaffold example, and JSON Schema. " +
347
+ "Call this FIRST before building any transform. Use it to decide which operation(s) to use, " +
348
+ "understand what attributes are required, and see a working scaffold to start from. " +
349
+ "This eliminates the need to call catalog, getSchema, and scaffold separately. " +
350
+ "OFFLINE — no ISC tenant required.",
351
+ inputSchema: z.object({
352
+ operation_types: z
353
+ .array(z.string())
354
+ .optional()
355
+ .describe("Optional list of specific operation types to return (e.g. ['dateCompare','dateMath','conditional']). " +
356
+ "Omit to return all 39 operations."),
357
+ }),
358
+ }, async ({ operation_types }) => {
359
+ const allTypes = listTransformTypes();
360
+ const requested = operation_types && operation_types.length > 0
361
+ ? operation_types
362
+ .map((t) => toCanonicalType(t) ?? t)
363
+ .filter((t) => allTypes.includes(t))
364
+ : allTypes;
365
+ const items = requested.map((t) => {
366
+ const s = TRANSFORM_CATALOG[t];
367
+ const schema = getOperationSchema(t);
368
+ return {
369
+ type: s.type,
370
+ title: s.title,
371
+ doc_url: s.docUrl,
372
+ required_attributes: s.requiredAttributes ?? [],
373
+ attributes_optional: Boolean(s.attributesOptional),
374
+ is_rule_backed: Boolean(s.injectedAttributes),
375
+ scaffold: s.scaffold(`my-${t}-transform`),
376
+ json_schema: schema ?? null,
377
+ };
378
+ });
379
+ return {
380
+ content: [{
381
+ type: "text",
382
+ text: asText({
383
+ instruction: "Use this catalog to select the right operation type(s) for the requirement. " +
384
+ "Check required_attributes to know what you must supply. " +
385
+ "Use scaffold as your starting JSON shape. " +
386
+ "Validate and lint the result after building.",
387
+ total_operations: items.length,
388
+ operations: items,
389
+ }),
390
+ }],
391
+ };
392
+ });
341
393
  // ── 8. Get JSON Schema for an operation ──────────────────────────────────
342
394
  server.registerTool(tn("isc.transforms.getSchema"), {
343
395
  title: "Get Operation JSON Schema",
@@ -45,7 +45,7 @@ const ALLOWED_ATTRS = {
45
45
  rightPad: new Set(["length", "padding", "input"]),
46
46
  rule: "open", // rule-specific attributes vary
47
47
  split: new Set(["delimiter", "index", "input"]),
48
- static: new Set(["value"]),
48
+ static: "open", // value + any named VTL dynamic variable keys allowed per docs
49
49
  substring: new Set(["begin", "end", "input"]),
50
50
  trim: new Set(["input"]),
51
51
  upper: new Set(["input"]),
@@ -704,9 +704,12 @@ function lintSubstring(attrs) {
704
704
  // ---------------------------------------------------------------------------
705
705
  function lintStatic(attrs) {
706
706
  const msgs = [];
707
- if (attrs?.value !== undefined && typeof attrs.value !== "string") {
707
+ // Presence check is handled by checkRequired (spec.requiredAttributes).
708
+ // Here we only validate the type when value is present.
709
+ if (attrs?.value !== undefined && attrs?.value !== null && typeof attrs.value !== "string") {
708
710
  push(msgs, "error", "value must be a string.", "attributes.value");
709
711
  }
712
+ // Any other keys in attributes are valid dynamic VTL variable definitions — no unknown-attribute errors.
710
713
  return msgs;
711
714
  }
712
715
  // ---------------------------------------------------------------------------
@@ -835,5 +838,51 @@ export function lintTransform(input) {
835
838
  messages.push(...lintRandom(requestedType, attrs));
836
839
  if (requestedType === "rfc5646")
837
840
  messages.push(...lintRfc5646(attrs));
841
+ // --- Recursive nested transform lint ---
842
+ // Recursively lint every nested transform found inside attributes.
843
+ // We start from normalized.attributes (not the root) to avoid double-linting root.
844
+ if (normalized.attributes && typeof normalized.attributes === "object") {
845
+ messages.push(...lintNestedTransforms(normalized.attributes, "attributes"));
846
+ }
838
847
  return { normalized, messages };
839
848
  }
849
+ /**
850
+ * Recursively walks a subtree (starting from a transform's attributes object)
851
+ * and lints every nested object that carries a 'type' field — i.e. nested transforms.
852
+ * Reports errors with a path prefix so the user knows exactly where the problem is.
853
+ */
854
+ function lintNestedTransforms(subtree, path) {
855
+ if (!subtree || typeof subtree !== "object")
856
+ return [];
857
+ const msgs = [];
858
+ if (Array.isArray(subtree)) {
859
+ subtree.forEach((item, idx) => {
860
+ const itemPath = `${path}[${idx}]`;
861
+ if (item && typeof item === "object" && typeof item.type === "string") {
862
+ // lintTransform already recurses into the nested transform's own attributes,
863
+ // so we only call it here — no additional recursion needed.
864
+ const result = lintTransform(item);
865
+ result.messages.forEach((m) => msgs.push({ ...m, path: `${itemPath}${m.path ? "." + m.path : ""}` }));
866
+ }
867
+ else if (item && typeof item === "object") {
868
+ msgs.push(...lintNestedTransforms(item, itemPath));
869
+ }
870
+ });
871
+ return msgs;
872
+ }
873
+ for (const [key, value] of Object.entries(subtree)) {
874
+ const childPath = `${path}.${key}`;
875
+ if (value && typeof value === "object" && typeof value.type === "string") {
876
+ // Nested transform object — lintTransform handles further recursion internally.
877
+ const result = lintTransform(value);
878
+ result.messages.forEach((m) => msgs.push({ ...m, path: `${childPath}${m.path ? "." + m.path : ""}` }));
879
+ }
880
+ else if (Array.isArray(value)) {
881
+ msgs.push(...lintNestedTransforms(value, childPath));
882
+ }
883
+ else if (value && typeof value === "object") {
884
+ msgs.push(...lintNestedTransforms(value, childPath));
885
+ }
886
+ }
887
+ return msgs;
888
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "isc-transforms-mcp",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
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": {