tdoms-mcp 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +164 -0
- package/package.json +52 -0
- package/skills/tdoms-mcp-routing/SKILL.md +48 -0
- package/skills/tdoms-mcp-routing/agents/openai.yaml +4 -0
- package/src/capability-registry.js +124 -0
- package/src/client-manager.js +571 -0
- package/src/config.js +49 -0
- package/src/connection-store.js +126 -0
- package/src/index.js +79 -0
- package/src/mcp-server.js +2771 -0
- package/src/secret-store.js +109 -0
- package/src/storage-utils.js +176 -0
- package/src/tdoms-client.js +172 -0
- package/src/ui-page.js +408 -0
- package/src/web-app.js +127 -0
|
@@ -0,0 +1,2771 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { CAPABILITIES, CAPABILITY_IDS, OPTION_SETS, getCapability } from "./capability-registry.js";
|
|
6
|
+
import { ConnectionStore } from "./connection-store.js";
|
|
7
|
+
import { TdomsClient, summarizeSystemInfo } from "./tdoms-client.js";
|
|
8
|
+
|
|
9
|
+
const INSTRUCTIONS = [
|
|
10
|
+
"TD/OMS MCP connects agents to locally configured TD/OMS REST profiles.",
|
|
11
|
+
"Use tdoms_get_current_context first; it automatically selects the only saved connection and derives the TD/OMS programmer identity.",
|
|
12
|
+
"Use tdoms_discover_options before choosing site-specific TD/OMS values, then plan and execute mutations through the workflow tools.",
|
|
13
|
+
"Never ask the user to paste credentials into agent chat; credentials should be saved through the localhost UI.",
|
|
14
|
+
"Tools that create, save, queue, release, process, or delete require confirm: true."
|
|
15
|
+
].join(" ");
|
|
16
|
+
|
|
17
|
+
const fieldSelectionSchema = z.object({
|
|
18
|
+
field: z.string().optional().describe("Normalized TD/OMS field name, such as applicationCode or fixNumber."),
|
|
19
|
+
value: z.string().optional().describe("Comparison value."),
|
|
20
|
+
operator: z.enum([
|
|
21
|
+
"IN",
|
|
22
|
+
"NOTIN",
|
|
23
|
+
"BETWEEN",
|
|
24
|
+
"NOTBETWEEN",
|
|
25
|
+
"REGEX",
|
|
26
|
+
"EQ",
|
|
27
|
+
"LT",
|
|
28
|
+
"GT",
|
|
29
|
+
"LE",
|
|
30
|
+
"GE",
|
|
31
|
+
"NE",
|
|
32
|
+
"STARTSWITH",
|
|
33
|
+
"ENDSWITH",
|
|
34
|
+
"CONTAINS",
|
|
35
|
+
"NOTSTARTSWITH",
|
|
36
|
+
"NOTENDSWITH",
|
|
37
|
+
"NOTCONTAINS",
|
|
38
|
+
"AND",
|
|
39
|
+
"OR"
|
|
40
|
+
]).default("IN"),
|
|
41
|
+
group: z.number().optional(),
|
|
42
|
+
parentGroup: z.number().optional(),
|
|
43
|
+
ignoreCase: z.boolean().optional()
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const sortFieldSchema = z.object({
|
|
47
|
+
fieldName: z.string(),
|
|
48
|
+
sortBy: z.enum(["ASC", "DESC"]).default("ASC")
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const sourceObjectSchema = {
|
|
52
|
+
applicationCode: z.string().optional().describe("TD/OMS application code. Required when reading source connected to a task."),
|
|
53
|
+
taskNumber: z.string().optional().describe("TD/OMS task/fix number. Required when reading source connected to a task."),
|
|
54
|
+
path: z.string().optional().describe("Optional IFS path for IFS source."),
|
|
55
|
+
objectCode: z.string().optional().describe("Object or source file code, such as QRPGLESRC."),
|
|
56
|
+
objectType: z.string().optional().describe("Object type, such as *PGM, *FILE, *MODULE, or *SRVPGM."),
|
|
57
|
+
objectLibrary: z.string().optional().describe("Object/source library."),
|
|
58
|
+
memberCode: z.string().optional().describe("Member/detail name. Use *NONE when there is no member."),
|
|
59
|
+
pageNumber: z.number().int().min(0).default(1).describe("Use 0 to fetch metadata without source.")
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const buildObjectSchema = {
|
|
63
|
+
applicationCode: z.string().describe("TD/OMS application code."),
|
|
64
|
+
taskNumber: z.string().describe("TD/OMS task/fix number."),
|
|
65
|
+
objectCode: z.string().optional().describe("Object code. Leave blank to operate on the whole task."),
|
|
66
|
+
objectType: z.string().optional().describe("Object type, such as *PGM, *FILE, *MODULE, or *SRVPGM."),
|
|
67
|
+
objectLibrary: z.string().optional().describe("Object library."),
|
|
68
|
+
detail: z.string().optional().describe("Detail/member, if any.")
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export function createMcpServer({
|
|
72
|
+
store = new ConnectionStore(),
|
|
73
|
+
clientFactory = (connection) => new TdomsClient(connection),
|
|
74
|
+
profile = process.env.TDOMS_MCP_PROFILE || "core",
|
|
75
|
+
clock = () => Date.now()
|
|
76
|
+
} = {}) {
|
|
77
|
+
const normalizedProfile = normalizeProfile(profile);
|
|
78
|
+
const workflowPlans = new Map();
|
|
79
|
+
const server = new McpServer(
|
|
80
|
+
{
|
|
81
|
+
name: "tdoms-mcp",
|
|
82
|
+
version: "0.3.0"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
instructions: INSTRUCTIONS
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
server.registerTool(
|
|
90
|
+
"tdoms_list_connections",
|
|
91
|
+
{
|
|
92
|
+
description: "List locally saved TD/OMS connection profiles without returning secrets.",
|
|
93
|
+
inputSchema: {}
|
|
94
|
+
},
|
|
95
|
+
async () => {
|
|
96
|
+
const connections = await store.listConnections();
|
|
97
|
+
return result({ connections });
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
registerAgentTools(server, { store, clientFactory, workflowPlans, clock, profile: normalizedProfile });
|
|
102
|
+
|
|
103
|
+
if (normalizedProfile === "core") {
|
|
104
|
+
return server;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
server.registerTool(
|
|
108
|
+
"tdoms_call_api",
|
|
109
|
+
{
|
|
110
|
+
description: "Call a documented TD/OMS REST endpoint directly. Requires confirm: true because most TD/OMS APIs are POST and may mutate state.",
|
|
111
|
+
inputSchema: {
|
|
112
|
+
connectionId: z.string(),
|
|
113
|
+
service: z.string().describe("TD/OMS service code, such as OMQRTA, OMQRTO, OMQRSO, OMQRSF, OMQROB, or OMRRQS."),
|
|
114
|
+
path: z.string().describe("Endpoint path, such as /change, /select, /list, or /progress."),
|
|
115
|
+
body: z.record(z.string(), z.unknown()).default({}),
|
|
116
|
+
confirm: z.boolean().describe("Must be true to call a generic TD/OMS endpoint.")
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
async (input) => {
|
|
120
|
+
requireConfirmation(input.confirm, `call TD/OMS ${input.service}${input.path}`);
|
|
121
|
+
const response = await tdomsPost(store, input.connectionId, input.service, input.path, input.body, clientFactory);
|
|
122
|
+
return result({ ok: true, response });
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
server.registerTool(
|
|
127
|
+
"tdoms_login_connection",
|
|
128
|
+
{
|
|
129
|
+
description: "Login to a saved TD/OMS connection using locally stored credentials, save the JWT, and probe OMSINFO/system.",
|
|
130
|
+
inputSchema: {
|
|
131
|
+
connectionId: z.string().describe("Saved TD/OMS connection id from tdoms_list_connections.")
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
async ({ connectionId }) => {
|
|
135
|
+
const { connection, client, token } = await loginAndSave(store, connectionId, clientFactory);
|
|
136
|
+
const systemInfo = await client.getSystemInfo(token);
|
|
137
|
+
await store.updateStatus(connection.id, {
|
|
138
|
+
lastStatus: "system-probe-ok",
|
|
139
|
+
lastSystemProbeAt: new Date().toISOString()
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return result({
|
|
143
|
+
ok: true,
|
|
144
|
+
connection: await store.getConnection(connection.id),
|
|
145
|
+
system: summarizeSystemInfo(systemInfo)
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
server.registerTool(
|
|
151
|
+
"tdoms_get_system_info",
|
|
152
|
+
{
|
|
153
|
+
description: "Fetch TD/OMS OMSINFO/system for a saved connection. Automatically logs in again if needed.",
|
|
154
|
+
inputSchema: {
|
|
155
|
+
connectionId: z.string().describe("Saved TD/OMS connection id from tdoms_list_connections."),
|
|
156
|
+
applications: z.string().optional().describe("Optional comma-separated application codes to load.")
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
async ({ connectionId, applications }) => {
|
|
160
|
+
const suffix = applications ? `/system?applications=${encodeURIComponent(applications)}` : "/system";
|
|
161
|
+
const systemInfo = await tdomsGet(store, connectionId, "OMSINFO", suffix, clientFactory);
|
|
162
|
+
await store.updateStatus(connectionId, {
|
|
163
|
+
lastStatus: "system-probe-ok",
|
|
164
|
+
lastSystemProbeAt: new Date().toISOString()
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return result({
|
|
168
|
+
ok: true,
|
|
169
|
+
system: summarizeSystemInfo(systemInfo),
|
|
170
|
+
raw: systemInfo
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
server.registerTool(
|
|
176
|
+
"tdoms_query_database",
|
|
177
|
+
{
|
|
178
|
+
description: "Read TD/OMS table data through OMRDBA/get. Use for discovery when a specific higher-level tool is not enough.",
|
|
179
|
+
inputSchema: {
|
|
180
|
+
connectionId: z.string(),
|
|
181
|
+
table: z.string().describe("TD/OMS table or view name, such as OMXFix."),
|
|
182
|
+
returnFields: z.array(z.string()).optional().describe("Normalized field names to return. Leave empty for all fields."),
|
|
183
|
+
fieldSelections: z.array(fieldSelectionSchema).optional(),
|
|
184
|
+
whereClause: z.string().optional(),
|
|
185
|
+
sortFields: z.array(sortFieldSchema).optional(),
|
|
186
|
+
pageNumber: z.number().int().min(1).default(1),
|
|
187
|
+
recordsPerPage: z.number().int().min(1).max(50).default(25),
|
|
188
|
+
returnTotalRecords: z.boolean().default(true),
|
|
189
|
+
distinct: z.boolean().optional(),
|
|
190
|
+
schemaOverride: z.string().optional()
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
async (input) => {
|
|
194
|
+
const response = await queryDatabase(store, input.connectionId, {
|
|
195
|
+
table: input.table,
|
|
196
|
+
returnFields: input.returnFields,
|
|
197
|
+
fieldSelections: input.fieldSelections,
|
|
198
|
+
whereClause: input.whereClause,
|
|
199
|
+
sortFields: input.sortFields,
|
|
200
|
+
pageNumber: input.pageNumber,
|
|
201
|
+
recordsPerPage: input.recordsPerPage,
|
|
202
|
+
returnTotalRecords: input.returnTotalRecords,
|
|
203
|
+
distinct: input.distinct,
|
|
204
|
+
schemaOverride: input.schemaOverride
|
|
205
|
+
}, clientFactory);
|
|
206
|
+
return result({ ok: true, response });
|
|
207
|
+
}
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
server.registerTool(
|
|
211
|
+
"tdoms_search_tasks",
|
|
212
|
+
{
|
|
213
|
+
description: "Search TD/OMS tasks/fixes using the OMRDBA OMXFix table.",
|
|
214
|
+
inputSchema: {
|
|
215
|
+
connectionId: z.string(),
|
|
216
|
+
applicationCode: z.string().optional(),
|
|
217
|
+
taskNumber: z.string().optional().describe("Task/fix number."),
|
|
218
|
+
programmer: z.string().optional(),
|
|
219
|
+
status: z.string().optional().describe("TD/OMS fix status, if known."),
|
|
220
|
+
text: z.string().optional().describe("Text to search in the short task description."),
|
|
221
|
+
pageNumber: z.number().int().min(1).default(1),
|
|
222
|
+
recordsPerPage: z.number().int().min(1).max(50).default(25)
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
async (input) => {
|
|
226
|
+
const fieldSelections = compactArray([
|
|
227
|
+
input.applicationCode && { field: "applicationCode", value: input.applicationCode, operator: "EQ" },
|
|
228
|
+
input.taskNumber && { field: "fixNumber", value: input.taskNumber, operator: "EQ" },
|
|
229
|
+
input.programmer && { field: "programmer", value: input.programmer, operator: "EQ" },
|
|
230
|
+
input.status && { field: "fixStatus", value: input.status, operator: "EQ" },
|
|
231
|
+
input.text && { field: "shortFixDescription", value: input.text, operator: "CONTAINS", ignoreCase: true }
|
|
232
|
+
]);
|
|
233
|
+
|
|
234
|
+
const response = await queryDatabase(store, input.connectionId, {
|
|
235
|
+
table: "OMXFix",
|
|
236
|
+
returnFields: [
|
|
237
|
+
"applicationCode",
|
|
238
|
+
"fixNumber",
|
|
239
|
+
"shortFixDescription",
|
|
240
|
+
"programmer",
|
|
241
|
+
"fixStatus",
|
|
242
|
+
"release",
|
|
243
|
+
"pathCode",
|
|
244
|
+
"expectedStartDate",
|
|
245
|
+
"expectedCompletionDate"
|
|
246
|
+
],
|
|
247
|
+
fieldSelections,
|
|
248
|
+
sortFields: [
|
|
249
|
+
{ fieldName: "applicationCode", sortBy: "ASC" },
|
|
250
|
+
{ fieldName: "fixNumber", sortBy: "DESC" }
|
|
251
|
+
],
|
|
252
|
+
pageNumber: input.pageNumber ?? 1,
|
|
253
|
+
recordsPerPage: input.recordsPerPage ?? 25,
|
|
254
|
+
returnTotalRecords: true
|
|
255
|
+
}, clientFactory);
|
|
256
|
+
|
|
257
|
+
return result({ ok: true, response });
|
|
258
|
+
}
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
server.registerTool(
|
|
262
|
+
"tdoms_get_task",
|
|
263
|
+
{
|
|
264
|
+
description: "Fetch one TD/OMS task/fix from OMXFix by applicationCode and taskNumber.",
|
|
265
|
+
inputSchema: {
|
|
266
|
+
connectionId: z.string(),
|
|
267
|
+
applicationCode: z.string(),
|
|
268
|
+
taskNumber: z.string()
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
async ({ connectionId, applicationCode, taskNumber }) => {
|
|
272
|
+
const response = await queryDatabase(store, connectionId, {
|
|
273
|
+
table: "OMXFix",
|
|
274
|
+
fieldSelections: [
|
|
275
|
+
{ field: "applicationCode", value: applicationCode, operator: "EQ" },
|
|
276
|
+
{ field: "fixNumber", value: taskNumber, operator: "EQ" }
|
|
277
|
+
],
|
|
278
|
+
pageNumber: 1,
|
|
279
|
+
recordsPerPage: 1,
|
|
280
|
+
returnTotalRecords: true
|
|
281
|
+
}, clientFactory);
|
|
282
|
+
return result({ ok: true, response });
|
|
283
|
+
}
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
server.registerTool(
|
|
287
|
+
"tdoms_create_task",
|
|
288
|
+
{
|
|
289
|
+
description: "Create a TD/OMS task/fix through OMQRTA/add. Requires confirm: true.",
|
|
290
|
+
inputSchema: {
|
|
291
|
+
connectionId: z.string(),
|
|
292
|
+
applicationCode: z.string().max(5),
|
|
293
|
+
shortDescription: z.string().max(50),
|
|
294
|
+
longDescription: z.string().optional(),
|
|
295
|
+
programmer: z.string().optional().describe("Defaults to *CURRENT."),
|
|
296
|
+
release: z.enum(["*SAME", "*CURRENT"]).default("*SAME"),
|
|
297
|
+
priorityNumeric: z.enum(["*SAME", "*DFT"]).default("*SAME"),
|
|
298
|
+
pathCode: z.enum(["*SAME", "*DFT"]).default("*SAME"),
|
|
299
|
+
sendMessageToProgrammer: z.boolean().default(true),
|
|
300
|
+
confirm: z.boolean().describe("Must be true to create a task.")
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
async (input) => {
|
|
304
|
+
requireConfirmation(input.confirm, "create TD/OMS task");
|
|
305
|
+
const body = compact({
|
|
306
|
+
interfaceLevel: "V3R0M0",
|
|
307
|
+
addProcessingOptions: {
|
|
308
|
+
sendMessageToProgrammer: input.sendMessageToProgrammer ? "1" : "0",
|
|
309
|
+
updateDb: "1",
|
|
310
|
+
updateBuffer: "1"
|
|
311
|
+
},
|
|
312
|
+
addData: {
|
|
313
|
+
applicationCode: input.applicationCode,
|
|
314
|
+
fixNumber: "*GEN",
|
|
315
|
+
release: input.release,
|
|
316
|
+
priorityNumeric: input.priorityNumeric,
|
|
317
|
+
programmer: input.programmer || "*CURRENT",
|
|
318
|
+
shortFixDescription: input.shortDescription,
|
|
319
|
+
longDescription: input.longDescription,
|
|
320
|
+
pathCode: input.pathCode
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
const response = await tdomsPost(store, input.connectionId, "OMQRTA", "/add", body, clientFactory);
|
|
324
|
+
return result({ ok: true, response });
|
|
325
|
+
}
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
registerActionTool(server, {
|
|
329
|
+
name: "tdoms_task_action",
|
|
330
|
+
description: "Run TD/OMS task lifecycle actions through OMQRTA, including change, status, complete, copy, delete, ratify, and link/unlink request or ticket. Requires confirm: true.",
|
|
331
|
+
service: "OMQRTA",
|
|
332
|
+
actions: ["/change", "/complete", "/link/request", "/unlink/request", "/copy", "/delete", "/ratify", "/status", "/link/ticket", "/unlink/ticket"],
|
|
333
|
+
store,
|
|
334
|
+
clientFactory,
|
|
335
|
+
confirm: true
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
registerActionTool(server, {
|
|
339
|
+
name: "tdoms_request_action",
|
|
340
|
+
description: "Run TD/OMS request lifecycle actions through OMRRQS, including add, change, connect, disconnect, status, complete, ratify, rename, copy, and delete. Requires confirm: true.",
|
|
341
|
+
service: "OMQRRE",
|
|
342
|
+
actions: ["/add", "/change", "/connect", "/delete", "/complete", "/disconnect", "/ratify", "/rename", "/status", "/copy"],
|
|
343
|
+
store,
|
|
344
|
+
clientFactory,
|
|
345
|
+
confirm: true
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
server.registerTool(
|
|
349
|
+
"tdoms_read_source",
|
|
350
|
+
{
|
|
351
|
+
description: "Read source/member text through OMRSRC/object/get. Returns base64 and a best-effort UTF-8 decode.",
|
|
352
|
+
inputSchema: {
|
|
353
|
+
connectionId: z.string(),
|
|
354
|
+
...sourceObjectSchema
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
async (input) => {
|
|
358
|
+
const response = await tdomsPost(store, input.connectionId, "OMRSRC", "/object/get", compact({
|
|
359
|
+
applicationCode: input.applicationCode,
|
|
360
|
+
taskNumber: input.taskNumber,
|
|
361
|
+
path: input.path,
|
|
362
|
+
objectCode: input.objectCode,
|
|
363
|
+
objectType: input.objectType,
|
|
364
|
+
objectLibrary: input.objectLibrary,
|
|
365
|
+
memberCode: input.memberCode,
|
|
366
|
+
pageNumber: input.pageNumber
|
|
367
|
+
}), clientFactory);
|
|
368
|
+
return result({
|
|
369
|
+
ok: true,
|
|
370
|
+
response,
|
|
371
|
+
sourceTextUtf8: decodeBase64Text(response?.sourceCode),
|
|
372
|
+
sourceCodeBase64: response?.sourceCode
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
server.registerTool(
|
|
378
|
+
"tdoms_read_source_all",
|
|
379
|
+
{
|
|
380
|
+
description: "Read all pages of a TD/OMS source/member through OMRSRC/object/get and return decoded text plus save metadata. Use this before editing large source so agents do not have to manually page base64 payloads.",
|
|
381
|
+
inputSchema: {
|
|
382
|
+
connectionId: z.string(),
|
|
383
|
+
...sourceObjectSchema
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
async (input) => {
|
|
387
|
+
const read = await readAllSourcePages(store, input.connectionId, input, clientFactory);
|
|
388
|
+
return result({ ok: true, ...read });
|
|
389
|
+
}
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
server.registerTool(
|
|
393
|
+
"tdoms_save_source",
|
|
394
|
+
{
|
|
395
|
+
description: "Save source through OMRSRC/object/save. Pass lastChangeDate from tdoms_read_source. Requires confirm: true.",
|
|
396
|
+
inputSchema: {
|
|
397
|
+
connectionId: z.string(),
|
|
398
|
+
...sourceObjectSchema,
|
|
399
|
+
sourceText: z.string().optional().describe("Plain source text to encode as base64."),
|
|
400
|
+
sourceCodeBase64: z.string().optional().describe("Already encoded sourceCode value."),
|
|
401
|
+
ccsid: z.number().int().optional(),
|
|
402
|
+
numberOfPages: z.number().int().min(1).default(1),
|
|
403
|
+
lastChangeDate: z.string().describe("lastChangeDate returned by tdoms_read_source."),
|
|
404
|
+
forceSave: z.boolean().default(false),
|
|
405
|
+
confirm: z.boolean().describe("Must be true to save source.")
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
async (input) => {
|
|
409
|
+
requireConfirmation(input.confirm, "save TD/OMS source");
|
|
410
|
+
if (input.sourceText === undefined && input.sourceCodeBase64 === undefined) {
|
|
411
|
+
throw new Error("Provide either sourceText or sourceCodeBase64.");
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const sourceCode = input.sourceCodeBase64 ?? Buffer.from(input.sourceText, "utf8").toString("base64");
|
|
415
|
+
const response = await tdomsPost(store, input.connectionId, "OMRSRC", "/object/save", compact({
|
|
416
|
+
applicationCode: input.applicationCode,
|
|
417
|
+
taskNumber: input.taskNumber,
|
|
418
|
+
path: input.path,
|
|
419
|
+
objectCode: input.objectCode,
|
|
420
|
+
objectType: input.objectType,
|
|
421
|
+
objectLibrary: input.objectLibrary,
|
|
422
|
+
memberCode: input.memberCode,
|
|
423
|
+
pageNumber: input.pageNumber,
|
|
424
|
+
numberOfPages: input.numberOfPages,
|
|
425
|
+
sourceCode,
|
|
426
|
+
ccsid: input.ccsid,
|
|
427
|
+
lastChangeDate: input.lastChangeDate,
|
|
428
|
+
forceSave: input.forceSave
|
|
429
|
+
}), clientFactory);
|
|
430
|
+
return result({ ok: true, response });
|
|
431
|
+
}
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
server.registerTool(
|
|
435
|
+
"tdoms_save_source_all",
|
|
436
|
+
{
|
|
437
|
+
description: "Save complete plain source text through OMRSRC/object/save, chunking into pages automatically. Pass lastChangeDate from tdoms_read_source_all. Requires confirm: true.",
|
|
438
|
+
inputSchema: {
|
|
439
|
+
connectionId: z.string(),
|
|
440
|
+
...sourceObjectSchema,
|
|
441
|
+
sourceText: z.string().describe("Complete plain source text to save."),
|
|
442
|
+
ccsid: z.number().int().optional(),
|
|
443
|
+
lastChangeDate: z.string().describe("lastChangeDate returned by tdoms_read_source_all or tdoms_read_source."),
|
|
444
|
+
forceSave: z.boolean().default(false),
|
|
445
|
+
maxPageBytes: z.number().int().min(1000).max(48000).default(48000).describe("Maximum UTF-8 bytes per saved page. Default stays below OMRSRC sourceCode limits."),
|
|
446
|
+
confirm: z.boolean().describe("Must be true to save source.")
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
async (input) => {
|
|
450
|
+
requireConfirmation(input.confirm, "save complete TD/OMS source");
|
|
451
|
+
const save = await saveAllSourcePages(store, input.connectionId, input, clientFactory);
|
|
452
|
+
return result({ ok: true, ...save });
|
|
453
|
+
}
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
server.registerTool(
|
|
457
|
+
"tdoms_patch_source_literal",
|
|
458
|
+
{
|
|
459
|
+
description: "Read a complete TD/OMS source member, perform a literal text replacement, and save it back with optimistic lastChangeDate checking. Requires confirm: true.",
|
|
460
|
+
inputSchema: {
|
|
461
|
+
connectionId: z.string(),
|
|
462
|
+
...sourceObjectSchema,
|
|
463
|
+
searchText: z.string().describe("Exact text to replace in the decoded source."),
|
|
464
|
+
replacementText: z.string().describe("Replacement text."),
|
|
465
|
+
occurrence: z.number().int().min(1).default(1).describe("Which occurrence to replace when replaceAll is false."),
|
|
466
|
+
replaceAll: z.boolean().default(false),
|
|
467
|
+
forceSave: z.boolean().default(false),
|
|
468
|
+
confirm: z.boolean().describe("Must be true to patch source.")
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
async (input) => {
|
|
472
|
+
requireConfirmation(input.confirm, "patch TD/OMS source");
|
|
473
|
+
const read = await readAllSourcePages(store, input.connectionId, input, clientFactory);
|
|
474
|
+
if (read.canEdit === false && !input.forceSave) {
|
|
475
|
+
throw new Error("TD/OMS reports canEdit:false. Refusing to save unless forceSave:true is explicitly set.");
|
|
476
|
+
}
|
|
477
|
+
const patch = replaceLiteral(read.sourceTextUtf8, input.searchText, input.replacementText, {
|
|
478
|
+
occurrence: input.occurrence,
|
|
479
|
+
replaceAll: input.replaceAll
|
|
480
|
+
});
|
|
481
|
+
const save = await saveAllSourcePages(store, input.connectionId, {
|
|
482
|
+
...input,
|
|
483
|
+
sourceText: patch.text,
|
|
484
|
+
ccsid: read.ccsid,
|
|
485
|
+
lastChangeDate: read.lastChangeDate,
|
|
486
|
+
forceSave: input.forceSave
|
|
487
|
+
}, clientFactory);
|
|
488
|
+
return result({
|
|
489
|
+
ok: true,
|
|
490
|
+
replacements: patch.replacements,
|
|
491
|
+
beforeLength: read.sourceTextUtf8.length,
|
|
492
|
+
afterLength: patch.text.length,
|
|
493
|
+
read: {
|
|
494
|
+
numberOfPages: read.numberOfPages,
|
|
495
|
+
lastChangeDate: read.lastChangeDate,
|
|
496
|
+
ccsid: read.ccsid,
|
|
497
|
+
canEdit: read.canEdit
|
|
498
|
+
},
|
|
499
|
+
save
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
server.registerTool(
|
|
505
|
+
"tdoms_connection_list_add",
|
|
506
|
+
{
|
|
507
|
+
description: "Add an object/member to a TD/OMS connection list through OMQRCL/add. Requires confirm: true.",
|
|
508
|
+
inputSchema: {
|
|
509
|
+
connectionId: z.string(),
|
|
510
|
+
applicationCode: z.string(),
|
|
511
|
+
connectionListCode: z.string().default("*USER"),
|
|
512
|
+
taskNumber: z.string().describe("Task/fix number."),
|
|
513
|
+
objectCode: z.string(),
|
|
514
|
+
objectLibrary: z.string(),
|
|
515
|
+
objectType: z.string(),
|
|
516
|
+
memberCode: z.string().default("*NONE"),
|
|
517
|
+
ifsObject: z.string().default("*OBJC"),
|
|
518
|
+
solutionType: z.enum(["*CHANGE", "*COMPILE", "*TERMINATE"]).default("*CHANGE"),
|
|
519
|
+
connectWithRules: z.boolean().default(false),
|
|
520
|
+
applicationRelation: z.boolean().default(false),
|
|
521
|
+
confirm: z.boolean().describe("Must be true to add to a connection list.")
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
async (input) => {
|
|
525
|
+
requireConfirmation(input.confirm, "add TD/OMS connection-list entry");
|
|
526
|
+
const response = await tdomsPost(store, input.connectionId, "OMQRCL", "/add", {
|
|
527
|
+
addProcessingOption: {
|
|
528
|
+
connectWithRules: input.connectWithRules ? "1" : "0",
|
|
529
|
+
applicationRelation: input.applicationRelation ? "1" : "0"
|
|
530
|
+
},
|
|
531
|
+
addData: {
|
|
532
|
+
applicationCode: input.applicationCode,
|
|
533
|
+
connectionListCode: input.connectionListCode,
|
|
534
|
+
taskNumber: input.taskNumber,
|
|
535
|
+
objectCode: input.objectCode,
|
|
536
|
+
objectLibrary: input.objectLibrary,
|
|
537
|
+
objectType: input.objectType,
|
|
538
|
+
memberCode: input.memberCode,
|
|
539
|
+
ifsObject: input.ifsObject,
|
|
540
|
+
solutionType: input.solutionType
|
|
541
|
+
}
|
|
542
|
+
}, clientFactory);
|
|
543
|
+
return result({ ok: true, response });
|
|
544
|
+
}
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
server.registerTool(
|
|
548
|
+
"tdoms_connection_list_process",
|
|
549
|
+
{
|
|
550
|
+
description: "Process/connect entries from a TD/OMS connection list through OMQRCL/process. Requires confirm: true.",
|
|
551
|
+
inputSchema: {
|
|
552
|
+
connectionId: z.string(),
|
|
553
|
+
applicationCode: z.string(),
|
|
554
|
+
connectionListCode: z.string().default("*USER"),
|
|
555
|
+
taskNumber: z.string().describe("Task/fix number."),
|
|
556
|
+
objectCode: z.string().default("*ALL"),
|
|
557
|
+
objectLibrary: z.string().optional(),
|
|
558
|
+
objectType: z.string().optional(),
|
|
559
|
+
memberCode: z.string().default("*NONE"),
|
|
560
|
+
ifsObject: z.string().default("*OBJC"),
|
|
561
|
+
promptOnConflicts: z.boolean().default(false),
|
|
562
|
+
forced: z.boolean().default(false),
|
|
563
|
+
confirm: z.boolean().describe("Must be true to process a connection list.")
|
|
564
|
+
}
|
|
565
|
+
},
|
|
566
|
+
async (input) => {
|
|
567
|
+
requireConfirmation(input.confirm, "process TD/OMS connection list");
|
|
568
|
+
const response = await tdomsPost(store, input.connectionId, "OMQRCL", "/process", compact({
|
|
569
|
+
processProcessingOption: {
|
|
570
|
+
promptOnConflicts: input.promptOnConflicts ? "1" : "0",
|
|
571
|
+
forced: input.forced ? "1" : "0"
|
|
572
|
+
},
|
|
573
|
+
processData: {
|
|
574
|
+
applicationCode: input.applicationCode,
|
|
575
|
+
connectionListCode: input.connectionListCode,
|
|
576
|
+
taskNumber: input.taskNumber,
|
|
577
|
+
objectCode: input.objectCode,
|
|
578
|
+
objectLibrary: input.objectLibrary,
|
|
579
|
+
objectType: input.objectType,
|
|
580
|
+
memberCode: input.memberCode,
|
|
581
|
+
ifsObject: input.ifsObject
|
|
582
|
+
}
|
|
583
|
+
}), clientFactory);
|
|
584
|
+
return result({ ok: true, response });
|
|
585
|
+
}
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
server.registerTool(
|
|
589
|
+
"tdoms_compile_queue_add",
|
|
590
|
+
{
|
|
591
|
+
description: "Queue one object or a whole task for compile through OMQRSQ/queue. Requires confirm: true.",
|
|
592
|
+
inputSchema: {
|
|
593
|
+
connectionId: z.string(),
|
|
594
|
+
...buildObjectSchema,
|
|
595
|
+
confirm: z.boolean().describe("Must be true to queue compile.")
|
|
596
|
+
}
|
|
597
|
+
},
|
|
598
|
+
async (input) => {
|
|
599
|
+
requireConfirmation(input.confirm, "queue TD/OMS compile");
|
|
600
|
+
const response = await tdomsPost(store, input.connectionId, "OMQRSQ", "/queue", compileQueueBody(input), clientFactory);
|
|
601
|
+
return result({ ok: true, response });
|
|
602
|
+
}
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
server.registerTool(
|
|
606
|
+
"tdoms_compile_queue_release",
|
|
607
|
+
{
|
|
608
|
+
description: "Release one object or a whole task from the compile queue through OMQRSQ/release. Requires confirm: true.",
|
|
609
|
+
inputSchema: {
|
|
610
|
+
connectionId: z.string(),
|
|
611
|
+
...buildObjectSchema,
|
|
612
|
+
confirm: z.boolean().describe("Must be true to release compile queue.")
|
|
613
|
+
}
|
|
614
|
+
},
|
|
615
|
+
async (input) => {
|
|
616
|
+
requireConfirmation(input.confirm, "release TD/OMS compile queue");
|
|
617
|
+
const response = await tdomsPost(store, input.connectionId, "OMQRSQ", "/release", compileQueueBody(input), clientFactory);
|
|
618
|
+
return result({ ok: true, response });
|
|
619
|
+
}
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
server.registerTool(
|
|
623
|
+
"tdoms_compile_queue_delete",
|
|
624
|
+
{
|
|
625
|
+
description: "Delete held compile-queue entries through OMQRSQ/delete. Requires confirm: true.",
|
|
626
|
+
inputSchema: {
|
|
627
|
+
connectionId: z.string(),
|
|
628
|
+
...buildObjectSchema,
|
|
629
|
+
clearAll: z.boolean().default(false).describe("Delete all occurrences for the object when true."),
|
|
630
|
+
confirm: z.boolean().describe("Must be true to delete compile queue entries.")
|
|
631
|
+
}
|
|
632
|
+
},
|
|
633
|
+
async (input) => {
|
|
634
|
+
requireConfirmation(input.confirm, "delete TD/OMS compile queue entries");
|
|
635
|
+
const response = await tdomsPost(store, input.connectionId, "OMQRSQ", "/delete", compileQueueBody(input, { clearAll: input.clearAll }), clientFactory);
|
|
636
|
+
return result({ ok: true, response });
|
|
637
|
+
}
|
|
638
|
+
);
|
|
639
|
+
|
|
640
|
+
server.registerTool(
|
|
641
|
+
"tdoms_parse_compile_events",
|
|
642
|
+
{
|
|
643
|
+
description: "Parse IBM i EVF compile event records through OMQREV/parseEvent for a library/member.",
|
|
644
|
+
inputSchema: {
|
|
645
|
+
connectionId: z.string(),
|
|
646
|
+
library: z.string().max(10),
|
|
647
|
+
memberName: z.string().max(10)
|
|
648
|
+
}
|
|
649
|
+
},
|
|
650
|
+
async ({ connectionId, library, memberName }) => {
|
|
651
|
+
const response = await tdomsPost(store, connectionId, "OMQREV", "/parseEvent", { library, memberName }, clientFactory);
|
|
652
|
+
return result({ ok: true, response });
|
|
653
|
+
}
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
registerActionTool(server, {
|
|
657
|
+
name: "tdoms_transfer_action",
|
|
658
|
+
description: "Run TD/OMS transfer/checkout/promote flow actions through OMQRTO: select, prepare, execute, cancel, or progress. select/prepare/execute/cancel require confirm: true; progress is read-only.",
|
|
659
|
+
service: "OMQRTO",
|
|
660
|
+
actions: ["/select", "/prepare", "/execute", "/cancel", "/progress"],
|
|
661
|
+
store,
|
|
662
|
+
clientFactory,
|
|
663
|
+
confirm: ({ action }) => action !== "/progress"
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
registerActionTool(server, {
|
|
667
|
+
name: "tdoms_solution_action",
|
|
668
|
+
description: "Run TD/OMS solution actions through OMQRSO, including connect, disconnect, change, lock, unlock, move, and multi operations. Requires confirm: true.",
|
|
669
|
+
service: "OMQRSO",
|
|
670
|
+
actions: ["/change", "/connect", "/disconnect", "/lock", "/unlock", "/move", "/multiChange", "/multiDisconnect", "/multiLock", "/multiMove", "/multiUnlock"],
|
|
671
|
+
store,
|
|
672
|
+
clientFactory,
|
|
673
|
+
confirm: true
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
registerActionTool(server, {
|
|
677
|
+
name: "tdoms_branch_action",
|
|
678
|
+
description: "Run TD/OMS branch actions through OMQBRN: add, change, delete, status, stash, restore, or purge. Requires confirm: true.",
|
|
679
|
+
service: "OMQRBR",
|
|
680
|
+
actions: ["/add", "/change", "/delete", "/status", "/stash", "/restore", "/purge"],
|
|
681
|
+
store,
|
|
682
|
+
clientFactory,
|
|
683
|
+
confirm: true
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
registerActionTool(server, {
|
|
687
|
+
name: "tdoms_spool_action",
|
|
688
|
+
description: "Run TD/OMS spool file actions through OMQRSF: list, fetch, or delete. delete requires confirm: true; list/fetch are read-only.",
|
|
689
|
+
service: "OMQRSF",
|
|
690
|
+
actions: ["/list", "/fetch", "/delete"],
|
|
691
|
+
store,
|
|
692
|
+
clientFactory,
|
|
693
|
+
confirm: ({ action }) => action === "/delete"
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
registerActionTool(server, {
|
|
697
|
+
name: "tdoms_log_action",
|
|
698
|
+
description: "Read TD/OMS log history/details through OMQLOG: getLogHistory, listMessages, or getMessage.",
|
|
699
|
+
service: "OMQRLH",
|
|
700
|
+
actions: ["/getLogHistory", "/listMessages", "/getMessage"],
|
|
701
|
+
store,
|
|
702
|
+
clientFactory,
|
|
703
|
+
confirm: false
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
registerActionTool(server, {
|
|
707
|
+
name: "tdoms_remote_job_action",
|
|
708
|
+
description: "Run TD/OMS remote job monitor actions through OMQJOB: close or reopen. Requires confirm: true.",
|
|
709
|
+
service: "OMQRRJ",
|
|
710
|
+
actions: ["/close", "/reopen"],
|
|
711
|
+
store,
|
|
712
|
+
clientFactory,
|
|
713
|
+
confirm: true
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
registerActionTool(server, {
|
|
717
|
+
name: "tdoms_object_action",
|
|
718
|
+
description: "Run TD/OMS object/component actions through OMQROB: add, change, delete, relation, refill, or close. Requires confirm: true.",
|
|
719
|
+
service: "OMQROB",
|
|
720
|
+
actions: ["/add", "/change", "/delete", "/jobRelation", "/objectRelation", "/refill", "/close"],
|
|
721
|
+
store,
|
|
722
|
+
clientFactory,
|
|
723
|
+
confirm: true
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
registerActionTool(server, {
|
|
727
|
+
name: "tdoms_comment_action",
|
|
728
|
+
description: "Run TD/OMS comment actions through OMQCMT. Requires confirm: true.",
|
|
729
|
+
service: "OMQRCM",
|
|
730
|
+
actions: ["/add", "/change", "/delete", "/reply"],
|
|
731
|
+
store,
|
|
732
|
+
clientFactory,
|
|
733
|
+
confirm: true
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
registerActionTool(server, {
|
|
737
|
+
name: "tdoms_label_action",
|
|
738
|
+
description: "Run TD/OMS label actions through OMQLBL. Requires confirm: true except list/read style calls.",
|
|
739
|
+
service: "OMQRLB",
|
|
740
|
+
actions: ["/add", "/change", "/delete", "/label", "/unlabel"],
|
|
741
|
+
store,
|
|
742
|
+
clientFactory,
|
|
743
|
+
confirm: true
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
registerActionTool(server, {
|
|
747
|
+
name: "tdoms_user_options_action",
|
|
748
|
+
description: "Run TD/OMS user option actions through OMQRUO. Requires confirm: true.",
|
|
749
|
+
service: "OMQRUO",
|
|
750
|
+
actions: ["/add", "/change", "/copy", "/delete", "/execute"],
|
|
751
|
+
store,
|
|
752
|
+
clientFactory,
|
|
753
|
+
confirm: true
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
registerActionTool(server, {
|
|
757
|
+
name: "tdoms_version_conflict_action",
|
|
758
|
+
description: "Run TD/OMS version conflict actions through OMQVCS. Requires confirm: true.",
|
|
759
|
+
service: "OMQRVC",
|
|
760
|
+
actions: ["/confirm"],
|
|
761
|
+
store,
|
|
762
|
+
clientFactory,
|
|
763
|
+
confirm: true
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
registerActionTool(server, {
|
|
767
|
+
name: "tdoms_compile_override_action",
|
|
768
|
+
description: "Run TD/OMS compile override actions through OMQRCO: add, change, copy, delete, or authorize. Requires confirm: true.",
|
|
769
|
+
service: "OMQRCO",
|
|
770
|
+
actions: ["/add", "/change", "/copy", "/delete", "/authorize"],
|
|
771
|
+
store,
|
|
772
|
+
clientFactory,
|
|
773
|
+
confirm: true
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
registerActionTool(server, {
|
|
777
|
+
name: "tdoms_managed_deployment_action",
|
|
778
|
+
description: "Run TD/OMS managed deployment actions through OMQRMD: hold, release, purge, schedule, or close. Requires confirm: true.",
|
|
779
|
+
service: "OMQRMD",
|
|
780
|
+
actions: ["/hold", "/release", "/purge", "/schedule", "/close"],
|
|
781
|
+
store,
|
|
782
|
+
clientFactory,
|
|
783
|
+
confirm: true
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
registerActionTool(server, {
|
|
787
|
+
name: "tdoms_fill_object_action",
|
|
788
|
+
description: "Run TD/OMS fill-object actions through OMQRFO: fillObject or jobStatus. fillObject requires confirm: true; jobStatus is read-only.",
|
|
789
|
+
service: "OMQRFO",
|
|
790
|
+
actions: ["/fillObject", "/jobStatus"],
|
|
791
|
+
store,
|
|
792
|
+
clientFactory,
|
|
793
|
+
confirm: ({ action }) => action === "/fillObject"
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
registerActionTool(server, {
|
|
797
|
+
name: "tdoms_new_object_action",
|
|
798
|
+
description: "Raw TD/OMS new-object escape hatch through OMQRNO: add or location. Prefer tdoms_new_object_create, tdoms_new_source_member_create, or tdoms_new_object_location. OMQRNO/add uses task, environmentCode, newObject, newObjectLibrary, newObjectType, newAttribute, and newSource* field names; it does not use fixNumber, taskNumber, objectCode, objectLibrary, or objectAttribute. add requires confirm: true; location is read-only.",
|
|
799
|
+
service: "OMQRNO",
|
|
800
|
+
actions: ["/add", "/location"],
|
|
801
|
+
store,
|
|
802
|
+
clientFactory,
|
|
803
|
+
confirm: ({ action }) => action === "/add",
|
|
804
|
+
validateBody: validateNewObjectActionBody
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
registerFamilyTools(server, { store, clientFactory });
|
|
808
|
+
|
|
809
|
+
return server;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
export async function startMcpServer({ store, profile } = {}) {
|
|
813
|
+
const selectedProfile = profile || process.env.TDOMS_MCP_PROFILE || "core";
|
|
814
|
+
const server = createMcpServer({ store, profile: selectedProfile });
|
|
815
|
+
const transport = new StdioServerTransport();
|
|
816
|
+
await server.connect(transport);
|
|
817
|
+
console.error(`TD/OMS MCP server running on stdio (${normalizeProfile(selectedProfile)} profile).`);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function registerAgentTools(server, { store, clientFactory, workflowPlans, clock, profile }) {
|
|
821
|
+
const optionalConnectionId = z.string().optional().describe("Saved connection id. Omit when exactly one connection is configured.");
|
|
822
|
+
const contextSchema = z.record(z.string(), z.unknown()).default({});
|
|
823
|
+
|
|
824
|
+
registerAliasTool(server, "tdoms_get_current_context", "Resolve the active TD/OMS connection, programmer identity, applications, and environments without requiring Octo sign-in.", {
|
|
825
|
+
connectionId: optionalConnectionId
|
|
826
|
+
}, async ({ connectionId }) => {
|
|
827
|
+
const resolvedId = await resolveConnectionId(store, connectionId);
|
|
828
|
+
const connection = await store.getConnection(resolvedId);
|
|
829
|
+
const systemInfo = await tdomsGet(store, resolvedId, "OMSINFO", "/system", clientFactory);
|
|
830
|
+
const system = summarizeSystemInfo(systemInfo);
|
|
831
|
+
return result({
|
|
832
|
+
ok: true,
|
|
833
|
+
connection,
|
|
834
|
+
user: {
|
|
835
|
+
tdomsUsername: connection.username,
|
|
836
|
+
programmerId: String(connection.username || "").toUpperCase()
|
|
837
|
+
},
|
|
838
|
+
system
|
|
839
|
+
});
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
registerAliasTool(server, "tdoms_list_capabilities", "List TD/OMS workflows and their option sets, required context, Octo wizard mapping, support level, and execution mode.", {
|
|
843
|
+
support: z.enum(["supported", "unsupported"]).optional(),
|
|
844
|
+
mode: z.enum(["read-only", "mutation"]).optional()
|
|
845
|
+
}, async (input) => {
|
|
846
|
+
const capabilities = CAPABILITIES.filter((capability) =>
|
|
847
|
+
(!input.support || capability.support === input.support) && (!input.mode || capability.mode === input.mode)
|
|
848
|
+
);
|
|
849
|
+
return result({ ok: true, profile, count: capabilities.length, capabilities });
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
registerAliasTool(server, "tdoms_discover_options", "Return authoritative TD/OMS choices or structured read records for tasks, source, queues, logs, histories, and other extension-like views. Never guess values when this tool reports unsupported.", {
|
|
853
|
+
connectionId: optionalConnectionId,
|
|
854
|
+
workflow: z.enum(CAPABILITY_IDS).optional(),
|
|
855
|
+
optionSet: z.enum(OPTION_SETS),
|
|
856
|
+
context: contextSchema,
|
|
857
|
+
search: z.string().optional(),
|
|
858
|
+
pageNumber: z.number().int().min(1).default(1),
|
|
859
|
+
recordsPerPage: z.number().int().min(1).max(50).default(50)
|
|
860
|
+
}, async (input) => {
|
|
861
|
+
const resolvedId = await resolveConnectionId(store, input.connectionId);
|
|
862
|
+
const discovered = await discoverOptions({
|
|
863
|
+
store,
|
|
864
|
+
clientFactory,
|
|
865
|
+
connectionId: resolvedId,
|
|
866
|
+
optionSet: input.optionSet,
|
|
867
|
+
context: input.context || {},
|
|
868
|
+
search: input.search,
|
|
869
|
+
pageNumber: input.pageNumber ?? 1,
|
|
870
|
+
recordsPerPage: input.recordsPerPage ?? 50
|
|
871
|
+
});
|
|
872
|
+
return result({ ...discovered, workflow: input.workflow, connectionId: resolvedId });
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
registerAliasTool(server, "tdoms_plan_workflow", "Create a read-only, ten-minute preview for a TD/OMS mutation. Planning never calls a mutating endpoint.", {
|
|
876
|
+
connectionId: optionalConnectionId,
|
|
877
|
+
workflow: z.enum(CAPABILITY_IDS),
|
|
878
|
+
context: contextSchema,
|
|
879
|
+
selections: contextSchema,
|
|
880
|
+
optionSelections: z.record(z.string(), z.string()).default({}).describe("Chosen option values keyed by option-set name. Supplied values are validated now and again before execution.")
|
|
881
|
+
}, async (input) => {
|
|
882
|
+
prunePlans(workflowPlans, clock());
|
|
883
|
+
const capability = getCapability(input.workflow);
|
|
884
|
+
const connectionId = await resolveConnectionId(store, input.connectionId);
|
|
885
|
+
if (capability.support !== "supported") {
|
|
886
|
+
return result({ ok: false, supported: false, workflow: capability.id, blockers: [capability.note] });
|
|
887
|
+
}
|
|
888
|
+
if (capability.mode !== "mutation" || !capability.execution) {
|
|
889
|
+
return result({ ok: false, supported: true, workflow: capability.id, blockers: [capability.note || "This is a read-only capability and has nothing to execute."] });
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const context = compact(input.context || {});
|
|
893
|
+
const selections = compact(input.selections || {});
|
|
894
|
+
const optionSelections = compact(input.optionSelections || {});
|
|
895
|
+
const combined = { ...context, ...selections };
|
|
896
|
+
const missing = capability.requiredContext.filter((field) => !hasValue(combined[field]));
|
|
897
|
+
if (missing.length > 0) {
|
|
898
|
+
return result({
|
|
899
|
+
ok: false,
|
|
900
|
+
supported: true,
|
|
901
|
+
workflow: capability.id,
|
|
902
|
+
blockers: [`Missing required context: ${missing.join(", ")}`],
|
|
903
|
+
optionSets: capability.optionSets
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const optionValidation = await validateOptionSelections({
|
|
908
|
+
store,
|
|
909
|
+
clientFactory,
|
|
910
|
+
connectionId,
|
|
911
|
+
capability,
|
|
912
|
+
context: combined,
|
|
913
|
+
optionSelections
|
|
914
|
+
});
|
|
915
|
+
if (!optionValidation.ok) {
|
|
916
|
+
return result({ ok: false, supported: true, workflow: capability.id, blockers: optionValidation.blockers, optionSets: capability.optionSets });
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
const execution = resolveExecution(capability, combined);
|
|
920
|
+
const body = buildWorkflowBody(capability, context, selections);
|
|
921
|
+
const snapshot = await readWorkflowState({ store, clientFactory, connectionId, capability, context: combined });
|
|
922
|
+
const createdAt = clock();
|
|
923
|
+
const expiresAt = createdAt + 10 * 60 * 1000;
|
|
924
|
+
const planId = crypto.randomUUID();
|
|
925
|
+
const plan = {
|
|
926
|
+
planId,
|
|
927
|
+
workflow: capability.id,
|
|
928
|
+
connectionId,
|
|
929
|
+
execution,
|
|
930
|
+
body,
|
|
931
|
+
context,
|
|
932
|
+
selections,
|
|
933
|
+
optionSelections,
|
|
934
|
+
snapshot,
|
|
935
|
+
state: "pending",
|
|
936
|
+
createdAt,
|
|
937
|
+
expiresAt
|
|
938
|
+
};
|
|
939
|
+
plan.digest = digestPlan(plan);
|
|
940
|
+
workflowPlans.set(planId, plan);
|
|
941
|
+
|
|
942
|
+
return result({
|
|
943
|
+
ok: true,
|
|
944
|
+
planId,
|
|
945
|
+
workflow: capability.id,
|
|
946
|
+
title: capability.title,
|
|
947
|
+
expiresAt: new Date(expiresAt).toISOString(),
|
|
948
|
+
preview: { execution, body },
|
|
949
|
+
optionSets: capability.optionSets,
|
|
950
|
+
warnings: snapshot.ok ? [] : [`Current-state snapshot unavailable: ${snapshot.error}`],
|
|
951
|
+
next: "Review the preview, obtain explicit user approval, then call tdoms_execute_workflow with this planId and confirm: true."
|
|
952
|
+
});
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
registerAliasTool(server, "tdoms_execute_workflow", "Execute one previously planned TD/OMS mutation after revalidating current state. Requires the matching planId and confirm: true.", {
|
|
956
|
+
planId: z.string(),
|
|
957
|
+
confirm: z.boolean()
|
|
958
|
+
}, async ({ planId, confirm }) => {
|
|
959
|
+
requireConfirmation(confirm, "execute the planned TD/OMS workflow");
|
|
960
|
+
const plan = workflowPlans.get(planId);
|
|
961
|
+
if (!plan) {
|
|
962
|
+
throw new Error("Workflow plan not found. Create a new plan with tdoms_plan_workflow.");
|
|
963
|
+
}
|
|
964
|
+
if (plan.state !== "pending") {
|
|
965
|
+
throw new Error(`Workflow plan is ${plan.state} and cannot be executed again.`);
|
|
966
|
+
}
|
|
967
|
+
if (clock() >= plan.expiresAt) {
|
|
968
|
+
plan.state = "expired";
|
|
969
|
+
throw new Error("Workflow plan expired. Create and approve a new plan.");
|
|
970
|
+
}
|
|
971
|
+
if (plan.digest !== digestPlan(plan)) {
|
|
972
|
+
throw new Error("Workflow plan integrity check failed. Create a new plan.");
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
const capability = getCapability(plan.workflow);
|
|
976
|
+
assertAllowedExecution(capability, plan.execution);
|
|
977
|
+
const optionValidation = await validateOptionSelections({
|
|
978
|
+
store,
|
|
979
|
+
clientFactory,
|
|
980
|
+
connectionId: plan.connectionId,
|
|
981
|
+
capability,
|
|
982
|
+
context: { ...plan.context, ...plan.selections },
|
|
983
|
+
optionSelections: plan.optionSelections
|
|
984
|
+
});
|
|
985
|
+
if (!optionValidation.ok) {
|
|
986
|
+
plan.state = "stale";
|
|
987
|
+
throw new Error(`Selected TD/OMS options changed after planning: ${optionValidation.blockers.join("; ")}`);
|
|
988
|
+
}
|
|
989
|
+
const current = await readWorkflowState({
|
|
990
|
+
store,
|
|
991
|
+
clientFactory,
|
|
992
|
+
connectionId: plan.connectionId,
|
|
993
|
+
capability,
|
|
994
|
+
context: { ...plan.context, ...plan.selections }
|
|
995
|
+
});
|
|
996
|
+
if (plan.snapshot.ok && (!current.ok || current.digest !== plan.snapshot.digest)) {
|
|
997
|
+
plan.state = "stale";
|
|
998
|
+
throw new Error("TD/OMS state changed after planning. Create a new plan and review the updated choices.");
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
const response = await tdomsPost(
|
|
1002
|
+
store,
|
|
1003
|
+
plan.connectionId,
|
|
1004
|
+
plan.execution.service,
|
|
1005
|
+
plan.execution.path,
|
|
1006
|
+
plan.body,
|
|
1007
|
+
clientFactory
|
|
1008
|
+
);
|
|
1009
|
+
plan.state = "executed";
|
|
1010
|
+
plan.executedAt = clock();
|
|
1011
|
+
plan.response = response;
|
|
1012
|
+
return result({ ok: true, planId, workflow: plan.workflow, state: plan.state, response });
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
registerAliasTool(server, "tdoms_get_workflow_status", "Get the state and result of a planned workflow without changing TD/OMS.", {
|
|
1016
|
+
planId: z.string()
|
|
1017
|
+
}, async ({ planId }) => {
|
|
1018
|
+
const plan = workflowPlans.get(planId);
|
|
1019
|
+
if (!plan) {
|
|
1020
|
+
return result({ ok: false, state: "not-found", planId });
|
|
1021
|
+
}
|
|
1022
|
+
if (plan.state === "pending" && clock() >= plan.expiresAt) {
|
|
1023
|
+
plan.state = "expired";
|
|
1024
|
+
}
|
|
1025
|
+
return result({
|
|
1026
|
+
ok: true,
|
|
1027
|
+
planId,
|
|
1028
|
+
workflow: plan.workflow,
|
|
1029
|
+
state: plan.state,
|
|
1030
|
+
createdAt: new Date(plan.createdAt).toISOString(),
|
|
1031
|
+
expiresAt: new Date(plan.expiresAt).toISOString(),
|
|
1032
|
+
executedAt: plan.executedAt ? new Date(plan.executedAt).toISOString() : undefined,
|
|
1033
|
+
response: plan.response
|
|
1034
|
+
});
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
async function discoverOptions({ store, clientFactory, connectionId, optionSet, context, search, pageNumber, recordsPerPage }) {
|
|
1039
|
+
try {
|
|
1040
|
+
if (optionSet === "applications" || optionSet === "environments") {
|
|
1041
|
+
const raw = await tdomsGet(store, connectionId, "OMSINFO", "/system", clientFactory);
|
|
1042
|
+
const applications = summarizeSystemInfo(raw).applications;
|
|
1043
|
+
const rows = optionSet === "applications"
|
|
1044
|
+
? applications.map((application) => ({ value: application.applicationCode, label: application.applicationName || application.applicationCode, metadata: application }))
|
|
1045
|
+
: applications.flatMap((application) => [
|
|
1046
|
+
environmentOption(application, "developmentEnvironment", "Development"),
|
|
1047
|
+
environmentOption(application, "productionEnvironment", "Production"),
|
|
1048
|
+
environmentOption(application, "emergencyEnvironment", "Emergency")
|
|
1049
|
+
].filter(Boolean));
|
|
1050
|
+
return optionResult(optionSet, rows, { kind: "OMSINFO/system", authoritative: true }, search);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
if (optionSet === "transferTargets" || optionSet === "subEnvironments") {
|
|
1054
|
+
const application = context.application || context.applicationCode;
|
|
1055
|
+
const taskNumber = context.taskNumber || context.task || context.fixNumber;
|
|
1056
|
+
if (!application || !taskNumber) {
|
|
1057
|
+
return unsupportedOptions(optionSet, ["applicationCode", "taskNumber"], "Transfer target discovery requires application and task context.");
|
|
1058
|
+
}
|
|
1059
|
+
const response = await tdomsPost(store, connectionId, "OMQRTO", "/select", compact({
|
|
1060
|
+
action: context.action || "Checkout",
|
|
1061
|
+
application,
|
|
1062
|
+
taskNumber,
|
|
1063
|
+
fromEnvironment: context.fromEnvironment || " ",
|
|
1064
|
+
objectList: context.objectList || []
|
|
1065
|
+
}), clientFactory);
|
|
1066
|
+
const candidates = optionSet === "subEnvironments"
|
|
1067
|
+
? collectNamedObjects(response, /subenvironment/i)
|
|
1068
|
+
: collectNamedObjects(response, /target|environment/i);
|
|
1069
|
+
return optionResult(optionSet, candidates.map(normalizeRemoteOption), { kind: "OMQRTO/select", authoritative: true }, search, response);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
if (optionSet === "objectTemplates" || optionSet === "objectLocations") {
|
|
1073
|
+
const task = context.task || context.taskNumber || context.fixNumber;
|
|
1074
|
+
const required = ["applicationCode", "environmentCode", "objectType", "objectAttribute"].filter((field) => !hasValue(context[field]));
|
|
1075
|
+
if (!task) required.push("taskNumber");
|
|
1076
|
+
if (required.length > 0) {
|
|
1077
|
+
return unsupportedOptions(optionSet, required, "New-object choices are resolved by OMQRNO/location after object context is known.");
|
|
1078
|
+
}
|
|
1079
|
+
const response = await tdomsPost(store, connectionId, "OMQRNO", "/location", {
|
|
1080
|
+
applicationCode: context.applicationCode,
|
|
1081
|
+
environmentCode: context.environmentCode,
|
|
1082
|
+
task,
|
|
1083
|
+
objectType: context.objectType,
|
|
1084
|
+
objectAttribute: context.objectAttribute
|
|
1085
|
+
}, clientFactory);
|
|
1086
|
+
const candidates = optionSet === "objectLocations"
|
|
1087
|
+
? collectLocationOptions(response)
|
|
1088
|
+
: collectNamedObjects(response, /template/i);
|
|
1089
|
+
return optionResult(optionSet, candidates.map(normalizeRemoteOption), { kind: "OMQRNO/location", authoritative: true }, search, response);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
if (optionSet === "source") {
|
|
1093
|
+
const required = ["objectCode", "objectType", "objectLibrary"].filter((field) => !hasValue(context[field]));
|
|
1094
|
+
if (required.length > 0) return unsupportedOptions(optionSet, required, "Source reads require an object, type, and library.");
|
|
1095
|
+
const response = await tdomsPost(store, connectionId, "OMRSRC", "/object/get", compact({
|
|
1096
|
+
applicationCode: context.applicationCode,
|
|
1097
|
+
taskNumber: context.taskNumber || context.task || context.fixNumber,
|
|
1098
|
+
path: context.path,
|
|
1099
|
+
objectCode: context.objectCode,
|
|
1100
|
+
objectType: context.objectType,
|
|
1101
|
+
objectLibrary: context.objectLibrary,
|
|
1102
|
+
memberCode: context.memberCode,
|
|
1103
|
+
pageNumber: context.pageNumber || 1
|
|
1104
|
+
}), clientFactory);
|
|
1105
|
+
return {
|
|
1106
|
+
...optionResult(optionSet, [{
|
|
1107
|
+
value: [context.objectLibrary, context.objectCode, context.memberCode].filter(Boolean).join("/"),
|
|
1108
|
+
label: context.memberCode || context.objectCode,
|
|
1109
|
+
metadata: response
|
|
1110
|
+
}], { kind: "OMRSRC/object/get", authoritative: true }, search),
|
|
1111
|
+
sourceTextUtf8: decodeBase64Text(response?.sourceCode),
|
|
1112
|
+
sourceCodeBase64: response?.sourceCode
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
if (optionSet === "spoolFiles") {
|
|
1117
|
+
const response = await tdomsPost(store, connectionId, "OMQRSF", "/list", context, clientFactory);
|
|
1118
|
+
const candidates = collectNamedObjects(response, /spool|record|file/i);
|
|
1119
|
+
return optionResult(optionSet, candidates.map(normalizeRemoteOption), { kind: "OMQRSF/list", authoritative: true }, search, response);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
if (optionSet === "logs") {
|
|
1123
|
+
const response = await tdomsPost(store, connectionId, "OMQRLH", "/getLogHistory", compact({
|
|
1124
|
+
applicationCode: context.applicationCode,
|
|
1125
|
+
fixNumber: context.fixNumber || context.taskNumber || context.task,
|
|
1126
|
+
qualifiedJobName: context.qualifiedJobName,
|
|
1127
|
+
pageNumber: context.pageNumber || pageNumber,
|
|
1128
|
+
recordsPerPage: context.recordsPerPage || recordsPerPage
|
|
1129
|
+
}), clientFactory);
|
|
1130
|
+
const candidates = collectNamedObjects(response, /log|record|message/i);
|
|
1131
|
+
return optionResult(optionSet, candidates.map(normalizeRemoteOption), { kind: "OMQRLH/getLogHistory", authoritative: true }, search, response);
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
if (optionSet === "dependencies") {
|
|
1135
|
+
const endpointPath = context.path || buildImpactPath(context);
|
|
1136
|
+
if (!endpointPath) return unsupportedOptions(optionSet, ["path or objectCode/objectType/objectLibrary/memberCode"], "Dependency reads require a complete OMQRIA reference path.");
|
|
1137
|
+
const response = await tdomsGet(store, connectionId, "OMQRIA", endpointPath, clientFactory);
|
|
1138
|
+
const candidates = collectObjectNodes(response?.reportResult || response);
|
|
1139
|
+
return optionResult(optionSet, candidates.map(normalizeRemoteOption), { kind: `OMQRIA${endpointPath}`, authoritative: true }, search, response);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
const spec = DATABASE_OPTION_SPECS[optionSet];
|
|
1143
|
+
if (!spec) {
|
|
1144
|
+
return unsupportedOptions(optionSet, [], "No documented TD/OMS source is available for this option set.");
|
|
1145
|
+
}
|
|
1146
|
+
const contextualSelections = optionContextSelections(context, spec);
|
|
1147
|
+
if (optionSet === "tasks" && !context.programmer && context.allUsers !== true) {
|
|
1148
|
+
const connection = await store.getConnection(connectionId);
|
|
1149
|
+
if (connection?.username) contextualSelections.push({ field: "programmer", value: String(connection.username).toUpperCase(), operator: "EQ" });
|
|
1150
|
+
}
|
|
1151
|
+
const response = await queryDatabase(store, connectionId, {
|
|
1152
|
+
table: spec.table,
|
|
1153
|
+
returnFields: spec.fields,
|
|
1154
|
+
fieldSelections: contextualSelections,
|
|
1155
|
+
pageNumber,
|
|
1156
|
+
recordsPerPage,
|
|
1157
|
+
returnTotalRecords: true,
|
|
1158
|
+
distinct: spec.distinct !== false
|
|
1159
|
+
}, clientFactory);
|
|
1160
|
+
const rows = extractRecords(response);
|
|
1161
|
+
const options = rows.map((row) => optionFromRecord(row, spec)).filter((option) => option.value);
|
|
1162
|
+
return optionResult(optionSet, options, { kind: `OMRDBA/${spec.table}`, authoritative: spec.authoritative !== false }, search, response);
|
|
1163
|
+
} catch (error) {
|
|
1164
|
+
return {
|
|
1165
|
+
ok: true,
|
|
1166
|
+
supported: false,
|
|
1167
|
+
optionSet,
|
|
1168
|
+
source: { kind: "unavailable", authoritative: false },
|
|
1169
|
+
options: [],
|
|
1170
|
+
warnings: [String(error?.message || error)],
|
|
1171
|
+
next: "Use a documented site-specific TD/OMS source or ask the user for this value. Do not guess."
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
const DATABASE_OPTION_SPECS = {
|
|
1177
|
+
programmers: { table: "OMXFix", fields: ["programmer"], value: ["programmer"], contextFields: ["applicationCode"], authoritative: false },
|
|
1178
|
+
userClasses: { table: "OMXFix", fields: ["programmer"], value: ["programmer"], contextFields: ["applicationCode"], authoritative: false },
|
|
1179
|
+
taskStatuses: { table: "OMXFix", fields: ["fixStatus"], value: ["fixStatus"], contextFields: ["applicationCode"], authoritative: false },
|
|
1180
|
+
taskTypes: { table: "OMXFix", fields: ["fixType"], value: ["fixType"], contextFields: ["applicationCode"], authoritative: false },
|
|
1181
|
+
releases: { table: "OMXFix", fields: ["release"], value: ["release"], contextFields: ["applicationCode"], authoritative: false },
|
|
1182
|
+
priorities: { table: "OMXFix", fields: ["priorityNumeric"], value: ["priorityNumeric"], contextFields: ["applicationCode"], authoritative: false },
|
|
1183
|
+
branchesPaths: { table: "OMXTransferPath", fields: ["pathCode", "transferPathDescription", "pathType"], value: ["pathCode"], label: ["transferPathDescription", "pathCode"], contextFields: ["applicationCode"] },
|
|
1184
|
+
taskObjects: { table: "OMXSolutionExtended", value: ["objectCode", "memberCode"], label: ["objectDescription", "objectCode"], contextFields: ["applicationCode", "taskNumber", "objectCode", "objectType", "objectLibrary", "memberCode"] },
|
|
1185
|
+
solutions: { table: "OMXSolutionExtended", value: ["solutionNumber", "objectCode", "memberCode"], label: ["shortSolutionDescription", "objectDescription", "objectCode"], contextFields: ["applicationCode", "taskNumber", "objectCode", "objectType", "objectLibrary", "memberCode"] },
|
|
1186
|
+
completionReasons: { table: "OMXFix", fields: ["reasonCode"], value: ["reasonCode"], contextFields: ["applicationCode"], authoritative: false },
|
|
1187
|
+
ratificationReasons: { table: "OMXFix", fields: ["reasonCode"], value: ["reasonCode"], contextFields: ["applicationCode"], authoritative: false },
|
|
1188
|
+
memberAttributes: { table: "OMXObjectDetailExtended", fields: ["memberAttributeMBSEU"], value: ["memberAttributeMBSEU"], contextFields: ["applicationCode", "objectCode", "objectLibrary", "memberCode"] },
|
|
1189
|
+
solutionTypes: { table: "OMXSolutionExtended", fields: ["solutionType"], value: ["solutionType"], contextFields: ["applicationCode"], authoritative: false },
|
|
1190
|
+
compileOverrides: { table: "OMXSolutionOverride", value: ["objectCode", "relatedObjectCode", "statusField"], label: ["objectCode", "relatedObjectCode", "statusField"], contextFields: ["applicationCode", "taskNumber", "objectCode", "objectType", "memberCode"] },
|
|
1191
|
+
labels: { table: "OMXLabel", value: ["labelCode", "labelID"], label: ["labelDescription", "labelCode"], contextFields: [] },
|
|
1192
|
+
revisions: { table: "OMXObjectTransferHistory", value: ["transferCode", "versionNumber", "systemDate"], label: ["objectCode", "transferCode", "versionNumber"], contextFields: ["applicationCode", "objectCode", "objectType", "memberCode"] },
|
|
1193
|
+
conflicts: { table: "OMXObjectVersionConflict", value: ["conflictNumber", "objectCode"], label: ["conflictDescription", "objectCode"], contextFields: ["applicationCode", "objectCode", "objectType", "memberCode"] },
|
|
1194
|
+
remoteJobs: { table: "OMXRemoteJobMonitorExtended", value: ["transferCode", "qualifiedJobName"], label: ["shortFixDescription", "transferCode", "qualifiedJobName"], contextFields: ["applicationCode", "taskNumber"] },
|
|
1195
|
+
trackerLinks: { table: "OMXRemoteIssueInformation", value: ["issueUrl", "remoteIssueNumber", "trackerIssueUrl"], label: ["issueTitle", "remoteIssueNumber", "issueUrl"], contextFields: ["applicationCode", "taskNumber"] },
|
|
1196
|
+
userOptions: { table: "OMXUserOption", value: ["userOption3", "userOption", "optionCode"], label: ["userOptionDescription", "description", "userOption3"], contextFields: [] },
|
|
1197
|
+
tasks: { table: "OMXFix", value: ["fixNumber"], label: ["shortFixDescription", "fixNumber"], distinct: false, contextFields: ["applicationCode", "taskNumber"] },
|
|
1198
|
+
requests: { table: "OMXRequest", value: ["requestNumber"], label: ["shortRequestDescription", "requestNumber"], distinct: false, contextFields: ["applicationCode", "requestNumber"] },
|
|
1199
|
+
components: { table: "OMXObjectExtended", value: ["objectCode", "memberCode"], label: ["objectDescription", "objectCode"], distinct: false, contextFields: ["applicationCode", "objectCode", "objectType", "objectLibrary", "memberCode"] },
|
|
1200
|
+
connectionLists: { table: "OMXConnectionListExtended", value: ["connectionListCode", "listCode"], label: ["connectionListDescription", "connectionListCode"], distinct: false, contextFields: ["applicationCode"] },
|
|
1201
|
+
buildQueue: { table: "OMXCreateObjectMonitorExtended", value: ["qualifiedJobName", "objectCode"], label: ["statusDescription", "objectCode"], distinct: false, contextFields: ["applicationCode", "taskNumber", "objectCode", "objectType", "objectLibrary", "memberCode"] },
|
|
1202
|
+
compileResults: { table: "OMXCreateObjectMonitorExtended", value: ["qualifiedJobName", "objectCode"], label: ["compileStatus", "objectCode"], distinct: false, contextFields: ["applicationCode", "taskNumber", "objectCode", "objectType", "objectLibrary", "memberCode"] },
|
|
1203
|
+
transferHistory: { table: "OMXObjectTransferHistory", value: ["transferCode", "objectCode"], label: ["objectCode", "transferCode"], distinct: false, contextFields: ["applicationCode", "objectCode", "objectType", "memberCode"] },
|
|
1204
|
+
trackerRecords: { table: "OMXRequestTaskLink", value: ["uUID", "urlToItem", "taskNumber"], label: ["remoteIssueInformation", "taskNumber", "requestNumber"], distinct: false, contextFields: ["applicationCode", "taskNumber", "requestNumber"], taskField: "taskNumber" },
|
|
1205
|
+
dashboards: { table: "OMDSH", value: ["uUID", "dashboardCode", "name"], label: ["descriptionType", "dashboardDescription", "name", "dashboardCode"], distinct: false, contextFields: [] }
|
|
1206
|
+
};
|
|
1207
|
+
|
|
1208
|
+
async function readWorkflowState({ store, clientFactory, connectionId, capability, context }) {
|
|
1209
|
+
if (!capability.stateTable) {
|
|
1210
|
+
return { ok: true, digest: hashValue({ workflow: capability.id, context: stableValue(context) }), records: [] };
|
|
1211
|
+
}
|
|
1212
|
+
try {
|
|
1213
|
+
const response = await queryDatabase(store, connectionId, {
|
|
1214
|
+
table: capability.stateTable,
|
|
1215
|
+
fieldSelections: optionContextSelections(context),
|
|
1216
|
+
pageNumber: 1,
|
|
1217
|
+
recordsPerPage: 50,
|
|
1218
|
+
returnTotalRecords: true
|
|
1219
|
+
}, clientFactory);
|
|
1220
|
+
const records = extractRecords(response);
|
|
1221
|
+
return { ok: true, digest: hashValue(records), records };
|
|
1222
|
+
} catch (error) {
|
|
1223
|
+
return { ok: false, error: String(error?.message || error) };
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
async function resolveConnectionId(store, requestedId) {
|
|
1228
|
+
const connections = await store.listConnections();
|
|
1229
|
+
if (requestedId) {
|
|
1230
|
+
if (!connections.some((connection) => connection.id === requestedId)) {
|
|
1231
|
+
throw new Error(`Connection not found: ${requestedId}`);
|
|
1232
|
+
}
|
|
1233
|
+
return requestedId;
|
|
1234
|
+
}
|
|
1235
|
+
if (connections.length === 1) {
|
|
1236
|
+
return connections[0].id;
|
|
1237
|
+
}
|
|
1238
|
+
if (connections.length === 0) {
|
|
1239
|
+
throw new Error("No TD/OMS connection is configured. Add one through the localhost TD/OMS MCP UI.");
|
|
1240
|
+
}
|
|
1241
|
+
throw new Error(`Multiple TD/OMS connections are configured. Choose connectionId: ${connections.map((connection) => `${connection.name} (${connection.id})`).join(", ")}`);
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
function resolveExecution(capability, values) {
|
|
1245
|
+
const execution = { ...capability.execution };
|
|
1246
|
+
if (capability.id === "transfer" && values.transferNumber) {
|
|
1247
|
+
execution.path = "/execute";
|
|
1248
|
+
}
|
|
1249
|
+
if (capability.id === "compile-override" && ["add", "change", "copy", "delete", "authorize"].includes(values.action)) {
|
|
1250
|
+
execution.path = `/${values.action}`;
|
|
1251
|
+
}
|
|
1252
|
+
if (capability.id === "manage-label" && ["add", "change", "delete", "label", "unlabel"].includes(values.action)) {
|
|
1253
|
+
execution.path = `/${values.action}`;
|
|
1254
|
+
}
|
|
1255
|
+
return execution;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
function buildWorkflowBody(capability, context, selections) {
|
|
1259
|
+
if (selections.requestBody && typeof selections.requestBody === "object" && !Array.isArray(selections.requestBody)) {
|
|
1260
|
+
return compact(selections.requestBody);
|
|
1261
|
+
}
|
|
1262
|
+
const body = compact({ ...context, ...selections });
|
|
1263
|
+
delete body.requestBody;
|
|
1264
|
+
delete body.action;
|
|
1265
|
+
if (capability.id === "new-object" && body.taskNumber && !body.task) {
|
|
1266
|
+
body.task = body.taskNumber;
|
|
1267
|
+
delete body.taskNumber;
|
|
1268
|
+
}
|
|
1269
|
+
if (capability.id === "transfer") {
|
|
1270
|
+
if (body.applicationCode && !body.application) body.application = body.applicationCode;
|
|
1271
|
+
delete body.applicationCode;
|
|
1272
|
+
}
|
|
1273
|
+
return body;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
function assertAllowedExecution(capability, execution) {
|
|
1277
|
+
const allowed = new Set([`${capability.execution?.service}${capability.execution?.path}`]);
|
|
1278
|
+
if (capability.id === "transfer") allowed.add("OMQRTO/execute");
|
|
1279
|
+
if (capability.id === "compile-override") ["add", "change", "copy", "delete", "authorize"].forEach((action) => allowed.add(`OMQRCO/${action}`));
|
|
1280
|
+
if (capability.id === "manage-label") ["add", "change", "delete", "label", "unlabel"].forEach((action) => allowed.add(`OMQRLB/${action}`));
|
|
1281
|
+
if (!allowed.has(`${execution.service}${execution.path}`)) {
|
|
1282
|
+
throw new Error(`Execution endpoint is not allowed for workflow ${capability.id}.`);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
function optionContextSelections(context, spec = {}) {
|
|
1287
|
+
const allowed = new Set(spec.contextFields || ["applicationCode", "taskNumber", "requestNumber", "pathCode", "objectCode", "objectType", "objectLibrary", "memberCode"]);
|
|
1288
|
+
return compactArray([
|
|
1289
|
+
allowed.has("applicationCode") && context.applicationCode && { field: "applicationCode", value: context.applicationCode, operator: "EQ" },
|
|
1290
|
+
allowed.has("taskNumber") && (context.taskNumber || context.task || context.fixNumber) && { field: spec.taskField || "fixNumber", value: context.taskNumber || context.task || context.fixNumber, operator: "EQ" },
|
|
1291
|
+
allowed.has("requestNumber") && context.requestNumber && { field: "requestNumber", value: context.requestNumber, operator: "EQ" },
|
|
1292
|
+
allowed.has("pathCode") && context.pathCode && { field: "pathCode", value: context.pathCode, operator: "EQ" },
|
|
1293
|
+
allowed.has("objectCode") && context.objectCode && { field: "objectCode", value: context.objectCode, operator: "EQ" },
|
|
1294
|
+
allowed.has("objectType") && context.objectType && { field: "objectType", value: context.objectType, operator: "EQ" },
|
|
1295
|
+
allowed.has("objectLibrary") && context.objectLibrary && { field: "objectLibrary", value: context.objectLibrary, operator: "EQ" },
|
|
1296
|
+
allowed.has("memberCode") && context.memberCode && { field: "memberCode", value: context.memberCode, operator: "EQ" }
|
|
1297
|
+
]);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
function buildImpactPath(context) {
|
|
1301
|
+
if (![context.objectCode, context.objectType, context.objectLibrary, context.memberCode].every(hasValue)) return undefined;
|
|
1302
|
+
return `/ref/${encodeURIComponent(context.objectCode)}/${encodeURIComponent(context.objectType)}/${encodeURIComponent(context.objectLibrary)}/${encodeURIComponent(context.memberCode)}`;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
function extractRecords(response) {
|
|
1306
|
+
if (Array.isArray(response)) return response;
|
|
1307
|
+
for (const candidate of [response?.records, response?.listOutput?.records, response?.data?.records, response?.result?.records]) {
|
|
1308
|
+
if (Array.isArray(candidate)) return candidate;
|
|
1309
|
+
}
|
|
1310
|
+
return [];
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
function optionFromRecord(record, spec) {
|
|
1314
|
+
const value = firstValue(record, spec.value || []);
|
|
1315
|
+
const label = firstValue(record, spec.label || spec.value || []) || value;
|
|
1316
|
+
return { value: String(value || ""), label: String(label || value || ""), enabled: true, selected: false, description: "", metadata: record };
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
function firstValue(record, fields) {
|
|
1320
|
+
for (const field of fields) {
|
|
1321
|
+
if (hasValue(record?.[field])) return record[field];
|
|
1322
|
+
}
|
|
1323
|
+
return undefined;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
function optionResult(optionSet, options, source, search, raw) {
|
|
1327
|
+
const failure = tdomsFailure(raw);
|
|
1328
|
+
if (failure) {
|
|
1329
|
+
return {
|
|
1330
|
+
ok: true,
|
|
1331
|
+
supported: false,
|
|
1332
|
+
optionSet,
|
|
1333
|
+
source: { ...source, authoritative: false },
|
|
1334
|
+
count: 0,
|
|
1335
|
+
options: [],
|
|
1336
|
+
warnings: [failure],
|
|
1337
|
+
next: "Use another documented TD/OMS source or ask the user. Do not guess."
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
const normalized = deduplicateOptions(options.map(normalizeOption));
|
|
1341
|
+
const filtered = search
|
|
1342
|
+
? normalized.filter((option) => `${option.value} ${option.label} ${option.description}`.toLowerCase().includes(search.toLowerCase()))
|
|
1343
|
+
: normalized;
|
|
1344
|
+
const warnings = [];
|
|
1345
|
+
if (source.authoritative === false) warnings.push("Choices are inferred from existing TD/OMS records; confirm site policy when the workflow requires an authoritative configured list.");
|
|
1346
|
+
if (filtered.length === 0) warnings.push("TD/OMS returned no choices for the supplied context.");
|
|
1347
|
+
return {
|
|
1348
|
+
ok: true,
|
|
1349
|
+
supported: true,
|
|
1350
|
+
optionSet,
|
|
1351
|
+
source,
|
|
1352
|
+
count: filtered.length,
|
|
1353
|
+
options: filtered,
|
|
1354
|
+
warnings,
|
|
1355
|
+
next: filtered.length > 0 ? "Choose one of these returned values; do not substitute an unlisted value." : "Review the context or ask the user for guidance.",
|
|
1356
|
+
raw
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
function tdomsFailure(response) {
|
|
1361
|
+
if (!response || typeof response !== "object") return undefined;
|
|
1362
|
+
const status = response.returnStatus?.status || response.status;
|
|
1363
|
+
if (!/^\*(TERM|ERROR|FAIL)/i.test(String(status || ""))) return undefined;
|
|
1364
|
+
const messages = response.returnStatus?.messages || response.messages;
|
|
1365
|
+
const values = Array.isArray(messages) ? messages : messages ? [messages] : [];
|
|
1366
|
+
return values
|
|
1367
|
+
.map((message) => message?.messageTextExtended || message?.messageText || message?.message || String(message))
|
|
1368
|
+
.filter(Boolean)
|
|
1369
|
+
.join("; ") || `TD/OMS returned terminal status ${status}.`;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
function unsupportedOptions(optionSet, requiredContext, warning) {
|
|
1373
|
+
return {
|
|
1374
|
+
ok: true,
|
|
1375
|
+
supported: false,
|
|
1376
|
+
optionSet,
|
|
1377
|
+
requiredContext,
|
|
1378
|
+
source: { kind: "unavailable", authoritative: false },
|
|
1379
|
+
options: [],
|
|
1380
|
+
warnings: [warning],
|
|
1381
|
+
next: "Supply the missing context or ask the user. Do not guess."
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
function environmentOption(application, field, role) {
|
|
1386
|
+
const value = application[field];
|
|
1387
|
+
if (!hasValue(value)) return undefined;
|
|
1388
|
+
return { value, label: `${value} (${role})`, metadata: { applicationCode: application.applicationCode, role } };
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
function collectNamedObjects(value, keyPattern, output = []) {
|
|
1392
|
+
if (!value || typeof value !== "object") return output;
|
|
1393
|
+
if (Array.isArray(value)) {
|
|
1394
|
+
for (const entry of value) collectNamedObjects(entry, keyPattern, output);
|
|
1395
|
+
return output;
|
|
1396
|
+
}
|
|
1397
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1398
|
+
if (keyPattern.test(key) && Array.isArray(entry)) {
|
|
1399
|
+
output.push(...entry.map((item) => typeof item === "object" ? item : { value: item }));
|
|
1400
|
+
} else if (entry && typeof entry === "object") {
|
|
1401
|
+
collectNamedObjects(entry, keyPattern, output);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
return output;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
function collectObjectNodes(value, output = []) {
|
|
1408
|
+
if (!value || typeof value !== "object") return output;
|
|
1409
|
+
if (Array.isArray(value)) {
|
|
1410
|
+
for (const entry of value) collectObjectNodes(entry, output);
|
|
1411
|
+
return output;
|
|
1412
|
+
}
|
|
1413
|
+
if (hasValue(value.objectCode) || hasValue(value.refLink)) output.push(value);
|
|
1414
|
+
for (const entry of Object.values(value)) {
|
|
1415
|
+
if (entry && typeof entry === "object") collectObjectNodes(entry, output);
|
|
1416
|
+
}
|
|
1417
|
+
return output;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
function collectLocationOptions(response) {
|
|
1421
|
+
const options = [];
|
|
1422
|
+
for (const [field, label] of [
|
|
1423
|
+
["objectLibrary", "Object library"],
|
|
1424
|
+
["sourceLibrary", "Source library"],
|
|
1425
|
+
["sourceFile", "Source file"],
|
|
1426
|
+
["environmentCode", "Environment"]
|
|
1427
|
+
]) {
|
|
1428
|
+
if (hasValue(response?.[field])) options.push({ value: response[field], label: `${response[field]} (${label})`, field });
|
|
1429
|
+
}
|
|
1430
|
+
options.push(...collectNamedObjects(response, /location|library|source/i));
|
|
1431
|
+
return options;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
function normalizeRemoteOption(value) {
|
|
1435
|
+
if (!value || typeof value !== "object") return { value: String(value || "") };
|
|
1436
|
+
return {
|
|
1437
|
+
value: firstValue(value, ["value", "refLink", "spoolFileName", "qualifiedJobName", "transferCode", "messageId", "environmentCode", "targetEnvironment", "objectCode", "code", "id", "name", "library", "templateName"]),
|
|
1438
|
+
label: firstValue(value, ["label", "description", "objectDescription", "messageText", "environmentDescription", "name", "spoolFileName", "qualifiedJobName", "value", "environmentCode", "targetEnvironment", "objectCode"]),
|
|
1439
|
+
enabled: value.enabled !== false && value.disabled !== true,
|
|
1440
|
+
selected: value.selected === true,
|
|
1441
|
+
description: value.description || "",
|
|
1442
|
+
metadata: value
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
function normalizeOption(option) {
|
|
1447
|
+
return {
|
|
1448
|
+
value: String(option.value ?? ""),
|
|
1449
|
+
label: String(option.label ?? option.value ?? ""),
|
|
1450
|
+
enabled: option.enabled !== false,
|
|
1451
|
+
selected: option.selected === true,
|
|
1452
|
+
description: String(option.description || ""),
|
|
1453
|
+
metadata: option.metadata || {}
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
function deduplicateOptions(options) {
|
|
1458
|
+
const seen = new Set();
|
|
1459
|
+
return options.filter((option) => {
|
|
1460
|
+
if (!option.value || seen.has(option.value)) return false;
|
|
1461
|
+
seen.add(option.value);
|
|
1462
|
+
return true;
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
function digestPlan(plan) {
|
|
1467
|
+
return hashValue({
|
|
1468
|
+
planId: plan.planId,
|
|
1469
|
+
workflow: plan.workflow,
|
|
1470
|
+
connectionId: plan.connectionId,
|
|
1471
|
+
execution: plan.execution,
|
|
1472
|
+
body: plan.body,
|
|
1473
|
+
context: plan.context,
|
|
1474
|
+
selections: plan.selections,
|
|
1475
|
+
optionSelections: plan.optionSelections,
|
|
1476
|
+
snapshot: plan.snapshot,
|
|
1477
|
+
createdAt: plan.createdAt,
|
|
1478
|
+
expiresAt: plan.expiresAt
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
async function validateOptionSelections({ store, clientFactory, connectionId, capability, context, optionSelections }) {
|
|
1483
|
+
const blockers = [];
|
|
1484
|
+
for (const [optionSet, selectedValue] of Object.entries(optionSelections || {})) {
|
|
1485
|
+
if (!capability.optionSets.includes(optionSet)) {
|
|
1486
|
+
blockers.push(`${optionSet} is not an option set for ${capability.id}`);
|
|
1487
|
+
continue;
|
|
1488
|
+
}
|
|
1489
|
+
const discovered = await discoverOptions({
|
|
1490
|
+
store,
|
|
1491
|
+
clientFactory,
|
|
1492
|
+
connectionId,
|
|
1493
|
+
optionSet,
|
|
1494
|
+
context,
|
|
1495
|
+
pageNumber: 1,
|
|
1496
|
+
recordsPerPage: 50
|
|
1497
|
+
});
|
|
1498
|
+
if (!discovered.supported) {
|
|
1499
|
+
blockers.push(`${optionSet} could not be validated: ${discovered.warnings?.join("; ") || "source unavailable"}`);
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
const match = discovered.options.find((option) => option.value === String(selectedValue));
|
|
1503
|
+
if (!match) blockers.push(`${selectedValue} is not a current ${optionSet} choice`);
|
|
1504
|
+
else if (!match.enabled) blockers.push(`${selectedValue} is currently disabled for ${optionSet}`);
|
|
1505
|
+
}
|
|
1506
|
+
return { ok: blockers.length === 0, blockers };
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
function hashValue(value) {
|
|
1510
|
+
return crypto.createHash("sha256").update(JSON.stringify(stableValue(value))).digest("hex");
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
function stableValue(value) {
|
|
1514
|
+
if (Array.isArray(value)) return value.map(stableValue);
|
|
1515
|
+
if (!value || typeof value !== "object") return value;
|
|
1516
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableValue(value[key])]));
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
function prunePlans(plans, now) {
|
|
1520
|
+
for (const [planId, plan] of plans) {
|
|
1521
|
+
if (plan.state !== "pending" && now - (plan.executedAt || plan.expiresAt) > 10 * 60 * 1000) plans.delete(planId);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
function hasValue(value) {
|
|
1526
|
+
return value !== undefined && value !== null && String(value).trim() !== "";
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
function normalizeProfile(profile) {
|
|
1530
|
+
const normalized = String(profile || "core").toLowerCase();
|
|
1531
|
+
if (!["core", "full"].includes(normalized)) {
|
|
1532
|
+
throw new Error(`Unknown TD/OMS MCP profile: ${profile}. Expected core or full.`);
|
|
1533
|
+
}
|
|
1534
|
+
return normalized;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
async function queryDatabase(store, connectionId, input, clientFactory) {
|
|
1538
|
+
return tdomsPost(store, connectionId, "OMRDBA", "/get", compact({
|
|
1539
|
+
listInput: {
|
|
1540
|
+
table: input.table,
|
|
1541
|
+
pageNumber: input.pageNumber,
|
|
1542
|
+
recordsPerPage: input.recordsPerPage,
|
|
1543
|
+
returnTotalRecords: input.returnTotalRecords,
|
|
1544
|
+
returnFields: input.returnFields,
|
|
1545
|
+
whereClause: input.whereClause,
|
|
1546
|
+
fieldSelections: input.fieldSelections,
|
|
1547
|
+
distinct: input.distinct,
|
|
1548
|
+
schemaOverride: input.schemaOverride
|
|
1549
|
+
},
|
|
1550
|
+
sortFields: input.sortFields,
|
|
1551
|
+
returnExtendedMessageText: true
|
|
1552
|
+
}), clientFactory);
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
async function tdomsPost(store, connectionId, service, path, body, clientFactory) {
|
|
1556
|
+
const { connection, client, token } = await getAuthenticatedClient(store, connectionId, clientFactory);
|
|
1557
|
+
try {
|
|
1558
|
+
return await client.post(service, path, body, token);
|
|
1559
|
+
} catch (error) {
|
|
1560
|
+
if (!isUnauthorized(error)) {
|
|
1561
|
+
throw error;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
const login = await loginAndSave(store, connection.id, clientFactory);
|
|
1565
|
+
return login.client.post(service, path, body, login.token);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
async function tdomsGet(store, connectionId, service, path, clientFactory) {
|
|
1570
|
+
const { connection, client, token } = await getAuthenticatedClient(store, connectionId, clientFactory);
|
|
1571
|
+
try {
|
|
1572
|
+
if (typeof client.get === "function") {
|
|
1573
|
+
return await client.get(service, path, token);
|
|
1574
|
+
}
|
|
1575
|
+
return await client.request(service, path, { method: "GET", token });
|
|
1576
|
+
} catch (error) {
|
|
1577
|
+
if (!isUnauthorized(error)) {
|
|
1578
|
+
throw error;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
const login = await loginAndSave(store, connection.id, clientFactory);
|
|
1582
|
+
if (typeof login.client.get === "function") {
|
|
1583
|
+
return login.client.get(service, path, login.token);
|
|
1584
|
+
}
|
|
1585
|
+
return login.client.request(service, path, { method: "GET", token: login.token });
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
async function readAllSourcePages(store, connectionId, input, clientFactory) {
|
|
1590
|
+
const first = await readSourcePage(store, connectionId, input, input.pageNumber && input.pageNumber > 0 ? input.pageNumber : 1, clientFactory);
|
|
1591
|
+
const numberOfPages = Math.max(1, Number(first.numberOfPages || 1));
|
|
1592
|
+
const pages = [first];
|
|
1593
|
+
|
|
1594
|
+
for (let pageNumber = 2; pageNumber <= numberOfPages; pageNumber += 1) {
|
|
1595
|
+
pages.push(await readSourcePage(store, connectionId, input, pageNumber, clientFactory));
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
const sourceTextUtf8 = pages.map((page) => decodeBase64Text(page.sourceCode)).join("");
|
|
1599
|
+
return {
|
|
1600
|
+
response: first,
|
|
1601
|
+
pages: pages.map((page) => ({
|
|
1602
|
+
pageNumber: page.pageNumber,
|
|
1603
|
+
numberOfPages: page.numberOfPages,
|
|
1604
|
+
sourceCodeBase64Length: page.sourceCode ? String(page.sourceCode).length : 0
|
|
1605
|
+
})),
|
|
1606
|
+
sourceTextUtf8,
|
|
1607
|
+
sourceCodeBase64: Buffer.from(sourceTextUtf8, "utf8").toString("base64"),
|
|
1608
|
+
canEdit: first.canEdit,
|
|
1609
|
+
ccsid: first.ccsid,
|
|
1610
|
+
sourceType: first.sourceType,
|
|
1611
|
+
maximumLineLength: first.maximumLineLength,
|
|
1612
|
+
lastChangeDate: first.lastChangeDate,
|
|
1613
|
+
numberOfPages
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
async function readSourcePage(store, connectionId, input, pageNumber, clientFactory) {
|
|
1618
|
+
return tdomsPost(store, connectionId, "OMRSRC", "/object/get", compact({
|
|
1619
|
+
applicationCode: input.applicationCode,
|
|
1620
|
+
taskNumber: input.taskNumber,
|
|
1621
|
+
path: input.path,
|
|
1622
|
+
objectCode: input.objectCode,
|
|
1623
|
+
objectType: input.objectType,
|
|
1624
|
+
objectLibrary: input.objectLibrary,
|
|
1625
|
+
memberCode: input.memberCode,
|
|
1626
|
+
pageNumber
|
|
1627
|
+
}), clientFactory);
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
async function saveAllSourcePages(store, connectionId, input, clientFactory) {
|
|
1631
|
+
const chunks = splitTextByUtf8Bytes(input.sourceText, input.maxPageBytes || 48000);
|
|
1632
|
+
const numberOfPages = chunks.length || 1;
|
|
1633
|
+
const responses = [];
|
|
1634
|
+
|
|
1635
|
+
for (let index = 0; index < numberOfPages; index += 1) {
|
|
1636
|
+
const sourceCode = Buffer.from(chunks[index] || "", "utf8").toString("base64");
|
|
1637
|
+
responses.push(await tdomsPost(store, connectionId, "OMRSRC", "/object/save", compact({
|
|
1638
|
+
applicationCode: input.applicationCode,
|
|
1639
|
+
taskNumber: input.taskNumber,
|
|
1640
|
+
path: input.path,
|
|
1641
|
+
objectCode: input.objectCode,
|
|
1642
|
+
objectType: input.objectType,
|
|
1643
|
+
objectLibrary: input.objectLibrary,
|
|
1644
|
+
memberCode: input.memberCode,
|
|
1645
|
+
pageNumber: index + 1,
|
|
1646
|
+
numberOfPages,
|
|
1647
|
+
sourceCode,
|
|
1648
|
+
ccsid: input.ccsid,
|
|
1649
|
+
lastChangeDate: input.lastChangeDate,
|
|
1650
|
+
forceSave: input.forceSave
|
|
1651
|
+
}), clientFactory));
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
return {
|
|
1655
|
+
responses,
|
|
1656
|
+
numberOfPages,
|
|
1657
|
+
savedBytes: Buffer.byteLength(input.sourceText, "utf8")
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
async function getAuthenticatedClient(store, connectionId, clientFactory = (connection) => new TdomsClient(connection)) {
|
|
1662
|
+
const connection = await store.getConnection(connectionId, { includeSecrets: true });
|
|
1663
|
+
if (!connection) {
|
|
1664
|
+
throw new Error(`Connection not found: ${connectionId}`);
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
const client = clientFactory(connection);
|
|
1668
|
+
if (connection.token) {
|
|
1669
|
+
return { connection, client, token: connection.token };
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
return loginAndSave(store, connectionId, clientFactory);
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
async function loginAndSave(store, connectionId, clientFactory = (connection) => new TdomsClient(connection)) {
|
|
1676
|
+
const connection = await store.getConnection(connectionId, { includeSecrets: true });
|
|
1677
|
+
if (!connection) {
|
|
1678
|
+
throw new Error(`Connection not found: ${connectionId}`);
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
const client = clientFactory(connection);
|
|
1682
|
+
const token = await client.login();
|
|
1683
|
+
await store.saveToken(connection.id, token);
|
|
1684
|
+
await store.updateStatus(connection.id, {
|
|
1685
|
+
lastStatus: "login-ok",
|
|
1686
|
+
lastLoginAt: new Date().toISOString()
|
|
1687
|
+
});
|
|
1688
|
+
return { connection, client, token };
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
function registerActionTool(server, { name, description, service, actions, store, clientFactory, confirm, validateBody }) {
|
|
1692
|
+
server.registerTool(
|
|
1693
|
+
name,
|
|
1694
|
+
{
|
|
1695
|
+
description,
|
|
1696
|
+
inputSchema: {
|
|
1697
|
+
connectionId: z.string(),
|
|
1698
|
+
action: z.enum(actions),
|
|
1699
|
+
body: z.record(z.string(), z.unknown()).default({}).describe("Request body exactly as documented by the TD/OMS REST API for this action."),
|
|
1700
|
+
confirm: z.boolean().optional().describe("Must be true for actions that can create, change, process, delete, release, transfer, close, or otherwise mutate TD/OMS state.")
|
|
1701
|
+
}
|
|
1702
|
+
},
|
|
1703
|
+
async (input) => {
|
|
1704
|
+
const needsConfirmation = typeof confirm === "function" ? confirm(input) : confirm;
|
|
1705
|
+
if (needsConfirmation) {
|
|
1706
|
+
requireConfirmation(input.confirm, `run ${service}${input.action}`);
|
|
1707
|
+
}
|
|
1708
|
+
if (validateBody) {
|
|
1709
|
+
validateBody(input);
|
|
1710
|
+
}
|
|
1711
|
+
const response = await tdomsPost(store, input.connectionId, service, input.action, input.body, clientFactory);
|
|
1712
|
+
return result({ ok: true, response });
|
|
1713
|
+
}
|
|
1714
|
+
);
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
function validateNewObjectActionBody({ action, body = {} }) {
|
|
1718
|
+
if (!["/add", "/location"].includes(action)) {
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
const confusingFields = action === "/add" ? {
|
|
1723
|
+
fixNumber: "task",
|
|
1724
|
+
taskNumber: "task",
|
|
1725
|
+
requestNumber: "task",
|
|
1726
|
+
objectCode: "newObject",
|
|
1727
|
+
objectLibrary: "newObjectLibrary",
|
|
1728
|
+
objectAttribute: "newAttribute",
|
|
1729
|
+
sourceFile: "newSourceFile",
|
|
1730
|
+
sourceLibrary: "newSourceLibrary",
|
|
1731
|
+
sourceMember: "newSourceMember",
|
|
1732
|
+
sourceAttribute: "newSourceAttribute"
|
|
1733
|
+
} : {
|
|
1734
|
+
fixNumber: "task",
|
|
1735
|
+
taskNumber: "task",
|
|
1736
|
+
requestNumber: "task",
|
|
1737
|
+
objectCode: "objectType/objectAttribute",
|
|
1738
|
+
objectLibrary: "remove objectLibrary",
|
|
1739
|
+
sourceFile: "remove sourceFile",
|
|
1740
|
+
sourceLibrary: "remove sourceLibrary",
|
|
1741
|
+
sourceMember: "remove sourceMember",
|
|
1742
|
+
sourceAttribute: "remove sourceAttribute"
|
|
1743
|
+
};
|
|
1744
|
+
const found = Object.entries(confusingFields)
|
|
1745
|
+
.filter(([field]) => Object.prototype.hasOwnProperty.call(body, field))
|
|
1746
|
+
.map(([field, replacement]) => replacement ? `${field} -> ${replacement}` : field);
|
|
1747
|
+
|
|
1748
|
+
if (found.length > 0) {
|
|
1749
|
+
throw new Error(`OMQRNO ${action} body uses documented NewObjectAPI field names. Replace: ${found.join(", ")}. Prefer tdoms_new_object_create, tdoms_new_source_member_create, or tdoms_new_object_location.`);
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
if (action === "/add") {
|
|
1753
|
+
const required = ["applicationCode", "task", "environmentCode", "newObject", "newObjectLibrary", "newObjectType", "newAttribute"];
|
|
1754
|
+
const missing = required.filter((field) => body[field] === undefined);
|
|
1755
|
+
if (missing.length > 0) {
|
|
1756
|
+
throw new Error(`OMQRNO /add body is missing required NewObjectAPI fields: ${missing.join(", ")}. Prefer tdoms_new_object_create or tdoms_new_source_member_create.`);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
if (action === "/location") {
|
|
1761
|
+
const required = ["applicationCode", "task", "environmentCode", "objectType", "objectAttribute"];
|
|
1762
|
+
const missing = required.filter((field) => body[field] === undefined);
|
|
1763
|
+
if (missing.length > 0) {
|
|
1764
|
+
throw new Error(`OMQRNO /location body is missing required NewObjectAPI fields: ${missing.join(", ")}. Prefer tdoms_new_object_location.`);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
function registerFamilyTools(server, { store, clientFactory }) {
|
|
1770
|
+
const connectionId = z.string().describe("Saved TD/OMS connection id from tdoms_connection_list.");
|
|
1771
|
+
const rawBody = z.record(z.string(), z.unknown()).default({}).describe("Request body exactly as documented by TD/OMS REST API.");
|
|
1772
|
+
const confirm = z.boolean().optional().describe("Must be true for operations that can change TD/OMS state.");
|
|
1773
|
+
const applicationCode = z.string().max(10).describe("TD/OMS application code.");
|
|
1774
|
+
const taskNumber = z.string().max(20).describe("TD/OMS task/fix number.");
|
|
1775
|
+
const objectCode = z.string().optional().describe("Object or source file code.");
|
|
1776
|
+
const objectType = z.string().optional().describe("Object type, such as *PGM, *FILE, *MODULE, or *SRVPGM.");
|
|
1777
|
+
const objectLibrary = z.string().optional().describe("Object/source library.");
|
|
1778
|
+
const memberCode = z.string().optional().describe("Member/detail name.");
|
|
1779
|
+
const transferObject = z.object({
|
|
1780
|
+
objectCode: z.string().max(128).describe("TD/OMS object code. For source members this is usually the source file, such as QRPGLESRC."),
|
|
1781
|
+
objectType: z.string().max(20).describe("TD/OMS object type, such as *PGM, *FILE, *MODULE, *SRVPGM, or *STMF."),
|
|
1782
|
+
memberCode: z.string().default("").describe("Member/detail for source-file objects. Leave blank for object-level transfers."),
|
|
1783
|
+
routeCode: z.string().default("").describe("Route code for IFS or routed objects. Leave blank for normal IBM i objects."),
|
|
1784
|
+
pathExtensionCode: z.string().default("").describe("Path extension code for IFS objects. Leave blank for normal IBM i objects.")
|
|
1785
|
+
});
|
|
1786
|
+
const environmentCode = z.string().max(10).describe("TD/OMS environment code for new object workflows, such as DEV.");
|
|
1787
|
+
const newObjectTemplate = z.string().optional().describe("Exact TD/OMS new-object template label from tdoms_new_object_location, such as a site-specific RPG program template.");
|
|
1788
|
+
const template = z.object({
|
|
1789
|
+
templateObjectType: z.string().optional().describe("Template object type, such as *PGM."),
|
|
1790
|
+
templateObject: z.string().optional().describe("Template object name."),
|
|
1791
|
+
templateObjectLibrary: z.string().optional().describe("Template object library."),
|
|
1792
|
+
templateObjectDetail: z.string().optional().describe("Template detail/member, if any.")
|
|
1793
|
+
}).optional().describe("Optional TD/OMS template object to copy from.");
|
|
1794
|
+
|
|
1795
|
+
registerAliasTool(server, "tdoms_connection_list", "List locally saved TD/OMS connection profiles.", {}, async () => {
|
|
1796
|
+
const connections = await store.listConnections();
|
|
1797
|
+
return result({ connections });
|
|
1798
|
+
});
|
|
1799
|
+
|
|
1800
|
+
registerAliasTool(server, "tdoms_connection_login", "Login to a saved TD/OMS connection and save the JWT.", { connectionId }, async ({ connectionId }) => {
|
|
1801
|
+
const { connection, client, token } = await loginAndSave(store, connectionId, clientFactory);
|
|
1802
|
+
const systemInfo = await client.getSystemInfo(token);
|
|
1803
|
+
return result({ ok: true, connection: await store.getConnection(connection.id), system: summarizeSystemInfo(systemInfo) });
|
|
1804
|
+
});
|
|
1805
|
+
|
|
1806
|
+
registerAliasTool(server, "tdoms_connection_system_info", "Fetch TD/OMS system/application info.", {
|
|
1807
|
+
connectionId,
|
|
1808
|
+
applications: z.string().optional()
|
|
1809
|
+
}, async ({ connectionId, applications }) => {
|
|
1810
|
+
const suffix = applications ? `/system?applications=${encodeURIComponent(applications)}` : "/system";
|
|
1811
|
+
const systemInfo = await tdomsGet(store, connectionId, "OMSINFO", suffix, clientFactory);
|
|
1812
|
+
await store.updateStatus(connectionId, {
|
|
1813
|
+
lastStatus: "system-probe-ok",
|
|
1814
|
+
lastSystemProbeAt: new Date().toISOString()
|
|
1815
|
+
});
|
|
1816
|
+
return result({ ok: true, system: summarizeSystemInfo(systemInfo), raw: systemInfo });
|
|
1817
|
+
});
|
|
1818
|
+
|
|
1819
|
+
registerAliasTool(server, "tdoms_connection_test", "Login and probe OMSINFO/system for a saved connection.", { connectionId }, async ({ connectionId }) => {
|
|
1820
|
+
const { connection, client, token } = await loginAndSave(store, connectionId, clientFactory);
|
|
1821
|
+
const systemInfo = await client.getSystemInfo(token);
|
|
1822
|
+
await store.updateStatus(connection.id, {
|
|
1823
|
+
lastStatus: "system-probe-ok",
|
|
1824
|
+
lastSystemProbeAt: new Date().toISOString()
|
|
1825
|
+
});
|
|
1826
|
+
return result({ ok: true, connection: await store.getConnection(connection.id), system: summarizeSystemInfo(systemInfo) });
|
|
1827
|
+
});
|
|
1828
|
+
|
|
1829
|
+
registerAliasTool(server, "tdoms_connection_refresh_token", "Refresh the saved TD/OMS JWT by logging in again.", { connectionId }, async ({ connectionId }) => {
|
|
1830
|
+
const { connection, token } = await loginAndSave(store, connectionId, clientFactory);
|
|
1831
|
+
return result({ ok: true, connection: await store.getConnection(connection.id), tokenRefreshed: Boolean(token) });
|
|
1832
|
+
});
|
|
1833
|
+
|
|
1834
|
+
registerAliasTool(server, "tdoms_connection_license_info", "Fetch OMSINFO/license for a saved connection.", { connectionId }, async ({ connectionId }) => {
|
|
1835
|
+
return result({ ok: true, response: await tdomsGet(store, connectionId, "OMSINFO", "/license", clientFactory) });
|
|
1836
|
+
});
|
|
1837
|
+
|
|
1838
|
+
registerAliasTool(server, "tdoms_connection_user_context", "Return the locally known TD/OMS user context without secrets.", { connectionId }, async ({ connectionId }) => {
|
|
1839
|
+
const connection = await store.getConnection(connectionId);
|
|
1840
|
+
if (!connection) {
|
|
1841
|
+
throw new Error(`Connection not found: ${connectionId}`);
|
|
1842
|
+
}
|
|
1843
|
+
return result({ ok: true, user: { connectionId, name: connection.name, username: connection.username, baseUrl: connection.baseUrl } });
|
|
1844
|
+
});
|
|
1845
|
+
|
|
1846
|
+
registerAliasTool(server, "tdoms_connection_applications", "List TD/OMS applications visible to the current user.", {
|
|
1847
|
+
connectionId,
|
|
1848
|
+
applications: z.string().optional()
|
|
1849
|
+
}, async ({ connectionId, applications }) => {
|
|
1850
|
+
const suffix = applications ? `/system?applications=${encodeURIComponent(applications)}` : "/system";
|
|
1851
|
+
const systemInfo = await tdomsGet(store, connectionId, "OMSINFO", suffix, clientFactory);
|
|
1852
|
+
return result({ ok: true, applications: summarizeSystemInfo(systemInfo).applications, raw: systemInfo?.applications || [] });
|
|
1853
|
+
});
|
|
1854
|
+
|
|
1855
|
+
const queryTools = [
|
|
1856
|
+
["tdoms_task_search", "Search tasks/fixes with richer family naming.", "OMXFix", ["applicationCode", "fixNumber", "shortFixDescription", "programmer", "fixStatus", "release", "pathCode", "expectedStartDate", "expectedCompletionDate"]],
|
|
1857
|
+
["tdoms_task_get", "Get one task/fix by application and task number.", "OMXFix"],
|
|
1858
|
+
["tdoms_task_objects", "List objects/solutions connected to a task.", "OMXSolutionExtended"],
|
|
1859
|
+
["tdoms_task_requests", "List requests linked to a task.", "OMXRequestTaskLink"],
|
|
1860
|
+
["tdoms_task_tickets", "List external ticket/backlink rows for a task when available.", "OMXRemoteIssueInformation"],
|
|
1861
|
+
["tdoms_task_backlinks", "List task backlinks/external ticket rows when available.", "OMXRemoteIssueInformation"],
|
|
1862
|
+
["tdoms_task_comments", "List comments related to a task when available.", "OMXCommentLink"],
|
|
1863
|
+
["tdoms_task_labels", "List labels related to a task when available.", "OMXLabel"],
|
|
1864
|
+
["tdoms_task_log_history", "List log history rows related to a task.", "OMXLogHistoryExtended"],
|
|
1865
|
+
["tdoms_task_transfer_history", "List transfer history rows related to a task.", "OMXObjectTransferHistory"],
|
|
1866
|
+
["tdoms_task_build_queue", "List build queue rows related to a task.", "OMXCreateObjectMonitorExtended"],
|
|
1867
|
+
["tdoms_task_compile_results", "List compile result rows related to a task.", "OMXCreateObjectMonitorExtended"],
|
|
1868
|
+
["tdoms_request_search", "Search TD/OMS requests.", "OMXRequest"],
|
|
1869
|
+
["tdoms_request_get", "Get one TD/OMS request.", "OMXRequest"],
|
|
1870
|
+
["tdoms_solution_search", "Search TD/OMS solutions/connected objects.", "OMXSolutionExtended"],
|
|
1871
|
+
["tdoms_solution_get", "Get one TD/OMS solution/connected object.", "OMXSolutionExtended"],
|
|
1872
|
+
["tdoms_solution_compile_overrides", "List compile overrides for a solution.", "OMXCompileOverrideExtended"],
|
|
1873
|
+
["tdoms_solution_transfer_history", "List transfer history for a solution.", "OMXObjectTransferHistory"],
|
|
1874
|
+
["tdoms_solution_build_queue", "List build queue rows for a solution.", "OMXCreateObjectMonitorExtended"],
|
|
1875
|
+
["tdoms_solution_compile_results", "List compile results for a solution.", "OMXCreateObjectMonitorExtended"],
|
|
1876
|
+
["tdoms_object_search", "Search TD/OMS objects/components.", "OMXObjectExtended"],
|
|
1877
|
+
["tdoms_object_get", "Get object/component rows.", "OMXObjectExtended"],
|
|
1878
|
+
["tdoms_object_details", "Get object/member detail rows.", "OMXObjectDetailExtended"],
|
|
1879
|
+
["tdoms_object_members", "List object members/details.", "OMXObjectDetailExtended"],
|
|
1880
|
+
["tdoms_object_transfer_history", "List object transfer history.", "OMXObjectTransferHistory"],
|
|
1881
|
+
["tdoms_object_version_conflicts", "List object version conflicts.", "OMXObjectVersionConflict"],
|
|
1882
|
+
["tdoms_branch_search", "Search TD/OMS branches.", "OMXTransferPath"],
|
|
1883
|
+
["tdoms_branch_get", "Get TD/OMS branch rows.", "OMXTransferPath"],
|
|
1884
|
+
["tdoms_branch_tasks", "List tasks assigned to a branch/path.", "OMXFixExtended"],
|
|
1885
|
+
["tdoms_branch_solutions", "List solutions connected to branch tasks.", "OMXSolutionExtended"],
|
|
1886
|
+
["tdoms_connection_list_get", "Get connection-list metadata rows.", "OMXConnectionListExtended"],
|
|
1887
|
+
["tdoms_connection_list_entries", "List connection-list entries.", "OMXConnectionListEntryExtended"],
|
|
1888
|
+
["tdoms_build_queue_list", "List build queue entries.", "OMXCreateObjectMonitorExtended"],
|
|
1889
|
+
["tdoms_build_queue_for_task", "List build queue entries for a task.", "OMXCreateObjectMonitorExtended"],
|
|
1890
|
+
["tdoms_build_queue_for_solution", "List build queue entries for a solution.", "OMXCreateObjectMonitorExtended"],
|
|
1891
|
+
["tdoms_build_queue_for_object", "List build queue entries for an object.", "OMXCreateObjectMonitorExtended"],
|
|
1892
|
+
["tdoms_compile_results", "List compile results.", "OMXCreateObjectMonitorExtended"],
|
|
1893
|
+
["tdoms_compile_errors", "List compile errors/diagnostics where available.", "OMXCreateObjectMonitorExtended"],
|
|
1894
|
+
["tdoms_transfer_history", "List transfer history.", "OMXObjectTransferHistory"],
|
|
1895
|
+
["tdoms_managed_deployment_list", "List managed deployments.", "OMXManagedDeploymentExtended"],
|
|
1896
|
+
["tdoms_remote_job_list", "List remote jobs.", "OMXRemoteJobMonitorExtended"],
|
|
1897
|
+
["tdoms_remote_job_get", "Get remote job rows.", "OMXRemoteJobMonitorExtended"],
|
|
1898
|
+
["tdoms_comment_list", "List comments.", "OMXComment"],
|
|
1899
|
+
["tdoms_label_list", "List labels.", "OMXLabel"],
|
|
1900
|
+
["tdoms_user_option_list", "List user options.", "OMXUserOption"],
|
|
1901
|
+
["tdoms_dashboard_list", "List dashboard definitions.", "OMDSH"],
|
|
1902
|
+
["tdoms_dashboard_missing_source", "Run/query missing-source dashboard data when represented in tables.", "OMXObjectExtended"],
|
|
1903
|
+
["tdoms_dashboard_problem_transfers", "Run/query problematic transfer dashboard data when represented in tables.", "OMXObjectTransferHistory"],
|
|
1904
|
+
["tdoms_tracker_search", "Search tracker/request-task style records.", "OMXRequestTaskLink"],
|
|
1905
|
+
["tdoms_tracker_get", "Get tracker/request-task style records.", "OMXRequestTaskLink"],
|
|
1906
|
+
["tdoms_admin_data_dictionary", "Query the TD/OMS data dictionary.", "OMXDataDictionary"],
|
|
1907
|
+
["tdoms_admin_table_fields", "Query fields for a TD/OMS table from the data dictionary.", "OMXDataDictionary"],
|
|
1908
|
+
["tdoms_admin_saved_searches", "List reusable saved search rows when available.", "OMXStoredSearchTerm"],
|
|
1909
|
+
["tdoms_admin_saved_search_run", "Run a reusable saved search by querying stored search metadata.", "OMXStoredSearchTerm"]
|
|
1910
|
+
];
|
|
1911
|
+
|
|
1912
|
+
for (const [name, description, table, returnFields] of queryTools) {
|
|
1913
|
+
registerDatabaseTool(server, { name, description, table, returnFields, store, clientFactory });
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
registerAliasTool(server, "tdoms_task_details", "Fetch a task plus common extension-like related data in one read-only call.", {
|
|
1917
|
+
connectionId,
|
|
1918
|
+
applicationCode,
|
|
1919
|
+
taskNumber
|
|
1920
|
+
}, async (input) => result({
|
|
1921
|
+
ok: true,
|
|
1922
|
+
task: await safeQuery(store, input.connectionId, taskQuery("OMXFixExtended", input), clientFactory),
|
|
1923
|
+
objects: await safeQuery(store, input.connectionId, taskQuery("OMXSolutionExtended", input), clientFactory),
|
|
1924
|
+
requests: await safeQuery(store, input.connectionId, taskQuery("OMXRequestTaskLink", input), clientFactory),
|
|
1925
|
+
tickets: await safeQuery(store, input.connectionId, taskQuery("OMXRemoteIssueInformation", input), clientFactory),
|
|
1926
|
+
comments: await safeQuery(store, input.connectionId, taskQuery("OMXCommentLink", input), clientFactory),
|
|
1927
|
+
labels: await safeQuery(store, input.connectionId, taskQuery("OMXLabel", input), clientFactory),
|
|
1928
|
+
transferHistory: await safeQuery(store, input.connectionId, taskQuery("OMXObjectTransferHistory", input), clientFactory),
|
|
1929
|
+
buildQueue: await safeQuery(store, input.connectionId, taskQuery("OMXCreateObjectMonitorExtended", input), clientFactory),
|
|
1930
|
+
compileResults: await safeQuery(store, input.connectionId, taskQuery("OMXCreateObjectMonitorExtended", input), clientFactory)
|
|
1931
|
+
}));
|
|
1932
|
+
|
|
1933
|
+
registerAliasTool(server, "tdoms_request_details", "Fetch request plus linked tasks in one read-only call.", {
|
|
1934
|
+
connectionId,
|
|
1935
|
+
applicationCode,
|
|
1936
|
+
requestNumber: z.string()
|
|
1937
|
+
}, async (input) => result({
|
|
1938
|
+
ok: true,
|
|
1939
|
+
request: await safeQuery(store, input.connectionId, requestQuery("OMXRequest", input), clientFactory),
|
|
1940
|
+
tasks: await safeQuery(store, input.connectionId, requestQuery("OMXRequestTaskLink", input), clientFactory)
|
|
1941
|
+
}));
|
|
1942
|
+
|
|
1943
|
+
registerAliasTool(server, "tdoms_solution_details", "Fetch solution plus object details, transfer history, queue, and compile results.", {
|
|
1944
|
+
connectionId,
|
|
1945
|
+
applicationCode,
|
|
1946
|
+
taskNumber,
|
|
1947
|
+
objectCode,
|
|
1948
|
+
objectType,
|
|
1949
|
+
objectLibrary,
|
|
1950
|
+
memberCode
|
|
1951
|
+
}, async (input) => result({
|
|
1952
|
+
ok: true,
|
|
1953
|
+
solution: await safeQuery(store, input.connectionId, objectQuery("OMXSolutionExtended", input), clientFactory),
|
|
1954
|
+
objectDetails: await safeQuery(store, input.connectionId, objectQuery("OMXObjectDetailExtended", input), clientFactory),
|
|
1955
|
+
transferHistory: await safeQuery(store, input.connectionId, objectQuery("OMXObjectTransferHistory", input), clientFactory),
|
|
1956
|
+
buildQueue: await safeQuery(store, input.connectionId, objectQuery("OMXCreateObjectMonitorExtended", input), clientFactory),
|
|
1957
|
+
compileResults: await safeQuery(store, input.connectionId, objectQuery("OMXCreateObjectMonitorExtended", input), clientFactory)
|
|
1958
|
+
}));
|
|
1959
|
+
|
|
1960
|
+
const actionTools = [
|
|
1961
|
+
["tdoms_task_change", "Change a task through OMQRTA/change.", "OMQRTA", "/change"],
|
|
1962
|
+
["tdoms_task_complete", "Complete a task through OMQRTA/complete.", "OMQRTA", "/complete"],
|
|
1963
|
+
["tdoms_task_copy", "Copy a task through OMQRTA/copy.", "OMQRTA", "/copy"],
|
|
1964
|
+
["tdoms_task_delete", "Delete a task through OMQRTA/delete.", "OMQRTA", "/delete"],
|
|
1965
|
+
["tdoms_task_status", "Recalculate task status through OMQRTA/status.", "OMQRTA", "/status"],
|
|
1966
|
+
["tdoms_task_ratify", "Ratify a task through OMQRTA/ratify.", "OMQRTA", "/ratify"],
|
|
1967
|
+
["tdoms_task_link_request", "Link a request to a task through OMQRTA/link/request.", "OMQRTA", "/link/request"],
|
|
1968
|
+
["tdoms_task_unlink_request", "Unlink a request from a task through OMQRTA/unlink/request.", "OMQRTA", "/unlink/request"],
|
|
1969
|
+
["tdoms_task_link_ticket", "Link an external ticket/backlink through OMQRTA/link/ticket.", "OMQRTA", "/link/ticket"],
|
|
1970
|
+
["tdoms_task_unlink_ticket", "Unlink an external ticket/backlink through OMQRTA/unlink/ticket.", "OMQRTA", "/unlink/ticket"],
|
|
1971
|
+
["tdoms_request_create", "Create a request through OMQRRE/add.", "OMQRRE", "/add"],
|
|
1972
|
+
["tdoms_request_change", "Change a request through OMQRRE/change.", "OMQRRE", "/change"],
|
|
1973
|
+
["tdoms_request_complete", "Complete a request through OMQRRE/complete.", "OMQRRE", "/complete"],
|
|
1974
|
+
["tdoms_request_copy", "Copy a request through OMQRRE/copy.", "OMQRRE", "/copy"],
|
|
1975
|
+
["tdoms_request_delete", "Delete a request through OMQRRE/delete.", "OMQRRE", "/delete"],
|
|
1976
|
+
["tdoms_request_connect_task", "Connect a request/task through OMQRRE/connect.", "OMQRRE", "/connect"],
|
|
1977
|
+
["tdoms_request_disconnect_task", "Disconnect a request/task through OMQRRE/disconnect.", "OMQRRE", "/disconnect"],
|
|
1978
|
+
["tdoms_request_rename", "Rename a request through OMQRRE/rename.", "OMQRRE", "/rename"],
|
|
1979
|
+
["tdoms_request_ratify", "Ratify a request through OMQRRE/ratify.", "OMQRRE", "/ratify"],
|
|
1980
|
+
["tdoms_solution_connect", "Connect a solution through OMQRSO/connect.", "OMQRSO", "/connect"],
|
|
1981
|
+
["tdoms_solution_disconnect", "Disconnect a solution through OMQRSO/disconnect.", "OMQRSO", "/disconnect"],
|
|
1982
|
+
["tdoms_solution_change", "Change a solution through OMQRSO/change.", "OMQRSO", "/change"],
|
|
1983
|
+
["tdoms_solution_lock", "Lock a solution through OMQRSO/lock.", "OMQRSO", "/lock"],
|
|
1984
|
+
["tdoms_solution_unlock", "Unlock a solution through OMQRSO/unlock.", "OMQRSO", "/unlock"],
|
|
1985
|
+
["tdoms_solution_move", "Move a solution through OMQRSO/move.", "OMQRSO", "/move"],
|
|
1986
|
+
["tdoms_object_add", "Add an object/component through OMQROB/add.", "OMQROB", "/add"],
|
|
1987
|
+
["tdoms_object_change", "Change an object/component through OMQROB/change.", "OMQROB", "/change"],
|
|
1988
|
+
["tdoms_object_delete", "Delete an object/component through OMQROB/delete.", "OMQROB", "/delete"],
|
|
1989
|
+
["tdoms_object_refill", "Refill an object/component through OMQROB/refill.", "OMQROB", "/refill"],
|
|
1990
|
+
["tdoms_object_close", "Close an object/component through OMQROB/close.", "OMQROB", "/close"],
|
|
1991
|
+
["tdoms_component_change_member_attributes", "Change member attributes through the object change endpoint.", "OMQROB", "/change"],
|
|
1992
|
+
["tdoms_connection_list_change", "Change a connection-list entry through OMQRCL/change.", "OMQRCL", "/change"],
|
|
1993
|
+
["tdoms_connection_list_delete", "Delete a connection-list entry through OMQRCL/delete.", "OMQRCL", "/delete"],
|
|
1994
|
+
["tdoms_connection_list_clear", "Clear connection-list entries through OMQRCL/delete using a caller-provided body.", "OMQRCL", "/delete"],
|
|
1995
|
+
["tdoms_compile_override_add", "Add a compile override through OMQRCO/add.", "OMQRCO", "/add"],
|
|
1996
|
+
["tdoms_compile_override_change", "Change a compile override through OMQRCO/change.", "OMQRCO", "/change"],
|
|
1997
|
+
["tdoms_compile_override_delete", "Delete a compile override through OMQRCO/delete.", "OMQRCO", "/delete"],
|
|
1998
|
+
["tdoms_compile_override_authorize", "Authorize a compile override through OMQRCO/authorize.", "OMQRCO", "/authorize"],
|
|
1999
|
+
["tdoms_managed_deployment_schedule", "Schedule a managed deployment through OMQRMD/schedule.", "OMQRMD", "/schedule"],
|
|
2000
|
+
["tdoms_managed_deployment_hold", "Hold a managed deployment through OMQRMD/hold.", "OMQRMD", "/hold"],
|
|
2001
|
+
["tdoms_managed_deployment_release", "Release a managed deployment through OMQRMD/release.", "OMQRMD", "/release"],
|
|
2002
|
+
["tdoms_managed_deployment_close", "Close a managed deployment through OMQRMD/close.", "OMQRMD", "/close"],
|
|
2003
|
+
["tdoms_branch_create", "Create a branch through OMQRBR/add.", "OMQRBR", "/add"],
|
|
2004
|
+
["tdoms_branch_change", "Change a branch through OMQRBR/change.", "OMQRBR", "/change"],
|
|
2005
|
+
["tdoms_branch_status", "Recalculate branch status through OMQRBR/status.", "OMQRBR", "/status"],
|
|
2006
|
+
["tdoms_branch_delete", "Delete a branch through OMQRBR/delete.", "OMQRBR", "/delete"],
|
|
2007
|
+
["tdoms_branch_stash", "Stash a branch through OMQRBR/stash.", "OMQRBR", "/stash"],
|
|
2008
|
+
["tdoms_branch_restore", "Restore a branch through OMQRBR/restore.", "OMQRBR", "/restore"],
|
|
2009
|
+
["tdoms_branch_purge", "Purge a stashed branch through OMQRBR/purge.", "OMQRBR", "/purge"],
|
|
2010
|
+
["tdoms_comment_add", "Add a comment through OMQRCM/add.", "OMQRCM", "/add"],
|
|
2011
|
+
["tdoms_comment_change", "Change a comment through OMQRCM/change.", "OMQRCM", "/change"],
|
|
2012
|
+
["tdoms_comment_reply", "Reply to a comment through OMQRCM/reply.", "OMQRCM", "/reply"],
|
|
2013
|
+
["tdoms_comment_delete", "Delete a comment through OMQRCM/delete.", "OMQRCM", "/delete"],
|
|
2014
|
+
["tdoms_label_add", "Add a label through OMQRLB/add.", "OMQRLB", "/add"],
|
|
2015
|
+
["tdoms_label_change", "Change a label through OMQRLB/change.", "OMQRLB", "/change"],
|
|
2016
|
+
["tdoms_label_delete", "Delete a label through OMQRLB/delete.", "OMQRLB", "/delete"],
|
|
2017
|
+
["tdoms_label_apply", "Apply a label through OMQRLB/label.", "OMQRLB", "/label"],
|
|
2018
|
+
["tdoms_label_remove", "Remove a label through OMQRLB/unlabel.", "OMQRLB", "/unlabel"],
|
|
2019
|
+
["tdoms_user_option_execute", "Execute a TD/OMS user option through OMQRUO/execute.", "OMQRUO", "/execute"],
|
|
2020
|
+
["tdoms_admin_saved_search_save", "Save/update reusable search metadata with a caller-provided endpoint body.", "OMRDBA", "/get"],
|
|
2021
|
+
["tdoms_admin_call_program_request", "Call OMRPGM/callRequest. Advanced and always requires confirmation.", "OMRPGM", "/callRequest"]
|
|
2022
|
+
];
|
|
2023
|
+
|
|
2024
|
+
for (const [name, description, service, path] of actionTools) {
|
|
2025
|
+
registerEndpointTool(server, { name, description, service, path, store, clientFactory, confirm: true });
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
const readEndpointTools = [
|
|
2029
|
+
["tdoms_source_get", "Read source through OMRSRC/source/get.", "OMRSRC", "/source/get"],
|
|
2030
|
+
["tdoms_object_relations", "Read object relations through OMQROB/objectRelation.", "OMQROB", "/objectRelation"],
|
|
2031
|
+
["tdoms_object_job_relations", "Read object job relations through OMQROB/jobRelation.", "OMQROB", "/jobRelation"],
|
|
2032
|
+
["tdoms_compile_event_raw_records", "Fetch raw compile event records through OMQREV/getRawRecords.", "OMQREV", "/getRawRecords"],
|
|
2033
|
+
["tdoms_compile_event_parse", "Parse compile event records through OMQREV/parseEvent.", "OMQREV", "/parseEvent"],
|
|
2034
|
+
["tdoms_transfer_targets", "Select transfer targets through OMQRTO/select. Some TD/OMS configurations may treat this as a preparatory action.", "OMQRTO", "/select"],
|
|
2035
|
+
["tdoms_transfer_progress", "Fetch transfer progress through OMQRTO/progress.", "OMQRTO", "/progress"],
|
|
2036
|
+
["tdoms_log_history", "Fetch log history through OMQRLH/getLogHistory.", "OMQRLH", "/getLogHistory"],
|
|
2037
|
+
["tdoms_log_messages", "List log messages through OMQRLH/listMessages.", "OMQRLH", "/listMessages"],
|
|
2038
|
+
["tdoms_log_message_get", "Get one log message through OMQRLH/getMessage.", "OMQRLH", "/getMessage"],
|
|
2039
|
+
["tdoms_spool_list", "List spool files through OMQRSF/list.", "OMQRSF", "/list"],
|
|
2040
|
+
["tdoms_spool_fetch", "Fetch a spool file through OMQRSF/fetch.", "OMQRSF", "/fetch"],
|
|
2041
|
+
["tdoms_analysis_impact", "Fetch impact analysis through OMQRIA/ref-style endpoint. Use path for dynamic object path.", "OMQRIA", "", "GET"],
|
|
2042
|
+
["tdoms_analysis_dependency_graph", "Fetch dependency graph data through OMQRIA/ref-style endpoint. Use path for dynamic object path.", "OMQRIA", "", "GET"],
|
|
2043
|
+
["tdoms_dashboard_execute", "Execute a dashboard query through OMRDASH/dashboard/execute/query.", "OMRDASH", "/dashboard/execute/query"],
|
|
2044
|
+
["tdoms_kanban_boards", "Query TD/OMS board-like data through OMRDBA/get with a caller-provided body.", "OMRDBA", "/get"],
|
|
2045
|
+
["tdoms_kanban_cards", "Query TD/OMS card-like task data through OMRDBA/get with a caller-provided body.", "OMRDBA", "/get"]
|
|
2046
|
+
];
|
|
2047
|
+
|
|
2048
|
+
for (const [name, description, service, path, method] of readEndpointTools) {
|
|
2049
|
+
registerEndpointTool(server, {
|
|
2050
|
+
name,
|
|
2051
|
+
description,
|
|
2052
|
+
service,
|
|
2053
|
+
path,
|
|
2054
|
+
store,
|
|
2055
|
+
clientFactory,
|
|
2056
|
+
confirm: false,
|
|
2057
|
+
allowPathOverride: service === "OMQRIA",
|
|
2058
|
+
method
|
|
2059
|
+
});
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
registerAliasTool(server, "tdoms_transfer_select_targets", "Discover valid TD/OMS transfer targets through OMQRTO/select using the documented transfer schema. This is the first step for checkout, promote, copy, or move.", {
|
|
2063
|
+
connectionId,
|
|
2064
|
+
action: z.enum(["Checkout", "Promote", "Copy", "Move"]).default("Checkout"),
|
|
2065
|
+
application: applicationCode.describe("TD/OMS application code. OMQRTO calls this field application, not applicationCode."),
|
|
2066
|
+
taskNumber,
|
|
2067
|
+
fromEnvironment: z.string().default(" ").describe("Environment to transfer from, such as *PRD, UAT, *ACC, or blank to let TD/OMS resolve defaults."),
|
|
2068
|
+
objectList: z.array(transferObject).default([]).describe("Objects to select. Leave empty to select all eligible task objects.")
|
|
2069
|
+
}, async (input) => {
|
|
2070
|
+
const body = compact({
|
|
2071
|
+
action: input.action,
|
|
2072
|
+
taskNumber: input.taskNumber,
|
|
2073
|
+
application: input.application,
|
|
2074
|
+
fromEnvironment: input.fromEnvironment,
|
|
2075
|
+
objectList: input.objectList
|
|
2076
|
+
});
|
|
2077
|
+
const response = await tdomsPost(store, input.connectionId, "OMQRTO", "/select", body, clientFactory);
|
|
2078
|
+
return result({ ok: true, response, body, next: "Choose a target from response.targets, then call tdoms_transfer_prepare with fromEnvironment, toEnvironment, and the same objects." });
|
|
2079
|
+
});
|
|
2080
|
+
|
|
2081
|
+
registerAliasTool(server, "tdoms_transfer_prepare", "Prepare a TD/OMS transfer through OMQRTO/prepare after selecting a target. Requires confirm: true.", {
|
|
2082
|
+
connectionId,
|
|
2083
|
+
action: z.enum(["Copy", "Move"]).default("Copy").describe("Use Copy for normal checkout/promote style transfers unless TD/OMS specifically requires Move."),
|
|
2084
|
+
application: applicationCode.describe("TD/OMS application code. OMQRTO calls this field application, not applicationCode."),
|
|
2085
|
+
taskNumber,
|
|
2086
|
+
fromEnvironment: z.string().describe("Source environment chosen from tdoms_transfer_select_targets."),
|
|
2087
|
+
toEnvironment: z.string().describe("Target environment chosen from tdoms_transfer_select_targets."),
|
|
2088
|
+
objects: z.array(transferObject).default([]).describe("Objects to transfer. Leave empty for task-level transfer."),
|
|
2089
|
+
targetSubEnvironments: z.array(z.number()).default([]),
|
|
2090
|
+
confirmation: z.boolean().default(true).describe("TD/OMS pre-transfer confirmation flag."),
|
|
2091
|
+
confirm
|
|
2092
|
+
}, async (input) => {
|
|
2093
|
+
requireConfirmation(input.confirm, "prepare TD/OMS transfer");
|
|
2094
|
+
const body = compact({
|
|
2095
|
+
action: input.action,
|
|
2096
|
+
application: input.application,
|
|
2097
|
+
taskNumber: input.taskNumber,
|
|
2098
|
+
fromEnvironment: input.fromEnvironment,
|
|
2099
|
+
toEnvironment: input.toEnvironment,
|
|
2100
|
+
objects: input.objects,
|
|
2101
|
+
targetSubEnvironments: input.targetSubEnvironments,
|
|
2102
|
+
confirmation: input.confirmation
|
|
2103
|
+
});
|
|
2104
|
+
const response = await tdomsPost(store, input.connectionId, "OMQRTO", "/prepare", body, clientFactory);
|
|
2105
|
+
return result({ ok: true, response, body, next: "If prepare returns a transferNumber, call tdoms_transfer_execute_number. If it returns warnings/conflicts, inspect them before executing." });
|
|
2106
|
+
});
|
|
2107
|
+
|
|
2108
|
+
registerAliasTool(server, "tdoms_transfer_execute_number", "Execute a prepared TD/OMS transfer through OMQRTO/execute using a transferNumber. Requires confirm: true.", {
|
|
2109
|
+
connectionId,
|
|
2110
|
+
transferNumber: z.string().max(20),
|
|
2111
|
+
schedules: z.array(z.record(z.string(), z.unknown())).default([]),
|
|
2112
|
+
confirm
|
|
2113
|
+
}, async (input) => {
|
|
2114
|
+
requireConfirmation(input.confirm, "execute TD/OMS transfer");
|
|
2115
|
+
const body = compact({
|
|
2116
|
+
transferNumber: input.transferNumber,
|
|
2117
|
+
schedules: input.schedules
|
|
2118
|
+
});
|
|
2119
|
+
const response = await tdomsPost(store, input.connectionId, "OMQRTO", "/execute", body, clientFactory);
|
|
2120
|
+
return result({ ok: true, response, body, next: "Call tdoms_transfer_progress_number with the transferNumber or returned job to monitor completion." });
|
|
2121
|
+
});
|
|
2122
|
+
|
|
2123
|
+
registerAliasTool(server, "tdoms_transfer_progress_number", "Check TD/OMS transfer progress through OMQRTO/progress using a transferNumber and optional job object.", {
|
|
2124
|
+
connectionId,
|
|
2125
|
+
transferNumber: z.string().max(20).optional(),
|
|
2126
|
+
job: z.record(z.string(), z.unknown()).optional()
|
|
2127
|
+
}, async (input) => {
|
|
2128
|
+
const body = compact({
|
|
2129
|
+
transferNumber: input.transferNumber,
|
|
2130
|
+
job: input.job
|
|
2131
|
+
});
|
|
2132
|
+
const response = await tdomsPost(store, input.connectionId, "OMQRTO", "/progress", body, clientFactory);
|
|
2133
|
+
return result({ ok: true, response, body });
|
|
2134
|
+
});
|
|
2135
|
+
|
|
2136
|
+
registerAliasTool(server, "tdoms_new_object_location", "Get TD/OMS new-object location/template defaults through OMQRNO/location. Use this before creating a new object so the exact environment/template values match the site.", {
|
|
2137
|
+
connectionId,
|
|
2138
|
+
applicationCode,
|
|
2139
|
+
task: taskNumber.describe("TD/OMS task/fix number. OMQRNO calls this field task, not taskNumber or fixNumber."),
|
|
2140
|
+
environmentCode,
|
|
2141
|
+
objectType: z.string().describe("Object type for the new object, such as *PGM, *FILE, *MODULE, or *SRVPGM."),
|
|
2142
|
+
objectAttribute: z.string().describe("Object attribute for the new object, such as RPGLE, SQLRPGLE, PF, or LF.")
|
|
2143
|
+
}, async (input) => {
|
|
2144
|
+
const response = await tdomsPost(store, input.connectionId, "OMQRNO", "/location", {
|
|
2145
|
+
applicationCode: input.applicationCode,
|
|
2146
|
+
environmentCode: input.environmentCode,
|
|
2147
|
+
task: input.task,
|
|
2148
|
+
objectType: input.objectType,
|
|
2149
|
+
objectAttribute: input.objectAttribute
|
|
2150
|
+
}, clientFactory);
|
|
2151
|
+
return result({ ok: true, response });
|
|
2152
|
+
});
|
|
2153
|
+
|
|
2154
|
+
registerAliasTool(server, "tdoms_new_object_create", "Create a new TD/OMS object through OMQRNO/add with the documented lower-camel field names. Requires confirm: true.", {
|
|
2155
|
+
connectionId,
|
|
2156
|
+
applicationCode,
|
|
2157
|
+
task: taskNumber.describe("TD/OMS task/fix number. OMQRNO/add requires task, not fixNumber or taskNumber."),
|
|
2158
|
+
environmentCode,
|
|
2159
|
+
copyObject: z.enum(["0", "1"]).default("0").describe("Use 1 when creating by copying a template object; otherwise 0."),
|
|
2160
|
+
template,
|
|
2161
|
+
newObjectTemplate,
|
|
2162
|
+
newObject: z.string().max(128).describe("New object name, such as CGPPDS."),
|
|
2163
|
+
newObjectLibrary: z.string().max(128).describe("Target object library."),
|
|
2164
|
+
newObjectType: z.string().describe("New object type, such as *PGM, *FILE, *MODULE, or *SRVPGM."),
|
|
2165
|
+
newDetail: z.string().default("").describe("Object detail/member. Usually blank for a new program object."),
|
|
2166
|
+
newDescription: z.string().default("").describe("New object description."),
|
|
2167
|
+
newAttribute: z.string().describe("New object attribute, such as RPGLE or SQLRPGLE."),
|
|
2168
|
+
newSourceFile: z.string().optional().describe("Source file for source-backed objects, such as QRPGLESRC."),
|
|
2169
|
+
newSourceLibrary: z.string().optional().describe("Source library for source-backed objects."),
|
|
2170
|
+
newSourceMember: z.string().optional().describe("Source member for source-backed objects."),
|
|
2171
|
+
newSourceAttribute: z.string().optional().describe("Source member attribute, such as RPGLE or SQLRPGLE."),
|
|
2172
|
+
temporaryOrVirtualObject: z.enum(["0", "1"]).default("0").describe("Use 1 only for temporary/virtual TD/OMS objects."),
|
|
2173
|
+
confirm
|
|
2174
|
+
}, async (input) => {
|
|
2175
|
+
requireConfirmation(input.confirm, "create TD/OMS new object");
|
|
2176
|
+
const body = compact({
|
|
2177
|
+
applicationCode: input.applicationCode,
|
|
2178
|
+
task: input.task,
|
|
2179
|
+
environmentCode: input.environmentCode,
|
|
2180
|
+
copyObject: input.copyObject ?? "0",
|
|
2181
|
+
template: input.template,
|
|
2182
|
+
newObjectTemplate: input.newObjectTemplate,
|
|
2183
|
+
newObject: input.newObject,
|
|
2184
|
+
newObjectLibrary: input.newObjectLibrary,
|
|
2185
|
+
newObjectType: input.newObjectType,
|
|
2186
|
+
newDetail: input.newDetail ?? "",
|
|
2187
|
+
newDescription: input.newDescription ?? "",
|
|
2188
|
+
newAttribute: input.newAttribute,
|
|
2189
|
+
newSourceFile: input.newSourceFile,
|
|
2190
|
+
newSourceLibrary: input.newSourceLibrary,
|
|
2191
|
+
newSourceMember: input.newSourceMember,
|
|
2192
|
+
newSourceAttribute: input.newSourceAttribute,
|
|
2193
|
+
temporaryOrVirtualObject: input.temporaryOrVirtualObject ?? "0"
|
|
2194
|
+
});
|
|
2195
|
+
const response = await tdomsPost(store, input.connectionId, "OMQRNO", "/add", body, clientFactory);
|
|
2196
|
+
return result({ ok: true, response, body });
|
|
2197
|
+
});
|
|
2198
|
+
|
|
2199
|
+
registerAliasTool(server, "tdoms_new_source_member_create", "Create a new source member/detail in an existing source file through OMQRNO/add. Requires confirm: true.", {
|
|
2200
|
+
connectionId,
|
|
2201
|
+
applicationCode,
|
|
2202
|
+
task: taskNumber.describe("TD/OMS task/fix number. OMQRNO/add requires task, not fixNumber or taskNumber."),
|
|
2203
|
+
environmentCode,
|
|
2204
|
+
newObject: z.string().max(128).describe("Source file object, such as QRPGLESRC or QSQLTABLES."),
|
|
2205
|
+
newObjectLibrary: z.string().max(128).describe("Library that contains the source file."),
|
|
2206
|
+
newDetail: z.string().max(128).describe("New source member/detail name."),
|
|
2207
|
+
newDescription: z.string().default("").describe("New member description."),
|
|
2208
|
+
newAttribute: z.string().describe("New member/source attribute, such as RPGLE or SQL."),
|
|
2209
|
+
newObjectTemplate,
|
|
2210
|
+
newObjectType: z.string().default("*FILE").describe("Official OMQRNO new-member shape uses *FILE for the source file object."),
|
|
2211
|
+
copyObject: z.enum(["0", "1"]).default("0"),
|
|
2212
|
+
temporaryOrVirtualObject: z.enum(["0", "1"]).default("0"),
|
|
2213
|
+
confirm
|
|
2214
|
+
}, async (input) => {
|
|
2215
|
+
requireConfirmation(input.confirm, "create TD/OMS new source member");
|
|
2216
|
+
const body = compact({
|
|
2217
|
+
applicationCode: input.applicationCode,
|
|
2218
|
+
task: input.task,
|
|
2219
|
+
environmentCode: input.environmentCode,
|
|
2220
|
+
copyObject: input.copyObject ?? "0",
|
|
2221
|
+
newObjectTemplate: input.newObjectTemplate,
|
|
2222
|
+
newObject: input.newObject,
|
|
2223
|
+
newObjectLibrary: input.newObjectLibrary,
|
|
2224
|
+
newObjectType: input.newObjectType ?? "*FILE",
|
|
2225
|
+
newDetail: input.newDetail,
|
|
2226
|
+
newDescription: input.newDescription ?? "",
|
|
2227
|
+
newAttribute: input.newAttribute,
|
|
2228
|
+
temporaryOrVirtualObject: input.temporaryOrVirtualObject ?? "0"
|
|
2229
|
+
});
|
|
2230
|
+
const response = await tdomsPost(store, input.connectionId, "OMQRNO", "/add", body, clientFactory);
|
|
2231
|
+
return result({ ok: true, response, body });
|
|
2232
|
+
});
|
|
2233
|
+
|
|
2234
|
+
const mutatingEndpointTools = [
|
|
2235
|
+
["tdoms_source_save_object", "Save object source through OMRSRC/object/save.", "OMRSRC", "/object/save"],
|
|
2236
|
+
["tdoms_ifs_save", "Save IFS source through OMRSRC/object/save with IFS fields in the body.", "OMRSRC", "/object/save"],
|
|
2237
|
+
["tdoms_transfer_select", "Run transfer select through OMQRTO/select.", "OMQRTO", "/select"],
|
|
2238
|
+
["tdoms_transfer_prepare", "Run transfer prepare through OMQRTO/prepare.", "OMQRTO", "/prepare"],
|
|
2239
|
+
["tdoms_transfer_execute", "Run transfer execute through OMQRTO/execute.", "OMQRTO", "/execute"],
|
|
2240
|
+
["tdoms_transfer_cancel", "Cancel transfer through OMQRTO/cancel.", "OMQRTO", "/cancel"],
|
|
2241
|
+
["tdoms_transfer_hold_job", "Execute transfer with hold-job semantics using a caller-provided body.", "OMQRTO", "/execute"],
|
|
2242
|
+
["tdoms_transfer_move", "Run move transfer using caller-provided transfer body.", "OMQRTO", "/execute"],
|
|
2243
|
+
["tdoms_transfer_copy", "Run copy transfer using caller-provided transfer body.", "OMQRTO", "/execute"],
|
|
2244
|
+
["tdoms_spool_delete", "Delete a spool file through OMQRSF/delete.", "OMQRSF", "/delete"],
|
|
2245
|
+
["tdoms_remote_job_close", "Close a remote job through OMQRRJ/close.", "OMQRRJ", "/close"],
|
|
2246
|
+
["tdoms_remote_job_reopen", "Reopen a remote job through OMQRRJ/reopen.", "OMQRRJ", "/reopen"],
|
|
2247
|
+
["tdoms_kanban_move_card", "Move a Kanban card by changing task/request status using a caller-provided body.", "OMQRTA", "/status"]
|
|
2248
|
+
];
|
|
2249
|
+
|
|
2250
|
+
for (const [name, description, service, path] of mutatingEndpointTools) {
|
|
2251
|
+
registerEndpointTool(server, { name, description, service, path, store, clientFactory, confirm: true });
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
registerAliasTool(server, "tdoms_source_read_object", "Read object/member source through OMRSRC/object/get.", {
|
|
2255
|
+
connectionId,
|
|
2256
|
+
applicationCode: applicationCode.optional(),
|
|
2257
|
+
taskNumber: taskNumber.optional(),
|
|
2258
|
+
path: z.string().optional(),
|
|
2259
|
+
objectCode,
|
|
2260
|
+
objectType,
|
|
2261
|
+
objectLibrary,
|
|
2262
|
+
memberCode,
|
|
2263
|
+
pageNumber: z.number().int().min(0).default(1)
|
|
2264
|
+
}, async (input) => {
|
|
2265
|
+
const response = await tdomsPost(store, input.connectionId, "OMRSRC", "/object/get", compact({
|
|
2266
|
+
applicationCode: input.applicationCode,
|
|
2267
|
+
taskNumber: input.taskNumber,
|
|
2268
|
+
path: input.path,
|
|
2269
|
+
objectCode: input.objectCode,
|
|
2270
|
+
objectType: input.objectType,
|
|
2271
|
+
objectLibrary: input.objectLibrary,
|
|
2272
|
+
memberCode: input.memberCode,
|
|
2273
|
+
pageNumber: input.pageNumber
|
|
2274
|
+
}), clientFactory);
|
|
2275
|
+
return result({ ok: true, response, sourceTextUtf8: decodeBase64Text(response?.sourceCode), sourceCodeBase64: response?.sourceCode });
|
|
2276
|
+
});
|
|
2277
|
+
|
|
2278
|
+
registerAliasTool(server, "tdoms_ifs_search", "Search IFS objects/components through OMXObjectExtended.", {
|
|
2279
|
+
connectionId,
|
|
2280
|
+
applicationCode: applicationCode.optional(),
|
|
2281
|
+
ifsPath: z.string().optional(),
|
|
2282
|
+
pageNumber: z.number().int().min(1).default(1),
|
|
2283
|
+
recordsPerPage: z.number().int().min(1).max(50).default(25)
|
|
2284
|
+
}, async (input) => {
|
|
2285
|
+
const fieldSelections = compactArray([
|
|
2286
|
+
input.applicationCode && { field: "applicationCode", value: input.applicationCode, operator: "EQ" },
|
|
2287
|
+
{ field: "objectClass", value: "B", operator: "EQ" },
|
|
2288
|
+
input.ifsPath && { field: "iFSObjectCode", value: input.ifsPath, operator: "CONTAINS", ignoreCase: true }
|
|
2289
|
+
]);
|
|
2290
|
+
const response = await queryDatabase(store, input.connectionId, {
|
|
2291
|
+
table: "OMXObjectExtended",
|
|
2292
|
+
fieldSelections,
|
|
2293
|
+
pageNumber: input.pageNumber,
|
|
2294
|
+
recordsPerPage: input.recordsPerPage,
|
|
2295
|
+
returnTotalRecords: true
|
|
2296
|
+
}, clientFactory);
|
|
2297
|
+
return result({ ok: true, response });
|
|
2298
|
+
});
|
|
2299
|
+
|
|
2300
|
+
registerAliasTool(server, "tdoms_ifs_read", "Read IFS source through OMRSRC/object/get using path/IFS body fields.", {
|
|
2301
|
+
connectionId,
|
|
2302
|
+
body: rawBody
|
|
2303
|
+
}, async (input) => {
|
|
2304
|
+
const response = await tdomsPost(store, input.connectionId, "OMRSRC", "/object/get", input.body, clientFactory);
|
|
2305
|
+
return result({ ok: true, response, sourceTextUtf8: decodeBase64Text(response?.sourceCode), sourceCodeBase64: response?.sourceCode });
|
|
2306
|
+
});
|
|
2307
|
+
|
|
2308
|
+
for (const name of ["tdoms_source_history", "tdoms_source_revision", "tdoms_source_includes", "tdoms_component_editor_get"]) {
|
|
2309
|
+
registerDatabaseTool(server, {
|
|
2310
|
+
name,
|
|
2311
|
+
description: `${name.replaceAll("_", " ")} through TD/OMS database discovery tables.`,
|
|
2312
|
+
table: name === "tdoms_component_editor_get" ? "OMXObjectExtended" : "OMXObjectTransferHistory",
|
|
2313
|
+
store,
|
|
2314
|
+
clientFactory
|
|
2315
|
+
});
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
registerDiffTool(server, "tdoms_source_diff", "Diff two plain source strings.");
|
|
2319
|
+
registerSourceCompareTool(server, "tdoms_source_compare", "Compare two TD/OMS source bodies by reading both and returning a simple line diff.", store, clientFactory);
|
|
2320
|
+
registerSourceCompareTool(server, "tdoms_source_compare_production", "Compare current source to a production/revision source body.", store, clientFactory);
|
|
2321
|
+
registerSourceCompareTool(server, "tdoms_source_compare_revision", "Compare current source to another revision source body.", store, clientFactory);
|
|
2322
|
+
registerSourceCompareTool(server, "tdoms_ifs_compare", "Compare two IFS source bodies.", store, clientFactory);
|
|
2323
|
+
|
|
2324
|
+
for (const name of ["tdoms_compile_task", "tdoms_compile_solution", "tdoms_compile_object"]) {
|
|
2325
|
+
registerAliasTool(server, name, "Queue and optionally release compile work. Requires confirm: true.", {
|
|
2326
|
+
connectionId,
|
|
2327
|
+
applicationCode,
|
|
2328
|
+
taskNumber,
|
|
2329
|
+
objectCode,
|
|
2330
|
+
objectType,
|
|
2331
|
+
objectLibrary,
|
|
2332
|
+
detail: z.string().optional(),
|
|
2333
|
+
release: z.boolean().default(false),
|
|
2334
|
+
confirm
|
|
2335
|
+
}, async (input) => {
|
|
2336
|
+
requireConfirmation(input.confirm, name);
|
|
2337
|
+
const queue = await tdomsPost(store, input.connectionId, "OMQRSQ", "/queue", compileQueueBody(input), clientFactory);
|
|
2338
|
+
const releaseResponse = input.release
|
|
2339
|
+
? await tdomsPost(store, input.connectionId, "OMQRSQ", "/release", compileQueueBody(input), clientFactory)
|
|
2340
|
+
: undefined;
|
|
2341
|
+
return result({ ok: true, queue, release: releaseResponse });
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
registerAliasTool(server, "tdoms_compile_and_check", "Queue, release, and optionally parse IBM i compile events for a TD/OMS object in one workflow. Use this instead of asking the user to manually paste compile errors. Requires confirm: true.", {
|
|
2346
|
+
connectionId,
|
|
2347
|
+
applicationCode,
|
|
2348
|
+
taskNumber,
|
|
2349
|
+
objectCode,
|
|
2350
|
+
objectType,
|
|
2351
|
+
objectLibrary,
|
|
2352
|
+
detail: z.string().optional(),
|
|
2353
|
+
parseEvents: z.boolean().default(true),
|
|
2354
|
+
eventLibrary: z.string().optional().describe("Library that contains the EVENTF/source member. Defaults to objectLibrary."),
|
|
2355
|
+
eventMember: z.string().optional().describe("Member name for compile event parsing. Defaults to detail, then objectCode."),
|
|
2356
|
+
confirm
|
|
2357
|
+
}, async (input) => {
|
|
2358
|
+
requireConfirmation(input.confirm, "compile and check TD/OMS object");
|
|
2359
|
+
const body = compileQueueBody(input);
|
|
2360
|
+
const queue = await tdomsPost(store, input.connectionId, "OMQRSQ", "/queue", body, clientFactory);
|
|
2361
|
+
const release = await tdomsPost(store, input.connectionId, "OMQRSQ", "/release", body, clientFactory);
|
|
2362
|
+
const eventLibraryValue = input.eventLibrary || input.objectLibrary;
|
|
2363
|
+
const eventMemberValue = input.eventMember || input.detail || input.objectCode;
|
|
2364
|
+
const events = input.parseEvents && eventLibraryValue && eventMemberValue
|
|
2365
|
+
? await tdomsPost(store, input.connectionId, "OMQREV", "/parseEvent", { library: eventLibraryValue, memberName: eventMemberValue }, clientFactory)
|
|
2366
|
+
: undefined;
|
|
2367
|
+
return result({
|
|
2368
|
+
ok: true,
|
|
2369
|
+
queue,
|
|
2370
|
+
release,
|
|
2371
|
+
events,
|
|
2372
|
+
eventSummary: summarizeCompileEvents(events),
|
|
2373
|
+
body
|
|
2374
|
+
});
|
|
2375
|
+
});
|
|
2376
|
+
|
|
2377
|
+
for (const name of ["tdoms_checkout_task", "tdoms_checkout_solution", "tdoms_checkout_object", "tdoms_promote_task", "tdoms_promote_solution", "tdoms_promote_object"]) {
|
|
2378
|
+
registerTransferWorkflowTool(server, name, store, clientFactory);
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
function registerAliasTool(server, name, description, inputSchema, handler) {
|
|
2383
|
+
if (server._registeredTools[name]) {
|
|
2384
|
+
return;
|
|
2385
|
+
}
|
|
2386
|
+
server.registerTool(name, { description, inputSchema }, handler);
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
function registerDatabaseTool(server, { name, description, table, returnFields, store, clientFactory }) {
|
|
2390
|
+
registerAliasTool(server, name, description, {
|
|
2391
|
+
connectionId: z.string(),
|
|
2392
|
+
applicationCode: z.string().optional(),
|
|
2393
|
+
taskNumber: z.string().optional(),
|
|
2394
|
+
requestNumber: z.string().optional(),
|
|
2395
|
+
programmer: z.string().optional(),
|
|
2396
|
+
status: z.string().optional().describe("TD/OMS task/fix status, if known."),
|
|
2397
|
+
text: z.string().optional().describe("Text to search in the short task description."),
|
|
2398
|
+
pathCode: z.string().optional(),
|
|
2399
|
+
objectCode: z.string().optional(),
|
|
2400
|
+
objectType: z.string().optional(),
|
|
2401
|
+
objectLibrary: z.string().optional(),
|
|
2402
|
+
memberCode: z.string().optional(),
|
|
2403
|
+
table: z.string().optional().describe("Override the default TD/OMS table/view for this tool."),
|
|
2404
|
+
returnFields: z.array(z.string()).optional(),
|
|
2405
|
+
fieldSelections: z.array(fieldSelectionSchema).optional(),
|
|
2406
|
+
whereClause: z.string().optional(),
|
|
2407
|
+
sortFields: z.array(sortFieldSchema).optional(),
|
|
2408
|
+
pageNumber: z.number().int().min(0).default(1),
|
|
2409
|
+
recordsPerPage: z.number().int().min(1).max(50).default(25),
|
|
2410
|
+
returnTotalRecords: z.boolean().default(true)
|
|
2411
|
+
}, async (input) => {
|
|
2412
|
+
const response = await queryDatabase(store, input.connectionId, {
|
|
2413
|
+
table: input.table || table,
|
|
2414
|
+
returnFields: input.returnFields || returnFields,
|
|
2415
|
+
fieldSelections: input.fieldSelections || commonFieldSelections(input),
|
|
2416
|
+
whereClause: input.whereClause,
|
|
2417
|
+
sortFields: input.sortFields,
|
|
2418
|
+
pageNumber: input.pageNumber ?? 1,
|
|
2419
|
+
recordsPerPage: input.recordsPerPage ?? 25,
|
|
2420
|
+
returnTotalRecords: input.returnTotalRecords ?? true
|
|
2421
|
+
}, clientFactory);
|
|
2422
|
+
return result({ ok: true, table: input.table || table, response });
|
|
2423
|
+
});
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
function registerEndpointTool(server, { name, description, service, path, store, clientFactory, confirm, allowPathOverride = false, method = "POST" }) {
|
|
2427
|
+
const inputSchema = {
|
|
2428
|
+
connectionId: z.string(),
|
|
2429
|
+
body: z.record(z.string(), z.unknown()).default({}),
|
|
2430
|
+
confirm: z.boolean().optional()
|
|
2431
|
+
};
|
|
2432
|
+
if (allowPathOverride) {
|
|
2433
|
+
inputSchema.path = z.string().optional().describe("Dynamic endpoint path, such as /ref/{object}/{type}/{library}/{member}.");
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
registerAliasTool(server, name, description, inputSchema, async (input) => {
|
|
2437
|
+
if (confirm) {
|
|
2438
|
+
requireConfirmation(input.confirm, name);
|
|
2439
|
+
}
|
|
2440
|
+
const endpointPath = allowPathOverride ? input.path : path;
|
|
2441
|
+
if (!endpointPath) {
|
|
2442
|
+
throw new Error(`Provide path for ${name}.`);
|
|
2443
|
+
}
|
|
2444
|
+
const response = method === "GET"
|
|
2445
|
+
? await tdomsGet(store, input.connectionId, service, endpointPath, clientFactory)
|
|
2446
|
+
: await tdomsPost(store, input.connectionId, service, endpointPath, input.body || {}, clientFactory);
|
|
2447
|
+
return result({ ok: true, response });
|
|
2448
|
+
});
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
function registerDiffTool(server, name, description) {
|
|
2452
|
+
registerAliasTool(server, name, description, {
|
|
2453
|
+
left: z.string(),
|
|
2454
|
+
right: z.string(),
|
|
2455
|
+
leftLabel: z.string().default("left"),
|
|
2456
|
+
rightLabel: z.string().default("right")
|
|
2457
|
+
}, async (input) => result({ ok: true, diff: simpleLineDiff(input.left, input.right, input.leftLabel, input.rightLabel) }));
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
function registerSourceCompareTool(server, name, description, store, clientFactory) {
|
|
2461
|
+
registerAliasTool(server, name, description, {
|
|
2462
|
+
connectionId: z.string(),
|
|
2463
|
+
leftBody: z.record(z.string(), z.unknown()).describe("Body for OMRSRC/object/get for the left/current source."),
|
|
2464
|
+
rightBody: z.record(z.string(), z.unknown()).describe("Body for OMRSRC/object/get for the right/production/revision source."),
|
|
2465
|
+
leftLabel: z.string().default("left"),
|
|
2466
|
+
rightLabel: z.string().default("right")
|
|
2467
|
+
}, async (input) => {
|
|
2468
|
+
const left = await tdomsPost(store, input.connectionId, "OMRSRC", "/object/get", input.leftBody, clientFactory);
|
|
2469
|
+
const right = await tdomsPost(store, input.connectionId, "OMRSRC", "/object/get", input.rightBody, clientFactory);
|
|
2470
|
+
const leftText = decodeBase64Text(left?.sourceCode);
|
|
2471
|
+
const rightText = decodeBase64Text(right?.sourceCode);
|
|
2472
|
+
return result({
|
|
2473
|
+
ok: true,
|
|
2474
|
+
left,
|
|
2475
|
+
right,
|
|
2476
|
+
diff: simpleLineDiff(leftText, rightText, input.leftLabel, input.rightLabel)
|
|
2477
|
+
});
|
|
2478
|
+
});
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
function registerTransferWorkflowTool(server, name, store, clientFactory) {
|
|
2482
|
+
registerAliasTool(server, name, "Run a transfer workflow with select, prepare, execute, and optional progress polling. Requires confirm: true.", {
|
|
2483
|
+
connectionId: z.string(),
|
|
2484
|
+
selectBody: z.record(z.string(), z.unknown()).optional(),
|
|
2485
|
+
prepareBody: z.record(z.string(), z.unknown()).optional(),
|
|
2486
|
+
executeBody: z.record(z.string(), z.unknown()).optional(),
|
|
2487
|
+
progressBody: z.record(z.string(), z.unknown()).optional(),
|
|
2488
|
+
runSelect: z.boolean().default(true),
|
|
2489
|
+
runPrepare: z.boolean().default(true),
|
|
2490
|
+
runExecute: z.boolean().default(true),
|
|
2491
|
+
getProgress: z.boolean().default(true),
|
|
2492
|
+
confirm: z.boolean().optional()
|
|
2493
|
+
}, async (input) => {
|
|
2494
|
+
requireConfirmation(input.confirm, name);
|
|
2495
|
+
const steps = {};
|
|
2496
|
+
if (input.runSelect) {
|
|
2497
|
+
steps.select = await tdomsPost(store, input.connectionId, "OMQRTO", "/select", input.selectBody || {}, clientFactory);
|
|
2498
|
+
}
|
|
2499
|
+
if (input.runPrepare) {
|
|
2500
|
+
steps.prepare = await tdomsPost(store, input.connectionId, "OMQRTO", "/prepare", input.prepareBody || input.selectBody || {}, clientFactory);
|
|
2501
|
+
}
|
|
2502
|
+
if (input.runExecute) {
|
|
2503
|
+
steps.execute = await tdomsPost(store, input.connectionId, "OMQRTO", "/execute", input.executeBody || input.prepareBody || input.selectBody || {}, clientFactory);
|
|
2504
|
+
}
|
|
2505
|
+
if (input.getProgress) {
|
|
2506
|
+
steps.progress = await tdomsPost(store, input.connectionId, "OMQRTO", "/progress", input.progressBody || input.executeBody || {}, clientFactory);
|
|
2507
|
+
}
|
|
2508
|
+
return result({ ok: true, steps });
|
|
2509
|
+
});
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
async function safeQuery(store, connectionId, input, clientFactory) {
|
|
2513
|
+
try {
|
|
2514
|
+
return await queryDatabase(store, connectionId, input, clientFactory);
|
|
2515
|
+
} catch (error) {
|
|
2516
|
+
return { ok: false, error: String(error?.message || error), table: input.table };
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
function taskQuery(table, input) {
|
|
2521
|
+
return {
|
|
2522
|
+
table,
|
|
2523
|
+
fieldSelections: [
|
|
2524
|
+
{ field: "applicationCode", value: input.applicationCode, operator: "EQ" },
|
|
2525
|
+
{ field: "fixNumber", value: input.taskNumber, operator: "EQ" }
|
|
2526
|
+
],
|
|
2527
|
+
pageNumber: 1,
|
|
2528
|
+
recordsPerPage: 50,
|
|
2529
|
+
returnTotalRecords: true
|
|
2530
|
+
};
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
function requestQuery(table, input) {
|
|
2534
|
+
return {
|
|
2535
|
+
table,
|
|
2536
|
+
fieldSelections: [
|
|
2537
|
+
{ field: "applicationCode", value: input.applicationCode, operator: "EQ" },
|
|
2538
|
+
{ field: "requestNumber", value: input.requestNumber, operator: "EQ" }
|
|
2539
|
+
],
|
|
2540
|
+
pageNumber: 1,
|
|
2541
|
+
recordsPerPage: 50,
|
|
2542
|
+
returnTotalRecords: true
|
|
2543
|
+
};
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
function objectQuery(table, input) {
|
|
2547
|
+
return {
|
|
2548
|
+
table,
|
|
2549
|
+
fieldSelections: commonFieldSelections(input),
|
|
2550
|
+
pageNumber: 1,
|
|
2551
|
+
recordsPerPage: 50,
|
|
2552
|
+
returnTotalRecords: true
|
|
2553
|
+
};
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
function commonFieldSelections(input) {
|
|
2557
|
+
return compactArray([
|
|
2558
|
+
input.applicationCode && { field: "applicationCode", value: input.applicationCode, operator: "EQ" },
|
|
2559
|
+
input.taskNumber && { field: "fixNumber", value: input.taskNumber, operator: "EQ" },
|
|
2560
|
+
input.requestNumber && { field: "requestNumber", value: input.requestNumber, operator: "EQ" },
|
|
2561
|
+
input.programmer && { field: "programmer", value: input.programmer, operator: "EQ" },
|
|
2562
|
+
input.status && { field: "fixStatus", value: input.status, operator: "EQ" },
|
|
2563
|
+
input.text && { field: "shortFixDescription", value: input.text, operator: "CONTAINS", ignoreCase: true },
|
|
2564
|
+
input.pathCode && { field: "pathCode", value: input.pathCode, operator: "EQ" },
|
|
2565
|
+
input.objectCode && { field: "objectCode", value: input.objectCode, operator: "EQ" },
|
|
2566
|
+
input.objectType && { field: "objectType", value: input.objectType, operator: "EQ" },
|
|
2567
|
+
input.objectLibrary && { field: "objectLibrary", value: input.objectLibrary, operator: "EQ" },
|
|
2568
|
+
input.memberCode && { field: "memberCode", value: input.memberCode, operator: "EQ" }
|
|
2569
|
+
]);
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
function simpleLineDiff(leftText, rightText, leftLabel = "left", rightLabel = "right") {
|
|
2573
|
+
const left = String(leftText || "").split(/\r?\n/);
|
|
2574
|
+
const right = String(rightText || "").split(/\r?\n/);
|
|
2575
|
+
const max = Math.max(left.length, right.length);
|
|
2576
|
+
const lines = [`--- ${leftLabel}`, `+++ ${rightLabel}`];
|
|
2577
|
+
for (let index = 0; index < max; index += 1) {
|
|
2578
|
+
const leftLine = left[index];
|
|
2579
|
+
const rightLine = right[index];
|
|
2580
|
+
if (leftLine === rightLine) {
|
|
2581
|
+
continue;
|
|
2582
|
+
}
|
|
2583
|
+
lines.push(`@@ line ${index + 1} @@`);
|
|
2584
|
+
if (leftLine !== undefined) {
|
|
2585
|
+
lines.push(`- ${leftLine}`);
|
|
2586
|
+
}
|
|
2587
|
+
if (rightLine !== undefined) {
|
|
2588
|
+
lines.push(`+ ${rightLine}`);
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
return lines.join("\n");
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
function replaceLiteral(sourceText, searchText, replacementText, { occurrence = 1, replaceAll = false } = {}) {
|
|
2595
|
+
if (!searchText) {
|
|
2596
|
+
throw new Error("searchText cannot be empty.");
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
if (replaceAll) {
|
|
2600
|
+
const replacements = sourceText.split(searchText).length - 1;
|
|
2601
|
+
if (replacements === 0) {
|
|
2602
|
+
throw new Error("searchText was not found in source.");
|
|
2603
|
+
}
|
|
2604
|
+
return {
|
|
2605
|
+
text: sourceText.split(searchText).join(replacementText),
|
|
2606
|
+
replacements
|
|
2607
|
+
};
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
let fromIndex = 0;
|
|
2611
|
+
let foundAt = -1;
|
|
2612
|
+
for (let index = 0; index < occurrence; index += 1) {
|
|
2613
|
+
foundAt = sourceText.indexOf(searchText, fromIndex);
|
|
2614
|
+
if (foundAt === -1) {
|
|
2615
|
+
throw new Error(`searchText occurrence ${occurrence} was not found in source.`);
|
|
2616
|
+
}
|
|
2617
|
+
fromIndex = foundAt + searchText.length;
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
return {
|
|
2621
|
+
text: `${sourceText.slice(0, foundAt)}${replacementText}${sourceText.slice(foundAt + searchText.length)}`,
|
|
2622
|
+
replacements: 1
|
|
2623
|
+
};
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
function splitTextByUtf8Bytes(text, maxBytes) {
|
|
2627
|
+
if (Buffer.byteLength(text, "utf8") <= maxBytes) {
|
|
2628
|
+
return [text];
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
const chunks = [];
|
|
2632
|
+
let current = "";
|
|
2633
|
+
for (const char of text) {
|
|
2634
|
+
const candidate = current + char;
|
|
2635
|
+
if (current && Buffer.byteLength(candidate, "utf8") > maxBytes) {
|
|
2636
|
+
chunks.push(current);
|
|
2637
|
+
current = char;
|
|
2638
|
+
} else {
|
|
2639
|
+
current = candidate;
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
if (current || chunks.length === 0) {
|
|
2643
|
+
chunks.push(current);
|
|
2644
|
+
}
|
|
2645
|
+
return chunks;
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
function compileQueueBody(input, processingArray = { clearAll: false }) {
|
|
2649
|
+
return compact({
|
|
2650
|
+
applicationCode: input.applicationCode,
|
|
2651
|
+
taskNumber: input.taskNumber,
|
|
2652
|
+
objectCode: input.objectCode || "",
|
|
2653
|
+
objectType: input.objectType || "",
|
|
2654
|
+
objectLibrary: input.objectLibrary || "",
|
|
2655
|
+
detail: input.detail || "",
|
|
2656
|
+
processingArray
|
|
2657
|
+
});
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
function summarizeCompileEvents(events) {
|
|
2661
|
+
const messages = collectMessageObjects(events);
|
|
2662
|
+
const severityCounts = {};
|
|
2663
|
+
const errorLike = [];
|
|
2664
|
+
|
|
2665
|
+
for (const message of messages) {
|
|
2666
|
+
const severity = message.severity ?? message.messageSeverity ?? message.type ?? message.Type;
|
|
2667
|
+
const normalizedSeverity = severity === undefined ? "unknown" : String(severity);
|
|
2668
|
+
severityCounts[normalizedSeverity] = (severityCounts[normalizedSeverity] || 0) + 1;
|
|
2669
|
+
|
|
2670
|
+
const numericSeverity = Number(normalizedSeverity);
|
|
2671
|
+
const messageText = message.messageText || message.text || message.Text || message.message || "";
|
|
2672
|
+
if (Number.isFinite(numericSeverity) && numericSeverity >= 30) {
|
|
2673
|
+
errorLike.push({
|
|
2674
|
+
severity: numericSeverity,
|
|
2675
|
+
line: message.lineNumber ?? message.lineNr ?? message["Line Nr."] ?? message.line,
|
|
2676
|
+
messageId: message.messageId ?? message.messageID ?? message["Message ID"],
|
|
2677
|
+
text: messageText,
|
|
2678
|
+
sourceFile: message.sourceFile ?? message.SourceFile
|
|
2679
|
+
});
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
return {
|
|
2684
|
+
totalMessages: messages.length,
|
|
2685
|
+
severityCounts,
|
|
2686
|
+
hasErrors: errorLike.length > 0,
|
|
2687
|
+
errors: errorLike.slice(0, 20)
|
|
2688
|
+
};
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
function collectMessageObjects(value, output = []) {
|
|
2692
|
+
if (!value) {
|
|
2693
|
+
return output;
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
if (Array.isArray(value)) {
|
|
2697
|
+
for (const entry of value) {
|
|
2698
|
+
collectMessageObjects(entry, output);
|
|
2699
|
+
}
|
|
2700
|
+
return output;
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
if (typeof value !== "object") {
|
|
2704
|
+
return output;
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
const keys = Object.keys(value);
|
|
2708
|
+
if (keys.some((key) => /message|severity|sourcefile|line/i.test(key))) {
|
|
2709
|
+
output.push(value);
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
for (const entry of Object.values(value)) {
|
|
2713
|
+
collectMessageObjects(entry, output);
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
return output;
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2719
|
+
function requireConfirmation(confirm, action) {
|
|
2720
|
+
if (confirm !== true) {
|
|
2721
|
+
throw new Error(`Refusing to ${action}. Re-run with confirm: true after the user approves the action.`);
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
function isUnauthorized(error) {
|
|
2726
|
+
return /\b401\b/.test(String(error?.message || error));
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
function decodeBase64Text(value) {
|
|
2730
|
+
if (!value) {
|
|
2731
|
+
return "";
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
try {
|
|
2735
|
+
return Buffer.from(value, "base64").toString("utf8");
|
|
2736
|
+
} catch {
|
|
2737
|
+
return "";
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
function compactArray(values) {
|
|
2742
|
+
return values.filter(Boolean);
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
function compact(value) {
|
|
2746
|
+
if (Array.isArray(value)) {
|
|
2747
|
+
return value.map(compact);
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
if (!value || typeof value !== "object") {
|
|
2751
|
+
return value;
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
return Object.fromEntries(
|
|
2755
|
+
Object.entries(value)
|
|
2756
|
+
.filter(([, entry]) => entry !== undefined)
|
|
2757
|
+
.map(([key, entry]) => [key, compact(entry)])
|
|
2758
|
+
);
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
function result(structuredContent) {
|
|
2762
|
+
return {
|
|
2763
|
+
content: [
|
|
2764
|
+
{
|
|
2765
|
+
type: "text",
|
|
2766
|
+
text: JSON.stringify(structuredContent, null, 2)
|
|
2767
|
+
}
|
|
2768
|
+
],
|
|
2769
|
+
structuredContent
|
|
2770
|
+
};
|
|
2771
|
+
}
|