@aiconnect/agentjobs-mcp 1.1.0 → 1.3.0

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.
@@ -47,6 +47,37 @@ const jobConfigSchema = z.object({
47
47
  failure_cooldown_minutes: z.number().optional(),
48
48
  start_prompt: z.string().optional(),
49
49
  }).passthrough();
50
+ const activitySourceSchema = z.object({
51
+ type: z.enum(['dispatch', 'process_module', 'direct']),
52
+ reference_id: z.string().optional(),
53
+ execution_id: z.string().optional(),
54
+ job_id: z.string().optional(),
55
+ chat_id: z.string().optional(),
56
+ agent_job_type_id: z.string().optional(),
57
+ channel_code: z.string().optional(),
58
+ }).passthrough();
59
+ // Required-by-contract (per `job-activities-query` spec): every activity entry
60
+ // rendered by the formatter MUST surface id, created_at, status, activity_type_code,
61
+ // source.type, consumed_credits, allocated_credits. Records missing any of these
62
+ // fail the parse and fall back to "[unparseable activity]" so upstream audit-data
63
+ // corruption is surfaced rather than silently rendered as `unknown`/`n/a`.
64
+ const activitySchema = z.object({
65
+ id: z.string().min(1),
66
+ org_id: z.string().optional(),
67
+ activity_type_code: z.string().min(1),
68
+ status: z.enum(['submitted', 'completed', 'canceled']),
69
+ allocated_credits: z.number(),
70
+ consumed_credits: z.number(),
71
+ credits_rule_id: z.number().optional(),
72
+ payloads: z.object({
73
+ input: z.any().optional(),
74
+ output: z.any().optional(),
75
+ }).passthrough().optional(),
76
+ processed_at: isoOrMs.nullable().optional(),
77
+ created_at: z.union([z.string(), z.number()]),
78
+ updated_at: isoOrMs,
79
+ source: activitySourceSchema,
80
+ }).passthrough();
50
81
  const jobDetailsSchema = z.object({
51
82
  job_id: z.string(),
52
83
  job_type_id: z.string(),
@@ -66,6 +97,13 @@ const jobDetailsSchema = z.object({
66
97
  channel_data: channelDataSchema.optional().default({}),
67
98
  job_config: jobConfigSchema.optional().default({}),
68
99
  params: z.record(z.any()).optional().default({}),
100
+ activities_count: z.number().optional(),
101
+ // Activities are intentionally validated as `unknown[]` here (not as
102
+ // `z.array(activitySchema)`): per-entry parsing happens inside
103
+ // `formatActivityEntry`, so a single malformed record degrades to
104
+ // "[unparseable activity]" without making the whole job document fall back
105
+ // to the raw-JSON branch in formatJobDetails.
106
+ Activities: z.array(z.unknown()).optional(),
69
107
  }).passthrough();
70
108
  const bool = (v) => (v === true ? 'yes' : v === false ? 'no' : 'n/a');
71
109
  const safe = (v, fallback = 'n/a') => v === undefined || v === null || v === '' ? fallback : String(v);
@@ -75,7 +113,134 @@ const truncate = (s, max = 300) => {
75
113
  return s.length > max ? `${s.slice(0, max)}…` : s;
76
114
  };
77
115
  const fmtList = (arr) => (arr && arr.length ? arr.join(', ') : 'n/a');
78
- export function formatJobDetails(job) {
116
+ const ACTIVITY_OUTPUT_MAX = 200;
117
+ function isBinaryValue(value) {
118
+ if (value === null || value === undefined || typeof value !== 'object')
119
+ return false;
120
+ if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value))
121
+ return true;
122
+ if (value instanceof ArrayBuffer)
123
+ return true;
124
+ if (typeof ArrayBuffer.isView === 'function' && ArrayBuffer.isView(value))
125
+ return true;
126
+ return false;
127
+ }
128
+ function containsBinary(value, seen = new WeakSet()) {
129
+ if (isBinaryValue(value))
130
+ return true;
131
+ if (value === null || typeof value !== 'object')
132
+ return false;
133
+ if (seen.has(value))
134
+ return false;
135
+ seen.add(value);
136
+ if (Array.isArray(value)) {
137
+ for (const v of value) {
138
+ if (containsBinary(v, seen))
139
+ return true;
140
+ }
141
+ return false;
142
+ }
143
+ for (const v of Object.values(value)) {
144
+ if (containsBinary(v, seen))
145
+ return true;
146
+ }
147
+ return false;
148
+ }
149
+ function safeStringifyOutput(value) {
150
+ if (value === null || value === undefined)
151
+ return '';
152
+ if (typeof value === 'string')
153
+ return value;
154
+ if (containsBinary(value))
155
+ return '[non-serializable]';
156
+ try {
157
+ const text = JSON.stringify(value);
158
+ return text === undefined ? '[non-serializable]' : text;
159
+ }
160
+ catch {
161
+ return '[non-serializable]';
162
+ }
163
+ }
164
+ /**
165
+ * Collapses newlines/CR/tabs into escaped literals and clamps the result so it
166
+ * always fits in a single visual line of the activity output. Used by every
167
+ * code path that injects user-controlled activity content into the rendered
168
+ * line — both the normal `payloads.output` branch and the malformed-entry
169
+ * fallback — so a bad record cannot visually corrupt the surrounding entries.
170
+ */
171
+ function sanitizeForActivityLine(value, max = ACTIVITY_OUTPUT_MAX) {
172
+ const escaped = value
173
+ .replace(/\r\n/g, '\\n')
174
+ .replace(/\n/g, '\\n')
175
+ .replace(/\r/g, '\\n')
176
+ .replace(/\t/g, '\\t');
177
+ return escaped.length > max ? `${escaped.slice(0, max)}…` : escaped;
178
+ }
179
+ function formatActivitySource(source) {
180
+ if (!source || typeof source !== 'object')
181
+ return 'unknown';
182
+ const type = source.type ?? 'unknown';
183
+ const extras = [];
184
+ if (source.reference_id)
185
+ extras.push(`ref: ${source.reference_id}`);
186
+ if (source.execution_id)
187
+ extras.push(`exec: ${source.execution_id}`);
188
+ if (source.chat_id)
189
+ extras.push(`chat: ${source.chat_id}`);
190
+ if (source.agent_job_type_id)
191
+ extras.push(`type: ${source.agent_job_type_id}`);
192
+ if (source.channel_code)
193
+ extras.push(`channel: ${source.channel_code}`);
194
+ return extras.length ? `${type} (${extras.join(', ')})` : String(type);
195
+ }
196
+ export function formatActivityEntry(activity) {
197
+ let a;
198
+ try {
199
+ a = activitySchema.parse(activity);
200
+ }
201
+ catch {
202
+ const raw = safeStringifyOutput(activity);
203
+ return `- [unparseable activity] ${sanitizeForActivityLine(raw)}`;
204
+ }
205
+ const createdIso = toIso(a.created_at) ?? 'n/a';
206
+ const status = a.status ?? 'unknown';
207
+ const typeCode = a.activity_type_code ?? 'unknown';
208
+ const sourceLine = formatActivitySource(a.source);
209
+ const consumed = a.consumed_credits ?? 0;
210
+ const allocated = a.allocated_credits ?? 0;
211
+ const lines = [
212
+ `- ${createdIso} [${status}] ${typeCode} via ${sourceLine}`,
213
+ ` credits: ${consumed}/${allocated} id: ${a.id}`,
214
+ ];
215
+ const output = a.payloads?.output;
216
+ if (output !== undefined && output !== null && output !== '') {
217
+ const stringified = safeStringifyOutput(output);
218
+ if (stringified !== '') {
219
+ lines.push(` output: ${sanitizeForActivityLine(stringified)}`);
220
+ }
221
+ }
222
+ return lines.join('\n');
223
+ }
224
+ export function formatJobActivitiesList(jobId, activities, meta, offset = 0) {
225
+ if (!meta || typeof meta.count !== 'number' || typeof meta.limit !== 'number' || typeof meta.total !== 'number') {
226
+ throw new Error('formatJobActivitiesList: meta is required with numeric `count`, `limit`, and `total`. ' +
227
+ `Received: ${JSON.stringify(meta)}`);
228
+ }
229
+ const safeActivities = activities || [];
230
+ const { count, total } = meta;
231
+ const hasMore = (offset + count) < total;
232
+ // Advance by the number of rows actually returned, not by `limit`. If the
233
+ // backend ships a short non-terminal page (count < limit), advancing by
234
+ // `limit` would tell the caller to skip unseen rows.
235
+ const nextOffset = hasMore ? offset + count : null;
236
+ const footer = `Returned: ${count} | Total matching: ${total} | Has more: ${hasMore} | Next offset: ${nextOffset === null ? 'null' : nextOffset}`;
237
+ if (safeActivities.length === 0) {
238
+ return `No activities found for job ${jobId}.\n\n${footer}`;
239
+ }
240
+ const entries = safeActivities.map(formatActivityEntry).join('\n');
241
+ return `Activities for job ${jobId} (showing ${count}):\n\n${entries}\n\n${footer}`;
242
+ }
243
+ export function formatJobDetails(job, meta) {
79
244
  try {
80
245
  const j = jobDetailsSchema.parse(job);
81
246
  // Derivados
@@ -120,6 +285,27 @@ export function formatJobDetails(job) {
120
285
  }
121
286
  })();
122
287
  const startPrompt = j.job_config?.start_prompt ? truncate(j.job_config.start_prompt, 500) : undefined;
288
+ // Fail-closed: the only signal that the caller actually requested the
289
+ // overlay is `meta.activities_meta` (a field the backend returns ONLY for
290
+ // ?include=activities). Without that signal we omit the Activities block
291
+ // entirely — even if `j.Activities` happens to be populated in the payload —
292
+ // to keep flag-off output byte-identical to the legacy formatter.
293
+ let activitiesBlock = '';
294
+ const overlayRequested = meta?.activities_meta !== undefined;
295
+ if (overlayRequested) {
296
+ if (Array.isArray(j.Activities) && j.Activities.length > 0) {
297
+ const entries = j.Activities.map(formatActivityEntry).join('\n');
298
+ const total = meta?.activities_meta?.count;
299
+ const limit = meta?.activities_meta?.limit;
300
+ const truncationLine = (typeof total === 'number' && typeof limit === 'number' && total > limit)
301
+ ? `\n(showing ${j.Activities.length} of ${total} activities — use get_job_activities for full pagination)`
302
+ : '';
303
+ activitiesBlock = `\n\nActivities:\n${entries}${truncationLine}`;
304
+ }
305
+ else {
306
+ activitiesBlock = `\n\nActivities:\n - (no activities recorded for this job)`;
307
+ }
308
+ }
123
309
  return (`Job Details
124
310
  ===========
125
311
 
@@ -172,7 +358,7 @@ Result / Tags / Log:
172
358
  - Result: ${safe(j.result)}
173
359
  - Tags: ${fmtList(tagsList)}
174
360
  - Execution Log (last 5):
175
- ${lastLogs.length ? lastLogs.join('\n') : ' - n/a'}
361
+ ${lastLogs.length ? lastLogs.join('\n') : ' - n/a'}${activitiesBlock}
176
362
  `).trim();
177
363
  }
178
364
  catch (e) {
@@ -190,15 +376,32 @@ const jobSchema = z.object({
190
376
  job_status: z.string(),
191
377
  result: z.string().nullable(),
192
378
  job_type_id: z.string(),
379
+ activities_count: z.number().optional(),
380
+ Activities: z.array(z.any()).optional(),
193
381
  }).passthrough(); // .passthrough() permite outros campos não definidos no schema.
194
382
  /**
195
383
  * Formata um resumo de um job, com os campos principais.
196
- * @param job - O objeto do job.
197
- * @returns Uma string formatada com o resumo do job.
198
384
  */
199
- export function formatJobSummary(job) {
385
+ export function formatJobSummary(job, options = {}) {
200
386
  try {
201
387
  const parsedJob = jobSchema.parse(job);
388
+ const overlayConsidered = options.includeActivities === true && Array.isArray(parsedJob.Activities);
389
+ const overlayLen = overlayConsidered ? parsedJob.Activities.length : undefined;
390
+ const totalCount = typeof parsedJob.activities_count === 'number' ? parsedJob.activities_count : undefined;
391
+ let activitiesLine = '';
392
+ if (totalCount !== undefined && overlayLen !== undefined && overlayLen !== totalCount) {
393
+ // Surface any divergence between the canonical total and the overlay size,
394
+ // not just the truncation direction (overlay < total). A backend bug that
395
+ // returns more overlay entries than the canonical count is also a real
396
+ // mismatch the caller should see.
397
+ activitiesLine = `\n- Activities: ${totalCount} (overlay: ${overlayLen})`;
398
+ }
399
+ else if (totalCount !== undefined) {
400
+ activitiesLine = `\n- Activities: ${totalCount}`;
401
+ }
402
+ else if (overlayLen !== undefined) {
403
+ activitiesLine = `\n- Activities: ${overlayLen}`;
404
+ }
202
405
  return `
203
406
  - Job ID: ${parsedJob.job_id}
204
407
  - Status: ${parsedJob.job_status}
@@ -206,7 +409,7 @@ export function formatJobSummary(job) {
206
409
  - Channel: ${parsedJob.channel_code}
207
410
  - Scheduled At: ${parsedJob.scheduled_at}
208
411
  - Updated At: ${parsedJob.updated_at}
209
- - Result: ${parsedJob.result || 'N/A'}
412
+ - Result: ${parsedJob.result || 'N/A'}${activitiesLine}
210
413
  `.trim();
211
414
  }
212
415
  catch {
@@ -214,19 +417,27 @@ export function formatJobSummary(job) {
214
417
  return JSON.stringify(job, null, 2);
215
418
  }
216
419
  }
217
- /**
218
- * Formata a resposta para a lista de jobs.
219
- * @param jobs - Um array de jobs.
220
- * @param pagination - O objeto de paginação.
221
- * @returns Uma string formatada com a lista de resumos de jobs.
222
- */
223
- export function formatJobList(jobs, pagination) {
224
- if (!jobs || jobs.length === 0) {
225
- return "No jobs found for the given criteria.";
420
+ export function formatJobList(jobs, meta, offset = 0, options = {}) {
421
+ if (!meta || typeof meta.count !== 'number' || typeof meta.limit !== 'number' || typeof meta.total !== 'number') {
422
+ throw new Error('formatJobList: meta is required with numeric `count`, `limit`, and `total`. ' +
423
+ `Received: ${JSON.stringify(meta)}`);
424
+ }
425
+ const safeJobs = jobs || [];
426
+ const { count, limit, total } = meta;
427
+ const hasMore = (offset + count) < total;
428
+ const nextOffset = hasMore ? offset + limit : null;
429
+ let footer = `Returned: ${count} | Total matching: ${total} | Has more: ${hasMore} | Next offset: ${nextOffset === null ? 'null' : nextOffset}`;
430
+ if (options.includeActivities === true && meta.activities_truncated === true) {
431
+ const returned = typeof meta.activities_total_returned === 'number' ? meta.activities_total_returned : '?';
432
+ const available = typeof meta.activities_total_available === 'number' ? meta.activities_total_available : '?';
433
+ footer += `\nActivities truncated: yes (returned ${returned} of ${available} available)`;
226
434
  }
227
- const jobSummaries = jobs.map(job => formatJobSummary(job)).join('\n\n');
228
- const paginationSummary = `Page: ${Math.floor((pagination.offset || 0) / (pagination.limit || 20)) + 1} | Total Jobs: ${pagination.total}`;
229
- return `Found ${jobs.length} jobs.\n\n${jobSummaries}\n\n${paginationSummary}`;
435
+ if (safeJobs.length === 0) {
436
+ return `Found 0 jobs.\n\n${footer}`;
437
+ }
438
+ const summaryOpts = { includeActivities: options.includeActivities === true };
439
+ const jobSummaries = safeJobs.map(job => formatJobSummary(job, summaryOpts)).join('\n\n');
440
+ return `Found ${safeJobs.length} jobs.\n\n${jobSummaries}\n\n${footer}`;
230
441
  }
231
442
  // Schema for job type details
232
443
  const jobTypeSchema = z.object({
@@ -454,21 +665,81 @@ export function formatJobTypeSummary(jobType) {
454
665
  return JSON.stringify(jobType, null, 2);
455
666
  }
456
667
  }
457
- export function formatJobStats(stats, filters) {
668
+ const DATE_FILTER_KEYS = [
669
+ 'scheduled_at_gte',
670
+ 'scheduled_at_lte',
671
+ 'created_at_gte',
672
+ 'created_at_lte',
673
+ ];
674
+ const renderDateRange = (label, gte, lte) => {
675
+ if (gte === undefined && lte === undefined)
676
+ return null;
677
+ const left = gte !== undefined ? String(gte) : '(open)';
678
+ const right = lte !== undefined ? String(lte) : '(open)';
679
+ return `${label}: ${left} → ${right}`;
680
+ };
681
+ const CONTEXT_LABEL_WIDTH = 16; // "Server version: " == 16 chars
682
+ const JOB_TYPE_ID_WIDTH = 22;
683
+ const JOB_TYPE_EMOJI_WIDTH = 4;
684
+ function padLabel(label) {
685
+ return (label + ':').padEnd(CONTEXT_LABEL_WIDTH, ' ');
686
+ }
687
+ function formatJobTypeLine(jt) {
688
+ const id = jt.id.padEnd(JOB_TYPE_ID_WIDTH, ' ');
689
+ const emoji = (jt.emoji ?? '').padEnd(JOB_TYPE_EMOJI_WIDTH, ' ');
690
+ const description = jt.description ? ` — ${jt.description}` : '';
691
+ return ` - ${id} ${emoji} ${jt.name}${description}`;
692
+ }
693
+ export function formatContext(input) {
694
+ const { localConfig, jobTypes, total, jobTypesError } = input;
695
+ const contextSection = [
696
+ 'Context:',
697
+ ` ${padLabel('Org ID')} ${localConfig.org_id}`,
698
+ ` ${padLabel('Timezone')} ${localConfig.timezone}`,
699
+ ` ${padLabel('API URL')} ${localConfig.api_url}`,
700
+ ` ${padLabel('Server version')} ${localConfig.server_version}`
701
+ ].join('\n');
702
+ let jobsSection;
703
+ if (jobTypesError !== undefined) {
704
+ jobsSection = `Job types: unavailable (error: ${jobTypesError})`;
705
+ }
706
+ else if (jobTypes === undefined) {
707
+ jobsSection = 'Job types: unavailable (error: unknown)';
708
+ }
709
+ else {
710
+ const totalCount = typeof total === 'number' ? total : jobTypes.length;
711
+ if (totalCount === 0) {
712
+ jobsSection = 'Job types available (0):\n (no job types registered for this org)';
713
+ }
714
+ else {
715
+ const lines = jobTypes.map(formatJobTypeLine);
716
+ let trailing = '';
717
+ if (totalCount > jobTypes.length) {
718
+ const missing = totalCount - jobTypes.length;
719
+ trailing = `\n … and ${missing} more job types not shown`;
720
+ }
721
+ jobsSection = `Job types available (${totalCount}):\n${lines.join('\n')}${trailing}`;
722
+ }
723
+ }
724
+ return `${contextSection}\n\n${jobsSection}`;
725
+ }
726
+ export function formatJobStats(stats, appliedFilters = {}) {
458
727
  const { waiting = 0, running = 0, completed = 0, failed = 0, canceled = 0, scheduled = 0, } = stats.status;
459
728
  const totalJobs = waiting + running + completed + failed + canceled + scheduled;
460
729
  const successRate = totalJobs > 0 ? ((completed / (totalJobs - waiting - scheduled - running)) * 100).toFixed(1) : "0.0";
461
730
  const completionRate = (completed + failed) > 0 ? (completed / (completed + failed) * 100).toFixed(1) : "0.0";
462
731
  const activeJobs = running + waiting + scheduled;
463
- let period = "All time";
464
- if (filters) {
465
- if (filters.scheduled_at_gte || filters.scheduled_at_lte) {
466
- const startDate = filters.scheduled_at_gte ? new Date(filters.scheduled_at_gte).toLocaleDateString() : "";
467
- const endDate = filters.scheduled_at_lte ? new Date(filters.scheduled_at_lte).toLocaleDateString() : "";
468
- period = `${startDate} to ${endDate}`;
469
- }
470
- }
471
- const org = filters?.org_id ? `Organization: ${filters.org_id}` : "";
732
+ const filters = appliedFilters || {};
733
+ const scheduledLine = renderDateRange('Scheduled', filters.scheduled_at_gte, filters.scheduled_at_lte);
734
+ const createdLine = renderDateRange('Created', filters.created_at_gte, filters.created_at_lte);
735
+ const hasAnyDateFilter = DATE_FILTER_KEYS.some((k) => filters[k] !== undefined);
736
+ const periodFallback = hasAnyDateFilter ? null : 'Period: All time';
737
+ const dateLines = [scheduledLine, createdLine, periodFallback].filter((l) => l !== null);
738
+ const dateFilterSet = new Set(DATE_FILTER_KEYS);
739
+ const otherFilterEntries = Object.entries(filters).filter(([k, v]) => !dateFilterSet.has(k) && v !== undefined && v !== null && v !== '');
740
+ const filtersSection = otherFilterEntries.length
741
+ ? `Filters:\n${otherFilterEntries.map(([k, v]) => `- ${k}: ${v}`).join('\n')}\n\n`
742
+ : '';
472
743
  const percentage = (value) => {
473
744
  if (totalJobs === 0)
474
745
  return "0.0";
@@ -478,10 +749,9 @@ export function formatJobStats(stats, filters) {
478
749
  Job Statistics Report
479
750
  ====================
480
751
 
481
- Period: ${period}
482
- ${org}
752
+ ${dateLines.join('\n')}
483
753
 
484
- Status Breakdown:
754
+ ${filtersSection}Status Breakdown:
485
755
  ✓ Completed: ${completed} jobs (${percentage(completed)}%)
486
756
  ⏳ Running: ${running} jobs (${percentage(running)}%)
487
757
  ⏰ Scheduled: ${scheduled} jobs (${percentage(scheduled)}%)
@@ -1,20 +1,32 @@
1
1
  import { z } from 'zod';
2
2
  /**
3
- * A flexible Zod schema for validating ISO 8601 date-time strings.
3
+ * Factory that returns a fresh Zod schema validating ISO 8601 date-time strings.
4
4
  *
5
- * This schema accepts any string that can be successfully parsed by the `Date` constructor,
6
- * which includes formats with 'Z' (UTC) and timezone offsets (e.g., '+01:00').
7
- * It refines a base string schema, providing a more specific error message if the
8
- * date-time string is invalid.
5
+ * Accepts any string parseable by `new Date(value)` (full ISO 8601 with `Z` or
6
+ * timezone offset, and date-only forms like `2024-07-23`).
7
+ *
8
+ * IMPORTANT why this is a factory and not a singleton: when the MCP SDK
9
+ * serializes a tool's input shape to JSON Schema (via `zod-to-json-schema`),
10
+ * reusing the same Zod *instance* across multiple fields causes the serializer
11
+ * to emit `$ref` from sibling fields back to the first one. Many clients/LLMs
12
+ * then treat those fields as `any` and send `null`, which the server rejects.
13
+ * Returning a new instance per call guarantees inline definitions per field.
14
+ *
15
+ * Convention for this project: every reusable Zod schema referenced by more
16
+ * than one tool field MUST be exported as a factory (`() => z.something(...)`),
17
+ * never as a singleton `const`.
9
18
  */
10
- export const flexibleDateTimeSchema = z.string().refine((value) => {
11
- // Try to parse the date string.
12
- // The Date constructor is quite flexible with ISO 8601 formats.
19
+ export const flexibleDateTimeSchema = () => z.string().refine((value) => {
13
20
  const date = new Date(value);
14
- // Check if the parsed date is valid.
15
- // `isNaN(date.getTime())` is a reliable way to check for invalid dates.
16
21
  return !isNaN(date.getTime());
17
22
  }, {
18
- // Custom error message for invalid date-time strings.
19
23
  message: "Invalid date-time string. Please use a valid ISO 8601 format.",
20
24
  });
25
+ /**
26
+ * Enum factories for activity record fields. Returned as factories for the
27
+ * same reason as `flexibleDateTimeSchema` — singletons would cause `$ref`
28
+ * collisions in the serialized JSON Schema across sibling tool fields.
29
+ */
30
+ export const activityStatusSchema = () => z.enum(['submitted', 'completed', 'canceled']);
31
+ export const activitySourceTypeSchema = () => z.enum(['dispatch', 'process_module', 'direct']);
32
+ export const activitiesSortSchema = () => z.enum(['created_at', '-created_at']);
@@ -0,0 +1,12 @@
1
+ import fs from 'fs';
2
+ function readVersion() {
3
+ try {
4
+ const raw = fs.readFileSync(new URL('../../package.json', import.meta.url), 'utf-8');
5
+ const pkg = JSON.parse(raw);
6
+ return pkg.version ?? 'unknown';
7
+ }
8
+ catch {
9
+ return 'unknown';
10
+ }
11
+ }
12
+ export const mcpServerVersion = readVersion();
@@ -239,6 +239,47 @@ Cancels a job by changing its status to CANCELED.
239
239
  - 404: Job not found
240
240
  - 409: Conflict (job is already in a terminal state)
241
241
 
242
+ ### List Job Types (`agent-jobs-type`)
243
+
244
+ Lists agent job types registered for an organization. Used by `get_job_type` and `get_context` in this MCP.
245
+
246
+ #### Path em uso pelas tools deste MCP hoje
247
+
248
+ ```http
249
+ GET /organizations/:org_id/agent-jobs-type
250
+ GET /organizations/:org_id/agent-jobs-type/:job_type_id
251
+ ```
252
+
253
+ (Sem prefixo `/services/`.) **Toda tool nova neste repo deve usar este caminho** até a unificação descrita abaixo, para manter consistência com `get_job_type`.
254
+
255
+ #### Path canônico upstream
256
+
257
+ ```http
258
+ GET /services/organizations/:org_id/agent-jobs-type
259
+ GET /services/organizations/:org_id/agent-jobs-type/:job_type_id
260
+ ```
261
+
262
+ Este é o caminho documentado upstream pelo AI Connect API. O backend serve a mesma controller em ambos os mounts (legacy + atual). Uma migração futura para o caminho canônico deve ser feita em **change separado abrangendo todas as tools afetadas**: `get_job_type`, `list_jobs`, `create_job`, `get_jobs_stats` e `get_context`.
263
+
264
+ #### Query parameters suportados
265
+
266
+ - `enrich=emoji` — adiciona o campo `emoji` no payload (cache server-side de 1h por job_type).
267
+ - `include=schema` — inclui `params_schema` completo de cada job type (não usado por `get_context`; pesado).
268
+ - `limit` — número máximo de itens por página. `get_context` fixa em `100`.
269
+ - `offset` — paginação numérica.
270
+ - `sort` — formato `field:direction`, ex: `name:asc` (default usado por `get_context`).
271
+
272
+ #### Response shape
273
+
274
+ ```json
275
+ {
276
+ "data": [
277
+ { "id": "string", "name": "string", "description": "string?", "emoji": "string?" }
278
+ ],
279
+ "meta": { "total": 0, "limit": 100, "offset": 0 }
280
+ }
281
+ ```
282
+
242
283
  ## Job Status Values
243
284
 
244
285
  Jobs can have the following status values:
@@ -334,3 +375,118 @@ curl -X DELETE https://api.example.com/services/agent-jobs/job123 \
334
375
  "reason": "No longer needed"
335
376
  }'
336
377
  ```
378
+
379
+ ## Activities
380
+
381
+ Cada agent job acumula uma trilha de auditoria de **activities** (registros de operações como `ai_completion`, chamadas de ferramentas e logs internos). A trilha pode ser consultada por dois caminhos: o endpoint dedicado `/services/activities` (paginação real, recomendado para investigação) ou o overlay `?include=activities` em `/services/agent-jobs/:id` e `/services/agent-jobs` (conveniência, com caps server-side).
382
+
383
+ ### `ActivityRecord` schema
384
+
385
+ ```ts
386
+ {
387
+ id: string; // UUID v4
388
+ org_id: string;
389
+ activity_type_code: string; // código aberto (ex.: "ai_completion")
390
+ status: 'submitted' | 'completed' | 'canceled';
391
+ allocated_credits: number;
392
+ consumed_credits: number;
393
+ credits_rule_id: number; // 0..3
394
+ payloads?: { input?: any; output?: any };
395
+ processed_at?: string; // ISO 8601, opcional
396
+ created_at: string;
397
+ updated_at: string;
398
+ source: {
399
+ type: 'dispatch' | 'process_module' | 'direct';
400
+ reference_id?: string;
401
+ execution_id?: string;
402
+ job_id?: string;
403
+ chat_id?: string;
404
+ agent_job_type_id?: string;
405
+ channel_code?: string;
406
+ };
407
+ }
408
+ ```
409
+
410
+ O contador `activities_count` é mantido server-side no próprio documento do agent job (incrementado a cada activity criada com `source.job_id`). Está disponível em qualquer resposta de job, sem necessidade de `?include=activities`.
411
+
412
+ ### Endpoint dedicado: `GET /services/activities`
413
+
414
+ Recomendado para consulta paginada da trilha de um job específico. Sem caps de truncamento; suporta filtros server-side adicionais.
415
+
416
+ **Query params:**
417
+ - `job_id` (string, recomendado) — filtra activities do job indicado.
418
+ - `org_id` (string, opcional) — escopo de organização.
419
+ - `status` (`submitted | completed | canceled`, opcional)
420
+ - `activity_type_code` (string, opcional) — código aberto.
421
+ - `source_type` (`dispatch | process_module | direct`, opcional) — filtra pelo `source.type`.
422
+ - `limit` (int, default 50)
423
+ - `offset` (int, default 0)
424
+ - `sort` (string, default `-created_at`)
425
+
426
+ **Resposta:**
427
+ ```json
428
+ {
429
+ "data": [ "ActivityRecord", "..." ],
430
+ "meta": { "count": 50, "limit": 50, "total": 1234, "offset": 0 }
431
+ }
432
+ ```
433
+
434
+ **Exemplo:**
435
+ ```bash
436
+ curl -X GET "https://api.example.com/services/activities?job_id=job_abc&status=completed&limit=100" \
437
+ -H "Authorization: Bearer YOUR_TOKEN"
438
+ ```
439
+
440
+ ### Overlay: `?include=activities` em `/services/agent-jobs/:id`
441
+
442
+ Anexa as activities mais recentes ao detalhe do job. Útil para combinar contexto do job com trilha em uma única round-trip.
443
+
444
+ **Query params adicionais:**
445
+ - `include=activities` (obrigatório para ativar)
446
+ - `include_limit` (int 1–100, default 50) — máximo de activities anexadas.
447
+ - `include_sort` (`created_at | -created_at`, default `-created_at`)
448
+
449
+ **Resposta:**
450
+ ```json
451
+ {
452
+ "data": {
453
+ "job_id": "...",
454
+ "status": "completed",
455
+ "activities_count": 1234,
456
+ "Activities": [ "ActivityRecord", "..." ]
457
+ },
458
+ "meta": {
459
+ "activities_meta": { "count": 1234, "limit": 50 }
460
+ }
461
+ }
462
+ ```
463
+
464
+ `Activities` vem em **PascalCase** por convenção do serializador da API. Quando `meta.activities_meta.count > meta.activities_meta.limit`, a lista foi truncada — use o endpoint dedicado para acessar o restante.
465
+
466
+ ### Overlay: `?include=activities` em `/services/agent-jobs` (lista)
467
+
468
+ Anexa activities a cada job da listagem.
469
+
470
+ **Query params adicionais:**
471
+ - `include=activities` (obrigatório)
472
+ - `activities_limit_per_job` (int 1–100, default 15)
473
+ - `activities_total_limit` (int 1–3000, default 500) — cap global agregado.
474
+ - `activities_sort` (`created_at | -created_at`, default `-created_at`)
475
+
476
+ **Resposta:**
477
+ ```json
478
+ {
479
+ "data": [ { "Activities": [] } ],
480
+ "meta": {
481
+ "activities_total_returned": 45,
482
+ "activities_total_available": 120,
483
+ "activities_truncated": true
484
+ }
485
+ }
486
+ ```
487
+
488
+ ### Comportamento sensível
489
+
490
+ - **Fail-closed**: se a busca de activities falhar internamente quando `include=activities` está ativo, a request **inteira** falha (status 4xx/5xx). Garante integridade da trilha — nunca retorna o job sem as activities solicitadas. Em caso de erro, refaça a request sem `include=activities` ou use o endpoint dedicado.
491
+ - **Cache bypass**: requests com `?include=activities` em `/services/agent-jobs` ignoram o cache server-side, garantindo dados sempre frescos para uso operacional/auditoria.
492
+ - **Activities count sempre disponível**: `activities_count` no objeto do job é mantido independentemente de `include=activities`, então é consultável diretamente em qualquer resposta de job.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiconnect/agentjobs-mcp",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "MCP (Model Context Protocol) server for managing Agent Jobs in the AI Connect platform. Developed by AI Connect - Advanced AI automation and integration solutions.",
5
5
  "type": "module",
6
6
  "main": "build/index.js",
@@ -61,7 +61,8 @@
61
61
  "eslint": "^9.14.0",
62
62
  "typescript-eslint": "^8.12.2",
63
63
  "typescript": "^5.7.2",
64
- "vitest": "^3.2.4"
64
+ "vitest": "^3.2.4",
65
+ "zod-to-json-schema": "^3.23.0"
65
66
  },
66
67
  "engines": {
67
68
  "node": ">=18.0.0"