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.
Files changed (2) hide show
  1. package/dist/mcp/tools.js +149 -44
  2. 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
- const files = await conn.content.getObjectList(args.library, ["*FILE"]);
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
- let rows;
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(${quoteClString(jobName)}) HOLD(${hold})`;
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(${quoteClString(jobName)})`;
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(${quoteClString(jobName)}) OPTION(${option}) DELAY(${delaySeconds})`;
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 command = `SNDPGMMSG MSG(${quoteClString(message)}) TOMSGQ(${library}/${messageQueue}) MSGTYPE(${messageType})`;
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
- const command = `SNDDTAQ DTAQ(${library}/${queue}) MSG(${quoteClString(message)})`;
2092
- const cmdResult = await runClCommand(conn, command, "ile", undefined, 45000);
2093
- return json({ ok: cmdResult.code === 0, command, ...cmdResult });
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 base = await conn.runSQL(`select * from table(qsys2.active_job_info()) fetch first ${Math.min(options.limit * 5, 5000)} rows only`);
2628
- let rows = base.map((row) => normalizeActiveJobRow(row));
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
- rows = rows.filter((row) => (row.subsystem || "") === subsystem);
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
- rows = rows.filter((row) => (row.userName || "") === userName);
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
- rows = rows.filter((row) => (row.status || "").includes(status));
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 rows.slice(0, options.limit);
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 * 5, 5000)} rows only`);
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 * 5, 5000)} rows only`);
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(data_queue_library => ${Tools.sqlString(options.library)}, data_queue_name => ${Tools.sqlString(options.queue)}, wait_time => ${options.waitSeconds}, remove_message => ${Tools.sqlString(options.remove ? "YES" : "NO")})) fetch first 1 rows only`);
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
- rows = await conn.runSQL(`select * from table(qsys2.data_queue_entries(data_queue_library => ${Tools.sqlString(options.library)}, data_queue_name => ${Tools.sqlString(options.queue)})) fetch first 1 rows only`);
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 spooledFileNumber = clampNumber(args.spooledFileNumber, 1, 1, 999999999);
2786
- return `${commandName} FILE(${spooledFileName}) JOB(${quoteClString(jobName)}) SPLNBR(${spooledFileNumber})`;
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 messageKey = String(value || "").trim().toUpperCase();
3042
- if (!messageKey)
3140
+ const raw = String(value || "").trim();
3141
+ if (!raw)
3043
3142
  throw new Error("messageKey is required");
3044
- if (!/^[0-9A-F]{4,32}$/.test(messageKey)) {
3045
- throw new Error(`Invalid messageKey: ${value}`);
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
- return messageKey;
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: String(row.JOB_NAME || ""),
3288
- spooledFileName: String(row.SPOOLED_FILE_NAME || ""),
3289
- spooledFileNumber: Number(row.SPOOLED_FILE_NUMBER || 0),
3290
- outputQueue: String(row.OUTPUT_QUEUE_NAME || ""),
3291
- totalPages: Number.isFinite(Number(row.TOTAL_PAGES)) ? Number(row.TOTAL_PAGES) : null,
3292
- status: String(row.FILE_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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-for-i",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "private": false,
5
5
  "description": "Local MCP server and control plane for IBM i workflows",
6
6
  "type": "module",