callman-mcp 0.1.0 → 0.2.0

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.
Files changed (204) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +45 -16
  3. package/dist/client/api.d.ts +64 -3
  4. package/dist/client/api.d.ts.map +1 -1
  5. package/dist/client/api.js +67 -2
  6. package/dist/client/api.js.map +1 -1
  7. package/dist/client/errors.d.ts.map +1 -1
  8. package/dist/client/errors.js +33 -7
  9. package/dist/client/errors.js.map +1 -1
  10. package/dist/client/types.d.ts +91 -2
  11. package/dist/client/types.d.ts.map +1 -1
  12. package/dist/context.d.ts +2 -0
  13. package/dist/context.d.ts.map +1 -1
  14. package/dist/context.js +7 -2
  15. package/dist/context.js.map +1 -1
  16. package/dist/domain/contracts.d.ts +893 -0
  17. package/dist/domain/contracts.d.ts.map +1 -0
  18. package/dist/domain/contracts.js +90 -0
  19. package/dist/domain/contracts.js.map +1 -0
  20. package/dist/domain/diagram/build.d.ts +520 -18
  21. package/dist/domain/diagram/build.d.ts.map +1 -1
  22. package/dist/domain/diagram/build.js +65 -19
  23. package/dist/domain/diagram/build.js.map +1 -1
  24. package/dist/domain/diagram/patch.d.ts +624 -51
  25. package/dist/domain/diagram/patch.d.ts.map +1 -1
  26. package/dist/domain/diagram/patch.js +180 -40
  27. package/dist/domain/diagram/patch.js.map +1 -1
  28. package/dist/domain/diagram/style.d.ts +315 -0
  29. package/dist/domain/diagram/style.d.ts.map +1 -0
  30. package/dist/domain/diagram/style.js +124 -0
  31. package/dist/domain/diagram/style.js.map +1 -0
  32. package/dist/domain/diagram/summarize.d.ts +8 -0
  33. package/dist/domain/diagram/summarize.d.ts.map +1 -1
  34. package/dist/domain/diagram/summarize.js +28 -13
  35. package/dist/domain/diagram/summarize.js.map +1 -1
  36. package/dist/domain/requestIntegrations.d.ts +719 -0
  37. package/dist/domain/requestIntegrations.d.ts.map +1 -0
  38. package/dist/domain/requestIntegrations.js +222 -0
  39. package/dist/domain/requestIntegrations.js.map +1 -0
  40. package/dist/domain/requestShape.d.ts +1350 -152
  41. package/dist/domain/requestShape.d.ts.map +1 -1
  42. package/dist/domain/requestShape.js +336 -121
  43. package/dist/domain/requestShape.js.map +1 -1
  44. package/dist/domain/resolve.d.ts +2 -1
  45. package/dist/domain/resolve.d.ts.map +1 -1
  46. package/dist/domain/resolve.js +5 -1
  47. package/dist/domain/resolve.js.map +1 -1
  48. package/dist/domain/scenario/build.d.ts +82 -26
  49. package/dist/domain/scenario/build.d.ts.map +1 -1
  50. package/dist/domain/scenario/build.js +196 -65
  51. package/dist/domain/scenario/build.js.map +1 -1
  52. package/dist/domain/scenario/nodeConfig.d.ts +11 -0
  53. package/dist/domain/scenario/nodeConfig.d.ts.map +1 -0
  54. package/dist/domain/scenario/nodeConfig.js +251 -0
  55. package/dist/domain/scenario/nodeConfig.js.map +1 -0
  56. package/dist/domain/scenario/summarize.d.ts +145 -25
  57. package/dist/domain/scenario/summarize.d.ts.map +1 -1
  58. package/dist/domain/scenario/summarize.js +84 -29
  59. package/dist/domain/scenario/summarize.js.map +1 -1
  60. package/dist/lib/mask.d.ts +0 -18
  61. package/dist/lib/mask.d.ts.map +1 -1
  62. package/dist/lib/mask.js +2 -44
  63. package/dist/lib/mask.js.map +1 -1
  64. package/dist/lib/pagination.d.ts +3 -2
  65. package/dist/lib/pagination.d.ts.map +1 -1
  66. package/dist/lib/pagination.js +13 -8
  67. package/dist/lib/pagination.js.map +1 -1
  68. package/dist/lib/redact.d.ts +12 -0
  69. package/dist/lib/redact.d.ts.map +1 -0
  70. package/dist/lib/redact.js +50 -0
  71. package/dist/lib/redact.js.map +1 -0
  72. package/dist/lib/strict.d.ts +4 -0
  73. package/dist/lib/strict.d.ts.map +1 -0
  74. package/dist/lib/strict.js +14 -0
  75. package/dist/lib/strict.js.map +1 -0
  76. package/dist/prompts/index.js +7 -7
  77. package/dist/prompts/index.js.map +1 -1
  78. package/dist/resources/docs/contracts.d.ts +2 -0
  79. package/dist/resources/docs/contracts.d.ts.map +1 -0
  80. package/dist/resources/docs/contracts.js +74 -0
  81. package/dist/resources/docs/contracts.js.map +1 -0
  82. package/dist/resources/docs/diagramSchema.d.ts +1 -1
  83. package/dist/resources/docs/diagramSchema.d.ts.map +1 -1
  84. package/dist/resources/docs/diagramSchema.js +95 -28
  85. package/dist/resources/docs/diagramSchema.js.map +1 -1
  86. package/dist/resources/docs/overview.d.ts +1 -1
  87. package/dist/resources/docs/overview.d.ts.map +1 -1
  88. package/dist/resources/docs/overview.js +29 -7
  89. package/dist/resources/docs/overview.js.map +1 -1
  90. package/dist/resources/docs/requestSchema.d.ts +1 -1
  91. package/dist/resources/docs/requestSchema.d.ts.map +1 -1
  92. package/dist/resources/docs/requestSchema.js +85 -16
  93. package/dist/resources/docs/requestSchema.js.map +1 -1
  94. package/dist/resources/generated/aiPacks.d.ts +1 -1
  95. package/dist/resources/generated/aiPacks.d.ts.map +1 -1
  96. package/dist/resources/generated/aiPacks.js +3 -2
  97. package/dist/resources/generated/aiPacks.js.map +1 -1
  98. package/dist/resources/index.d.ts.map +1 -1
  99. package/dist/resources/index.js +19 -19
  100. package/dist/resources/index.js.map +1 -1
  101. package/dist/run/executeCollection.d.ts +3 -1
  102. package/dist/run/executeCollection.d.ts.map +1 -1
  103. package/dist/run/executeCollection.js +16 -4
  104. package/dist/run/executeCollection.js.map +1 -1
  105. package/dist/run/summarize.d.ts +17 -6
  106. package/dist/run/summarize.d.ts.map +1 -1
  107. package/dist/run/summarize.js +48 -41
  108. package/dist/run/summarize.js.map +1 -1
  109. package/dist/run/types.d.ts +1 -0
  110. package/dist/run/types.d.ts.map +1 -1
  111. package/dist/server.d.ts +1 -1
  112. package/dist/server.d.ts.map +1 -1
  113. package/dist/server.js +1 -1
  114. package/dist/server.js.map +1 -1
  115. package/dist/tools/catalog.d.ts +1 -1
  116. package/dist/tools/catalog.d.ts.map +1 -1
  117. package/dist/tools/catalog.js +18 -0
  118. package/dist/tools/catalog.js.map +1 -1
  119. package/dist/tools/collections.d.ts.map +1 -1
  120. package/dist/tools/collections.js +53 -41
  121. package/dist/tools/collections.js.map +1 -1
  122. package/dist/tools/contracts.d.ts +4 -0
  123. package/dist/tools/contracts.d.ts.map +1 -0
  124. package/dist/tools/contracts.js +201 -0
  125. package/dist/tools/contracts.js.map +1 -0
  126. package/dist/tools/diagrams.d.ts.map +1 -1
  127. package/dist/tools/diagrams.js +19 -10
  128. package/dist/tools/diagrams.js.map +1 -1
  129. package/dist/tools/environments.d.ts.map +1 -1
  130. package/dist/tools/environments.js +184 -64
  131. package/dist/tools/environments.js.map +1 -1
  132. package/dist/tools/import.d.ts.map +1 -1
  133. package/dist/tools/import.js +111 -20
  134. package/dist/tools/import.js.map +1 -1
  135. package/dist/tools/index.d.ts +2 -1
  136. package/dist/tools/index.d.ts.map +1 -1
  137. package/dist/tools/index.js +19 -1
  138. package/dist/tools/index.js.map +1 -1
  139. package/dist/tools/lookups.d.ts +38 -7
  140. package/dist/tools/lookups.d.ts.map +1 -1
  141. package/dist/tools/lookups.js +113 -10
  142. package/dist/tools/lookups.js.map +1 -1
  143. package/dist/tools/meta.d.ts.map +1 -1
  144. package/dist/tools/meta.js +9 -4
  145. package/dist/tools/meta.js.map +1 -1
  146. package/dist/tools/requests.d.ts.map +1 -1
  147. package/dist/tools/requests.js +138 -50
  148. package/dist/tools/requests.js.map +1 -1
  149. package/dist/tools/run.d.ts.map +1 -1
  150. package/dist/tools/run.js +84 -28
  151. package/dist/tools/run.js.map +1 -1
  152. package/dist/tools/scenarios.d.ts.map +1 -1
  153. package/dist/tools/scenarios.js +238 -75
  154. package/dist/tools/scenarios.js.map +1 -1
  155. package/dist/tools/shared.d.ts +1 -0
  156. package/dist/tools/shared.d.ts.map +1 -1
  157. package/dist/tools/shared.js +9 -1
  158. package/dist/tools/shared.js.map +1 -1
  159. package/dist/tools/workspaces.d.ts.map +1 -1
  160. package/dist/tools/workspaces.js +44 -7
  161. package/dist/tools/workspaces.js.map +1 -1
  162. package/dist-cjs/client/api.js +67 -2
  163. package/dist-cjs/client/errors.js +33 -7
  164. package/dist-cjs/context.js +7 -2
  165. package/dist-cjs/domain/contracts.js +93 -0
  166. package/dist-cjs/domain/diagram/build.js +71 -20
  167. package/dist-cjs/domain/diagram/patch.js +179 -39
  168. package/dist-cjs/domain/diagram/style.js +130 -0
  169. package/dist-cjs/domain/diagram/summarize.js +28 -13
  170. package/dist-cjs/domain/requestIntegrations.js +228 -0
  171. package/dist-cjs/domain/requestShape.js +342 -121
  172. package/dist-cjs/domain/resolve.js +5 -1
  173. package/dist-cjs/domain/scenario/build.js +197 -65
  174. package/dist-cjs/domain/scenario/nodeConfig.js +260 -0
  175. package/dist-cjs/domain/scenario/summarize.js +86 -29
  176. package/dist-cjs/lib/mask.js +3 -48
  177. package/dist-cjs/lib/pagination.js +13 -8
  178. package/dist-cjs/lib/redact.js +53 -0
  179. package/dist-cjs/lib/strict.js +18 -0
  180. package/dist-cjs/prompts/index.js +7 -7
  181. package/dist-cjs/resources/docs/contracts.js +76 -0
  182. package/dist-cjs/resources/docs/diagramSchema.js +95 -28
  183. package/dist-cjs/resources/docs/overview.js +29 -7
  184. package/dist-cjs/resources/docs/requestSchema.js +85 -16
  185. package/dist-cjs/resources/generated/aiPacks.js +3 -2
  186. package/dist-cjs/resources/index.js +17 -17
  187. package/dist-cjs/run/executeCollection.js +16 -4
  188. package/dist-cjs/run/summarize.js +48 -41
  189. package/dist-cjs/server.js +1 -1
  190. package/dist-cjs/tools/catalog.js +18 -0
  191. package/dist-cjs/tools/collections.js +52 -40
  192. package/dist-cjs/tools/contracts.js +204 -0
  193. package/dist-cjs/tools/diagrams.js +19 -10
  194. package/dist-cjs/tools/environments.js +182 -62
  195. package/dist-cjs/tools/import.js +107 -16
  196. package/dist-cjs/tools/index.js +21 -2
  197. package/dist-cjs/tools/lookups.js +123 -11
  198. package/dist-cjs/tools/meta.js +9 -4
  199. package/dist-cjs/tools/requests.js +134 -46
  200. package/dist-cjs/tools/run.js +82 -26
  201. package/dist-cjs/tools/scenarios.js +235 -72
  202. package/dist-cjs/tools/shared.js +11 -2
  203. package/dist-cjs/tools/workspaces.js +43 -6
  204. package/package.json +2 -2
@@ -3,9 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerEnvironmentTools = void 0;
4
4
  const zod_1 = require("zod");
5
5
  const errors_js_1 = require("../client/errors.js");
6
- const requestShape_js_1 = require("../domain/requestShape.js");
7
6
  const resolve_js_1 = require("../domain/resolve.js");
8
7
  const mask_js_1 = require("../lib/mask.js");
8
+ const strict_js_1 = require("../lib/strict.js");
9
9
  const truncate_js_1 = require("../lib/truncate.js");
10
10
  const lookups_js_1 = require("./lookups.js");
11
11
  const shared_js_1 = require("./shared.js");
@@ -14,98 +14,163 @@ const shared_js_1 = require("./shared.js");
14
14
  // environments:reveal AND the call asks for reveal — the server enforces
15
15
  // both; this side only forwards the flag and refuses to write a masked
16
16
  // value back.
17
+ // An environment variable is exactly { key, value } on the server — no
18
+ // enabled flag, no description (strict: sending them is an error, not a no-op).
19
+ const VARIABLES_MAX = 500;
20
+ const variableSchema = (0, strict_js_1.strictObject)({
21
+ key: zod_1.z.string().trim().min(1).max(100),
22
+ value: zod_1.z.string().max(5000).default(""),
23
+ }, "variable field");
24
+ const refuseMasked = (variables, action) => {
25
+ const maskedKeys = variables.filter((variable) => (0, mask_js_1.isMasked)(variable.value)).map((variable) => variable.key);
26
+ if (maskedKeys.length > 0) {
27
+ throw new errors_js_1.ToolInputError("MASKED_VALUE", `Masked values cannot be ${action} (keys: ${maskedKeys.join(", ")}). Read them with get_environment reveal: true (needs environments:reveal) or supply the real values.`, { keys: maskedKeys });
28
+ }
29
+ };
30
+ const duplicateKeys = (variables) => {
31
+ const seen = new Set();
32
+ const duplicates = new Set();
33
+ for (const variable of variables)
34
+ (seen.has(variable.key) ? duplicates : seen).add(variable.key);
35
+ return [...duplicates];
36
+ };
37
+ // Global variables are optional (feature flag, scope) — say why they are
38
+ // missing instead of returning an empty list as if there were none.
39
+ const loadGlobals = async (ctx, ws, reveal) => {
40
+ try {
41
+ return { globals: await ctx.api.listGlobalVariables(ws, reveal) };
42
+ }
43
+ catch (error) {
44
+ return { globals: [], globalVariablesError: (0, shared_js_1.toolFailure)(error).structuredContent.error };
45
+ }
46
+ };
17
47
  const registerEnvironmentTools = (server, ctx) => {
18
48
  server.registerTool("list_environments", {
19
49
  title: "List environments",
20
- description: "List a workspace's environments with their variable KEYS (values are not included). Global variables (shared by every environment) are listed too.",
50
+ description: "List environments with their variable KEYS (values are not included) — across ALL your workspaces unless `workspace` narrows it, each workspace with its global variables (shared by that workspace's environments).",
21
51
  inputSchema: { workspace: shared_js_1.workspaceArg },
22
52
  annotations: shared_js_1.READ,
23
53
  }, (0, shared_js_1.guarded)(async ({ workspace }) => {
24
- const ws = await ctx.resolveWorkspace(workspace);
25
- const [environments, globals] = await Promise.all([
26
- (0, lookups_js_1.loadEnvironments)(ctx, ws.id),
27
- ctx.api.listGlobalVariables(ws.id).catch(() => []),
28
- ]);
29
- return (0, shared_js_1.ok)({
30
- workspace: { id: ws.id, name: ws.name },
31
- environments: environments.map((environment) => ({
32
- id: environment.id,
33
- name: environment.name,
34
- variableCount: environment.variables.length,
35
- variableKeys: environment.variables.map((variable) => variable.key).slice(0, 200),
36
- })),
37
- globalVariableKeys: globals.map((variable) => variable.key),
38
- });
54
+ const workspaces = await (0, lookups_js_1.workspacesForListing)(ctx, workspace);
55
+ const perWorkspace = await Promise.all(workspaces.map(async (ws) => {
56
+ const [environments, globals] = await Promise.all([(0, lookups_js_1.loadEnvironments)(ctx, ws.id), loadGlobals(ctx, ws.id, false)]);
57
+ return {
58
+ workspace: (0, lookups_js_1.workspaceTag)(ws),
59
+ environments: environments.map((environment) => ({
60
+ id: environment.id,
61
+ name: environment.name,
62
+ variableCount: environment.variables.length,
63
+ variableKeys: environment.variables.map((variable) => variable.key).slice(0, 200),
64
+ ...(environment.variables.length > 200 ? { variableKeysTruncated: true } : {}),
65
+ })),
66
+ globalVariableKeys: globals.globals.map((variable) => variable.key),
67
+ ...(globals.globalVariablesError ? { globalVariablesError: globals.globalVariablesError } : {}),
68
+ };
69
+ }));
70
+ // One workspace keeps the flat shape; several come back per workspace.
71
+ return (0, shared_js_1.ok)(perWorkspace.length === 1 ? perWorkspace[0] : { workspaces: perWorkspace });
39
72
  }));
40
73
  server.registerTool("get_environment", {
41
74
  title: "Get an environment",
42
- description: "Read one environment's variables. Values are masked (••••ab) unless reveal: true AND the token holds environments:reveal. Global variables are returned alongside.",
75
+ description: "Read one environment's variables ({ key, value }). Values are masked (••••ab) unless reveal: true AND the token holds environments:reveal. Global variables are returned alongside.",
43
76
  inputSchema: { workspace: shared_js_1.workspaceArg, environment: (0, shared_js_1.refArg)("Environment"), reveal: zod_1.z.boolean().optional() },
44
77
  annotations: shared_js_1.READ,
45
78
  }, (0, shared_js_1.guarded)(async ({ workspace, environment, reveal }) => {
46
- const ws = await ctx.resolveWorkspace(workspace);
47
- const resolved = await (0, lookups_js_1.resolveEnvironment)(ctx, ws.id, environment);
48
- const [fresh, globals] = await Promise.all([
49
- ctx.api.getEnvironment(ws.id, resolved.item.id, reveal ?? false),
50
- ctx.api.listGlobalVariables(ws.id, reveal ?? false).catch(() => []),
51
- ]);
79
+ const { ws, value: resolved, note } = await (0, lookups_js_1.findEnvironment)(ctx, workspace, environment);
80
+ const [fresh, globals] = await Promise.all([ctx.api.getEnvironment(ws.id, resolved.item.id, reveal ?? false), loadGlobals(ctx, ws.id, reveal ?? false)]);
52
81
  const masked = fresh.variables.some((variable) => variable.valueMasked);
53
82
  return (0, shared_js_1.ok)({
54
- environment: {
55
- id: fresh.id,
56
- name: fresh.name,
57
- variables: fresh.variables.map((variable) => ({ key: variable.key, value: variable.value, ...(variable.enabled === false ? { enabled: false } : {}) })),
58
- },
59
- globalVariables: globals.map((variable) => ({ key: variable.key, value: variable.value })),
83
+ environment: { id: fresh.id, name: fresh.name, variables: fresh.variables.map((variable) => ({ key: variable.key, value: variable.value })) },
84
+ workspace: (0, lookups_js_1.workspaceTag)(ws),
85
+ globalVariables: globals.globals.map((variable) => ({ key: variable.key, value: variable.value })),
86
+ ...(globals.globalVariablesError ? { globalVariablesError: globals.globalVariablesError } : {}),
60
87
  valuesMasked: masked,
61
88
  ...(masked && reveal ? { note: "Values stayed masked: the token lacks environments:reveal." } : {}),
62
- resolved: [(0, resolve_js_1.resolvedSummary)(environment, resolved)],
89
+ resolved: [(0, resolve_js_1.resolvedSummary)(environment, resolved), ...(note ? [{ input: environment, note }] : [])],
63
90
  });
64
91
  }));
65
92
  server.registerTool("create_environment", {
66
93
  title: "Create an environment",
67
- description: "Create an environment with optional variables (key/value). Use it for hosts ({{baseUrl}}) and credentials so requests never hard-code them.",
94
+ description: `Create an environment with variables ({ key, value }, up to ${VARIABLES_MAX}) in \`workspace\` (or the session default — use_workspace). Use it for hosts ({{baseUrl}}) and credentials so requests never hard-code them; requests use environments of their own workspace. Names must be unique in the workspace.`,
68
95
  inputSchema: {
69
96
  workspace: shared_js_1.workspaceArg,
70
- name: zod_1.z.string().min(1).max(truncate_js_1.LIMITS.NAME),
71
- variables: zod_1.z.array(requestShape_js_1.kvSchema).max(500).optional(),
97
+ name: zod_1.z.string().trim().min(1).max(truncate_js_1.LIMITS.NAME),
98
+ variables: zod_1.z.array(variableSchema).max(VARIABLES_MAX).optional(),
72
99
  },
73
100
  annotations: shared_js_1.WRITE,
74
101
  }, (0, shared_js_1.guarded)(async ({ workspace, name, variables }) => {
75
102
  const ws = await ctx.resolveWorkspace(workspace);
76
- const rows = (0, requestShape_js_1.toRows)(variables);
77
- const maskedKeys = rows.filter((row) => (0, mask_js_1.isMasked)(row.value)).map((row) => row.key);
78
- if (maskedKeys.length > 0)
79
- throw new errors_js_1.ToolInputError("MASKED_VALUE", `Masked values cannot be stored (keys: ${maskedKeys.join(", ")}). Provide the real values.`);
103
+ const rows = variables ?? [];
104
+ refuseMasked(rows, "stored");
105
+ const duplicates = duplicateKeys(rows);
106
+ if (duplicates.length > 0)
107
+ throw new errors_js_1.ToolInputError("DUPLICATE_KEYS", `Each key once: ${duplicates.join(", ")}.`);
108
+ const existing = (await (0, lookups_js_1.loadEnvironments)(ctx, ws.id)).find((entry) => (0, lookups_js_1.sameName)(entry.name, name));
109
+ if (existing) {
110
+ throw new errors_js_1.ToolInputError("ENVIRONMENT_NAME_EXISTS", `An environment named "${existing.name}" already exists (id ${existing.id}) — use set_environment_variables on it, or pick another name.`, { existing: { id: existing.id, name: existing.name } });
111
+ }
80
112
  const created = await ctx.api.createEnvironment(ws.id, { name: name.trim(), variables: rows });
81
113
  ctx.invalidate(`environments:${ws.id}`);
82
- return (0, shared_js_1.ok)({ environment: { id: created.id, name: created.name, variableKeys: created.variables.map((variable) => variable.key) } });
114
+ return (0, shared_js_1.ok)({ environment: { id: created.id, name: created.name, variableKeys: created.variables.map((variable) => variable.key) }, workspace: (0, lookups_js_1.workspaceTag)(ws) });
115
+ }));
116
+ server.registerTool("update_environment", {
117
+ title: "Rename an environment",
118
+ description: "Rename an environment. (Change its variables with set_environment_variables.)",
119
+ inputSchema: { workspace: shared_js_1.workspaceArg, environment: (0, shared_js_1.refArg)("Environment"), name: zod_1.z.string().trim().min(1).max(truncate_js_1.LIMITS.NAME) },
120
+ annotations: shared_js_1.IDEMPOTENT_WRITE,
121
+ }, (0, shared_js_1.guarded)(async ({ workspace, environment, name }) => {
122
+ const { ws, value: resolved } = await (0, lookups_js_1.findEnvironment)(ctx, workspace, environment);
123
+ const clash = (await (0, lookups_js_1.loadEnvironments)(ctx, ws.id)).find((entry) => entry.id !== resolved.item.id && (0, lookups_js_1.sameName)(entry.name, name));
124
+ if (clash)
125
+ throw new errors_js_1.ToolInputError("ENVIRONMENT_NAME_EXISTS", `Another environment is already named "${clash.name}".`);
126
+ const updated = await ctx.api.updateEnvironment(ws.id, resolved.item.id, { name: name.trim() });
127
+ ctx.invalidate(`environments:${ws.id}`);
128
+ return (0, shared_js_1.ok)({ environment: { id: updated.id, name: updated.name, previousName: resolved.item.name }, workspace: (0, lookups_js_1.workspaceTag)(ws), resolved: [(0, resolve_js_1.resolvedSummary)(environment, resolved)] });
83
129
  }));
84
130
  server.registerTool("set_environment_variables", {
85
- title: "Set / unset environment variables",
86
- description: "Upsert variables (`set`) and/or remove keys (`unset`) in an environment. Existing keys are updated in place, new keys appended, other keys untouched. A masked value (starting with ••••) is refused — read it with reveal or supply a real value.",
131
+ title: "Set / rename / unset environment variables",
132
+ description: "Change an environment's variables: `set` upserts ({ key, value } — existing keys updated in place, new keys appended), `rename` changes keys keeping their values, `unset` removes keys (needs environments:delete). Other keys are untouched. A masked value (••••…) is refused — read it with reveal or supply a real value. Reports what changed, what failed (with the error code) and unset keys that did not exist.",
87
133
  inputSchema: {
88
134
  workspace: shared_js_1.workspaceArg,
89
135
  environment: (0, shared_js_1.refArg)("Environment"),
90
- set: zod_1.z.array(requestShape_js_1.kvSchema).max(500).optional(),
91
- unset: zod_1.z.array(zod_1.z.string().min(1).max(100)).max(500).optional(),
136
+ set: zod_1.z.array(variableSchema).max(VARIABLES_MAX).optional(),
137
+ rename: zod_1.z.array((0, strict_js_1.strictObject)({ from: zod_1.z.string().trim().min(1).max(100), to: zod_1.z.string().trim().min(1).max(100) }, "rename field")).max(VARIABLES_MAX).optional(),
138
+ unset: zod_1.z.array(zod_1.z.string().trim().min(1).max(100)).max(VARIABLES_MAX).optional(),
92
139
  },
93
140
  annotations: shared_js_1.IDEMPOTENT_WRITE,
94
- }, (0, shared_js_1.guarded)(async ({ workspace, environment, set, unset }) => {
95
- const rows = (0, requestShape_js_1.toRows)(set);
96
- const removals = (unset ?? []).map((key) => key.trim()).filter(Boolean);
97
- if (rows.length === 0 && removals.length === 0)
98
- throw new errors_js_1.ToolInputError("NOTHING_TO_UPDATE", "Pass set and/or unset.");
99
- const maskedKeys = rows.filter((row) => (0, mask_js_1.isMasked)(row.value)).map((row) => row.key);
100
- if (maskedKeys.length > 0)
101
- throw new errors_js_1.ToolInputError("MASKED_VALUE", `Masked values cannot be written back (keys: ${maskedKeys.join(", ")}).`);
102
- const ws = await ctx.resolveWorkspace(workspace);
103
- const resolved = await (0, lookups_js_1.resolveEnvironment)(ctx, ws.id, environment);
141
+ }, (0, shared_js_1.guarded)(async ({ workspace, environment, set, rename, unset }) => {
142
+ const rows = set ?? [];
143
+ const renames = rename ?? [];
144
+ const removals = unset ?? [];
145
+ if (rows.length === 0 && renames.length === 0 && removals.length === 0)
146
+ throw new errors_js_1.ToolInputError("NOTHING_TO_UPDATE", "Pass set, rename and/or unset.");
147
+ refuseMasked(rows, "written back");
148
+ const duplicates = duplicateKeys(rows);
149
+ if (duplicates.length > 0)
150
+ throw new errors_js_1.ToolInputError("DUPLICATE_KEYS", `Each key once in set: ${duplicates.join(", ")}.`);
151
+ const { ws, value: resolved } = await (0, lookups_js_1.findEnvironment)(ctx, workspace, environment);
104
152
  const existingKeys = new Set(resolved.item.variables.map((variable) => variable.key));
105
153
  const updated = [];
106
154
  const added = [];
155
+ const renamed = [];
107
156
  const removed = [];
157
+ const notFound = [];
108
158
  const failed = [];
159
+ for (const { from, to } of renames) {
160
+ if (!existingKeys.has(from)) {
161
+ notFound.push(from);
162
+ continue;
163
+ }
164
+ try {
165
+ await ctx.api.updateEnvironmentVariable(ws.id, resolved.item.id, from, { key: to });
166
+ existingKeys.delete(from);
167
+ existingKeys.add(to);
168
+ renamed.push({ from, to });
169
+ }
170
+ catch (error) {
171
+ failed.push({ key: from, error: (0, shared_js_1.failureOf)(error) });
172
+ }
173
+ }
109
174
  for (const row of rows) {
110
175
  try {
111
176
  if (existingKeys.has(row.key)) {
@@ -119,35 +184,90 @@ const registerEnvironmentTools = (server, ctx) => {
119
184
  }
120
185
  }
121
186
  catch (error) {
122
- failed.push({ key: row.key, error: error instanceof Error ? error.message : String(error) });
187
+ failed.push({ key: row.key, error: (0, shared_js_1.failureOf)(error) });
123
188
  }
124
189
  }
125
190
  for (const key of removals) {
191
+ if (!existingKeys.has(key)) {
192
+ notFound.push(key);
193
+ continue;
194
+ }
126
195
  try {
127
- if (!existingKeys.has(key))
128
- continue;
129
196
  await ctx.api.deleteEnvironmentVariable(ws.id, resolved.item.id, key);
130
197
  removed.push(key);
131
198
  }
132
199
  catch (error) {
133
- failed.push({ key, error: error instanceof Error ? error.message : String(error) });
200
+ failed.push({ key, error: (0, shared_js_1.failureOf)(error) });
134
201
  }
135
202
  }
136
203
  ctx.invalidate(`environments:${ws.id}`);
137
- return (0, shared_js_1.ok)({ environment: { id: resolved.item.id, name: resolved.item.name }, updated, added, removed, failed, resolved: [(0, resolve_js_1.resolvedSummary)(environment, resolved)] });
204
+ const changed = updated.length + added.length + renamed.length + removed.length;
205
+ const body = { environment: { id: resolved.item.id, name: resolved.item.name }, workspace: (0, lookups_js_1.workspaceTag)(ws), updated, added, renamed, removed, notFound, failed, resolved: [(0, resolve_js_1.resolvedSummary)(environment, resolved)] };
206
+ if (failed.length > 0 && changed === 0) {
207
+ return { ...(0, shared_js_1.toolFailure)(new errors_js_1.ToolInputError("NOTHING_CHANGED", `No variable was changed — every write failed (first: ${String(failed[0].error.code ?? failed[0].error.message)}).`, body)) };
208
+ }
209
+ return (0, shared_js_1.ok)({ ...body, ...(failed.length > 0 ? { partial: true } : {}) });
210
+ }));
211
+ server.registerTool("set_global_variables", {
212
+ title: "Set / unset global variables",
213
+ description: "Upsert (`set`: { key, value }) and/or remove (`unset`, needs environments:delete) the GLOBAL variables of `workspace` (or the session default) — shared by that workspace's environments and resolved as {{key}} when no environment defines it, or explicitly as {{global.key}}. A masked value (••••…) is refused.",
214
+ inputSchema: {
215
+ workspace: shared_js_1.workspaceArg,
216
+ set: zod_1.z.array(variableSchema).max(VARIABLES_MAX).optional(),
217
+ unset: zod_1.z.array(zod_1.z.string().trim().min(1).max(100)).max(VARIABLES_MAX).optional(),
218
+ },
219
+ annotations: shared_js_1.IDEMPOTENT_WRITE,
220
+ }, (0, shared_js_1.guarded)(async ({ workspace, set, unset }) => {
221
+ const rows = set ?? [];
222
+ const removals = unset ?? [];
223
+ if (rows.length === 0 && removals.length === 0)
224
+ throw new errors_js_1.ToolInputError("NOTHING_TO_UPDATE", "Pass set and/or unset.");
225
+ refuseMasked(rows, "written back");
226
+ const ws = await ctx.resolveWorkspace(workspace);
227
+ const existing = new Set((await ctx.api.listGlobalVariables(ws.id)).map((variable) => variable.key));
228
+ const written = [];
229
+ const removed = [];
230
+ const notFound = [];
231
+ const failed = [];
232
+ for (const row of rows) {
233
+ try {
234
+ await ctx.api.setGlobalVariable(ws.id, { key: row.key, value: row.value });
235
+ written.push(row.key);
236
+ }
237
+ catch (error) {
238
+ failed.push({ key: row.key, error: (0, shared_js_1.failureOf)(error) });
239
+ }
240
+ }
241
+ for (const key of removals) {
242
+ if (!existing.has(key)) {
243
+ notFound.push(key);
244
+ continue;
245
+ }
246
+ try {
247
+ await ctx.api.deleteGlobalVariable(ws.id, key);
248
+ removed.push(key);
249
+ }
250
+ catch (error) {
251
+ failed.push({ key, error: (0, shared_js_1.failureOf)(error) });
252
+ }
253
+ }
254
+ const body = { workspace: { id: ws.id, name: ws.name }, written, removed, notFound, failed };
255
+ if (failed.length > 0 && written.length + removed.length === 0) {
256
+ return (0, shared_js_1.toolFailure)(new errors_js_1.ToolInputError("NOTHING_CHANGED", `No global variable was changed — every write failed (first: ${String(failed[0].error.code ?? failed[0].error.message)}).`, body));
257
+ }
258
+ return (0, shared_js_1.ok)({ ...body, ...(failed.length > 0 ? { partial: true } : {}) });
138
259
  }));
139
260
  server.registerTool("delete_environment", {
140
261
  title: "Delete an environment",
141
- description: "Delete an environment and all its variables. Permanent — confirm with the user first.",
262
+ description: "Delete an environment and all its variables (exact name or id). Permanent — confirm with the user first.",
142
263
  inputSchema: { workspace: shared_js_1.workspaceArg, environment: (0, shared_js_1.refArg)("Environment"), confirm: shared_js_1.confirmArg },
143
264
  annotations: shared_js_1.DESTRUCTIVE,
144
265
  }, (0, shared_js_1.guarded)(async ({ workspace, environment, confirm }) => {
145
266
  (0, shared_js_1.requireConfirm)(confirm, "an environment");
146
- const ws = await ctx.resolveWorkspace(workspace);
147
- const resolved = await (0, lookups_js_1.resolveEnvironment)(ctx, ws.id, environment);
267
+ const { ws, value: resolved } = await (0, lookups_js_1.findEnvironment)(ctx, workspace, environment, { exact: true });
148
268
  await ctx.api.deleteEnvironment(ws.id, resolved.item.id);
149
269
  ctx.invalidate(`environments:${ws.id}`);
150
- return (0, shared_js_1.ok)({ deleted: { id: resolved.item.id, name: resolved.item.name }, resolved: [(0, resolve_js_1.resolvedSummary)(environment, resolved)] });
270
+ return (0, shared_js_1.ok)({ deleted: { id: resolved.item.id, name: resolved.item.name }, workspace: (0, lookups_js_1.workspaceTag)(ws), resolved: [(0, resolve_js_1.resolvedSummary)(environment, resolved)] });
151
271
  }));
152
272
  };
153
273
  exports.registerEnvironmentTools = registerEnvironmentTools;
@@ -7,6 +7,7 @@ const errors_js_1 = require("../client/errors.js");
7
7
  const importDetect_js_1 = require("../domain/importDetect.js");
8
8
  const resolve_js_1 = require("../domain/resolve.js");
9
9
  const truncate_js_1 = require("../lib/truncate.js");
10
+ const strict_js_1 = require("../lib/strict.js");
10
11
  const lookups_js_1 = require("./lookups.js");
11
12
  const shared_js_1 = require("./shared.js");
12
13
  const CONTENT_MAX = 8 * 1024 * 1024;
@@ -14,7 +15,7 @@ const IMPORT_FORMATS = ["auto", "postman", "openapi", "insomnia", "bruno", "inte
14
15
  const registerImportTools = (server, ctx) => {
15
16
  server.registerTool("import_content", {
16
17
  title: "Import a file's content",
17
- description: "Import API definitions from file CONTENT you read: Postman collection, OpenAPI/Swagger (JSON or YAML), Insomnia, Bruno, a Callman export, a cURL script, or a .http/.rest file. The format is detected (`format: auto`). Document formats always create a new collection; cURL/.http create a new collection (`collectionName`) or add to an existing one (`targetCollection` + optional `folder`). Use dryRun: true to see what was detected before writing. For endpoints found in source code, read the routes yourself and call create_requests_bulk instead.",
18
+ description: "Import API definitions from file CONTENT you read: Postman collection, OpenAPI/Swagger (JSON or YAML), Insomnia, Bruno, a Callman export, a cURL script, or a .http/.rest file. The format is detected (`format: auto`). Document formats always create a new collection; cURL/.http create a new collection (`collectionName`) or add to an existing one (`targetCollection` + optional `folder`). Variables the file defines (Postman collection variables, OpenAPI servers → baseUrl, .http @vars) are written into `environment` when you name one — without it, requests reference {{variables}} that have no value anywhere. Use dryRun: true to see what was detected before writing. For endpoints found in source code, read the routes yourself and call create_requests_bulk instead.",
18
19
  inputSchema: {
19
20
  workspace: shared_js_1.workspaceArg,
20
21
  content: zod_1.z.string().min(1).max(CONTENT_MAX).describe("The raw file content"),
@@ -23,13 +24,21 @@ const registerImportTools = (server, ctx) => {
23
24
  collectionName: zod_1.z.string().max(truncate_js_1.LIMITS.NAME).optional().describe("Name for the new collection (documents: fallback when the file carries no name)"),
24
25
  targetCollection: zod_1.z.string().max(200).optional().describe("cURL/.http only: add to this existing collection instead of creating one"),
25
26
  folder: zod_1.z.string().max(300).optional().describe("cURL/.http + targetCollection: folder to put the requests in"),
27
+ environment: zod_1.z.string().max(200).optional().describe("Environment (id or name) that receives the variables the file defines"),
26
28
  dryRun: shared_js_1.dryRunArg,
27
29
  },
28
30
  annotations: shared_js_1.WRITE,
29
- }, (0, shared_js_1.guarded)(async ({ workspace, content, fileName, format, collectionName, targetCollection, folder, dryRun }) => {
30
- const ws = await ctx.resolveWorkspace(workspace);
31
+ }, (0, shared_js_1.guarded)(async ({ workspace, content, fileName, format, collectionName, targetCollection, folder, environment, dryRun }) => {
32
+ // Into an existing collection → that collection's workspace; otherwise
33
+ // the one named, the session default or the only one.
34
+ const target = targetCollection ? await (0, lookups_js_1.findCollection)(ctx, workspace, targetCollection) : null;
35
+ const ws = target ? target.ws : await ctx.resolveWorkspace(workspace);
31
36
  const detection = (0, importDetect_js_1.detectImport)(fileName, content);
32
37
  const resolvedFormat = format === "auto" ? detection.format : format;
38
+ const targetEnvironment = environment ? await (0, lookups_js_1.resolveEnvironment)(ctx, ws.id, environment) : null;
39
+ if (folder && !targetCollection) {
40
+ throw new errors_js_1.ToolInputError("FOLDER_NEEDS_TARGET_COLLECTION", "`folder` only applies together with targetCollection (cURL/.http into an existing collection).");
41
+ }
33
42
  if (resolvedFormat === "unknown" || resolvedFormat === "scenario" || resolvedFormat === "environment") {
34
43
  throw new errors_js_1.ToolInputError("IMPORT_FORMAT_UNSUPPORTED", resolvedFormat === "scenario"
35
44
  ? "This is a Callman scenario export — use import_scenario."
@@ -49,8 +58,28 @@ const registerImportTools = (server, ctx) => {
49
58
  if (collectionName)
50
59
  payload = { ...payload, collectionName: collectionName.trim() };
51
60
  const sample = payload.requests.slice(0, 5).map((request) => `${request.method} ${request.name}`);
61
+ // .http `@name = value` lines (callman-core ≥ 1.28.1 returns them).
62
+ const fileVariables = (payload.variables ?? []).filter((variable) => variable.key);
63
+ const applyFileVariables = async () => {
64
+ if (fileVariables.length === 0)
65
+ return {};
66
+ if (!targetEnvironment) {
67
+ return { fileVariables: fileVariables.map((variable) => variable.key), fileVariablesNote: "The file defines these variables; pass `environment` to store them, or set them with set_environment_variables." };
68
+ }
69
+ const existing = new Set(targetEnvironment.item.variables.map((variable) => variable.key));
70
+ const written = [];
71
+ for (const variable of fileVariables) {
72
+ if (existing.has(variable.key))
73
+ await ctx.api.updateEnvironmentVariable(ws.id, targetEnvironment.item.id, variable.key, { value: variable.value });
74
+ else
75
+ await ctx.api.addEnvironmentVariable(ws.id, targetEnvironment.item.id, { key: variable.key, value: variable.value });
76
+ written.push(variable.key);
77
+ }
78
+ ctx.invalidate(`environments:${ws.id}`);
79
+ return { environment: { id: targetEnvironment.item.id, name: targetEnvironment.item.name, variablesWritten: written } };
80
+ };
52
81
  if (targetCollection) {
53
- const resolved = await (0, lookups_js_1.resolveCollection)(ctx, ws.id, targetCollection);
82
+ const resolved = target.value;
54
83
  const targetFolder = await (0, lookups_js_1.resolveFolder)(ctx, ws.id, resolved.item.id, folder);
55
84
  if (dryRun) {
56
85
  return (0, shared_js_1.ok)({ dryRun: true, detected: detection, format: resolvedFormat, mode: "existing-collection", collection: { id: resolved.item.id, name: resolved.item.name }, requestCount: payload.requests.length, sample });
@@ -63,12 +92,12 @@ const registerImportTools = (server, ctx) => {
63
92
  created.push({ id: row.id, name: row.name, method: row.method });
64
93
  }
65
94
  catch (error) {
66
- failed.push({ name: request.name, error: error instanceof errors_js_1.CallmanApiError ? `${error.code}: ${error.message}` : String(error) });
95
+ failed.push({ name: request.name, error: (0, shared_js_1.failureOf)(error) });
67
96
  }
68
97
  }
69
98
  ctx.invalidate(`tree:${ws.id}:${resolved.item.id}`);
70
99
  ctx.invalidate(`collections:${ws.id}`);
71
- return (0, shared_js_1.ok)({ format: resolvedFormat, collection: { id: resolved.item.id, name: resolved.item.name }, created: created.length, failed, requests: created, resolved: [(0, resolve_js_1.resolvedSummary)(targetCollection, resolved)] });
100
+ return (0, shared_js_1.ok)({ format: resolvedFormat, collection: { id: resolved.item.id, name: resolved.item.name }, workspace: (0, lookups_js_1.workspaceTag)(ws), created: created.length, failed, requests: created, ...(await applyFileVariables()), resolved: [(0, resolve_js_1.resolvedSummary)(targetCollection, resolved)] });
72
101
  }
73
102
  const existing = (await (0, lookups_js_1.loadCollections)(ctx, ws.id)).find((entry) => (0, lookups_js_1.sameName)(entry.name, payload.collectionName));
74
103
  if (existing) {
@@ -81,7 +110,10 @@ const registerImportTools = (server, ctx) => {
81
110
  }
82
111
  const result = await ctx.api.importJson(ws.id, { format: "requests", payload });
83
112
  ctx.invalidate(`collections:${ws.id}`);
84
- return (0, shared_js_1.ok)({ format: resolvedFormat, collection: { id: result.collection.id, name: result.collection.name }, created: result.importedCounts });
113
+ return (0, shared_js_1.ok)({ format: resolvedFormat, collection: { id: result.collection.id, name: result.collection.name }, workspace: (0, lookups_js_1.workspaceTag)(ws), created: result.importedCounts, ...(await applyFileVariables()) });
114
+ }
115
+ if (targetCollection) {
116
+ throw new errors_js_1.ToolInputError("IMPORT_TARGET_UNSUPPORTED", `A ${resolvedFormat} document always creates a new collection — targetCollection only applies to cURL/.http. Import it, then use move_requests to move requests into "${targetCollection}".`);
85
117
  }
86
118
  if (!importDetect_js_1.SERVER_DOCUMENT_FORMATS.has(resolvedFormat)) {
87
119
  throw new errors_js_1.ToolInputError("IMPORT_FORMAT_UNSUPPORTED", `Format "${resolvedFormat}" is not importable here.`);
@@ -90,22 +122,45 @@ const registerImportTools = (server, ctx) => {
90
122
  if (dryRun) {
91
123
  return (0, shared_js_1.ok)({ dryRun: true, detected: detection, format: resolvedFormat, mode: "new-collection", note: "The server parses this document; request counts are known after the import." });
92
124
  }
93
- const result = await ctx.api.importJson(ws.id, { format: resolvedFormat, payload, ...(collectionName ? { fallbackCollectionName: collectionName.trim() } : {}) });
125
+ const result = await ctx.api.importJson(ws.id, {
126
+ format: resolvedFormat,
127
+ payload,
128
+ ...(collectionName ? { fallbackCollectionName: collectionName.trim() } : {}),
129
+ ...(targetEnvironment ? { targetEnvironmentId: targetEnvironment.item.id } : {}),
130
+ });
94
131
  ctx.invalidate(`collections:${ws.id}`);
95
- return (0, shared_js_1.ok)({ format: resolvedFormat, collection: { id: result.collection.id, name: result.collection.name }, created: result.importedCounts });
132
+ if (targetEnvironment)
133
+ ctx.invalidate(`environments:${ws.id}`);
134
+ return (0, shared_js_1.ok)({
135
+ format: resolvedFormat,
136
+ collection: { id: result.collection.id, name: result.collection.name },
137
+ workspace: (0, lookups_js_1.workspaceTag)(ws),
138
+ created: result.importedCounts,
139
+ // Keys only — the import response carries the environment's values.
140
+ ...(result.environment ? { environment: { id: result.environment.id, name: result.environment.name, variableKeys: (result.environment.variables ?? []).map((variable) => variable.key) } } : {}),
141
+ ...(!targetEnvironment ? { note: "Variables defined by the file were not stored — pass `environment` to write them into an environment." } : {}),
142
+ });
96
143
  }));
97
144
  server.registerTool("import_scenario", {
98
145
  title: "Import a scenario export",
99
- description: "Import a Callman scenario export (a `callman.scenario` envelope — the JSON of a .callman.scenario file, as text or object). Connections referenced by the export that have no counterpart here are skipped (the nodes import without a connection; the result lists warnings).",
146
+ description: "Import a Callman scenario export (a `callman.scenario` envelope — the JSON of a .callman.scenario file, as text or object). Connections the export references are matched to this workspace's connections of the same type and name automatically; `connections` overrides that per export connection (a workspace connection id or name, or \"skip\"). Unmatched ones are skipped (the nodes import without a connection) and listed. On-prem, `group` picks the scenario group.",
100
147
  inputSchema: {
101
148
  workspace: shared_js_1.workspaceArg,
102
149
  envelope: zod_1.z.union([zod_1.z.string().max(CONTENT_MAX), zod_1.z.record(zod_1.z.unknown())]).describe("The export envelope (JSON text or object)"),
103
150
  name: zod_1.z.string().max(200).optional().describe("Override the imported scenario's name"),
104
151
  folder: zod_1.z.string().max(200).optional().describe("Scenario folder id or name"),
152
+ group: zod_1.z.string().max(200).optional().describe('On-prem scenario group (name or id, or "personal")'),
153
+ connections: zod_1.z
154
+ .array((0, strict_js_1.strictObject)({
155
+ from: zod_1.z.string().min(1).max(300).describe("The export's connection (its refId or name)"),
156
+ to: zod_1.z.string().min(1).max(200).describe('Workspace connection id or name, or "skip"'),
157
+ }, "connection mapping field"))
158
+ .max(200)
159
+ .optional(),
105
160
  dryRun: shared_js_1.dryRunArg,
106
161
  },
107
162
  annotations: shared_js_1.WRITE,
108
- }, (0, shared_js_1.guarded)(async ({ workspace, envelope, name, folder, dryRun }) => {
163
+ }, (0, shared_js_1.guarded)(async ({ workspace, envelope, name, folder, group, connections: mapping, dryRun }) => {
109
164
  const ws = await ctx.resolveWorkspace(workspace);
110
165
  let parsed = envelope;
111
166
  if (typeof envelope === "string") {
@@ -121,23 +176,59 @@ const registerImportTools = (server, ctx) => {
121
176
  throw new errors_js_1.ToolInputError("IMPORT_NOT_SCENARIO", "This is not a Callman scenario export (expected kind: \"callman.scenario\").");
122
177
  }
123
178
  const targetFolder = folder ? await (0, lookups_js_1.resolveScenarioFolder)(ctx, ws.id, folder) : null;
124
- const connections = record.connections ?? [];
125
- const connectionMap = Object.fromEntries(connections.filter((connection) => connection.refId).map((connection) => [connection.refId, { action: "skip" }]));
179
+ let groupId;
180
+ if (group) {
181
+ if (group.trim().toLowerCase() === "personal")
182
+ groupId = "personal";
183
+ else {
184
+ const { items } = await ctx.api.listMyScenarioGroups(ws.id);
185
+ if (items.length > 0)
186
+ groupId = (0, resolve_js_1.resolveRef)("scenario group", group, items).item.id;
187
+ }
188
+ }
189
+ const exported = (record.connections ?? []).filter((connection) => connection.refId);
190
+ for (const entry of mapping ?? []) {
191
+ if (!exported.some((connection) => connection.refId === entry.from || connection.name === entry.from)) {
192
+ throw new errors_js_1.ToolInputError("IMPORT_CONNECTION_UNKNOWN", `The export has no connection "${entry.from}".`, { exportConnections: exported.map((connection) => ({ refId: connection.refId, name: connection.name, type: connection.type })) });
193
+ }
194
+ }
195
+ const available = exported.length > 0 ? await (0, lookups_js_1.loadConnections)(ctx, ws.id) : [];
196
+ const decisions = [];
197
+ const connectionMap = {};
198
+ for (const connection of exported) {
199
+ const override = (mapping ?? []).find((entry) => entry.from === connection.refId || entry.from === connection.name);
200
+ let target;
201
+ if (override && override.to.trim().toLowerCase() !== "skip") {
202
+ target = available.find((candidate) => candidate.id === override.to) ?? available.find((candidate) => candidate.name.toLowerCase() === override.to.toLowerCase());
203
+ if (!target)
204
+ throw new errors_js_1.ToolInputError("CONNECTION_NOT_FOUND", `No workspace connection "${override.to}" (list_connections shows them).`);
205
+ if (connection.type && target.type !== connection.type)
206
+ throw new errors_js_1.ToolInputError("CONNECTION_WRONG_TYPE", `"${target.name}" is a ${target.type} connection; the export's "${connection.name}" is ${connection.type}.`);
207
+ }
208
+ else if (!override) {
209
+ const sameName = available.filter((candidate) => candidate.type === connection.type && candidate.name.toLowerCase() === (connection.name ?? "").toLowerCase());
210
+ target = sameName.length === 1 ? sameName[0] : undefined;
211
+ }
212
+ connectionMap[connection.refId] = target ? { action: "map", connectionId: target.id } : { action: "skip" };
213
+ decisions.push({ export: connection.name ?? connection.refId, type: connection.type ?? null, mappedTo: target ? target.name : null });
214
+ }
126
215
  if (dryRun) {
127
216
  return (0, shared_js_1.ok)({
128
217
  dryRun: true,
129
218
  scenario: { name: name ?? record.scenario?.name ?? null, nodeCount: Array.isArray(record.scenario?.nodes) ? record.scenario?.nodes.length : null },
130
- connectionsSkipped: connections.map((connection) => `${connection.name ?? connection.refId} (${connection.type ?? "?"})`),
219
+ connections: decisions,
131
220
  folder: targetFolder ? { id: targetFolder.item.id, name: targetFolder.item.name } : null,
132
221
  });
133
222
  }
134
- const result = await ctx.api.importScenario(ws.id, { envelope: parsed, ...(name ? { name } : {}), folderId: targetFolder?.item.id ?? null, connectionMap });
223
+ const result = await ctx.api.importScenario(ws.id, { envelope: parsed, ...(name ? { name } : {}), folderId: targetFolder?.item.id ?? null, connectionMap, ...(groupId ? { groupId } : {}) });
135
224
  ctx.invalidate("scenarios:");
136
225
  const scenario = result.scenario;
226
+ const skipped = decisions.filter((decision) => !decision.mappedTo);
137
227
  return (0, shared_js_1.ok)({
138
228
  scenario: { id: scenario.id, name: scenario.name, nodeCount: scenario.nodes?.length ?? null, reviewStatus: scenario.reviewStatus ?? null },
229
+ connections: decisions,
139
230
  warnings: result.warnings ?? [],
140
- ...(connections.length > 0 ? { note: "Connections from the export were skipped — attach the right workspace connections to the DB/Kafka/Redis nodes in the desktop app." } : {}),
231
+ ...(skipped.length > 0 ? { note: `Not connected: ${skipped.map((decision) => decision.export).join(", ")} — their nodes need a connectionId (patch_scenario + list_connections).` } : {}),
141
232
  ...(targetFolder ? { resolved: [(0, resolve_js_1.resolvedSummary)(folder, targetFolder)] } : {}),
142
233
  });
143
234
  }));
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.registerAllTools = exports.toolAvailability = exports.TOOL_CATALOG = void 0;
3
+ exports.registerAllTools = exports.withStrictArguments = exports.toolAvailability = exports.TOOL_CATALOG = void 0;
4
+ const strict_js_1 = require("../lib/strict.js");
4
5
  const collections_js_1 = require("./collections.js");
6
+ const contracts_js_1 = require("./contracts.js");
5
7
  const diagrams_js_1 = require("./diagrams.js");
6
8
  const environments_js_1 = require("./environments.js");
7
9
  const import_js_1 = require("./import.js");
@@ -13,14 +15,31 @@ const workspaces_js_1 = require("./workspaces.js");
13
15
  var catalog_js_1 = require("./catalog.js");
14
16
  Object.defineProperty(exports, "TOOL_CATALOG", { enumerable: true, get: function () { return catalog_js_1.TOOL_CATALOG; } });
15
17
  Object.defineProperty(exports, "toolAvailability", { enumerable: true, get: function () { return catalog_js_1.toolAvailability; } });
18
+ // Every tool's arguments are strict: the SDK would otherwise build a
19
+ // strip-mode object from the raw shape, and a misspelled argument
20
+ // (`workspaceId` for `workspace`, `colour` for `color`) would vanish while the
21
+ // tool ran with its default. Runtime only — handler types still come from the
22
+ // raw shapes at each registerTool call.
23
+ const isRawShape = (schema) => typeof schema === "object" && schema !== null && !("_def" in schema) && !("_zod" in schema);
24
+ const withStrictArguments = (server) => new Proxy(server, {
25
+ get(target, property, receiver) {
26
+ if (property !== "registerTool")
27
+ return Reflect.get(target, property, receiver);
28
+ const register = (name, config, callback) => target.registerTool(name, { ...config, inputSchema: isRawShape(config.inputSchema) ? (0, strict_js_1.strictObject)(config.inputSchema, "argument", name) : config.inputSchema }, callback);
29
+ return register;
30
+ },
31
+ });
32
+ exports.withStrictArguments = withStrictArguments;
16
33
  // Registration order = the order clients list tools in. Run tools are only
17
34
  // registered when local execution is allowed (a backend-hosted server passes
18
35
  // allowLocalRun: false and they disappear from tools/list).
19
- const registerAllTools = (server, ctx) => {
36
+ const registerAllTools = (host, ctx) => {
37
+ const server = (0, exports.withStrictArguments)(host);
20
38
  (0, meta_js_1.registerMetaTools)(server, ctx);
21
39
  (0, workspaces_js_1.registerWorkspaceTools)(server, ctx);
22
40
  (0, collections_js_1.registerCollectionTools)(server, ctx);
23
41
  (0, requests_js_1.registerRequestTools)(server, ctx);
42
+ (0, contracts_js_1.registerContractTools)(server, ctx);
24
43
  (0, import_js_1.registerImportTools)(server, ctx);
25
44
  (0, environments_js_1.registerEnvironmentTools)(server, ctx);
26
45
  (0, scenarios_js_1.registerScenarioTools)(server, ctx);