@aiconnect/agentjobs-mcp 1.1.0 → 1.2.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.
package/build/config.js CHANGED
@@ -3,9 +3,11 @@ export const config = {
3
3
  apiUrl: process.env.AICONNECT_API_URL || 'https://api.aiconnect.cloud/api/v0',
4
4
  apiKey: process.env.AICONNECT_API_KEY || '',
5
5
  defaultOrgId: process.env.DEFAULT_ORG_ID || 'aiconnect',
6
+ defaultTimezone: process.env.DEFAULT_TIMEZONE || 'UTC',
6
7
  debugMode: process.env.DEBUG === 'true',
7
8
  // Legacy compatibility
8
9
  AICONNECT_API_URL: process.env.AICONNECT_API_URL || 'https://api.aiconnect.cloud/api/v0',
9
10
  AICONNECT_API_KEY: process.env.AICONNECT_API_KEY || '',
10
- DEFAULT_ORG_ID: process.env.DEFAULT_ORG_ID || 'aiconnect'
11
+ DEFAULT_ORG_ID: process.env.DEFAULT_ORG_ID || 'aiconnect',
12
+ DEFAULT_TIMEZONE: process.env.DEFAULT_TIMEZONE || 'UTC'
11
13
  };
@@ -0,0 +1,22 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ describe('config.defaultTimezone', () => {
3
+ beforeEach(() => {
4
+ vi.resetModules();
5
+ });
6
+ afterEach(() => {
7
+ vi.unstubAllEnvs();
8
+ vi.resetModules();
9
+ });
10
+ it('reflects DEFAULT_TIMEZONE when set', async () => {
11
+ vi.stubEnv('DEFAULT_TIMEZONE', 'America/Sao_Paulo');
12
+ const { config } = await import('./config.js');
13
+ expect(config.defaultTimezone).toBe('America/Sao_Paulo');
14
+ expect(config.DEFAULT_TIMEZONE).toBe('America/Sao_Paulo');
15
+ });
16
+ it('falls back to UTC when DEFAULT_TIMEZONE is unset', async () => {
17
+ vi.stubEnv('DEFAULT_TIMEZONE', '');
18
+ const { config } = await import('./config.js');
19
+ expect(config.defaultTimezone).toBe('UTC');
20
+ expect(config.DEFAULT_TIMEZONE).toBe('UTC');
21
+ });
22
+ });
package/build/index.js CHANGED
@@ -7,8 +7,11 @@ import { InitializeRequestSchema } from "@modelcontextprotocol/sdk/types.js";
7
7
  import fs from "fs/promises";
8
8
  import path from "path";
9
9
  import { fileURLToPath } from "url";
10
- // Get package version
10
+ import { mcpServerVersion } from "./utils/version.js";
11
+ // Get package metadata (description/homepage/author come from package.json directly;
12
+ // version comes from the shared helper to avoid duplication).
11
13
  const packageJson = JSON.parse(await import('fs').then(fs => fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8')));
14
+ packageJson.version = mcpServerVersion;
12
15
  // CLI argument parsing
13
16
  const args = process.argv.slice(2);
14
17
  // Help text
@@ -51,6 +54,8 @@ if (args.includes('--config') || args.includes('-c')) {
51
54
  console.log('Current Configuration:');
52
55
  console.log(` API URL: ${process.env.AICONNECT_API_URL || 'Not set'}`);
53
56
  console.log(` API Key: ${process.env.AICONNECT_API_KEY ? '[SET]' : 'Not set'}`);
57
+ console.log(` Default Org: ${process.env.DEFAULT_ORG_ID || 'aiconnect'}`);
58
+ console.log(` Default Timezone: ${process.env.DEFAULT_TIMEZONE || 'UTC'}`);
54
59
  console.log(` Node Version: ${process.version}`);
55
60
  console.log(` MCP Server Version: ${packageJson.version}`);
56
61
  process.exit(0);
@@ -74,7 +79,9 @@ console.error(`[DEBUG] Server version: ${packageJson.version}`);
74
79
  console.error(`[DEBUG] Default capabilities: tools, resources, prompts`);
75
80
  // Intercept initialization to detect protocol version
76
81
  const originalSetRequestHandler = server.server.setRequestHandler.bind(server.server);
77
- // Override initialization handler to capture protocol version
82
+ // Override initialization handler to capture protocol version.
83
+ // The schema is cast to `any` to short-circuit a TS2589 ("excessively deep") error
84
+ // from generic Zod inference in @modelcontextprotocol/sdk's setRequestHandler signature.
78
85
  server.server.setRequestHandler(InitializeRequestSchema, async (request) => {
79
86
  const initParams = request.params;
80
87
  clientProtocolVersion = initParams.protocolVersion || "2024-11-05";
@@ -31,6 +31,17 @@ class AgentJobsClient {
31
31
  this.handleError(error);
32
32
  }
33
33
  }
34
+ async listJobTypes(orgId, options = {}) {
35
+ const params = {
36
+ enrich: 'emoji',
37
+ limit: options.limit ?? 100,
38
+ sort: options.sort ?? 'name:asc'
39
+ };
40
+ if (options.offset !== undefined) {
41
+ params.offset = options.offset;
42
+ }
43
+ return (await this.getWithMeta(`/organizations/${orgId}/agent-jobs-type`, params));
44
+ }
34
45
  async getStats(filters = {}) {
35
46
  const params = {
36
47
  ...filters,
@@ -3,6 +3,7 @@ import agentJobsClient from "../lib/agentJobsClient.js";
3
3
  import { formatJobSummary } from '../utils/formatters.js';
4
4
  import { mcpDebugger, withTiming } from '../utils/debugger.js';
5
5
  export default (server) => {
6
+ // @ts-expect-error TS2589: registerTool generic Zod inference exceeds TS depth limit with this schema.
6
7
  server.registerTool("cancel_job", {
7
8
  description: "Cancels an agent job by its ID.",
8
9
  annotations: {
@@ -10,9 +11,11 @@ export default (server) => {
10
11
  },
11
12
  inputSchema: {
12
13
  job_id: z.string({
13
- description: "The unique identifier of the job to be canceled. Example: 'job-12345'.",
14
+ description: "The unique identifier of the job to be canceled. Example: 'job-12345'."
14
15
  }),
15
- reason: z.string().optional().describe("An optional reason explaining why the job is being canceled."),
16
+ reason: z.string({
17
+ description: "An optional reason explaining why the job is being canceled."
18
+ }).optional()
16
19
  }
17
20
  }, async (params) => {
18
21
  mcpDebugger.toolCall("cancel_job", params);
@@ -32,7 +32,7 @@ export default (server) => {
32
32
  .record(z.any())
33
33
  .optional()
34
34
  .describe('Free‑form params passed to the agent'),
35
- scheduled_at: flexibleDateTimeSchema
35
+ scheduled_at: flexibleDateTimeSchema()
36
36
  .optional()
37
37
  .describe('Schedule the job to run later')
38
38
  }
@@ -0,0 +1,61 @@
1
+ import { z } from 'zod';
2
+ import agentJobsClient from '../lib/agentJobsClient.js';
3
+ import { config } from '../config.js';
4
+ import { formatContext } from '../utils/formatters.js';
5
+ import { mcpServerVersion } from '../utils/version.js';
6
+ import { mcpDebugger, withTiming } from '../utils/debugger.js';
7
+ export default (server) => {
8
+ server.registerTool('get_context', {
9
+ description: 'Returns the MCP server runtime context: local defaults (org_id, timezone, API URL, server version) plus the list of agent job types available in the effective organization. Designed to be the first call an LLM client makes — surfaces what would otherwise be invisible env vars and avoids guessing valid job type IDs before calling create_job.',
10
+ annotations: {
11
+ title: 'Get MCP Server Runtime Context'
12
+ },
13
+ inputSchema: {
14
+ org_id: z
15
+ .string()
16
+ .optional()
17
+ .describe('Override DEFAULT_ORG_ID for this call (introspect a different org without restarting the server)')
18
+ }
19
+ }, async (params) => {
20
+ mcpDebugger.toolCall('get_context', params);
21
+ const effectiveOrgId = params.org_id || config.DEFAULT_ORG_ID;
22
+ const localConfig = {
23
+ org_id: effectiveOrgId,
24
+ timezone: config.DEFAULT_TIMEZONE,
25
+ api_url: config.AICONNECT_API_URL,
26
+ server_version: mcpServerVersion
27
+ };
28
+ let jobTypes;
29
+ let total;
30
+ let jobTypesError;
31
+ try {
32
+ const response = await withTiming(() => agentJobsClient.listJobTypes(effectiveOrgId), 'get_context.listJobTypes API call');
33
+ jobTypes = (response?.data ?? []).map((jt) => ({
34
+ id: jt.id,
35
+ name: jt.name,
36
+ description: jt.description,
37
+ emoji: jt.emoji
38
+ }));
39
+ total = response?.meta?.total ?? jobTypes.length;
40
+ }
41
+ catch (error) {
42
+ jobTypesError = error?.message ?? String(error);
43
+ mcpDebugger.toolError('get_context.listJobTypes', error);
44
+ }
45
+ const text = formatContext({ localConfig, jobTypes, total, jobTypesError });
46
+ mcpDebugger.toolResponse('get_context', {
47
+ org_id: effectiveOrgId,
48
+ jobTypesCount: jobTypes?.length,
49
+ hasError: !!jobTypesError,
50
+ resultLength: text.length
51
+ });
52
+ return {
53
+ content: [
54
+ {
55
+ type: 'text',
56
+ text
57
+ }
58
+ ]
59
+ };
60
+ });
61
+ };
@@ -3,42 +3,32 @@ import agentJobsClient from "../lib/agentJobsClient.js";
3
3
  import { formatJobStats } from "../utils/formatters.js";
4
4
  import { flexibleDateTimeSchema } from "../utils/schemas.js";
5
5
  import { mcpDebugger, withTiming } from "../utils/debugger.js";
6
- const jobStatusSchema = z.enum([
7
- "waiting",
8
- "scheduled",
9
- "running",
10
- "completed",
11
- "failed",
12
- "canceled"
13
- ]);
14
6
  export default (server) => {
15
7
  server.registerTool("get_jobs_stats", {
16
- description: "Get aggregated statistics for agent jobs without retrieving individual job data. Optimized for dashboards and monitoring with minimal network overhead.",
8
+ description: "Returns aggregated job counts broken down by status (waiting/scheduled/running/completed/failed/canceled) plus a summary (total, success rate, active, completion rate). To filter to a single status, use `list_jobs` with `status=` instead — this tool intentionally does not expose a `status` filter because the upstream stats endpoint ignores it (the breakdown is itself by status). Filters available here narrow the universe along orthogonal dimensions: `job_type_id`, `channel_code`, `tags`, `scheduled_at_*`, `created_at_*`. Result-code and duration aggregates are not yet available; for those today, fall back to `list_jobs` and aggregate client-side. Optimized for dashboards and monitoring with minimal network overhead.",
17
9
  annotations: {
18
10
  title: "Get Job Statistics"
19
11
  },
20
12
  inputSchema: {
21
13
  org_id: z.string().optional().describe("Filter by organization ID."),
22
- scheduled_at_gte: flexibleDateTimeSchema.optional().describe("Start of period (ISO 8601)"),
23
- scheduled_at_lte: flexibleDateTimeSchema.optional().describe("End of period (ISO 8601)"),
24
- created_at_gte: flexibleDateTimeSchema.optional().describe("Filter for jobs created at or after a specific time (ISO 8601)."),
25
- created_at_lte: flexibleDateTimeSchema.optional().describe("Filter for jobs created at or before a specific time (ISO 8601)."),
14
+ scheduled_at_gte: flexibleDateTimeSchema().optional().describe("Start of period (ISO 8601)"),
15
+ scheduled_at_lte: flexibleDateTimeSchema().optional().describe("End of period (ISO 8601)"),
16
+ created_at_gte: flexibleDateTimeSchema().optional().describe("Filter for jobs created at or after a specific time (ISO 8601)."),
17
+ created_at_lte: flexibleDateTimeSchema().optional().describe("Filter for jobs created at or before a specific time (ISO 8601)."),
26
18
  job_type_id: z.string().optional().describe("Job type filter"),
27
19
  channel_code: z.string().optional().describe("Channel filter"),
28
20
  tags: z.string().optional().describe("Tags filter (comma-separated)"),
29
- status: jobStatusSchema.optional().describe("Status filter"),
30
21
  }
31
22
  }, async (params) => {
32
23
  mcpDebugger.toolCall("get_jobs_stats", params);
33
24
  try {
34
25
  const response = await withTiming(() => agentJobsClient.getStats(params), "get_jobs_stats API call");
35
26
  const stats = response.meta?.stats || {};
36
- const filters = response.meta?.filters || {};
37
- mcpDebugger.debug("Raw API response", { stats, filters });
27
+ mcpDebugger.debug("Raw API response", { stats, appliedFilters: params });
38
28
  const result = {
39
29
  content: [{
40
30
  type: "text",
41
- text: formatJobStats(stats, filters),
31
+ text: formatJobStats(stats, params),
42
32
  }]
43
33
  };
44
34
  mcpDebugger.toolResponse("get_jobs_stats", result);
@@ -21,11 +21,11 @@ export default (server) => {
21
21
  inputSchema: {
22
22
  org_id: z.string().optional().describe("Filter by organization ID. If not provided, the default from the environment is used."),
23
23
  status: jobStatusSchema.optional().describe("Filter by job status. Possible values are: 'waiting', 'scheduled', 'running', 'completed', 'failed', 'canceled'."),
24
- scheduled_at: flexibleDateTimeSchema.optional().describe("Filter by the exact scheduled time in ISO 8601 format (e.g., '2024-07-23T10:00:00Z')."),
25
- scheduled_at_gte: flexibleDateTimeSchema.optional().describe("Filter for jobs scheduled at or after a specific time (ISO 8601)."),
26
- scheduled_at_lte: flexibleDateTimeSchema.optional().describe("Filter for jobs scheduled at or before a specific time (ISO 8601)."),
27
- created_at_gte: flexibleDateTimeSchema.optional().describe("Filter for jobs created at or after a specific time (ISO 8601)."),
28
- created_at_lte: flexibleDateTimeSchema.optional().describe("Filter for jobs created at or before a specific time (ISO 8601)."),
24
+ scheduled_at: flexibleDateTimeSchema().optional().describe("Filter by the exact scheduled time in ISO 8601 format (e.g., '2024-07-23T10:00:00Z')."),
25
+ scheduled_at_gte: flexibleDateTimeSchema().optional().describe("Filter for jobs scheduled at or after a specific time (ISO 8601)."),
26
+ scheduled_at_lte: flexibleDateTimeSchema().optional().describe("Filter for jobs scheduled at or before a specific time (ISO 8601)."),
27
+ created_at_gte: flexibleDateTimeSchema().optional().describe("Filter for jobs created at or after a specific time (ISO 8601)."),
28
+ created_at_lte: flexibleDateTimeSchema().optional().describe("Filter for jobs created at or before a specific time (ISO 8601)."),
29
29
  job_type_id: z.string().optional().describe("Filter by the specific job type ID (e.g., 'daily-report')."),
30
30
  channel_code: z.string().optional().describe("Filter by the channel code (e.g., 'C123456' for a Slack channel)."),
31
31
  limit: z.number().int().positive().optional().describe("Maximum number of jobs to return (e.g.,20). Default is 20."),
@@ -54,10 +54,11 @@ export default (server) => {
54
54
  meta,
55
55
  firstJob: jobs[0] || null
56
56
  });
57
+ const offset = typeof params.offset === 'number' ? params.offset : 0;
57
58
  const result = {
58
59
  content: [{
59
60
  type: "text",
60
- text: formatJobList(jobs, meta),
61
+ text: formatJobList(jobs, meta, offset),
61
62
  }]
62
63
  };
63
64
  mcpDebugger.toolResponse("list_jobs", {
@@ -216,17 +216,33 @@ export function formatJobSummary(job) {
216
216
  }
217
217
  /**
218
218
  * Formata a resposta para a lista de jobs.
219
+ *
220
+ * `meta` é obrigatório e os três campos (`count`, `limit`, `total`) precisam
221
+ * ser numéricos — confirmado empiricamente que o backend sempre os retorna em
222
+ * `/services/agent-jobs`. Falha explícita aqui é preferível a inventar um
223
+ * footer com valores inferidos, pois isso recriaria a ambiguidade
224
+ * "page count vs total" que esta tool deveria eliminar.
225
+ *
219
226
  * @param jobs - Um array de jobs.
220
- * @param pagination - O objeto de paginação.
227
+ * @param meta - O objeto `meta` retornado pelo backend.
228
+ * @param offset - O `offset` que a tool enviou na requisição (default 0).
221
229
  * @returns Uma string formatada com a lista de resumos de jobs.
222
230
  */
223
- export function formatJobList(jobs, pagination) {
224
- if (!jobs || jobs.length === 0) {
225
- return "No jobs found for the given criteria.";
231
+ export function formatJobList(jobs, meta, offset = 0) {
232
+ if (!meta || typeof meta.count !== 'number' || typeof meta.limit !== 'number' || typeof meta.total !== 'number') {
233
+ throw new Error('formatJobList: meta is required with numeric `count`, `limit`, and `total`. ' +
234
+ `Received: ${JSON.stringify(meta)}`);
226
235
  }
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}`;
236
+ const safeJobs = jobs || [];
237
+ const { count, limit, total } = meta;
238
+ const hasMore = (offset + count) < total;
239
+ const nextOffset = hasMore ? offset + limit : null;
240
+ const footer = `Returned: ${count} | Total matching: ${total} | Has more: ${hasMore} | Next offset: ${nextOffset === null ? 'null' : nextOffset}`;
241
+ if (safeJobs.length === 0) {
242
+ return `Found 0 jobs.\n\n${footer}`;
243
+ }
244
+ const jobSummaries = safeJobs.map(job => formatJobSummary(job)).join('\n\n');
245
+ return `Found ${safeJobs.length} jobs.\n\n${jobSummaries}\n\n${footer}`;
230
246
  }
231
247
  // Schema for job type details
232
248
  const jobTypeSchema = z.object({
@@ -454,21 +470,81 @@ export function formatJobTypeSummary(jobType) {
454
470
  return JSON.stringify(jobType, null, 2);
455
471
  }
456
472
  }
457
- export function formatJobStats(stats, filters) {
473
+ const DATE_FILTER_KEYS = [
474
+ 'scheduled_at_gte',
475
+ 'scheduled_at_lte',
476
+ 'created_at_gte',
477
+ 'created_at_lte',
478
+ ];
479
+ const renderDateRange = (label, gte, lte) => {
480
+ if (gte === undefined && lte === undefined)
481
+ return null;
482
+ const left = gte !== undefined ? String(gte) : '(open)';
483
+ const right = lte !== undefined ? String(lte) : '(open)';
484
+ return `${label}: ${left} → ${right}`;
485
+ };
486
+ const CONTEXT_LABEL_WIDTH = 16; // "Server version: " == 16 chars
487
+ const JOB_TYPE_ID_WIDTH = 22;
488
+ const JOB_TYPE_EMOJI_WIDTH = 4;
489
+ function padLabel(label) {
490
+ return (label + ':').padEnd(CONTEXT_LABEL_WIDTH, ' ');
491
+ }
492
+ function formatJobTypeLine(jt) {
493
+ const id = jt.id.padEnd(JOB_TYPE_ID_WIDTH, ' ');
494
+ const emoji = (jt.emoji ?? '').padEnd(JOB_TYPE_EMOJI_WIDTH, ' ');
495
+ const description = jt.description ? ` — ${jt.description}` : '';
496
+ return ` - ${id} ${emoji} ${jt.name}${description}`;
497
+ }
498
+ export function formatContext(input) {
499
+ const { localConfig, jobTypes, total, jobTypesError } = input;
500
+ const contextSection = [
501
+ 'Context:',
502
+ ` ${padLabel('Org ID')} ${localConfig.org_id}`,
503
+ ` ${padLabel('Timezone')} ${localConfig.timezone}`,
504
+ ` ${padLabel('API URL')} ${localConfig.api_url}`,
505
+ ` ${padLabel('Server version')} ${localConfig.server_version}`
506
+ ].join('\n');
507
+ let jobsSection;
508
+ if (jobTypesError !== undefined) {
509
+ jobsSection = `Job types: unavailable (error: ${jobTypesError})`;
510
+ }
511
+ else if (jobTypes === undefined) {
512
+ jobsSection = 'Job types: unavailable (error: unknown)';
513
+ }
514
+ else {
515
+ const totalCount = typeof total === 'number' ? total : jobTypes.length;
516
+ if (totalCount === 0) {
517
+ jobsSection = 'Job types available (0):\n (no job types registered for this org)';
518
+ }
519
+ else {
520
+ const lines = jobTypes.map(formatJobTypeLine);
521
+ let trailing = '';
522
+ if (totalCount > jobTypes.length) {
523
+ const missing = totalCount - jobTypes.length;
524
+ trailing = `\n … and ${missing} more job types not shown`;
525
+ }
526
+ jobsSection = `Job types available (${totalCount}):\n${lines.join('\n')}${trailing}`;
527
+ }
528
+ }
529
+ return `${contextSection}\n\n${jobsSection}`;
530
+ }
531
+ export function formatJobStats(stats, appliedFilters = {}) {
458
532
  const { waiting = 0, running = 0, completed = 0, failed = 0, canceled = 0, scheduled = 0, } = stats.status;
459
533
  const totalJobs = waiting + running + completed + failed + canceled + scheduled;
460
534
  const successRate = totalJobs > 0 ? ((completed / (totalJobs - waiting - scheduled - running)) * 100).toFixed(1) : "0.0";
461
535
  const completionRate = (completed + failed) > 0 ? (completed / (completed + failed) * 100).toFixed(1) : "0.0";
462
536
  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}` : "";
537
+ const filters = appliedFilters || {};
538
+ const scheduledLine = renderDateRange('Scheduled', filters.scheduled_at_gte, filters.scheduled_at_lte);
539
+ const createdLine = renderDateRange('Created', filters.created_at_gte, filters.created_at_lte);
540
+ const hasAnyDateFilter = DATE_FILTER_KEYS.some((k) => filters[k] !== undefined);
541
+ const periodFallback = hasAnyDateFilter ? null : 'Period: All time';
542
+ const dateLines = [scheduledLine, createdLine, periodFallback].filter((l) => l !== null);
543
+ const dateFilterSet = new Set(DATE_FILTER_KEYS);
544
+ const otherFilterEntries = Object.entries(filters).filter(([k, v]) => !dateFilterSet.has(k) && v !== undefined && v !== null && v !== '');
545
+ const filtersSection = otherFilterEntries.length
546
+ ? `Filters:\n${otherFilterEntries.map(([k, v]) => `- ${k}: ${v}`).join('\n')}\n\n`
547
+ : '';
472
548
  const percentage = (value) => {
473
549
  if (totalJobs === 0)
474
550
  return "0.0";
@@ -478,10 +554,9 @@ export function formatJobStats(stats, filters) {
478
554
  Job Statistics Report
479
555
  ====================
480
556
 
481
- Period: ${period}
482
- ${org}
557
+ ${dateLines.join('\n')}
483
558
 
484
- Status Breakdown:
559
+ ${filtersSection}Status Breakdown:
485
560
  ✓ Completed: ${completed} jobs (${percentage(completed)}%)
486
561
  ⏳ Running: ${running} jobs (${percentage(running)}%)
487
562
  ⏰ Scheduled: ${scheduled} jobs (${percentage(scheduled)}%)
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { formatJobDetails, formatJobStats, formatJobTypeDetails, formatJobTypeSummary } from './formatters.js';
2
+ import { formatJobDetails, formatJobList, formatJobStats, formatJobTypeDetails, formatJobTypeSummary } from './formatters.js';
3
3
  describe('formatJobDetails', () => {
4
4
  const fullJob = {
5
5
  job_id: 'job_123',
@@ -133,6 +133,168 @@ describe('formatJobStats', () => {
133
133
  const result = formatJobStats(stats, filters);
134
134
  expect(result).toContain('Period: All time');
135
135
  });
136
+ describe('header — Scheduled date range', () => {
137
+ const stats = {
138
+ status: { completed: 1, running: 0, failed: 0, canceled: 0, waiting: 0, scheduled: 0 },
139
+ };
140
+ it('renders both bounds when scheduled_at_gte and scheduled_at_lte are provided', () => {
141
+ const result = formatJobStats(stats, {
142
+ scheduled_at_gte: '2026-04-30T00:00:00Z',
143
+ scheduled_at_lte: '2026-05-01T00:00:00Z',
144
+ });
145
+ expect(result).toContain('Scheduled: 2026-04-30T00:00:00Z → 2026-05-01T00:00:00Z');
146
+ expect(result).not.toContain('Period: All time');
147
+ });
148
+ it('renders "(open)" upper bound when only scheduled_at_gte is provided', () => {
149
+ const result = formatJobStats(stats, {
150
+ scheduled_at_gte: '2026-04-30T00:00:00Z',
151
+ });
152
+ expect(result).toContain('Scheduled: 2026-04-30T00:00:00Z → (open)');
153
+ });
154
+ it('renders "(open)" lower bound when only scheduled_at_lte is provided', () => {
155
+ const result = formatJobStats(stats, {
156
+ scheduled_at_lte: '2026-05-01T00:00:00Z',
157
+ });
158
+ expect(result).toContain('Scheduled: (open) → 2026-05-01T00:00:00Z');
159
+ });
160
+ it('omits the Scheduled range line entirely when no scheduled_at bound is provided', () => {
161
+ const result = formatJobStats(stats, { status: 'failed' });
162
+ // The Status Breakdown contains "⏰ Scheduled:"; we only want to assert
163
+ // the header-range form ("Scheduled: <bound> → ...") is absent.
164
+ expect(result).not.toMatch(/(^|\n)Scheduled: .+→/);
165
+ });
166
+ });
167
+ describe('header — Created date range', () => {
168
+ const stats = {
169
+ status: { completed: 1, running: 0, failed: 0, canceled: 0, waiting: 0, scheduled: 0 },
170
+ };
171
+ it('renders both bounds when created_at_gte and created_at_lte are provided', () => {
172
+ const result = formatJobStats(stats, {
173
+ created_at_gte: '2026-04-01T00:00:00Z',
174
+ created_at_lte: '2026-04-30T00:00:00Z',
175
+ });
176
+ expect(result).toContain('Created: 2026-04-01T00:00:00Z → 2026-04-30T00:00:00Z');
177
+ });
178
+ it('renders "(open)" upper bound when only created_at_gte is provided', () => {
179
+ const result = formatJobStats(stats, {
180
+ created_at_gte: '2026-04-01T00:00:00Z',
181
+ });
182
+ expect(result).toContain('Created: 2026-04-01T00:00:00Z → (open)');
183
+ });
184
+ it('renders "(open)" lower bound when only created_at_lte is provided', () => {
185
+ const result = formatJobStats(stats, {
186
+ created_at_lte: '2026-04-30T00:00:00Z',
187
+ });
188
+ expect(result).toContain('Created: (open) → 2026-04-30T00:00:00Z');
189
+ });
190
+ it('omits the Created line entirely when no created_at bound is provided', () => {
191
+ const result = formatJobStats(stats, { scheduled_at_gte: '2026-04-30T00:00:00Z' });
192
+ expect(result).not.toContain('Created:');
193
+ });
194
+ it('renders both Scheduled and Created lines when both ranges are filtered', () => {
195
+ const result = formatJobStats(stats, {
196
+ scheduled_at_gte: '2026-04-30T00:00:00Z',
197
+ created_at_gte: '2026-04-01T00:00:00Z',
198
+ });
199
+ expect(result).toContain('Scheduled: 2026-04-30T00:00:00Z → (open)');
200
+ expect(result).toContain('Created: 2026-04-01T00:00:00Z → (open)');
201
+ });
202
+ });
203
+ describe('header — Period: All time fallback', () => {
204
+ const stats = {
205
+ status: { completed: 1, running: 0, failed: 0, canceled: 0, waiting: 0, scheduled: 0 },
206
+ };
207
+ it('renders "Period: All time" when no date filter is provided', () => {
208
+ const result = formatJobStats(stats, {});
209
+ expect(result).toContain('Period: All time');
210
+ });
211
+ it('does not render "Period: All time" when scheduled_at_gte is set', () => {
212
+ const result = formatJobStats(stats, { scheduled_at_gte: '2026-04-30T00:00:00Z' });
213
+ expect(result).not.toContain('Period: All time');
214
+ });
215
+ it('does not render "Period: All time" when created_at_lte is set', () => {
216
+ const result = formatJobStats(stats, { created_at_lte: '2026-04-30T00:00:00Z' });
217
+ expect(result).not.toContain('Period: All time');
218
+ });
219
+ });
220
+ describe('header — Filters section', () => {
221
+ const stats = {
222
+ status: { completed: 1, running: 0, failed: 0, canceled: 0, waiting: 0, scheduled: 0 },
223
+ };
224
+ it('renders a single line when one non-date filter is supplied', () => {
225
+ const result = formatJobStats(stats, { status: 'failed' });
226
+ expect(result).toContain('Filters:');
227
+ expect(result).toContain('- status: failed');
228
+ });
229
+ it('renders multiple lines when multiple non-date filters are supplied', () => {
230
+ const result = formatJobStats(stats, {
231
+ status: 'failed',
232
+ job_type_id: 'woba-supplier-ai-batch',
233
+ });
234
+ expect(result).toContain('- status: failed');
235
+ expect(result).toContain('- job_type_id: woba-supplier-ai-batch');
236
+ });
237
+ it('omits the Filters section entirely when only date filters are supplied', () => {
238
+ const result = formatJobStats(stats, { scheduled_at_gte: '2026-04-30T00:00:00Z' });
239
+ expect(result).not.toContain('Filters:');
240
+ });
241
+ });
242
+ });
243
+ describe('formatJobList', () => {
244
+ const sampleJob = {
245
+ job_id: 'job_1',
246
+ channel_code: 'ch_1',
247
+ created_at: '2026-04-30T00:00:00.000Z',
248
+ updated_at: '2026-04-30T00:10:00.000Z',
249
+ scheduled_at: '2026-04-30T00:00:00.000Z',
250
+ job_status: 'completed',
251
+ result: 'ok',
252
+ job_type_id: 'type_1',
253
+ };
254
+ it('renders the four-field footer on first page with more pages available', () => {
255
+ const jobs = Array.from({ length: 20 }, (_, i) => ({ ...sampleJob, job_id: `job_${i}` }));
256
+ const meta = { count: 20, limit: 20, total: 40 };
257
+ const result = formatJobList(jobs, meta, 0);
258
+ expect(result).toContain('Returned: 20 | Total matching: 40 | Has more: true | Next offset: 20');
259
+ expect(result).not.toContain('Page:');
260
+ expect(result).not.toContain('Total Jobs:');
261
+ });
262
+ it('renders Has more: false and Next offset: null on the last page', () => {
263
+ const jobs = Array.from({ length: 20 }, (_, i) => ({ ...sampleJob, job_id: `job_${i}` }));
264
+ const meta = { count: 20, limit: 20, total: 40 };
265
+ const result = formatJobList(jobs, meta, 20);
266
+ expect(result).toContain('Returned: 20 | Total matching: 40 | Has more: false | Next offset: null');
267
+ });
268
+ it('handles a partial last page (count < limit)', () => {
269
+ const jobs = Array.from({ length: 5 }, (_, i) => ({ ...sampleJob, job_id: `job_${i}` }));
270
+ const meta = { count: 5, limit: 20, total: 25 };
271
+ const result = formatJobList(jobs, meta, 20);
272
+ expect(result).toContain('Returned: 5 | Total matching: 25 | Has more: false | Next offset: null');
273
+ });
274
+ it('renders the footer on an empty result on the first page', () => {
275
+ const meta = { count: 0, limit: 20, total: 0 };
276
+ const result = formatJobList([], meta, 0);
277
+ expect(result).toContain('Found 0 jobs.');
278
+ expect(result).toContain('Returned: 0 | Total matching: 0 | Has more: false | Next offset: null');
279
+ expect(result).not.toContain('No jobs found for the given criteria.');
280
+ });
281
+ it('renders Total matching reflecting real total when offset overflows the result set', () => {
282
+ const meta = { count: 0, limit: 20, total: 40 };
283
+ const result = formatJobList([], meta, 100);
284
+ expect(result).toContain('Found 0 jobs.');
285
+ expect(result).toContain('Returned: 0 | Total matching: 40 | Has more: false | Next offset: null');
286
+ });
287
+ describe('fail-fast on missing meta fields', () => {
288
+ it('throws when meta is null', () => {
289
+ expect(() => formatJobList([], null, 0)).toThrow(/meta is required/);
290
+ });
291
+ it('throws when meta.total is missing', () => {
292
+ expect(() => formatJobList([], { count: 0, limit: 20 }, 0)).toThrow(/meta is required.*count.*limit.*total/);
293
+ });
294
+ it('throws when meta.count is non-numeric', () => {
295
+ expect(() => formatJobList([], { count: 'oops', limit: 20, total: 0 }, 0)).toThrow(/meta is required/);
296
+ });
297
+ });
136
298
  });
137
299
  describe('formatJobTypeDetails', () => {
138
300
  const fullJobType = {
@@ -385,3 +547,83 @@ describe('formatJobTypeSummary', () => {
385
547
  expect(result).toContain('"invalid": true');
386
548
  });
387
549
  });
550
+ import { formatContext } from './formatters.js';
551
+ describe('formatContext', () => {
552
+ const localConfig = {
553
+ org_id: 'woba',
554
+ timezone: 'America/Sao_Paulo',
555
+ api_url: 'https://api.aiconnect.cloud/api/v0',
556
+ server_version: '0.4.2'
557
+ };
558
+ it('formats happy path with two job types', () => {
559
+ const result = formatContext({
560
+ localConfig,
561
+ total: 2,
562
+ jobTypes: [
563
+ { id: 'billing-followup', name: 'Billing Follow-up', description: 'Triage de cobrança', emoji: '💳' },
564
+ { id: 'support-triage', name: 'Support Triage', description: 'Roteamento de suporte', emoji: '🛠️' }
565
+ ]
566
+ });
567
+ expect(result).toContain('Context:');
568
+ expect(result).toContain('Org ID: woba');
569
+ expect(result).toContain('Timezone: America/Sao_Paulo');
570
+ expect(result).toContain('API URL: https://api.aiconnect.cloud/api/v0');
571
+ expect(result).toContain('Server version: 0.4.2');
572
+ expect(result).toContain('Job types available (2):');
573
+ expect(result).toContain('billing-followup');
574
+ expect(result).toContain('💳');
575
+ expect(result).toContain('— Triage de cobrança');
576
+ });
577
+ it('handles zero job types', () => {
578
+ const result = formatContext({ localConfig, total: 0, jobTypes: [] });
579
+ expect(result).toContain('Job types available (0):');
580
+ expect(result).toContain('(no job types registered for this org)');
581
+ });
582
+ it('renders error line when jobTypesError present', () => {
583
+ const result = formatContext({
584
+ localConfig,
585
+ jobTypesError: 'API Error (500): Internal Server Error'
586
+ });
587
+ expect(result).toContain('Context:');
588
+ expect(result).toContain('Org ID: woba');
589
+ expect(result).toContain('Job types: unavailable (error: API Error (500): Internal Server Error)');
590
+ expect(result).not.toContain('Job types available');
591
+ });
592
+ it('preserves alignment when emoji missing', () => {
593
+ const result = formatContext({
594
+ localConfig,
595
+ total: 2,
596
+ jobTypes: [
597
+ { id: 'with-emoji', name: 'With Emoji', emoji: '✅' },
598
+ { id: 'no-emoji', name: 'No Emoji' }
599
+ ]
600
+ });
601
+ const lines = result.split('\n');
602
+ const withEmojiLine = lines.find((l) => l.includes('with-emoji'));
603
+ const noEmojiLine = lines.find((l) => l.includes('no-emoji'));
604
+ const idxWith = withEmojiLine.indexOf('With Emoji');
605
+ const idxNo = noEmojiLine.indexOf('No Emoji');
606
+ expect(idxWith).toBe(idxNo);
607
+ expect(result).not.toContain('undefined');
608
+ expect(result).not.toContain('null');
609
+ });
610
+ it('shows truncation hint when total > returned items', () => {
611
+ const jobTypes = Array.from({ length: 100 }, (_, i) => ({
612
+ id: `jt-${i}`,
613
+ name: `JT ${i}`
614
+ }));
615
+ const result = formatContext({ localConfig, total: 247, jobTypes });
616
+ expect(result).toContain('Job types available (247):');
617
+ expect(result).toContain('… and 147 more job types not shown');
618
+ });
619
+ it('produces byte-equal output across calls with identical input', () => {
620
+ const input = {
621
+ localConfig,
622
+ total: 1,
623
+ jobTypes: [{ id: 'a', name: 'A', emoji: '🅰️' }]
624
+ };
625
+ const a = formatContext(input);
626
+ const b = formatContext(input);
627
+ expect(a).toBe(b);
628
+ });
629
+ });
@@ -1,20 +1,24 @@
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
  });
@@ -1,40 +1,95 @@
1
1
  import { describe, it, expect } from 'vitest';
2
+ import { z } from 'zod';
3
+ import { zodToJsonSchema } from 'zod-to-json-schema';
2
4
  import { flexibleDateTimeSchema } from './schemas.js';
3
5
  describe('flexibleDateTimeSchema', () => {
4
- // Test case 1: Valid ISO 8601 with UTC 'Z'
5
6
  it('should validate a correct ISO 8601 string with Zulu time', () => {
6
- const validDate = '2025-07-23T21:00:00Z';
7
- const result = flexibleDateTimeSchema.safeParse(validDate);
7
+ const result = flexibleDateTimeSchema().safeParse('2025-07-23T21:00:00Z');
8
8
  expect(result.success).toBe(true);
9
9
  });
10
- // Test case 2: Valid ISO 8601 with a positive timezone offset
11
10
  it('should validate a correct ISO 8601 string with a positive offset', () => {
12
- const validDate = '2025-07-23T22:00:00+01:00';
13
- const result = flexibleDateTimeSchema.safeParse(validDate);
11
+ const result = flexibleDateTimeSchema().safeParse('2025-07-23T22:00:00+01:00');
14
12
  expect(result.success).toBe(true);
15
13
  });
16
- // Test case 3: Valid ISO 8601 with a negative timezone offset
17
14
  it('should validate a correct ISO 8601 string with a negative offset', () => {
18
- const validDate = '2025-07-23T16:00:00-05:00';
19
- const result = flexibleDateTimeSchema.safeParse(validDate);
15
+ const result = flexibleDateTimeSchema().safeParse('2025-07-23T16:00:00-05:00');
20
16
  expect(result.success).toBe(true);
21
17
  });
22
- // Test case 4: Invalid date format (not ISO 8601)
23
18
  it('should not validate an incorrect date format', () => {
24
- const invalidDate = '23/07/2025 21:00:00';
25
- const result = flexibleDateTimeSchema.safeParse(invalidDate);
19
+ const result = flexibleDateTimeSchema().safeParse('23/07/2025 21:00:00');
26
20
  expect(result.success).toBe(false);
27
21
  });
28
- // Test case 5: Invalid date string (gibberish)
29
22
  it('should not validate a gibberish string', () => {
30
- const gibberish = 'not-a-date';
31
- const result = flexibleDateTimeSchema.safeParse(gibberish);
23
+ const result = flexibleDateTimeSchema().safeParse('not-a-date');
32
24
  expect(result.success).toBe(false);
33
25
  });
34
- // Test case 6: Empty string
35
26
  it('should not validate an empty string', () => {
36
- const emptyString = '';
37
- const result = flexibleDateTimeSchema.safeParse(emptyString);
27
+ const result = flexibleDateTimeSchema().safeParse('');
38
28
  expect(result.success).toBe(false);
39
29
  });
30
+ it('should validate a date-only string (no time component)', () => {
31
+ const result = flexibleDateTimeSchema().safeParse('2024-07-23');
32
+ expect(result.success).toBe(true);
33
+ });
34
+ it('should reject invalid string with the exact ISO 8601 error message', () => {
35
+ const result = flexibleDateTimeSchema().safeParse('not-a-date');
36
+ expect(result.success).toBe(false);
37
+ if (!result.success) {
38
+ expect(result.error.issues[0].message).toBe('Invalid date-time string. Please use a valid ISO 8601 format.');
39
+ }
40
+ });
41
+ it('should reject null when used with .optional() (string | undefined, not nullable)', () => {
42
+ const schema = flexibleDateTimeSchema().optional();
43
+ expect(schema.safeParse(undefined).success).toBe(true);
44
+ const nullResult = schema.safeParse(null);
45
+ expect(nullResult.success).toBe(false);
46
+ if (!nullResult.success) {
47
+ const issue = nullResult.error.issues[0];
48
+ expect(issue.code).toBe('invalid_type');
49
+ // Zod 3: { expected: 'string', received: 'null' }
50
+ expect(issue.expected).toBe('string');
51
+ expect(issue.received).toBe('null');
52
+ }
53
+ });
54
+ });
55
+ describe('JSON Schema serialization', () => {
56
+ it('should inline each list_jobs date filter field without $ref', () => {
57
+ const shape = z.object({
58
+ scheduled_at: flexibleDateTimeSchema().optional(),
59
+ scheduled_at_gte: flexibleDateTimeSchema().optional(),
60
+ scheduled_at_lte: flexibleDateTimeSchema().optional(),
61
+ created_at_gte: flexibleDateTimeSchema().optional(),
62
+ created_at_lte: flexibleDateTimeSchema().optional(),
63
+ });
64
+ const json = zodToJsonSchema(shape);
65
+ const expectedFields = [
66
+ 'scheduled_at',
67
+ 'scheduled_at_gte',
68
+ 'scheduled_at_lte',
69
+ 'created_at_gte',
70
+ 'created_at_lte',
71
+ ];
72
+ for (const field of expectedFields) {
73
+ const prop = json.properties[field];
74
+ expect(prop.$ref, `expected ${field} to be inline, got $ref ${String(prop.$ref)}`).toBeUndefined();
75
+ expect(prop.type, `expected ${field} to have type 'string', got ${String(prop.type)}`).toBe('string');
76
+ }
77
+ });
78
+ it('should produce $ref-free JSON for both list_jobs and get_jobs_stats shapes', () => {
79
+ const listJobsShape = z.object({
80
+ scheduled_at: flexibleDateTimeSchema().optional(),
81
+ scheduled_at_gte: flexibleDateTimeSchema().optional(),
82
+ scheduled_at_lte: flexibleDateTimeSchema().optional(),
83
+ created_at_gte: flexibleDateTimeSchema().optional(),
84
+ created_at_lte: flexibleDateTimeSchema().optional(),
85
+ });
86
+ const statsShape = z.object({
87
+ scheduled_at_gte: flexibleDateTimeSchema().optional(),
88
+ scheduled_at_lte: flexibleDateTimeSchema().optional(),
89
+ created_at_gte: flexibleDateTimeSchema().optional(),
90
+ created_at_lte: flexibleDateTimeSchema().optional(),
91
+ });
92
+ expect(JSON.stringify(zodToJsonSchema(listJobsShape))).not.toContain('$ref');
93
+ expect(JSON.stringify(zodToJsonSchema(statsShape))).not.toContain('$ref');
94
+ });
40
95
  });
@@ -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();
@@ -0,0 +1,9 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mcpServerVersion } from './version.js';
3
+ describe('mcpServerVersion', () => {
4
+ it('resolves to a non-empty string from package.json in dev tree', () => {
5
+ expect(typeof mcpServerVersion).toBe('string');
6
+ expect(mcpServerVersion.length).toBeGreaterThan(0);
7
+ expect(mcpServerVersion).not.toBe('unknown');
8
+ });
9
+ });
@@ -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:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiconnect/agentjobs-mcp",
3
- "version": "1.1.0",
3
+ "version": "1.2.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"