@arbium/enterprise-agent-connect 0.1.1 → 0.1.2-dev.75145ef

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 (231) hide show
  1. package/README.md +164 -12
  2. package/dist/connectors/dell-techdirect/index.d.ts +8 -0
  3. package/dist/connectors/dell-techdirect/tools.d.ts +2 -0
  4. package/dist/connectors/dell-techdirect/vendor.d.ts +6 -0
  5. package/dist/connectors/dynamics-365/connector.json +228 -0
  6. package/dist/connectors/dynamics-365/index.d.ts +8 -0
  7. package/dist/connectors/dynamics-365/index.js +10 -0
  8. package/dist/connectors/dynamics-365/index.js.map +1 -0
  9. package/dist/connectors/dynamics-365/tools.d.ts +35 -0
  10. package/dist/connectors/dynamics-365/tools.js +806 -0
  11. package/dist/connectors/dynamics-365/tools.js.map +1 -0
  12. package/dist/connectors/dynamics-365/vendor.d.ts +9 -0
  13. package/dist/connectors/dynamics-365/vendor.js +110 -0
  14. package/dist/connectors/dynamics-365/vendor.js.map +1 -0
  15. package/dist/connectors/entra/index.d.ts +2 -0
  16. package/dist/connectors/entra/tools.d.ts +14 -0
  17. package/dist/connectors/fedex/index.d.ts +8 -0
  18. package/dist/connectors/fedex/tools.d.ts +3 -0
  19. package/dist/connectors/fedex/vendor.d.ts +7 -0
  20. package/dist/connectors/gong/connector.json +127 -0
  21. package/dist/connectors/gong/index.d.ts +8 -0
  22. package/dist/connectors/gong/index.js +7 -0
  23. package/dist/connectors/gong/index.js.map +1 -0
  24. package/dist/connectors/gong/tools.d.ts +8 -0
  25. package/dist/connectors/gong/tools.js +424 -0
  26. package/dist/connectors/gong/tools.js.map +1 -0
  27. package/dist/connectors/gong/vendor.d.ts +4 -0
  28. package/dist/connectors/gong/vendor.js +89 -0
  29. package/dist/connectors/gong/vendor.js.map +1 -0
  30. package/dist/connectors/http/index.d.ts +34 -0
  31. package/dist/connectors/http/index.js +154 -64
  32. package/dist/connectors/http/index.js.map +1 -1
  33. package/dist/connectors/intune/index.d.ts +2 -0
  34. package/dist/connectors/intune/tools.d.ts +12 -0
  35. package/dist/connectors/jamf/connector.json +182 -0
  36. package/dist/connectors/jamf/index.d.ts +8 -0
  37. package/dist/connectors/jamf/index.js +7 -0
  38. package/dist/connectors/jamf/index.js.map +1 -0
  39. package/dist/connectors/jamf/tools.d.ts +7 -0
  40. package/dist/connectors/jamf/tools.js +659 -0
  41. package/dist/connectors/jamf/tools.js.map +1 -0
  42. package/dist/connectors/jamf/vendor.d.ts +5 -0
  43. package/dist/connectors/jamf/vendor.js +70 -0
  44. package/dist/connectors/jamf/vendor.js.map +1 -0
  45. package/dist/connectors/jira/connector.json +146 -3
  46. package/dist/connectors/jira/index.d.ts +8 -0
  47. package/dist/connectors/jira/jql.d.ts +4 -0
  48. package/dist/connectors/jira/tools.d.ts +3 -0
  49. package/dist/connectors/jira/vendor.d.ts +3 -0
  50. package/dist/connectors/jira/vendor.js +8 -2
  51. package/dist/connectors/jira/vendor.js.map +1 -1
  52. package/dist/connectors/microsoft/client.d.ts +66 -0
  53. package/dist/connectors/microsoft/client.js +26 -8
  54. package/dist/connectors/microsoft/client.js.map +1 -1
  55. package/dist/connectors/netsuite/connector.json +317 -0
  56. package/dist/connectors/netsuite/index.d.ts +8 -0
  57. package/dist/connectors/netsuite/index.js +7 -0
  58. package/dist/connectors/netsuite/index.js.map +1 -0
  59. package/dist/connectors/netsuite/tools.d.ts +24 -0
  60. package/dist/connectors/netsuite/tools.js +447 -0
  61. package/dist/connectors/netsuite/tools.js.map +1 -0
  62. package/dist/connectors/netsuite/vendor.d.ts +26 -0
  63. package/dist/connectors/netsuite/vendor.js +247 -0
  64. package/dist/connectors/netsuite/vendor.js.map +1 -0
  65. package/dist/connectors/odata/index.d.ts +33 -0
  66. package/dist/connectors/odata/index.js +460 -0
  67. package/dist/connectors/odata/index.js.map +1 -0
  68. package/dist/connectors/okta/connector.json +194 -0
  69. package/dist/connectors/okta/index.d.ts +8 -0
  70. package/dist/connectors/okta/index.js +7 -0
  71. package/dist/connectors/okta/index.js.map +1 -0
  72. package/dist/connectors/okta/tools.d.ts +6 -0
  73. package/dist/connectors/okta/tools.js +429 -0
  74. package/dist/connectors/okta/tools.js.map +1 -0
  75. package/dist/connectors/okta/vendor.d.ts +14 -0
  76. package/dist/connectors/okta/vendor.js +204 -0
  77. package/dist/connectors/okta/vendor.js.map +1 -0
  78. package/dist/connectors/outlook-calendar/index.d.ts +2 -0
  79. package/dist/connectors/outlook-calendar/tools.d.ts +65 -0
  80. package/dist/connectors/outlook-mail/index.d.ts +2 -0
  81. package/dist/connectors/outlook-mail/tools.d.ts +8 -0
  82. package/dist/connectors/salesforce/index.d.ts +8 -0
  83. package/dist/connectors/salesforce/soql.d.ts +3 -0
  84. package/dist/connectors/salesforce/tools.d.ts +9 -0
  85. package/dist/connectors/salesforce/vendor.d.ts +3 -0
  86. package/dist/connectors/salesforce/vendor.js +9 -3
  87. package/dist/connectors/salesforce/vendor.js.map +1 -1
  88. package/dist/connectors/sap-s4hana/connector.json +264 -0
  89. package/dist/connectors/sap-s4hana/index.d.ts +8 -0
  90. package/dist/connectors/sap-s4hana/index.js +7 -0
  91. package/dist/connectors/sap-s4hana/index.js.map +1 -0
  92. package/dist/connectors/sap-s4hana/tools.d.ts +4 -0
  93. package/dist/connectors/sap-s4hana/tools.js +761 -0
  94. package/dist/connectors/sap-s4hana/tools.js.map +1 -0
  95. package/dist/connectors/sap-s4hana/vendor.d.ts +5 -0
  96. package/dist/connectors/sap-s4hana/vendor.js +176 -0
  97. package/dist/connectors/sap-s4hana/vendor.js.map +1 -0
  98. package/dist/connectors/servicenow/connector.json +228 -0
  99. package/dist/connectors/servicenow/index.d.ts +8 -0
  100. package/dist/connectors/servicenow/index.js +7 -0
  101. package/dist/connectors/servicenow/index.js.map +1 -0
  102. package/dist/connectors/servicenow/tools.d.ts +4 -0
  103. package/dist/connectors/servicenow/tools.js +884 -0
  104. package/dist/connectors/servicenow/tools.js.map +1 -0
  105. package/dist/connectors/servicenow/vendor.d.ts +3 -0
  106. package/dist/connectors/servicenow/vendor.js +106 -0
  107. package/dist/connectors/servicenow/vendor.js.map +1 -0
  108. package/dist/connectors/sharepoint-files/excel.d.ts +16 -0
  109. package/dist/connectors/sharepoint-files/index.d.ts +8 -0
  110. package/dist/connectors/sharepoint-files/index.js.map +1 -1
  111. package/dist/connectors/sharepoint-files/tools.d.ts +52 -0
  112. package/dist/connectors/sharepoint-files/vendor.d.ts +11 -0
  113. package/dist/connectors/slack/connector.json +148 -0
  114. package/dist/connectors/slack/index.d.ts +8 -0
  115. package/dist/connectors/slack/index.js +7 -0
  116. package/dist/connectors/slack/index.js.map +1 -0
  117. package/dist/connectors/slack/tools.d.ts +3 -0
  118. package/dist/connectors/slack/tools.js +355 -0
  119. package/dist/connectors/slack/tools.js.map +1 -0
  120. package/dist/connectors/slack/vendor.d.ts +17 -0
  121. package/dist/connectors/slack/vendor.js +189 -0
  122. package/dist/connectors/slack/vendor.js.map +1 -0
  123. package/dist/connectors/soap/index.d.ts +16 -0
  124. package/dist/connectors/soap/index.js +248 -0
  125. package/dist/connectors/soap/index.js.map +1 -0
  126. package/dist/connectors/soap/xml.d.ts +12 -0
  127. package/dist/connectors/soap/xml.js +126 -0
  128. package/dist/connectors/soap/xml.js.map +1 -0
  129. package/dist/connectors/sql/engines.d.ts +58 -0
  130. package/dist/connectors/sql/engines.js +292 -0
  131. package/dist/connectors/sql/engines.js.map +1 -0
  132. package/dist/connectors/sql/index.d.ts +16 -0
  133. package/dist/connectors/sql/index.js +214 -0
  134. package/dist/connectors/sql/index.js.map +1 -0
  135. package/dist/connectors/teams/index.d.ts +2 -0
  136. package/dist/connectors/teams/tools.d.ts +7 -0
  137. package/dist/connectors/teams/tools.js +3 -2
  138. package/dist/connectors/teams/tools.js.map +1 -1
  139. package/dist/connectors/workday/connector.json +188 -0
  140. package/dist/connectors/workday/index.d.ts +8 -0
  141. package/dist/connectors/workday/index.js +7 -0
  142. package/dist/connectors/workday/index.js.map +1 -0
  143. package/dist/connectors/workday/tools.d.ts +33 -0
  144. package/dist/connectors/workday/tools.js +410 -0
  145. package/dist/connectors/workday/tools.js.map +1 -0
  146. package/dist/connectors/workday/vendor.d.ts +18 -0
  147. package/dist/connectors/workday/vendor.js +199 -0
  148. package/dist/connectors/workday/vendor.js.map +1 -0
  149. package/dist/packages/connector-kit/src/branding.d.ts +57 -0
  150. package/dist/packages/connector-kit/src/certificate.d.ts +19 -0
  151. package/dist/packages/connector-kit/src/common.d.ts +36 -0
  152. package/dist/packages/connector-kit/src/common.js +4 -1
  153. package/dist/packages/connector-kit/src/common.js.map +1 -1
  154. package/dist/packages/connector-kit/src/connector.d.ts +1356 -0
  155. package/dist/packages/connector-kit/src/connector.js +634 -53
  156. package/dist/packages/connector-kit/src/connector.js.map +1 -1
  157. package/dist/packages/connector-kit/src/fetch.d.ts +6 -0
  158. package/dist/packages/connector-kit/src/fetch.js +44 -0
  159. package/dist/packages/connector-kit/src/fetch.js.map +1 -0
  160. package/dist/packages/connector-kit/src/index.d.ts +5 -0
  161. package/dist/packages/connector-kit/src/index.js +6 -0
  162. package/dist/packages/connector-kit/src/index.js.map +1 -0
  163. package/dist/packages/connector-kit/src/oauth.d.ts +15 -0
  164. package/dist/packages/connector-kit/src/oauth.js +25 -6
  165. package/dist/packages/connector-kit/src/oauth.js.map +1 -1
  166. package/dist/packages/connector-kit/src/types.d.ts +87 -0
  167. package/dist/packages/connector-kit/src/types.js.map +1 -1
  168. package/dist/packages/server/migrations/0007_app_events.sql +110 -0
  169. package/dist/packages/server/migrations/rollback/to-legacy.sql +3 -0
  170. package/dist/packages/server/src/api.d.ts +3730 -0
  171. package/dist/packages/server/src/api.js +137 -1
  172. package/dist/packages/server/src/api.js.map +1 -1
  173. package/dist/packages/server/src/app.d.ts +23 -0
  174. package/dist/packages/server/src/app.js +24 -1
  175. package/dist/packages/server/src/app.js.map +1 -1
  176. package/dist/packages/server/src/builtin.d.ts +5 -0
  177. package/dist/packages/server/src/builtin.js +18 -0
  178. package/dist/packages/server/src/builtin.js.map +1 -1
  179. package/dist/packages/server/src/consent.d.ts +27 -0
  180. package/dist/packages/server/src/events/admin.d.ts +156 -0
  181. package/dist/packages/server/src/events/admin.js +255 -0
  182. package/dist/packages/server/src/events/admin.js.map +1 -0
  183. package/dist/packages/server/src/events/deliver.d.ts +43 -0
  184. package/dist/packages/server/src/events/deliver.js +129 -0
  185. package/dist/packages/server/src/events/deliver.js.map +1 -0
  186. package/dist/packages/server/src/events/runtime.d.ts +28 -0
  187. package/dist/packages/server/src/events/runtime.js +404 -0
  188. package/dist/packages/server/src/events/runtime.js.map +1 -0
  189. package/dist/packages/server/src/events/store.d.ts +172 -0
  190. package/dist/packages/server/src/events/store.js +275 -0
  191. package/dist/packages/server/src/events/store.js.map +1 -0
  192. package/dist/packages/server/src/index.d.ts +40 -0
  193. package/dist/packages/server/src/index.js +68 -11
  194. package/dist/packages/server/src/index.js.map +1 -1
  195. package/dist/packages/server/src/migrate.d.ts +11 -0
  196. package/dist/packages/server/src/openapi.d.ts +4 -0
  197. package/dist/packages/server/src/openapi.js +2 -2
  198. package/dist/packages/server/src/openapi.js.map +1 -1
  199. package/dist/packages/server/src/queue.d.ts +10 -0
  200. package/dist/packages/server/src/queue.js +45 -0
  201. package/dist/packages/server/src/queue.js.map +1 -0
  202. package/dist/packages/server/src/secrets.d.ts +19 -0
  203. package/dist/packages/server/src/server.d.ts +1 -0
  204. package/dist/packages/server/src/server.js +14 -1
  205. package/dist/packages/server/src/server.js.map +1 -1
  206. package/dist/packages/server/src/service.d.ts +767 -0
  207. package/dist/packages/server/src/service.js +19 -3
  208. package/dist/packages/server/src/service.js.map +1 -1
  209. package/dist/packages/server/src/store.d.ts +157 -0
  210. package/dist/packages/server/src/store.js +4 -0
  211. package/dist/packages/server/src/store.js.map +1 -1
  212. package/dist/packages/server/src/teams/activity.d.ts +123 -0
  213. package/dist/packages/server/src/teams/activity.js +6 -5
  214. package/dist/packages/server/src/teams/activity.js.map +1 -1
  215. package/dist/packages/server/src/teams/admin.d.ts +335 -0
  216. package/dist/packages/server/src/teams/admin.js +1 -1
  217. package/dist/packages/server/src/teams/admin.js.map +1 -1
  218. package/dist/packages/server/src/teams/approval.d.ts +57 -0
  219. package/dist/packages/server/src/teams/bot.d.ts +25 -0
  220. package/dist/packages/server/src/teams/claude.d.ts +46 -0
  221. package/dist/packages/server/src/teams/jwt.d.ts +17 -0
  222. package/dist/packages/server/src/teams/package.d.ts +79 -0
  223. package/dist/packages/server/src/teams/provision.d.ts +84 -0
  224. package/dist/packages/server/src/teams/runner.d.ts +80 -0
  225. package/dist/packages/server/src/teams/runner.js +50 -6
  226. package/dist/packages/server/src/teams/runner.js.map +1 -1
  227. package/dist/packages/server/src/teams/runtime.d.ts +91 -0
  228. package/dist/packages/server/src/teams/runtime.js +25 -47
  229. package/dist/packages/server/src/teams/runtime.js.map +1 -1
  230. package/dist/packages/server/src/teams/store.d.ts +233 -0
  231. package/package.json +28 -2
@@ -0,0 +1,806 @@
1
+ import { z } from "zod";
2
+ import { API, dataverseError } from "./vendor.js";
3
+ import { audited, Denied, json, listLimit, VendorFailed, } from "../../packages/connector-kit/src/common.js";
4
+ const GUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
5
+ const COLUMN = /^_?[a-z][a-z0-9_]{0,99}$/;
6
+ const TABLE = /^[a-z][a-z0-9_]{0,63}$/;
7
+ const ISO_DATE = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d{1,7})?)?(Z|[+-]\d{2}:\d{2}))?$/;
8
+ const FORMATTED = "@OData.Community.Display.V1.FormattedValue";
9
+ const LOGICAL = "@Microsoft.Dynamics.CRM.lookuplogicalname";
10
+ // Formatted values (option labels, money, lookup names) and lookups' table names.
11
+ const PREFER = 'odata.include-annotations="*"';
12
+ const SCOPE_COLUMNS = ["_owningbusinessunit_value", "_owningteam_value"];
13
+ const MAX_TEXT = 2000;
14
+ // The tables the built-in tools use: their entity set, id column, the columns a search answers
15
+ // and the ones a single read adds, and what a text search looks in.
16
+ const TABLES = {
17
+ account: {
18
+ set: "accounts",
19
+ id: "accountid",
20
+ text: ["name", "accountnumber"],
21
+ list: [
22
+ "name",
23
+ "accountnumber",
24
+ "telephone1",
25
+ "emailaddress1",
26
+ "websiteurl",
27
+ "address1_city",
28
+ "address1_country",
29
+ "_primarycontactid_value",
30
+ "_parentaccountid_value",
31
+ "_ownerid_value",
32
+ "statecode",
33
+ "modifiedon",
34
+ ],
35
+ detail: ["industrycode", "numberofemployees", "revenue", "description", "createdon"],
36
+ },
37
+ contact: {
38
+ set: "contacts",
39
+ id: "contactid",
40
+ text: ["fullname", "emailaddress1"],
41
+ list: [
42
+ "fullname",
43
+ "jobtitle",
44
+ "emailaddress1",
45
+ "telephone1",
46
+ "mobilephone",
47
+ "_parentcustomerid_value",
48
+ "address1_city",
49
+ "_ownerid_value",
50
+ "statecode",
51
+ "modifiedon",
52
+ ],
53
+ detail: ["firstname", "lastname", "address1_country", "description", "createdon"],
54
+ },
55
+ opportunity: {
56
+ set: "opportunities",
57
+ id: "opportunityid",
58
+ text: ["name"],
59
+ list: [
60
+ "name",
61
+ "_customerid_value",
62
+ "estimatedvalue",
63
+ "estimatedclosedate",
64
+ "closeprobability",
65
+ "stepname",
66
+ "_ownerid_value",
67
+ "statecode",
68
+ "statuscode",
69
+ "modifiedon",
70
+ ],
71
+ detail: ["actualvalue", "actualclosedate", "description", "createdon"],
72
+ },
73
+ incident: {
74
+ set: "incidents",
75
+ id: "incidentid",
76
+ text: ["title", "ticketnumber"],
77
+ list: [
78
+ "ticketnumber",
79
+ "title",
80
+ "_customerid_value",
81
+ "prioritycode",
82
+ "casetypecode",
83
+ "_ownerid_value",
84
+ "statecode",
85
+ "statuscode",
86
+ "createdon",
87
+ "modifiedon",
88
+ ],
89
+ detail: ["description", "_primarycontactid_value", "caseorigincode"],
90
+ },
91
+ };
92
+ const PARENTS = Object.keys(TABLES);
93
+ function scopeOf(ctx) {
94
+ const units = listLimit(ctx.limits, "business_units") ?? [];
95
+ const teams = listLimit(ctx.limits, "teams") ?? [];
96
+ return units.length || teams.length
97
+ ? {
98
+ units: units.map((u) => u.toLowerCase()),
99
+ teams: teams.map((t) => t.toLowerCase()),
100
+ }
101
+ : null;
102
+ }
103
+ export function scopeFilter(scope) {
104
+ return `(${[
105
+ ...scope.units.map((u) => `_owningbusinessunit_value eq ${u}`),
106
+ ...scope.teams.map((t) => `_owningteam_value eq ${t}`),
107
+ ].join(" or ")})`;
108
+ }
109
+ export function inScope(scope, row) {
110
+ if (!scope)
111
+ return true;
112
+ const unit = String(row._owningbusinessunit_value ?? "").toLowerCase();
113
+ const team = String(row._owningteam_value ?? "").toLowerCase();
114
+ return scope.units.includes(unit) || (team !== "" && scope.teams.includes(team));
115
+ }
116
+ const OUTSIDE = "This agent may not use that record";
117
+ /** One answer value as an OData literal: GUIDs, dates, numbers and booleans as they are, any
118
+ * other text quoted. */
119
+ export function literal(value) {
120
+ if (typeof value === "boolean")
121
+ return String(value);
122
+ if (typeof value === "number") {
123
+ if (!Number.isFinite(value))
124
+ throw new Denied("Numbers must be finite");
125
+ return String(value);
126
+ }
127
+ if (GUID.test(value) || ISO_DATE.test(value))
128
+ return value;
129
+ return `'${value.replaceAll("'", "''")}'`;
130
+ }
131
+ const filterSchema = z.object({
132
+ column: z
133
+ .string()
134
+ .describe("Column logical name, e.g. statecode or address1_city. A lookup is _<name>_value, e.g. _parentcustomerid_value (compare it to a record id)."),
135
+ op: z.enum(["eq", "ne", "gt", "ge", "lt", "le", "contains", "startswith", "null", "not_null"]),
136
+ value: z
137
+ .union([z.string().max(400), z.number(), z.boolean()])
138
+ .optional()
139
+ .describe("Not used with null and not_null. Ids, dates (2026-10-27 or 2026-10-27T09:00:00Z) and numbers are compared as such. Option set columns such as statecode compare their number, e.g. 0 for active."),
140
+ });
141
+ const filtersArg = z
142
+ .array(filterSchema)
143
+ .max(10)
144
+ .default([])
145
+ .describe("Conditions every record must meet. No free-form queries.");
146
+ function checkColumn(column) {
147
+ if (!COLUMN.test(column))
148
+ throw new Denied(`${column} is not a column logical name`);
149
+ return column;
150
+ }
151
+ export function filterExpression(filters) {
152
+ return filters.map(({ column, op, value }) => {
153
+ checkColumn(column);
154
+ if (op === "null" || op === "not_null")
155
+ return `${column} ${op === "null" ? "eq" : "ne"} null`;
156
+ if (value === undefined)
157
+ throw new Denied(`Give a value to compare ${column} with`);
158
+ if (op === "contains" || op === "startswith") {
159
+ if (typeof value !== "string")
160
+ throw new Denied(`${op} compares text`);
161
+ return `${op}(${column},${literal(value)})`;
162
+ }
163
+ return `${column} ${op} ${literal(value)}`;
164
+ });
165
+ }
166
+ async function ok(ctx, mark, req) {
167
+ const res = await ctx.call({
168
+ ...req,
169
+ headers: { Prefer: PREFER, ...req.headers },
170
+ });
171
+ mark(res.status);
172
+ if (res.status < 200 || res.status >= 300)
173
+ throw new VendorFailed(res.status, dataverseError(res.status, res.data));
174
+ return res.data;
175
+ }
176
+ /** A row as agents get it: lookups as {id, name, table}, option sets and money as their
177
+ * labels, text and dates as they are (long text shortened). */
178
+ export function trimRow(row, idColumn) {
179
+ const out = { id: row[idColumn] ?? null };
180
+ for (const [key, value] of Object.entries(row)) {
181
+ if (key.includes("@") || key === idColumn || SCOPE_COLUMNS.includes(key))
182
+ continue;
183
+ const label = row[`${key}${FORMATTED}`];
184
+ const lookup = /^_(.+)_value$/.exec(key);
185
+ if (lookup)
186
+ out[lookup[1]] =
187
+ value === null
188
+ ? null
189
+ : {
190
+ id: value,
191
+ name: label ?? null,
192
+ table: row[`${key}${LOGICAL}`] ?? null,
193
+ };
194
+ else if (typeof value === "string")
195
+ out[key] = value.length > MAX_TEXT ? `${value.slice(0, MAX_TEXT)}…` : value;
196
+ else
197
+ out[key] = value === null ? null : (label ?? value);
198
+ }
199
+ return out;
200
+ }
201
+ const select = (columns, scope) => [...new Set([...columns, ...(scope ? SCOPE_COLUMNS : [])])].join(",");
202
+ function idArg(id) {
203
+ if (!GUID.test(id))
204
+ throw new Denied("Give a record id (a GUID)");
205
+ return id.toLowerCase();
206
+ }
207
+ // One record, read only when the key may use it. Under a scope, a record not found gets the
208
+ // same refusal as one outside it, so a key cannot learn which ids exist.
209
+ async function readRecord(ctx, mark, table, id, columns) {
210
+ const scope = scopeOf(ctx);
211
+ const res = await ctx.call({
212
+ method: "GET",
213
+ path: `${API}/${table.set}(${idArg(id)})?$select=${select(columns, scope)}`,
214
+ headers: { Prefer: PREFER },
215
+ });
216
+ mark(res.status);
217
+ if (scope && res.status === 404)
218
+ throw new Denied(OUTSIDE);
219
+ if (res.status !== 200)
220
+ throw new VendorFailed(res.status, dataverseError(res.status, res.data));
221
+ if (!inScope(scope, res.data))
222
+ throw new Denied(OUTSIDE);
223
+ return res.data;
224
+ }
225
+ const nextOf = (data) => {
226
+ const link = data["@odata.nextLink"];
227
+ return typeof link === "string" ? new URL(link).searchParams.get("$skiptoken") : null;
228
+ };
229
+ // A page of rows. Dataverse pages with `odata.maxpagesize` and answers a skip token, which comes
230
+ // back as `cursor` with the same arguments. `unscoped`: rows of a record already checked against
231
+ // the key's scope (its activities and notes are the record's, whoever owns them).
232
+ async function listRows(ctx, mark, input) {
233
+ const scope = input.unscoped ? null : scopeOf(ctx);
234
+ const conditions = [...input.conditions, ...(scope ? [scopeFilter(scope)] : [])];
235
+ if (input.cursor !== undefined && (!input.cursor || input.cursor.length > 4000))
236
+ throw new Denied("Pass the cursor exactly as it was answered");
237
+ const query = [
238
+ `$select=${select(input.columns, scope)}`,
239
+ ...(conditions.length ? [`$filter=${encodeURIComponent(conditions.join(" and "))}`] : []),
240
+ `$orderby=${encodeURIComponent(input.orderBy)}`,
241
+ ...(input.cursor ? [`$skiptoken=${encodeURIComponent(input.cursor)}`] : []),
242
+ ].join("&");
243
+ const data = await ok(ctx, mark, {
244
+ method: "GET",
245
+ path: `${API}/${input.table.set}?${query}`,
246
+ headers: { Prefer: `${PREFER},odata.maxpagesize=${input.top}` },
247
+ });
248
+ const rows = (data.value ?? []).filter((r) => inScope(scope, r));
249
+ const next = nextOf(data);
250
+ return {
251
+ records: rows.map((r) => trimRow(r, input.table.id)),
252
+ more: Boolean(next),
253
+ next,
254
+ };
255
+ }
256
+ const textSearch = (columns, text) => `(${columns.map((c) => `contains(${c},${literal(text)})`).join(" or ")})`;
257
+ const topArg = z.number().int().min(1).max(50).default(20);
258
+ const cursorArg = z
259
+ .string()
260
+ .optional()
261
+ .describe("The `next` of the previous answer, with the same other arguments.");
262
+ const fieldValues = z
263
+ .record(z.string(), z.union([z.string().max(4000), z.number(), z.boolean(), z.null()]))
264
+ .describe('Column logical names and their values, e.g. {"telephone1": "+1 555 0100"}. Option sets take their number. Lookups cannot be set here.');
265
+ function checkFields(fields) {
266
+ const names = Object.keys(fields);
267
+ if (names.length === 0 || names.length > 50)
268
+ throw new Denied("Give between 1 and 50 columns");
269
+ // Plain columns only: a lookup (`…@odata.bind`) could reassign the record's owner or parent.
270
+ names.forEach(checkColumn);
271
+ const lookup = names.find((n) => n.startsWith("_"));
272
+ if (lookup)
273
+ throw new Denied(`${lookup} is a lookup, which cannot be set here`);
274
+ }
275
+ // Records an agent creates are owned by the application user (its own business unit) unless a
276
+ // team is named. Under a scope, the new record must land inside it.
277
+ async function owner(ctx, mark, team) {
278
+ const scope = scopeOf(ctx);
279
+ if (team) {
280
+ const id = idArg(team);
281
+ if (scope && !scope.teams.includes(id)) {
282
+ const found = await ctx.call({
283
+ method: "GET",
284
+ path: `${API}/teams(${id})?$select=_businessunitid_value`,
285
+ });
286
+ mark(found.status);
287
+ const unit = String(found.data?._businessunitid_value ?? "").toLowerCase();
288
+ if (found.status !== 200 || !scope.units.includes(unit))
289
+ throw new Denied("This agent may not give records to that team");
290
+ }
291
+ return { "ownerid@odata.bind": `/teams(${id})` };
292
+ }
293
+ if (!scope)
294
+ return {};
295
+ if (scope.units.length) {
296
+ const who = await ok(ctx, mark, { method: "GET", path: `${API}/WhoAmI` });
297
+ if (scope.units.includes(String(who.BusinessUnitId).toLowerCase()))
298
+ return {};
299
+ }
300
+ if (scope.teams.length === 1)
301
+ return { "ownerid@odata.bind": `/teams(${scope.teams[0]})` };
302
+ throw new Denied("A record created here would be outside this agent's business units and teams. Name one of its teams as owner_team.");
303
+ }
304
+ const annotations = {
305
+ readOnlyHint: false,
306
+ destructiveHint: false,
307
+ openWorldHint: true,
308
+ };
309
+ const readOnly = { readOnlyHint: true, openWorldHint: true };
310
+ const regarding = (kind, table, id) => ({
311
+ [`regardingobjectid_${table}_${kind}@odata.bind`]: `/${TABLES[table].set}(${id})`,
312
+ });
313
+ const ACTIVITY_COLUMNS = [
314
+ "activityid",
315
+ "activitytypecode",
316
+ "subject",
317
+ "description",
318
+ "scheduledend",
319
+ "actualend",
320
+ "statecode",
321
+ "_ownerid_value",
322
+ "createdon",
323
+ ];
324
+ const NOTE_COLUMNS = [
325
+ "annotationid",
326
+ "subject",
327
+ "notetext",
328
+ "filename",
329
+ "_createdby_value",
330
+ "createdon",
331
+ ];
332
+ // Tables an agent may delete from. Deleting a customer, opportunity or case also deletes the
333
+ // records Dynamics cascades to, which may be outside a scoped key, so a scoped key deletes only
334
+ // activities and notes, checked through the record they belong to.
335
+ const DELETABLE = {
336
+ ...Object.fromEntries(PARENTS.map((t) => [t, { set: TABLES[t].set, parent: null }])),
337
+ task: { set: "tasks", parent: "_regardingobjectid_value" },
338
+ phonecall: { set: "phonecalls", parent: "_regardingobjectid_value" },
339
+ annotation: { set: "annotations", parent: "_objectid_value" },
340
+ };
341
+ async function listedTable(ctx, mark, table) {
342
+ const tables = listLimit(ctx.limits, "tables");
343
+ if (!tables)
344
+ throw new Denied("This agent has no tables to read. An administrator lists them on its key.");
345
+ if (!TABLE.test(table) || !tables.includes(table))
346
+ throw new Denied(`This agent may not read ${table}. It may read: ${tables.join(", ")}`);
347
+ const def = (await ok(ctx, mark, {
348
+ method: "GET",
349
+ path: `${API}/EntityDefinitions(LogicalName='${table}')?$select=EntitySetName,PrimaryIdAttribute,PrimaryNameAttribute,OwnershipType`,
350
+ }));
351
+ if (!def.EntitySetName || !def.PrimaryIdAttribute)
352
+ throw new Error(`Dynamics 365 metadata for ${table} has no entity set`);
353
+ if (scopeOf(ctx) && def.OwnershipType !== "UserOwned")
354
+ throw new Denied(`${table} records have no owner, so an agent kept to business units or teams cannot read them`);
355
+ return {
356
+ set: def.EntitySetName,
357
+ id: def.PrimaryIdAttribute,
358
+ defaults: [
359
+ def.PrimaryIdAttribute,
360
+ ...(def.PrimaryNameAttribute ? [def.PrimaryNameAttribute] : []),
361
+ ],
362
+ };
363
+ }
364
+ const columnsArg = z
365
+ .array(z.string())
366
+ .max(30)
367
+ .optional()
368
+ .describe("Column logical names to return. The id and primary name if omitted.");
369
+ const tableArg = (tables) => z
370
+ .enum(tables)
371
+ .default(tables[0])
372
+ .describe(tables.length > 1 ? "account (a company) or contact (a person)." : `Always ${tables[0]}.`);
373
+ export const dynamicsTools = {
374
+ names: [
375
+ "dynamics_search_customers",
376
+ "dynamics_get_customer",
377
+ "dynamics_create_customer",
378
+ "dynamics_update_customer",
379
+ "dynamics_search_opportunities",
380
+ "dynamics_get_opportunity",
381
+ "dynamics_close_opportunity",
382
+ "dynamics_search_cases",
383
+ "dynamics_get_case",
384
+ "dynamics_close_case",
385
+ "dynamics_list_activities",
386
+ "dynamics_add_activity",
387
+ "dynamics_list_rows",
388
+ "dynamics_get_row",
389
+ "dynamics_delete_record",
390
+ ],
391
+ register(server, ctx, allowed) {
392
+ const search = (name, tables, what) => {
393
+ if (!allowed.has(name))
394
+ return;
395
+ server.registerTool(name, {
396
+ description: `Find ${what}, newest change first, by text and column conditions. When \`more\` is true, call again with \`cursor\` set to \`next\`.`,
397
+ inputSchema: z.object({
398
+ table: tableArg(tables),
399
+ text: z
400
+ .string()
401
+ .max(200)
402
+ .optional()
403
+ .describe(`Part of ${tables.map((t) => TABLES[t].text.join(" or ")).join("; or ")}.`),
404
+ filters: filtersArg,
405
+ top: topArg,
406
+ cursor: cursorArg,
407
+ }),
408
+ annotations: readOnly,
409
+ }, (args) => audited(ctx, name, JSON.stringify(args).slice(0, 300), async (mark) => {
410
+ const def = TABLES[args.table];
411
+ const conditions = [
412
+ ...(args.text ? [textSearch(def.text, args.text)] : []),
413
+ ...filterExpression(args.filters),
414
+ ];
415
+ return json(await listRows(ctx, mark, {
416
+ table: def,
417
+ columns: [def.id, ...def.list],
418
+ conditions,
419
+ orderBy: "modifiedon desc",
420
+ top: args.top,
421
+ cursor: args.cursor,
422
+ }));
423
+ }));
424
+ };
425
+ const get = (name, tables, what) => {
426
+ if (!allowed.has(name))
427
+ return;
428
+ server.registerTool(name, {
429
+ description: `Read one ${what} by id, with its description and status.`,
430
+ inputSchema: z.object({
431
+ table: tableArg(tables),
432
+ id: z.string().describe("The record id (a GUID)."),
433
+ }),
434
+ annotations: readOnly,
435
+ }, (args) => audited(ctx, name, `${args.table}/${args.id}`, async (mark) => {
436
+ const def = TABLES[args.table];
437
+ const row = await readRecord(ctx, mark, def, args.id, [
438
+ def.id,
439
+ ...def.list,
440
+ ...def.detail,
441
+ ]);
442
+ return json(trimRow(row, def.id));
443
+ }));
444
+ };
445
+ search("dynamics_search_customers", ["account", "contact"], "accounts or contacts");
446
+ get("dynamics_get_customer", ["account", "contact"], "account or contact");
447
+ search("dynamics_search_opportunities", ["opportunity"], "opportunities");
448
+ get("dynamics_get_opportunity", ["opportunity"], "opportunity");
449
+ search("dynamics_search_cases", ["incident"], "cases");
450
+ get("dynamics_get_case", ["incident"], "case");
451
+ const customerArgs = {
452
+ fields: fieldValues,
453
+ account_id: z
454
+ .string()
455
+ .optional()
456
+ .describe("Contacts only: the account the contact works for."),
457
+ };
458
+ async function accountBind(mark, table, accountId) {
459
+ if (accountId === undefined)
460
+ return {};
461
+ if (table !== "contact")
462
+ throw new Denied("account_id is for contacts");
463
+ await readRecord(ctx, mark, TABLES.account, accountId, [TABLES.account.id]);
464
+ return {
465
+ "parentcustomerid_account@odata.bind": `/accounts(${idArg(accountId)})`,
466
+ };
467
+ }
468
+ if (allowed.has("dynamics_create_customer"))
469
+ server.registerTool("dynamics_create_customer", {
470
+ description: "Create an account (a company) or a contact (a person). Accounts need `name`; contacts need `lastname`. Returns the new record.",
471
+ inputSchema: z.object({
472
+ table: z.enum(["account", "contact"]),
473
+ ...customerArgs,
474
+ owner_team: z
475
+ .string()
476
+ .optional()
477
+ .describe("A team id to own the record. Needed when this agent is kept to several teams."),
478
+ }),
479
+ annotations,
480
+ }, ({ table, fields, account_id, owner_team }) => audited(ctx, "dynamics_create_customer", table, async (mark, retarget) => {
481
+ checkFields(fields);
482
+ const def = TABLES[table];
483
+ const body = {
484
+ ...fields,
485
+ ...(await accountBind(mark, table, account_id)),
486
+ ...(await owner(ctx, mark, owner_team)),
487
+ };
488
+ const created = await ok(ctx, mark, {
489
+ method: "POST",
490
+ path: `${API}/${def.set}?$select=${[def.id, ...def.list].join(",")}`,
491
+ body,
492
+ headers: { Prefer: `${PREFER},return=representation` },
493
+ });
494
+ retarget(`${table}/${created[def.id]}`);
495
+ return json(trimRow(created, def.id));
496
+ }));
497
+ if (allowed.has("dynamics_update_customer"))
498
+ server.registerTool("dynamics_update_customer", {
499
+ description: "Change columns of an existing account or contact, e.g. a phone number or address. Never creates a record.",
500
+ inputSchema: z.object({
501
+ table: z.enum(["account", "contact"]),
502
+ id: z.string().describe("The record id (a GUID)."),
503
+ ...customerArgs,
504
+ }),
505
+ annotations,
506
+ }, ({ table, id, fields, account_id }) => audited(ctx, "dynamics_update_customer", `${table}/${id}`, async (mark) => {
507
+ checkFields(fields);
508
+ const def = TABLES[table];
509
+ await readRecord(ctx, mark, def, id, [def.id]);
510
+ const body = {
511
+ ...fields,
512
+ ...(await accountBind(mark, table, account_id)),
513
+ };
514
+ // If-Match: an update to an id that does not exist would otherwise create it.
515
+ await ok(ctx, mark, {
516
+ method: "PATCH",
517
+ path: `${API}/${def.set}(${idArg(id)})`,
518
+ body,
519
+ headers: { "If-Match": "*" },
520
+ });
521
+ return json({
522
+ updated: true,
523
+ id: idArg(id),
524
+ columns: Object.keys(fields),
525
+ });
526
+ }));
527
+ if (allowed.has("dynamics_close_opportunity"))
528
+ server.registerTool("dynamics_close_opportunity", {
529
+ description: "Close an open opportunity as won or lost. Dynamics records the close with its subject and, for a win, the actual revenue.",
530
+ inputSchema: z.object({
531
+ id: z.string().describe("The opportunity id (a GUID)."),
532
+ outcome: z.enum(["won", "lost"]),
533
+ subject: z.string().min(1).max(200).describe("Why it closed, in a few words."),
534
+ description: z.string().max(2000).optional(),
535
+ actual_revenue: z.number().nonnegative().optional().describe("Won only."),
536
+ }),
537
+ annotations,
538
+ }, ({ id, outcome, subject, description, actual_revenue }) => audited(ctx, "dynamics_close_opportunity", `opportunity/${id} ${outcome}`, async (mark) => {
539
+ if (outcome === "lost" && actual_revenue !== undefined)
540
+ throw new Denied("actual_revenue is for a won opportunity");
541
+ await readRecord(ctx, mark, TABLES.opportunity, id, [TABLES.opportunity.id]);
542
+ // Won is status 3, lost 4: Dynamics' default status reasons for those states.
543
+ await ok(ctx, mark, {
544
+ method: "POST",
545
+ path: `${API}/${outcome === "won" ? "WinOpportunity" : "LoseOpportunity"}`,
546
+ body: {
547
+ Status: outcome === "won" ? 3 : 4,
548
+ OpportunityClose: {
549
+ subject,
550
+ ...(description ? { description } : {}),
551
+ ...(actual_revenue !== undefined ? { actualrevenue: actual_revenue } : {}),
552
+ "opportunityid@odata.bind": `/opportunities(${idArg(id)})`,
553
+ },
554
+ },
555
+ });
556
+ return json({ closed: true, id: idArg(id), outcome });
557
+ }));
558
+ if (allowed.has("dynamics_close_case"))
559
+ server.registerTool("dynamics_close_case", {
560
+ description: "Resolve a case with a resolution, or cancel it. Resolving records the resolution on the case.",
561
+ inputSchema: z.object({
562
+ id: z.string().describe("The case id (a GUID)."),
563
+ outcome: z.enum(["resolved", "cancelled"]),
564
+ resolution: z
565
+ .string()
566
+ .min(1)
567
+ .max(200)
568
+ .optional()
569
+ .describe("Resolved only: the resolution."),
570
+ description: z.string().max(2000).optional(),
571
+ }),
572
+ annotations,
573
+ }, ({ id, outcome, resolution, description }) => audited(ctx, "dynamics_close_case", `incident/${id} ${outcome}`, async (mark) => {
574
+ if (outcome === "resolved" && !resolution)
575
+ throw new Denied("Give the resolution of a resolved case");
576
+ await readRecord(ctx, mark, TABLES.incident, id, [TABLES.incident.id]);
577
+ const case_ = idArg(id);
578
+ if (outcome === "resolved")
579
+ // Status 5 is Problem Solved, the default resolved reason.
580
+ await ok(ctx, mark, {
581
+ method: "POST",
582
+ path: `${API}/CloseIncident`,
583
+ body: {
584
+ Status: 5,
585
+ IncidentResolution: {
586
+ subject: resolution,
587
+ ...(description ? { description } : {}),
588
+ "incidentid@odata.bind": `/incidents(${case_})`,
589
+ },
590
+ },
591
+ });
592
+ else
593
+ await ok(ctx, mark, {
594
+ method: "PATCH",
595
+ path: `${API}/incidents(${case_})`,
596
+ body: { statecode: 2, statuscode: 6 },
597
+ headers: { "If-Match": "*" },
598
+ });
599
+ return json({ closed: true, id: case_, outcome });
600
+ }));
601
+ const parentArgs = {
602
+ table: z
603
+ .enum(PARENTS)
604
+ .describe("The table of the record: account, contact, opportunity or incident (a case)."),
605
+ id: z.string().describe("The record id (a GUID)."),
606
+ };
607
+ if (allowed.has("dynamics_list_activities"))
608
+ server.registerTool("dynamics_list_activities", {
609
+ description: "List the activities (tasks, phone calls, emails, appointments) or the notes of an account, contact, opportunity or case, newest first. When `more` is true, call again with `cursor` set to `next`.",
610
+ inputSchema: z.object({
611
+ ...parentArgs,
612
+ kind: z.enum(["activities", "notes"]).default("activities"),
613
+ top: topArg,
614
+ cursor: cursorArg,
615
+ }),
616
+ annotations: readOnly,
617
+ }, ({ table, id, kind, top, cursor }) => audited(ctx, "dynamics_list_activities", `${table}/${id} ${kind}`, async (mark) => {
618
+ await readRecord(ctx, mark, TABLES[table], id, [TABLES[table].id]);
619
+ const notes = kind === "notes";
620
+ const page = await listRows(ctx, mark, {
621
+ table: notes
622
+ ? { set: "annotations", id: "annotationid" }
623
+ : { set: "activitypointers", id: "activityid" },
624
+ columns: notes ? NOTE_COLUMNS : ACTIVITY_COLUMNS,
625
+ conditions: [
626
+ `${notes ? "_objectid_value" : "_regardingobjectid_value"} eq ${idArg(id)}`,
627
+ ],
628
+ orderBy: "createdon desc",
629
+ top,
630
+ cursor,
631
+ unscoped: true,
632
+ });
633
+ return json(page);
634
+ }));
635
+ if (allowed.has("dynamics_add_activity"))
636
+ server.registerTool("dynamics_add_activity", {
637
+ description: "Add a note, a task or a logged phone call to an account, contact, opportunity or case.",
638
+ inputSchema: z.object({
639
+ ...parentArgs,
640
+ kind: z.enum(["note", "task", "phone_call"]),
641
+ subject: z.string().min(1).max(200),
642
+ text: z
643
+ .string()
644
+ .max(10000)
645
+ .optional()
646
+ .describe("The note, or the activity's description."),
647
+ due: z
648
+ .string()
649
+ .optional()
650
+ .describe("Tasks only: when it is due, e.g. 2026-11-03T17:00:00Z."),
651
+ phone_number: z.string().max(50).optional().describe("Phone calls only."),
652
+ direction: z
653
+ .enum(["outgoing", "incoming"])
654
+ .default("outgoing")
655
+ .describe("Phone calls only."),
656
+ completed: z
657
+ .boolean()
658
+ .default(true)
659
+ .describe("Phone calls only: false keeps it open as a call still to make."),
660
+ }),
661
+ annotations,
662
+ }, (args) => audited(ctx, "dynamics_add_activity", `${args.table}/${args.id} ${args.kind}`, async (mark, retarget) => {
663
+ const { table, id, kind, subject, text } = args;
664
+ if (args.due !== undefined && (kind !== "task" || !ISO_DATE.test(args.due)))
665
+ throw new Denied("due is a date or time such as 2026-11-03T17:00:00Z, for tasks");
666
+ if (args.phone_number !== undefined && kind !== "phone_call")
667
+ throw new Denied("phone_number is for phone calls");
668
+ await readRecord(ctx, mark, TABLES[table], id, [TABLES[table].id]);
669
+ const record = idArg(id);
670
+ const prefer = { Prefer: `${PREFER},return=representation` };
671
+ let created;
672
+ if (kind === "note") {
673
+ const note = await ok(ctx, mark, {
674
+ method: "POST",
675
+ path: `${API}/annotations?$select=annotationid`,
676
+ body: {
677
+ subject,
678
+ ...(text ? { notetext: text } : {}),
679
+ [`objectid_${table}@odata.bind`]: `/${TABLES[table].set}(${record})`,
680
+ },
681
+ headers: prefer,
682
+ });
683
+ created = String(note.annotationid);
684
+ }
685
+ else {
686
+ const set = kind === "task" ? "tasks" : "phonecalls";
687
+ const activity = await ok(ctx, mark, {
688
+ method: "POST",
689
+ path: `${API}/${set}?$select=activityid`,
690
+ body: {
691
+ subject,
692
+ ...(text ? { description: text } : {}),
693
+ ...(kind === "task"
694
+ ? { ...(args.due ? { scheduledend: args.due } : {}) }
695
+ : {
696
+ directioncode: args.direction === "outgoing",
697
+ ...(args.phone_number ? { phonenumber: args.phone_number } : {}),
698
+ }),
699
+ ...regarding(kind === "task" ? "task" : "phonecall", table, record),
700
+ },
701
+ headers: prefer,
702
+ });
703
+ created = String(activity.activityid);
704
+ // A logged call is a call that happened: Completed (state 1, status 2).
705
+ if (kind === "phone_call" && args.completed)
706
+ await ok(ctx, mark, {
707
+ method: "PATCH",
708
+ path: `${API}/phonecalls(${created})`,
709
+ body: { statecode: 1, statuscode: 2 },
710
+ headers: { "If-Match": "*" },
711
+ });
712
+ }
713
+ retarget(`${table}/${record} ${kind} ${created}`);
714
+ return json({ id: created, kind });
715
+ }));
716
+ if (allowed.has("dynamics_list_rows"))
717
+ server.registerTool("dynamics_list_rows", {
718
+ description: "List rows of a table this agent may read (its key lists them), with column conditions. When `more` is true, call again with `cursor` set to `next`.",
719
+ inputSchema: z.object({
720
+ table: z.string().describe("Table logical name, e.g. product or new_project."),
721
+ columns: columnsArg,
722
+ filters: filtersArg,
723
+ order_by: z
724
+ .string()
725
+ .optional()
726
+ .describe("A column to sort by. The table's own order if omitted."),
727
+ descending: z.boolean().default(false),
728
+ top: topArg,
729
+ cursor: cursorArg,
730
+ }),
731
+ annotations: readOnly,
732
+ }, (args) => audited(ctx, "dynamics_list_rows", `${args.table}: ${JSON.stringify(args).slice(0, 250)}`, async (mark) => {
733
+ const columns = (args.columns ?? []).map(checkColumn);
734
+ const conditions = filterExpression(args.filters);
735
+ const order = args.order_by ? checkColumn(args.order_by) : null;
736
+ const table = await listedTable(ctx, mark, args.table);
737
+ return json(await listRows(ctx, mark, {
738
+ table,
739
+ columns: columns.length ? [table.id, ...columns] : table.defaults,
740
+ conditions,
741
+ orderBy: `${order ?? table.id} ${args.descending ? "desc" : "asc"}`,
742
+ top: args.top,
743
+ cursor: args.cursor,
744
+ }));
745
+ }));
746
+ if (allowed.has("dynamics_get_row"))
747
+ server.registerTool("dynamics_get_row", {
748
+ description: "Read one row of a table this agent may read (its key lists them) by id.",
749
+ inputSchema: z.object({
750
+ table: z.string().describe("Table logical name, e.g. product or new_project."),
751
+ id: z.string().describe("The row id (a GUID)."),
752
+ columns: columnsArg,
753
+ }),
754
+ annotations: readOnly,
755
+ }, ({ table, id, columns }) => audited(ctx, "dynamics_get_row", `${table}/${id}`, async (mark) => {
756
+ const picked = (columns ?? []).map(checkColumn);
757
+ idArg(id);
758
+ const def = await listedTable(ctx, mark, table);
759
+ const row = await readRecord(ctx, mark, def, id, picked.length ? [def.id, ...picked] : def.defaults);
760
+ return json(trimRow(row, def.id));
761
+ }));
762
+ if (allowed.has("dynamics_delete_record"))
763
+ server.registerTool("dynamics_delete_record", {
764
+ description: "Delete an account, contact, opportunity, case (incident), task, phone call or note (annotation). Dynamics also deletes the records that belong to it, such as its activities and notes. It cannot be undone.",
765
+ inputSchema: z.object({
766
+ table: z.enum(Object.keys(DELETABLE)),
767
+ id: z.string().describe("The record id (a GUID)."),
768
+ }),
769
+ annotations: {
770
+ readOnlyHint: false,
771
+ destructiveHint: true,
772
+ openWorldHint: true,
773
+ },
774
+ }, ({ table, id }) => audited(ctx, "dynamics_delete_record", `${table}/${id}`, async (mark) => {
775
+ const target = DELETABLE[table];
776
+ const record = idArg(id);
777
+ if (scopeOf(ctx)) {
778
+ if (!target.parent)
779
+ throw new Denied(`An agent kept to business units or teams cannot delete ${table} records: Dynamics would also delete records that belong to it, which may be outside them`);
780
+ const found = await ctx.call({
781
+ method: "GET",
782
+ path: `${API}/${target.set}(${record})?$select=${target.parent}`,
783
+ headers: { Prefer: PREFER },
784
+ });
785
+ mark(found.status);
786
+ if (found.status === 404)
787
+ throw new Denied(OUTSIDE);
788
+ if (found.status !== 200)
789
+ throw new VendorFailed(found.status, dataverseError(found.status, found.data));
790
+ const row = found.data;
791
+ const parentTable = row[`${target.parent}${LOGICAL}`];
792
+ const parentId = row[target.parent];
793
+ if (!parentTable || !(parentTable in TABLES) || typeof parentId !== "string")
794
+ throw new Denied(OUTSIDE);
795
+ const def = TABLES[parentTable];
796
+ await readRecord(ctx, mark, def, parentId, [def.id]);
797
+ }
798
+ await ok(ctx, mark, {
799
+ method: "DELETE",
800
+ path: `${API}/${target.set}(${record})`,
801
+ });
802
+ return json({ deleted: true, table, id: record });
803
+ }));
804
+ },
805
+ };
806
+ //# sourceMappingURL=tools.js.map