mcp-for-i 0.1.9 → 0.1.10

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 +113 -41
  2. package/package.json +1 -1
package/dist/mcp/tools.js CHANGED
@@ -1892,15 +1892,7 @@ export async function handleTool(ctx, name, args) {
1892
1892
  case "ibmi.spool.list": {
1893
1893
  const conn = await ctx.ensureActive(args?.connectionName);
1894
1894
  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
- }
1895
+ const rows = await querySpoolEntries(conn, limit);
1904
1896
  return json(rows.map(normalizeSpoolEntry));
1905
1897
  }
1906
1898
  case "ibmi.spool.read": {
@@ -1959,7 +1951,7 @@ export async function handleTool(ctx, name, args) {
1959
1951
  enforceWritable(ctx, conn, "qsys.write", args);
1960
1952
  const jobName = requireJobName(args.jobName);
1961
1953
  const hold = Boolean(args?.holdOnJobQueue) ? "*JOBQ" : "*NO";
1962
- const command = `HLDJOB JOB(${quoteClString(jobName)}) HOLD(${hold})`;
1954
+ const command = `HLDJOB JOB(${jobName}) HOLD(${hold})`;
1963
1955
  const cmdResult = await runClCommand(conn, command, "ile", undefined, 45000);
1964
1956
  return json({ ok: cmdResult.code === 0, command, ...cmdResult });
1965
1957
  }
@@ -1967,7 +1959,7 @@ export async function handleTool(ctx, name, args) {
1967
1959
  const conn = await ctx.ensureActive(args?.connectionName);
1968
1960
  enforceWritable(ctx, conn, "qsys.write", args);
1969
1961
  const jobName = requireJobName(args.jobName);
1970
- const command = `RLSJOB JOB(${quoteClString(jobName)})`;
1962
+ const command = `RLSJOB JOB(${jobName})`;
1971
1963
  const cmdResult = await runClCommand(conn, command, "ile", undefined, 45000);
1972
1964
  return json({ ok: cmdResult.code === 0, command, ...cmdResult });
1973
1965
  }
@@ -1980,7 +1972,7 @@ export async function handleTool(ctx, name, args) {
1980
1972
  throw new Error("option must be *CNTRLD or *IMMED");
1981
1973
  }
1982
1974
  const delaySeconds = clampNumber(args?.delaySeconds, 30, 0, 99999);
1983
- const command = `ENDJOB JOB(${quoteClString(jobName)}) OPTION(${option}) DELAY(${delaySeconds})`;
1975
+ const command = `ENDJOB JOB(${jobName}) OPTION(${option}) DELAY(${delaySeconds})`;
1984
1976
  const cmdResult = await runClCommand(conn, command, "ile", undefined, 45000);
1985
1977
  return json({ ok: cmdResult.code === 0, command, ...cmdResult });
1986
1978
  }
@@ -2042,9 +2034,12 @@ export async function handleTool(ctx, name, args) {
2042
2034
  const message = String(args.message || "");
2043
2035
  if (!message.trim())
2044
2036
  throw new Error("message is required");
2045
- const command = `SNDPGMMSG MSG(${quoteClString(message)}) TOMSGQ(${library}/${messageQueue}) MSGTYPE(${messageType})`;
2037
+ const mappedMessageType = messageType === "*INQ" ? "*INQ" : "*INFO";
2038
+ const command = mappedMessageType === "*INQ"
2039
+ ? `SNDMSG MSG(${quoteClString(message)}) TOMSGQ(${library}/${messageQueue}) MSGTYPE(*INQ) RPYMSGQ(${library}/${messageQueue})`
2040
+ : `SNDMSG MSG(${quoteClString(message)}) TOMSGQ(${library}/${messageQueue}) MSGTYPE(*INFO)`;
2046
2041
  const cmdResult = await runClCommand(conn, command, "ile", undefined, 45000);
2047
- return json({ ok: cmdResult.code === 0, command, ...cmdResult });
2042
+ return json({ ok: cmdResult.code === 0, command, requestedMessageType: messageType, mappedMessageType, ...cmdResult });
2048
2043
  }
2049
2044
  case "ibmi.msgq.reply": {
2050
2045
  const conn = await ctx.ensureActive(args?.connectionName);
@@ -2088,9 +2083,21 @@ export async function handleTool(ctx, name, args) {
2088
2083
  const message = String(args.message || "");
2089
2084
  if (!message)
2090
2085
  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 });
2086
+ try {
2087
+ await conn.runSQL(`call qsys2.send_data_queue(${Tools.sqlString(message)}, ${Tools.sqlString(queue)}, ${Tools.sqlString(library)})`);
2088
+ return json({
2089
+ ok: true,
2090
+ method: "qsys2.send_data_queue",
2091
+ library,
2092
+ queue,
2093
+ messageLength: message.length
2094
+ });
2095
+ }
2096
+ catch {
2097
+ const command = `SNDDTAQ DTAQ(${library}/${queue}) DTA(${quoteClString(message)})`;
2098
+ const cmdResult = await runClCommand(conn, command, "ile", undefined, 45000);
2099
+ return json({ ok: cmdResult.code === 0, method: "cl.snddtaq", command, ...cmdResult });
2100
+ }
2094
2101
  }
2095
2102
  case "ibmi.dataqueue.receive": {
2096
2103
  const conn = await ctx.ensureActive(args?.connectionName);
@@ -2624,21 +2631,51 @@ async function queryJournalEntries(conn, options) {
2624
2631
  return normalized.slice(0, options.limit);
2625
2632
  }
2626
2633
  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));
2634
+ const scanLimit = Math.min(options.limit * 5, 5000);
2635
+ let activeRows = [];
2636
+ try {
2637
+ activeRows = await conn.runSQL(`select * from table(qsys2.active_job_info()) fetch first ${scanLimit} rows only`);
2638
+ }
2639
+ catch {
2640
+ // Continue with job_info fallback.
2641
+ }
2642
+ let rows = filterNormalizedJobs(activeRows.map((row) => normalizeActiveJobRow(row)), options);
2643
+ if (rows.length >= options.limit)
2644
+ return rows.slice(0, options.limit);
2645
+ try {
2646
+ const allRows = await conn.runSQL(`select * from table(qsys2.job_info('*ALL')) fetch first ${scanLimit} rows only`);
2647
+ const fallback = filterNormalizedJobs(allRows.map((row) => normalizeActiveJobRow(row)), options);
2648
+ const seen = new Set(rows.map((row) => String(row.jobName || "")));
2649
+ for (const row of fallback) {
2650
+ const key = String(row.jobName || "");
2651
+ if (!key || seen.has(key))
2652
+ continue;
2653
+ rows.push(row);
2654
+ seen.add(key);
2655
+ if (rows.length >= options.limit)
2656
+ break;
2657
+ }
2658
+ }
2659
+ catch {
2660
+ // Some systems may not provide job_info as a table function.
2661
+ }
2662
+ return rows.slice(0, options.limit);
2663
+ }
2664
+ function filterNormalizedJobs(rows, options) {
2665
+ let filtered = rows;
2629
2666
  if (options.subsystem) {
2630
2667
  const subsystem = String(options.subsystem).trim().toUpperCase();
2631
- rows = rows.filter((row) => (row.subsystem || "") === subsystem);
2668
+ filtered = filtered.filter((row) => (row.subsystem || "") === subsystem);
2632
2669
  }
2633
2670
  if (options.userName) {
2634
2671
  const userName = String(options.userName).trim().toUpperCase();
2635
- rows = rows.filter((row) => (row.userName || "") === userName);
2672
+ filtered = filtered.filter((row) => (row.userName || "") === userName);
2636
2673
  }
2637
2674
  if (options.status) {
2638
2675
  const status = String(options.status).trim().toUpperCase();
2639
- rows = rows.filter((row) => (row.status || "").includes(status));
2676
+ filtered = filtered.filter((row) => (row.status || "").includes(status));
2640
2677
  }
2641
- return rows.slice(0, options.limit);
2678
+ return filtered;
2642
2679
  }
2643
2680
  async function querySubsystemSummary(conn, limit) {
2644
2681
  const jobs = await queryJobs(conn, { limit: 5000 });
@@ -2656,10 +2693,10 @@ function normalizeActiveJobRow(row) {
2656
2693
  return {
2657
2694
  source: "job",
2658
2695
  jobName: toNullableString(pickFirst(row, ["JOB_NAME", "QUALIFIED_JOB_NAME", "JOB"])),
2659
- subsystem: toNullableString(pickFirst(row, ["SUBSYSTEM", "SUBSYSTEM_NAME"])),
2696
+ subsystem: toNullableString(pickFirst(row, ["SUBSYSTEM", "SUBSYSTEM_NAME", "JOB_SUBSYSTEM"])),
2660
2697
  status: toNullableString(pickFirst(row, ["JOB_STATUS", "STATUS"])),
2661
2698
  function: toNullableString(pickFirst(row, ["FUNCTION", "CURRENT_FUNCTION"])),
2662
- userName: toNullableString(pickFirst(row, ["AUTHORIZATION_NAME", "USER_NAME", "CURRENT_USER"])),
2699
+ userName: toNullableString(pickFirst(row, ["AUTHORIZATION_NAME", "USER_NAME", "CURRENT_USER", "JOB_USER"])),
2663
2700
  type: toNullableString(pickFirst(row, ["JOB_TYPE", "TYPE"])),
2664
2701
  cpuSeconds: toNullableNumber(pickFirst(row, ["CPU_TIME", "TOTAL_CPU_TIME", "ELAPSED_TOTAL_SECONDS"]))
2665
2702
  };
@@ -2674,6 +2711,25 @@ async function queryMessageQueueEntries(conn, options) {
2674
2711
  }
2675
2712
  return rows.map(normalizeMessageQueueEntry);
2676
2713
  }
2714
+ async function querySpoolEntries(conn, limit) {
2715
+ const queries = [
2716
+ `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`,
2717
+ `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`,
2718
+ `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`,
2719
+ `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`,
2720
+ `select * from qsys2.output_queue_entries fetch first ${limit} rows only`
2721
+ ];
2722
+ let lastError;
2723
+ for (const sql of queries) {
2724
+ try {
2725
+ return await conn.runSQL(sql);
2726
+ }
2727
+ catch (err) {
2728
+ lastError = err;
2729
+ }
2730
+ }
2731
+ throw lastError instanceof Error ? lastError : new Error(String(lastError || "Unable to read spool entries"));
2732
+ }
2677
2733
  function normalizeMessageQueueEntry(row) {
2678
2734
  return {
2679
2735
  source: "msgq",
@@ -2689,10 +2745,10 @@ function normalizeMessageQueueEntry(row) {
2689
2745
  async function queryLockEntries(conn, options) {
2690
2746
  let rows;
2691
2747
  try {
2692
- rows = await conn.runSQL(`select * from table(qsys2.object_lock_info()) fetch first ${Math.min(options.limit * 5, 5000)} rows only`);
2748
+ rows = await conn.runSQL(`select * from table(qsys2.object_lock_info()) fetch first ${Math.min(options.limit, 500)} rows only`);
2693
2749
  }
2694
2750
  catch {
2695
- rows = await conn.runSQL(`select * from qsys2.object_lock_info fetch first ${Math.min(options.limit * 5, 5000)} rows only`);
2751
+ rows = await conn.runSQL(`select * from qsys2.object_lock_info fetch first ${Math.min(options.limit, 500)} rows only`);
2696
2752
  }
2697
2753
  let normalized = rows.map(normalizeLockEntry);
2698
2754
  if (options.objectLibrary)
@@ -2742,10 +2798,20 @@ function normalizeObjectAuthorityRow(row) {
2742
2798
  async function queryDataQueueEntries(conn, options) {
2743
2799
  let rows;
2744
2800
  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`);
2801
+ 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
2802
  }
2747
2803
  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`);
2804
+ try {
2805
+ rows = await conn.runSQL(`select * from table(qsys2.receive_data_queue(${Tools.sqlString(options.queue)}, ${Tools.sqlString(options.library)})) fetch first 1 rows only`);
2806
+ }
2807
+ catch {
2808
+ try {
2809
+ rows = await conn.runSQL(`select * from table(qsys2.data_queue_entries(${Tools.sqlString(options.queue)}, ${Tools.sqlString(options.library)})) fetch first 1 rows only`);
2810
+ }
2811
+ catch {
2812
+ 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`);
2813
+ }
2814
+ }
2749
2815
  }
2750
2816
  return rows.map(normalizeDataQueueEntry);
2751
2817
  }
@@ -2783,7 +2849,7 @@ function buildSpoolCommand(commandName, args) {
2783
2849
  const jobName = requireJobName(args.jobName);
2784
2850
  const spooledFileName = requireIbmObjectName(args.spooledFileName, "spooledFileName");
2785
2851
  const spooledFileNumber = clampNumber(args.spooledFileNumber, 1, 1, 999999999);
2786
- return `${commandName} FILE(${spooledFileName}) JOB(${quoteClString(jobName)}) SPLNBR(${spooledFileNumber})`;
2852
+ return `${commandName} FILE(${spooledFileName}) JOB(${jobName}) SPLNBR(${spooledFileNumber})`;
2787
2853
  }
2788
2854
  async function queryJournalEntriesViaDisplayJournal(conn, options) {
2789
2855
  const sampleSize = Math.min(options.limit * 5, 5000);
@@ -3038,13 +3104,19 @@ function requireQualifiedObject(value, label) {
3038
3104
  };
3039
3105
  }
3040
3106
  function requireMessageKey(value) {
3041
- const messageKey = String(value || "").trim().toUpperCase();
3042
- if (!messageKey)
3107
+ const raw = String(value || "").trim();
3108
+ if (!raw)
3043
3109
  throw new Error("messageKey is required");
3044
- if (!/^[0-9A-F]{4,32}$/.test(messageKey)) {
3045
- throw new Error(`Invalid messageKey: ${value}`);
3110
+ const directHex = raw.toUpperCase();
3111
+ if (/^[0-9A-F]{4,64}$/.test(directHex)) {
3112
+ return `X'${directHex}'`;
3113
+ }
3114
+ const prefixed = raw.toUpperCase();
3115
+ const match = prefixed.match(/^X'([0-9A-F]{4,64})'$/);
3116
+ if (match) {
3117
+ return `X'${match[1]}'`;
3046
3118
  }
3047
- return messageKey;
3119
+ throw new Error(`Invalid messageKey: ${value}`);
3048
3120
  }
3049
3121
  async function updateConnectionSettings(ctx, name, update) {
3050
3122
  const conn = await ctx.store.getConnection(name);
@@ -3284,12 +3356,12 @@ function normalizeSpoolEntry(row) {
3284
3356
  return {
3285
3357
  ...row,
3286
3358
  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 || "")
3359
+ jobName: toNullableString(pickFirst(row, ["JOB_NAME", "JOB"])) || "",
3360
+ spooledFileName: toNullableString(pickFirst(row, ["SPOOLED_FILE_NAME", "SPOOLED_FILE", "SPLF_NAME"])) || "",
3361
+ spooledFileNumber: toNullableNumber(pickFirst(row, ["SPOOLED_FILE_NUMBER", "SPLF_NUMBER", "FILE_NUMBER"])),
3362
+ outputQueue: toNullableString(pickFirst(row, ["OUTPUT_QUEUE_NAME", "OUTQ_NAME", "OUTPUT_QUEUE"])) || "",
3363
+ totalPages: toNullableNumber(pickFirst(row, ["TOTAL_PAGES", "PAGES"])),
3364
+ status: toNullableString(pickFirst(row, ["FILE_STATUS", "STATUS"])) || ""
3293
3365
  };
3294
3366
  }
3295
3367
  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.10",
4
4
  "private": false,
5
5
  "description": "Local MCP server and control plane for IBM i workflows",
6
6
  "type": "module",