mcp-for-i 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mcp/tools.js +149 -44
- package/package.json +1 -1
package/dist/mcp/tools.js
CHANGED
|
@@ -1277,7 +1277,26 @@ export async function handleTool(ctx, name, args) {
|
|
|
1277
1277
|
}
|
|
1278
1278
|
case "ibmi.qsys.sourcefiles.list": {
|
|
1279
1279
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1280
|
-
|
|
1280
|
+
let files;
|
|
1281
|
+
try {
|
|
1282
|
+
files = await conn.content.getObjectList(args.library, ["*FILE"]);
|
|
1283
|
+
}
|
|
1284
|
+
catch {
|
|
1285
|
+
try {
|
|
1286
|
+
const allObjects = await conn.content.getObjectList(args.library, ["*ALL"]);
|
|
1287
|
+
files = allObjects.filter((obj) => String(obj?.type || "").toUpperCase() === "*FILE");
|
|
1288
|
+
}
|
|
1289
|
+
catch {
|
|
1290
|
+
const rows = await conn.runSQL(`select system_table_name as name, coalesce(table_text, '') as text from qsys2.systables where upper(system_table_schema)=${Tools.sqlString(String(args.library || "").toUpperCase())} fetch first 5000 rows only`);
|
|
1291
|
+
files = rows.map((row) => ({
|
|
1292
|
+
library: String(args.library || "").toUpperCase(),
|
|
1293
|
+
name: String(row?.NAME || row?.SYSTEM_TABLE_NAME || ""),
|
|
1294
|
+
type: "*FILE",
|
|
1295
|
+
text: String(row?.TEXT || row?.TABLE_TEXT || ""),
|
|
1296
|
+
attribute: ""
|
|
1297
|
+
}));
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1281
1300
|
const sourceByAttr = files.filter((obj) => {
|
|
1282
1301
|
const attr = String(obj?.attribute || "").toUpperCase();
|
|
1283
1302
|
return attr === "PF-SRC" || attr === "SOURCE";
|
|
@@ -1892,15 +1911,7 @@ export async function handleTool(ctx, name, args) {
|
|
|
1892
1911
|
case "ibmi.spool.list": {
|
|
1893
1912
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1894
1913
|
const limit = clampNumber(args?.limit, 200, 1, 5000);
|
|
1895
|
-
|
|
1896
|
-
try {
|
|
1897
|
-
rows = await conn.runSQL(`select job_name, spooled_file_name, spooled_file_number, output_queue_name, total_pages, file_status from qsys2.output_queue_entries fetch first ${limit} rows only`);
|
|
1898
|
-
}
|
|
1899
|
-
catch (err) {
|
|
1900
|
-
if (!isMissingColumnError(err, "FILE_STATUS"))
|
|
1901
|
-
throw err;
|
|
1902
|
-
rows = await conn.runSQL(`select job_name, spooled_file_name, spooled_file_number, output_queue_name, total_pages, cast('' as varchar(10)) as file_status from qsys2.output_queue_entries fetch first ${limit} rows only`);
|
|
1903
|
-
}
|
|
1914
|
+
const rows = await querySpoolEntries(conn, limit);
|
|
1904
1915
|
return json(rows.map(normalizeSpoolEntry));
|
|
1905
1916
|
}
|
|
1906
1917
|
case "ibmi.spool.read": {
|
|
@@ -1959,7 +1970,7 @@ export async function handleTool(ctx, name, args) {
|
|
|
1959
1970
|
enforceWritable(ctx, conn, "qsys.write", args);
|
|
1960
1971
|
const jobName = requireJobName(args.jobName);
|
|
1961
1972
|
const hold = Boolean(args?.holdOnJobQueue) ? "*JOBQ" : "*NO";
|
|
1962
|
-
const command = `HLDJOB JOB(${
|
|
1973
|
+
const command = `HLDJOB JOB(${jobName}) HOLD(${hold})`;
|
|
1963
1974
|
const cmdResult = await runClCommand(conn, command, "ile", undefined, 45000);
|
|
1964
1975
|
return json({ ok: cmdResult.code === 0, command, ...cmdResult });
|
|
1965
1976
|
}
|
|
@@ -1967,7 +1978,7 @@ export async function handleTool(ctx, name, args) {
|
|
|
1967
1978
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
1968
1979
|
enforceWritable(ctx, conn, "qsys.write", args);
|
|
1969
1980
|
const jobName = requireJobName(args.jobName);
|
|
1970
|
-
const command = `RLSJOB JOB(${
|
|
1981
|
+
const command = `RLSJOB JOB(${jobName})`;
|
|
1971
1982
|
const cmdResult = await runClCommand(conn, command, "ile", undefined, 45000);
|
|
1972
1983
|
return json({ ok: cmdResult.code === 0, command, ...cmdResult });
|
|
1973
1984
|
}
|
|
@@ -1980,7 +1991,7 @@ export async function handleTool(ctx, name, args) {
|
|
|
1980
1991
|
throw new Error("option must be *CNTRLD or *IMMED");
|
|
1981
1992
|
}
|
|
1982
1993
|
const delaySeconds = clampNumber(args?.delaySeconds, 30, 0, 99999);
|
|
1983
|
-
const command = `ENDJOB JOB(${
|
|
1994
|
+
const command = `ENDJOB JOB(${jobName}) OPTION(${option}) DELAY(${delaySeconds})`;
|
|
1984
1995
|
const cmdResult = await runClCommand(conn, command, "ile", undefined, 45000);
|
|
1985
1996
|
return json({ ok: cmdResult.code === 0, command, ...cmdResult });
|
|
1986
1997
|
}
|
|
@@ -2042,9 +2053,12 @@ export async function handleTool(ctx, name, args) {
|
|
|
2042
2053
|
const message = String(args.message || "");
|
|
2043
2054
|
if (!message.trim())
|
|
2044
2055
|
throw new Error("message is required");
|
|
2045
|
-
const
|
|
2056
|
+
const mappedMessageType = messageType === "*INQ" ? "*INQ" : "*INFO";
|
|
2057
|
+
const command = mappedMessageType === "*INQ"
|
|
2058
|
+
? `SNDMSG MSG(${quoteClString(message)}) TOMSGQ(${library}/${messageQueue}) MSGTYPE(*INQ) RPYMSGQ(${library}/${messageQueue})`
|
|
2059
|
+
: `SNDMSG MSG(${quoteClString(message)}) TOMSGQ(${library}/${messageQueue}) MSGTYPE(*INFO)`;
|
|
2046
2060
|
const cmdResult = await runClCommand(conn, command, "ile", undefined, 45000);
|
|
2047
|
-
return json({ ok: cmdResult.code === 0, command, ...cmdResult });
|
|
2061
|
+
return json({ ok: cmdResult.code === 0, command, requestedMessageType: messageType, mappedMessageType, ...cmdResult });
|
|
2048
2062
|
}
|
|
2049
2063
|
case "ibmi.msgq.reply": {
|
|
2050
2064
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
@@ -2088,9 +2102,21 @@ export async function handleTool(ctx, name, args) {
|
|
|
2088
2102
|
const message = String(args.message || "");
|
|
2089
2103
|
if (!message)
|
|
2090
2104
|
throw new Error("message is required");
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2105
|
+
try {
|
|
2106
|
+
await conn.runSQL(`call qsys2.send_data_queue(${Tools.sqlString(message)}, ${Tools.sqlString(queue)}, ${Tools.sqlString(library)})`);
|
|
2107
|
+
return json({
|
|
2108
|
+
ok: true,
|
|
2109
|
+
method: "qsys2.send_data_queue",
|
|
2110
|
+
library,
|
|
2111
|
+
queue,
|
|
2112
|
+
messageLength: message.length
|
|
2113
|
+
});
|
|
2114
|
+
}
|
|
2115
|
+
catch {
|
|
2116
|
+
const command = `SNDDTAQ DTAQ(${library}/${queue}) DTA(${quoteClString(message)})`;
|
|
2117
|
+
const cmdResult = await runClCommand(conn, command, "ile", undefined, 45000);
|
|
2118
|
+
return json({ ok: cmdResult.code === 0, method: "cl.snddtaq", command, ...cmdResult });
|
|
2119
|
+
}
|
|
2094
2120
|
}
|
|
2095
2121
|
case "ibmi.dataqueue.receive": {
|
|
2096
2122
|
const conn = await ctx.ensureActive(args?.connectionName);
|
|
@@ -2624,21 +2650,59 @@ async function queryJournalEntries(conn, options) {
|
|
|
2624
2650
|
return normalized.slice(0, options.limit);
|
|
2625
2651
|
}
|
|
2626
2652
|
async function queryJobs(conn, options) {
|
|
2627
|
-
const
|
|
2628
|
-
|
|
2653
|
+
const scanLimit = Math.min(options.limit * 5, 5000);
|
|
2654
|
+
const rows = [];
|
|
2655
|
+
const activeWhere = buildJobWhereClause(options, {
|
|
2656
|
+
subsystem: "SUBSYSTEM",
|
|
2657
|
+
userName: "AUTHORIZATION_NAME",
|
|
2658
|
+
status: "JOB_STATUS"
|
|
2659
|
+
});
|
|
2660
|
+
try {
|
|
2661
|
+
const activeRows = await conn.runSQL(`select * from table(qsys2.active_job_info())${activeWhere} fetch first ${scanLimit} rows only`);
|
|
2662
|
+
rows.push(...activeRows.map((row) => normalizeActiveJobRow(row)));
|
|
2663
|
+
}
|
|
2664
|
+
catch {
|
|
2665
|
+
// Continue with job_info fallback.
|
|
2666
|
+
}
|
|
2667
|
+
const allWhere = buildJobWhereClause(options, {
|
|
2668
|
+
subsystem: "JOB_SUBSYSTEM",
|
|
2669
|
+
userName: "JOB_USER",
|
|
2670
|
+
status: "JOB_STATUS"
|
|
2671
|
+
});
|
|
2672
|
+
try {
|
|
2673
|
+
const allRows = await conn.runSQL(`select * from table(qsys2.job_info('*ALL'))${allWhere} fetch first ${scanLimit} rows only`);
|
|
2674
|
+
const seen = new Set(rows.map((row) => String(row.jobName || "")));
|
|
2675
|
+
for (const row of allRows.map((raw) => normalizeActiveJobRow(raw))) {
|
|
2676
|
+
const key = String(row.jobName || "");
|
|
2677
|
+
if (!key || seen.has(key))
|
|
2678
|
+
continue;
|
|
2679
|
+
rows.push(row);
|
|
2680
|
+
seen.add(key);
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
catch {
|
|
2684
|
+
// Some systems may not provide job_info as a table function.
|
|
2685
|
+
}
|
|
2686
|
+
return rows.slice(0, options.limit);
|
|
2687
|
+
}
|
|
2688
|
+
function buildJobWhereClause(options, columns) {
|
|
2689
|
+
const predicates = [];
|
|
2629
2690
|
if (options.subsystem) {
|
|
2630
2691
|
const subsystem = String(options.subsystem).trim().toUpperCase();
|
|
2631
|
-
|
|
2692
|
+
if (subsystem)
|
|
2693
|
+
predicates.push(`upper(${columns.subsystem})=${Tools.sqlString(subsystem)}`);
|
|
2632
2694
|
}
|
|
2633
2695
|
if (options.userName) {
|
|
2634
2696
|
const userName = String(options.userName).trim().toUpperCase();
|
|
2635
|
-
|
|
2697
|
+
if (userName)
|
|
2698
|
+
predicates.push(`upper(${columns.userName})=${Tools.sqlString(userName)}`);
|
|
2636
2699
|
}
|
|
2637
2700
|
if (options.status) {
|
|
2638
|
-
const status = String(options.status).trim().toUpperCase();
|
|
2639
|
-
|
|
2701
|
+
const status = String(options.status).trim().toUpperCase().replace(/'/g, "''");
|
|
2702
|
+
if (status)
|
|
2703
|
+
predicates.push(`upper(${columns.status}) like '%${status}%'`);
|
|
2640
2704
|
}
|
|
2641
|
-
return
|
|
2705
|
+
return predicates.length > 0 ? ` where ${predicates.join(" and ")}` : "";
|
|
2642
2706
|
}
|
|
2643
2707
|
async function querySubsystemSummary(conn, limit) {
|
|
2644
2708
|
const jobs = await queryJobs(conn, { limit: 5000 });
|
|
@@ -2656,10 +2720,10 @@ function normalizeActiveJobRow(row) {
|
|
|
2656
2720
|
return {
|
|
2657
2721
|
source: "job",
|
|
2658
2722
|
jobName: toNullableString(pickFirst(row, ["JOB_NAME", "QUALIFIED_JOB_NAME", "JOB"])),
|
|
2659
|
-
subsystem: toNullableString(pickFirst(row, ["SUBSYSTEM", "SUBSYSTEM_NAME"])),
|
|
2723
|
+
subsystem: toNullableString(pickFirst(row, ["SUBSYSTEM", "SUBSYSTEM_NAME", "JOB_SUBSYSTEM"])),
|
|
2660
2724
|
status: toNullableString(pickFirst(row, ["JOB_STATUS", "STATUS"])),
|
|
2661
2725
|
function: toNullableString(pickFirst(row, ["FUNCTION", "CURRENT_FUNCTION"])),
|
|
2662
|
-
userName: toNullableString(pickFirst(row, ["AUTHORIZATION_NAME", "USER_NAME", "CURRENT_USER"])),
|
|
2726
|
+
userName: toNullableString(pickFirst(row, ["AUTHORIZATION_NAME", "USER_NAME", "CURRENT_USER", "JOB_USER"])),
|
|
2663
2727
|
type: toNullableString(pickFirst(row, ["JOB_TYPE", "TYPE"])),
|
|
2664
2728
|
cpuSeconds: toNullableNumber(pickFirst(row, ["CPU_TIME", "TOTAL_CPU_TIME", "ELAPSED_TOTAL_SECONDS"]))
|
|
2665
2729
|
};
|
|
@@ -2674,6 +2738,28 @@ async function queryMessageQueueEntries(conn, options) {
|
|
|
2674
2738
|
}
|
|
2675
2739
|
return rows.map(normalizeMessageQueueEntry);
|
|
2676
2740
|
}
|
|
2741
|
+
async function querySpoolEntries(conn, limit) {
|
|
2742
|
+
const queries = [
|
|
2743
|
+
`select * from qsys2.output_queue_entries fetch first ${limit} rows only`,
|
|
2744
|
+
`select * from table(qsys2.output_queue_entries()) fetch first ${limit} rows only`,
|
|
2745
|
+
`select job_name, spooled_file_name, spooled_file_number, output_queue_name, total_pages, file_status from qsys2.output_queue_entries fetch first ${limit} rows only`,
|
|
2746
|
+
`select job_name, spooled_file_name, spooled_file_number, output_queue_name, total_pages, cast('' as varchar(10)) as file_status from qsys2.output_queue_entries fetch first ${limit} rows only`,
|
|
2747
|
+
`select job_name, spooled_file_name, cast(0 as integer) as spooled_file_number, output_queue_name, total_pages, file_status from qsys2.output_queue_entries fetch first ${limit} rows only`,
|
|
2748
|
+
`select job_name, spooled_file_name, cast(0 as integer) as spooled_file_number, output_queue_name, total_pages, cast('' as varchar(10)) as file_status from qsys2.output_queue_entries fetch first ${limit} rows only`,
|
|
2749
|
+
`select * from qsys2.spooled_file_info fetch first ${limit} rows only`,
|
|
2750
|
+
`select * from table(qsys2.spooled_file_info()) fetch first ${limit} rows only`
|
|
2751
|
+
];
|
|
2752
|
+
let lastError;
|
|
2753
|
+
for (const sql of queries) {
|
|
2754
|
+
try {
|
|
2755
|
+
return await conn.runSQL(sql);
|
|
2756
|
+
}
|
|
2757
|
+
catch (err) {
|
|
2758
|
+
lastError = err;
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError || "Unable to read spool entries"));
|
|
2762
|
+
}
|
|
2677
2763
|
function normalizeMessageQueueEntry(row) {
|
|
2678
2764
|
return {
|
|
2679
2765
|
source: "msgq",
|
|
@@ -2689,10 +2775,10 @@ function normalizeMessageQueueEntry(row) {
|
|
|
2689
2775
|
async function queryLockEntries(conn, options) {
|
|
2690
2776
|
let rows;
|
|
2691
2777
|
try {
|
|
2692
|
-
rows = await conn.runSQL(`select * from table(qsys2.object_lock_info()) fetch first ${Math.min(options.limit
|
|
2778
|
+
rows = await conn.runSQL(`select * from table(qsys2.object_lock_info()) fetch first ${Math.min(options.limit, 500)} rows only`);
|
|
2693
2779
|
}
|
|
2694
2780
|
catch {
|
|
2695
|
-
rows = await conn.runSQL(`select * from qsys2.object_lock_info fetch first ${Math.min(options.limit
|
|
2781
|
+
rows = await conn.runSQL(`select * from qsys2.object_lock_info fetch first ${Math.min(options.limit, 500)} rows only`);
|
|
2696
2782
|
}
|
|
2697
2783
|
let normalized = rows.map(normalizeLockEntry);
|
|
2698
2784
|
if (options.objectLibrary)
|
|
@@ -2742,10 +2828,20 @@ function normalizeObjectAuthorityRow(row) {
|
|
|
2742
2828
|
async function queryDataQueueEntries(conn, options) {
|
|
2743
2829
|
let rows;
|
|
2744
2830
|
try {
|
|
2745
|
-
rows = await conn.runSQL(`select * from table(qsys2.receive_data_queue(
|
|
2831
|
+
rows = await conn.runSQL(`select * from table(qsys2.receive_data_queue(${Tools.sqlString(options.queue)}, ${Tools.sqlString(options.library)}, ${Tools.sqlString(options.remove ? "YES" : "NO")}, ${options.waitSeconds})) fetch first 1 rows only`);
|
|
2746
2832
|
}
|
|
2747
2833
|
catch {
|
|
2748
|
-
|
|
2834
|
+
try {
|
|
2835
|
+
rows = await conn.runSQL(`select * from table(qsys2.receive_data_queue(${Tools.sqlString(options.queue)}, ${Tools.sqlString(options.library)})) fetch first 1 rows only`);
|
|
2836
|
+
}
|
|
2837
|
+
catch {
|
|
2838
|
+
try {
|
|
2839
|
+
rows = await conn.runSQL(`select * from table(qsys2.data_queue_entries(${Tools.sqlString(options.queue)}, ${Tools.sqlString(options.library)})) fetch first 1 rows only`);
|
|
2840
|
+
}
|
|
2841
|
+
catch {
|
|
2842
|
+
rows = await conn.runSQL(`select * from qsys2.data_queue_entries where upper(data_queue_name)=${Tools.sqlString(options.queue)} and upper(data_queue_library)=${Tools.sqlString(options.library)} fetch first 1 rows only`);
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2749
2845
|
}
|
|
2750
2846
|
return rows.map(normalizeDataQueueEntry);
|
|
2751
2847
|
}
|
|
@@ -2782,8 +2878,11 @@ function normalizeDataAreaInfoRow(row) {
|
|
|
2782
2878
|
function buildSpoolCommand(commandName, args) {
|
|
2783
2879
|
const jobName = requireJobName(args.jobName);
|
|
2784
2880
|
const spooledFileName = requireIbmObjectName(args.spooledFileName, "spooledFileName");
|
|
2785
|
-
const
|
|
2786
|
-
|
|
2881
|
+
const rawNumber = Number(args.spooledFileNumber);
|
|
2882
|
+
const spooledFileNumber = Number.isFinite(rawNumber) && rawNumber > 0
|
|
2883
|
+
? String(Math.floor(rawNumber))
|
|
2884
|
+
: "*LAST";
|
|
2885
|
+
return `${commandName} FILE(${spooledFileName}) JOB(${jobName}) SPLNBR(${spooledFileNumber})`;
|
|
2787
2886
|
}
|
|
2788
2887
|
async function queryJournalEntriesViaDisplayJournal(conn, options) {
|
|
2789
2888
|
const sampleSize = Math.min(options.limit * 5, 5000);
|
|
@@ -3038,13 +3137,19 @@ function requireQualifiedObject(value, label) {
|
|
|
3038
3137
|
};
|
|
3039
3138
|
}
|
|
3040
3139
|
function requireMessageKey(value) {
|
|
3041
|
-
const
|
|
3042
|
-
if (!
|
|
3140
|
+
const raw = String(value || "").trim();
|
|
3141
|
+
if (!raw)
|
|
3043
3142
|
throw new Error("messageKey is required");
|
|
3044
|
-
|
|
3045
|
-
|
|
3143
|
+
const directHex = raw.toUpperCase();
|
|
3144
|
+
if (/^[0-9A-F]{4,64}$/.test(directHex)) {
|
|
3145
|
+
return `X'${directHex}'`;
|
|
3146
|
+
}
|
|
3147
|
+
const prefixed = raw.toUpperCase();
|
|
3148
|
+
const match = prefixed.match(/^X'([0-9A-F]{4,64})'$/);
|
|
3149
|
+
if (match) {
|
|
3150
|
+
return `X'${match[1]}'`;
|
|
3046
3151
|
}
|
|
3047
|
-
|
|
3152
|
+
throw new Error(`Invalid messageKey: ${value}`);
|
|
3048
3153
|
}
|
|
3049
3154
|
async function updateConnectionSettings(ctx, name, update) {
|
|
3050
3155
|
const conn = await ctx.store.getConnection(name);
|
|
@@ -3284,12 +3389,12 @@ function normalizeSpoolEntry(row) {
|
|
|
3284
3389
|
return {
|
|
3285
3390
|
...row,
|
|
3286
3391
|
source: "spool",
|
|
3287
|
-
jobName:
|
|
3288
|
-
spooledFileName:
|
|
3289
|
-
spooledFileNumber:
|
|
3290
|
-
outputQueue:
|
|
3291
|
-
totalPages:
|
|
3292
|
-
status:
|
|
3392
|
+
jobName: toNullableString(pickFirst(row, ["JOB_NAME", "JOB"])) || "",
|
|
3393
|
+
spooledFileName: toNullableString(pickFirst(row, ["SPOOLED_FILE_NAME", "SPOOLED_FILE", "SPLF_NAME"])) || "",
|
|
3394
|
+
spooledFileNumber: toNullableNumber(pickFirst(row, ["SPOOLED_FILE_NUMBER", "SPLF_NUMBER", "FILE_NUMBER"])),
|
|
3395
|
+
outputQueue: toNullableString(pickFirst(row, ["OUTPUT_QUEUE_NAME", "OUTQ_NAME", "OUTPUT_QUEUE"])) || "",
|
|
3396
|
+
totalPages: toNullableNumber(pickFirst(row, ["TOTAL_PAGES", "PAGES"])),
|
|
3397
|
+
status: toNullableString(pickFirst(row, ["FILE_STATUS", "STATUS"])) || ""
|
|
3293
3398
|
};
|
|
3294
3399
|
}
|
|
3295
3400
|
function normalizeSpoolLine(row) {
|