@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.
package/.env.example CHANGED
@@ -10,6 +10,10 @@ AICONNECT_API_KEY=your-api-key-here
10
10
  # Optional: Default organization ID (defaults to 'aiconnect' if not set)
11
11
  # DEFAULT_ORG_ID=your-organization-id
12
12
 
13
+ # Optional: Preferred timezone surfaced by the `get_context` tool (defaults to 'UTC' if not set).
14
+ # Informational only — does not change timestamp behavior of other tools.
15
+ # DEFAULT_TIMEZONE=America/Sao_Paulo
16
+
13
17
  # Debug Settings
14
18
  MCP_DEBUG=false
15
19
  NODE_ENV=development
package/README.md CHANGED
@@ -59,12 +59,14 @@ Edit the `.env` file with your credentials:
59
59
  DEFAULT_ORG_ID=your-organization # Default: aiconnect
60
60
  AICONNECT_API_KEY=your-api-key # Required: Must be provided
61
61
  AICONNECT_API_URL=https://api.aiconnect.cloud/api/v0 # Default
62
+ DEFAULT_TIMEZONE=America/Sao_Paulo # Default: UTC (informational only, surfaced via get_context)
62
63
  ```
63
64
 
64
65
  **Important**: If no environment variables are provided, the server will use these defaults:
65
66
  - `DEFAULT_ORG_ID`: `aiconnect`
66
67
  - `AICONNECT_API_URL`: `https://api.aiconnect.cloud/api/v0`
67
68
  - `AICONNECT_API_KEY`: empty (must be provided for API calls to work)
69
+ - `DEFAULT_TIMEZONE`: `UTC`
68
70
 
69
71
  4. **Build the project:**
70
72
  ```bash
@@ -108,6 +110,11 @@ npx @aiconnect/agentjobs-mcp
108
110
  - `AICONNECT_API_URL`: API endpoint URL (e.g., https://api.aiconnect.cloud/api/v0)
109
111
  - `AICONNECT_API_KEY`: Your API authentication key
110
112
 
113
+ **Optional Environment Variables:**
114
+
115
+ - `DEFAULT_ORG_ID`: Fallback organization ID when a tool's `org_id` parameter is omitted (default: `aiconnect`)
116
+ - `DEFAULT_TIMEZONE`: Preferred timezone surfaced by the `get_context` tool so LLM clients can format timestamps. Informational only — does not change behavior of other tools, which continue to emit timestamps in UTC (default: `UTC`)
117
+
111
118
  **CLI Command Examples:**
112
119
  ```bash
113
120
  # Quick help
@@ -148,14 +155,18 @@ This MCP server is designed to work out-of-the-box with minimal configuration. I
148
155
  3. **Partial configuration**: Mix of environment variables and defaults
149
156
 
150
157
  **Default Values (when no env vars are set):**
158
+
151
159
  - `DEFAULT_ORG_ID`: `"aiconnect"`
152
160
  - `AICONNECT_API_URL`: `"https://api.aiconnect.cloud/api/v0"`
153
161
  - `AICONNECT_API_KEY`: `""` (empty - you must provide this)
162
+ - `DEFAULT_TIMEZONE`: `"UTC"`
154
163
 
155
164
  **Error Handling:**
165
+
156
166
  - The server will always start, even if environment variables are missing.
157
167
  - If `AICONNECT_API_KEY` or `AICONNECT_API_URL` are not provided, each tool will return a clear error message upon execution, guiding the user to configure the environment correctly.
158
168
  - If `DEFAULT_ORG_ID` is not set, it defaults to "aiconnect".
169
+ - If `DEFAULT_TIMEZONE` is not set, it defaults to "UTC". This value is purely informational (returned by the `get_context` tool) and does not affect timestamp parsing or formatting in other tools.
159
170
 
160
171
  ### Running the MCP server
161
172
 
@@ -178,7 +189,8 @@ To use this MCP server with Claude Desktop, add the following configuration to y
178
189
  "env": {
179
190
  "DEFAULT_ORG_ID": "your-organization",
180
191
  "AICONNECT_API_KEY": "your-api-key",
181
- "AICONNECT_API_URL": "https://api.aiconnect.cloud/api/v0"
192
+ "AICONNECT_API_URL": "https://api.aiconnect.cloud/api/v0",
193
+ "DEFAULT_TIMEZONE": "America/Sao_Paulo"
182
194
  }
183
195
  }
184
196
  }
@@ -228,12 +240,32 @@ Lists all jobs with filtering and pagination options.
228
240
  - `limit` (optional): Result limit (default: 50)
229
241
  - `offset` (optional): Pagination offset
230
242
  - `sort` (optional): Field and direction for sorting
243
+ - `include_activities` (optional): Attach recent activities to each job (default: false)
244
+ - `activities_limit_per_job` (optional): Max activities per job (1–100, default 15)
245
+ - `activities_total_limit` (optional): Global cap across the response (1–3000, default 500)
246
+ - `activities_sort` (optional): `created_at` or `-created_at` (default `-created_at`)
231
247
 
232
248
  ### 🔍 `get_job`
233
249
  Gets details of a specific job.
234
250
 
235
251
  **Parameters:**
236
252
  - `job_id` (required): ID of the job to query
253
+ - `include_activities` (optional): Attach recent activities as an inline overlay (default: false)
254
+ - `include_limit` (optional): Max activities to attach (1–100, default 50)
255
+ - `include_sort` (optional): `created_at` or `-created_at` (default `-created_at`)
256
+
257
+ ### 🧾 `get_job_activities`
258
+ Retrieves the audit activity trail for a specific agent job via the dedicated `/services/activities` endpoint. Supports real pagination (no truncation) and server-side filtering. Use this for focused investigation of a job's activity log; for a quick overlay of recent activities, use `get_job` with `include_activities=true`.
259
+
260
+ **Parameters:**
261
+ - `job_id` (required): ID of the agent job
262
+ - `org_id` (optional): Organization scope
263
+ - `status` (optional): `submitted`, `completed`, or `canceled`
264
+ - `activity_type_code` (optional): Open-string code (e.g., `ai_completion`)
265
+ - `source_type` (optional): `dispatch`, `process_module`, or `direct`
266
+ - `limit` (optional): Page size (default 50)
267
+ - `offset` (optional): Pagination offset (default 0)
268
+ - `sort` (optional): Sort field/direction (default `-created_at`)
237
269
 
238
270
  ### ✅ `create_job`
239
271
  Creates a new job for execution.
@@ -294,11 +326,12 @@ agentjobs-mcp/
294
326
  │ ├── index.ts # Main MCP server entry point
295
327
  │ ├── config.ts # Configuration loader
296
328
  │ └── tools/ # Directory for all MCP tools
297
- │ ├── get_jobs_stats.ts # Tool for getting job statistics
298
- │ ├── list_jobs.ts # Tool for listing jobs
299
- │ ├── get_job.ts # Tool for getting a job
300
- │ ├── create_job.ts # Tool for creating a job
301
- └── cancel_job.ts # Tool for canceling a job
329
+ │ ├── get_jobs_stats.ts # Tool for getting job statistics
330
+ │ ├── list_jobs.ts # Tool for listing jobs
331
+ │ ├── get_job.ts # Tool for getting a job
332
+ │ ├── get_job_activities.ts # Tool for getting a job's activity trail
333
+ ├── create_job.ts # Tool for creating a job
334
+ │ └── cancel_job.ts # Tool for canceling a job
302
335
  ├── build/ # Compiled JavaScript code
303
336
  ├── docs/ # Documentation
304
337
  │ └── agent-jobs-api.md # API documentation
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
  };
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";
@@ -104,7 +111,7 @@ const toolsDir = path.join(__dirname, 'tools');
104
111
  try {
105
112
  const toolFiles = await fs.readdir(toolsDir);
106
113
  for (const file of toolFiles) {
107
- if (file.endsWith('.js')) { // In production, files will be .js
114
+ if (file.endsWith('.js') && !file.endsWith('.test.js')) { // In production, files will be .js (exclude colocated tests)
108
115
  try {
109
116
  const toolModule = await import(`./tools/${file}`);
110
117
  if (typeof toolModule.default === 'function') {
@@ -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,
@@ -21,7 +21,7 @@ async function testTools() {
21
21
  const toolFiles = await fs.readdir(toolsDir);
22
22
  console.log(`📁 Found ${toolFiles.length} tool files`);
23
23
  for (const file of toolFiles) {
24
- if (file.endsWith('.js')) {
24
+ if (file.endsWith('.js') && !file.endsWith('.test.js')) {
25
25
  try {
26
26
  console.log(`⚙️ Loading tool: ${file}`);
27
27
  const toolModule = await import(`./tools/${file}`);
@@ -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
+ };
@@ -2,35 +2,65 @@ import { z } from 'zod';
2
2
  import agentJobsClient from '../lib/agentJobsClient.js';
3
3
  import { formatJobDetails } from '../utils/formatters.js';
4
4
  import { mcpDebugger, withTiming } from '../utils/debugger.js';
5
+ const ACTIVITIES_SORT_VALUES = ['created_at', '-created_at'];
5
6
  export default (server) => {
6
7
  server.registerTool('get_job', {
7
- description: 'Retrieves an agent job by its ID.',
8
+ description: 'Retrieves an agent job by its ID. Optionally includes recent activity records as an inline overlay (use include_activities=true).',
8
9
  annotations: {
9
10
  title: 'Get Agent Job'
10
11
  },
11
12
  inputSchema: {
12
13
  job_id: z.string({
13
14
  description: "The unique identifier of the job you want to retrieve. Example: 'job-12345'."
14
- }),
15
+ }).min(1, { message: 'job_id must be a non-empty string' }),
15
16
  org_id: z
16
17
  .string({
17
18
  description: "The organization ID. Example: 'aiconnect'."
18
19
  })
19
- .optional()
20
+ .optional(),
21
+ include_activities: z.boolean().optional().describe("When true, attaches recent activities as an overlay (?include=activities). For full pagination of activities use the get_job_activities tool."),
22
+ include_limit: z.number().int().optional().describe("Max activities to attach when include_activities=true. Range 1-100, default 50. Silently ignored when include_activities is false/omitted."),
23
+ include_sort: z.string().optional().describe("Sort order for attached activities when include_activities=true. 'created_at' or '-created_at' (default). Silently ignored when include_activities is false/omitted.")
20
24
  }
21
25
  }, async (params) => {
22
26
  mcpDebugger.toolCall("get_job", params);
23
- const { job_id } = params;
24
- const endpoint = `/services/agent-jobs/${job_id}${params.org_id ? `?org_id=${params.org_id}` : ''}`;
25
- mcpDebugger.debug("Built endpoint", { endpoint, job_id, org_id: params.org_id });
27
+ const { job_id, include_activities } = params;
28
+ const endpoint = `/services/agent-jobs/${job_id}`;
29
+ const queryParams = {};
30
+ if (params.org_id)
31
+ queryParams.org_id = params.org_id;
32
+ if (include_activities) {
33
+ // Per spec, range/enum on the overlay-only params are validated here
34
+ // (not in the Zod input schema) so that flag-off callers carrying stale
35
+ // configuration are silently tolerated. With the flag on, invalid
36
+ // values short-circuit before any HTTP call.
37
+ if (params.include_limit !== undefined) {
38
+ if (!Number.isInteger(params.include_limit) || params.include_limit < 1 || params.include_limit > 100) {
39
+ return {
40
+ content: [{ type: 'text', text: 'Error getting job: include_limit must be an integer in [1, 100]' }],
41
+ };
42
+ }
43
+ }
44
+ if (params.include_sort !== undefined && !ACTIVITIES_SORT_VALUES.includes(params.include_sort)) {
45
+ return {
46
+ content: [{ type: 'text', text: "Error getting job: include_sort must be 'created_at' or '-created_at'" }],
47
+ };
48
+ }
49
+ queryParams.include = 'activities';
50
+ queryParams.include_limit = params.include_limit ?? 50;
51
+ queryParams.include_sort = params.include_sort ?? '-created_at';
52
+ }
53
+ mcpDebugger.debug("Built endpoint", { endpoint, queryParams });
26
54
  try {
27
- const job = await withTiming(() => agentJobsClient.get(endpoint), "get_job API call");
28
- mcpDebugger.debug("Raw API response", { job });
55
+ const apiResponse = await withTiming(() => agentJobsClient.getWithMeta(endpoint, queryParams), "get_job API call");
56
+ const job = apiResponse?.data ?? apiResponse;
57
+ const meta = apiResponse?.meta;
58
+ mcpDebugger.debug("Raw API response", { job, meta });
29
59
  const result = {
30
60
  content: [
31
61
  {
32
62
  type: 'text',
33
- text: formatJobDetails(job)
63
+ text: formatJobDetails(job, meta)
34
64
  }
35
65
  ]
36
66
  };
@@ -0,0 +1,68 @@
1
+ import { z } from 'zod';
2
+ import agentJobsClient from '../lib/agentJobsClient.js';
3
+ import { formatJobActivitiesList } from '../utils/formatters.js';
4
+ import { activityStatusSchema, activitySourceTypeSchema, } from '../utils/schemas.js';
5
+ import { mcpDebugger, withTiming } from '../utils/debugger.js';
6
+ export default (server) => {
7
+ server.registerTool('get_job_activities', {
8
+ description: "Retrieves the audit activity trail for a specific agent job via the dedicated /services/activities endpoint. Supports real pagination (no truncation) and server-side filters by status, type and source. Use this for focused investigation of a job's activity log; for a quick overlay of recent activities on the job detail, use get_job with include_activities=true.",
9
+ annotations: {
10
+ title: 'Get Agent Job Activities'
11
+ },
12
+ inputSchema: {
13
+ job_id: z.string().min(1).describe("The unique identifier of the agent job whose activities you want to retrieve. Required."),
14
+ org_id: z.string().optional().describe("The organization ID. If omitted, the default from the environment is used."),
15
+ status: activityStatusSchema().optional().describe("Filter by activity status. Possible values: 'submitted', 'completed', 'canceled'."),
16
+ activity_type_code: z.string().optional().describe("Filter by activity type code (open string, e.g., 'ai_completion')."),
17
+ source_type: activitySourceTypeSchema().optional().describe("Filter by source type. Possible values: 'dispatch', 'process_module', 'direct'."),
18
+ limit: z.number().int().positive().optional().describe("Maximum number of activities to return per page. Default 50."),
19
+ offset: z.number().int().nonnegative().optional().describe("Number of activities to skip, used for pagination. Default 0."),
20
+ sort: z.string().optional().describe("Field and direction to sort by. Default '-created_at' (newest first).")
21
+ }
22
+ }, async (params) => {
23
+ mcpDebugger.toolCall('get_job_activities', params);
24
+ const endpoint = '/services/activities';
25
+ const queryParams = {
26
+ job_id: params.job_id,
27
+ limit: params.limit ?? 50,
28
+ offset: params.offset ?? 0,
29
+ sort: params.sort ?? '-created_at',
30
+ };
31
+ if (params.org_id)
32
+ queryParams.org_id = params.org_id;
33
+ if (params.status)
34
+ queryParams.status = params.status;
35
+ if (params.activity_type_code)
36
+ queryParams.activity_type_code = params.activity_type_code;
37
+ if (params.source_type)
38
+ queryParams.source_type = params.source_type;
39
+ mcpDebugger.debug('Built query parameters', { endpoint, queryParams });
40
+ try {
41
+ const apiResponse = await withTiming(() => agentJobsClient.getWithMeta(endpoint, queryParams), 'get_job_activities API call');
42
+ const activities = apiResponse?.data ?? [];
43
+ const meta = apiResponse?.meta ?? {};
44
+ const offset = queryParams.offset;
45
+ const result = {
46
+ content: [{
47
+ type: 'text',
48
+ text: formatJobActivitiesList(params.job_id, activities, meta, offset),
49
+ }]
50
+ };
51
+ mcpDebugger.toolResponse('get_job_activities', {
52
+ jobId: params.job_id,
53
+ activitiesReturned: activities.length,
54
+ resultLength: result.content[0].text.length
55
+ });
56
+ return result;
57
+ }
58
+ catch (error) {
59
+ mcpDebugger.toolError('get_job_activities', error);
60
+ return {
61
+ content: [{
62
+ type: 'text',
63
+ text: `Error getting job activities: ${error.message}`,
64
+ }],
65
+ };
66
+ }
67
+ });
68
+ };
@@ -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);
@@ -3,6 +3,7 @@ import agentJobsClient from "../lib/agentJobsClient.js";
3
3
  import { formatJobList } from "../utils/formatters.js";
4
4
  import { flexibleDateTimeSchema } from "../utils/schemas.js";
5
5
  import { mcpDebugger, withTiming } from "../utils/debugger.js";
6
+ const ACTIVITIES_SORT_VALUES = ['created_at', '-created_at'];
6
7
  // Define the schema for job status based on docs/agent-jobs-api.md:246-251
7
8
  const jobStatusSchema = z.enum([
8
9
  "waiting",
@@ -21,27 +22,60 @@ export default (server) => {
21
22
  inputSchema: {
22
23
  org_id: z.string().optional().describe("Filter by organization ID. If not provided, the default from the environment is used."),
23
24
  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)."),
25
+ scheduled_at: flexibleDateTimeSchema().optional().describe("Filter by the exact scheduled time in ISO 8601 format (e.g., '2024-07-23T10:00:00Z')."),
26
+ scheduled_at_gte: flexibleDateTimeSchema().optional().describe("Filter for jobs scheduled at or after a specific time (ISO 8601)."),
27
+ scheduled_at_lte: flexibleDateTimeSchema().optional().describe("Filter for jobs scheduled at or before a specific time (ISO 8601)."),
28
+ created_at_gte: flexibleDateTimeSchema().optional().describe("Filter for jobs created at or after a specific time (ISO 8601)."),
29
+ created_at_lte: flexibleDateTimeSchema().optional().describe("Filter for jobs created at or before a specific time (ISO 8601)."),
29
30
  job_type_id: z.string().optional().describe("Filter by the specific job type ID (e.g., 'daily-report')."),
30
31
  channel_code: z.string().optional().describe("Filter by the channel code (e.g., 'C123456' for a Slack channel)."),
31
32
  limit: z.number().int().positive().optional().describe("Maximum number of jobs to return (e.g.,20). Default is 20."),
32
33
  offset: z.number().int().nonnegative().optional().describe("Number of jobs to skip, used for pagination. Default is 0."),
33
- sort: z.string().optional().describe("Field to sort by and direction. Format is 'field:direction'. Example: 'created_at:desc'.")
34
+ sort: z.string().optional().describe("Field to sort by and direction. Format is 'field:direction'. Example: 'created_at:desc'."),
35
+ include_activities: z.boolean().optional().describe("When true, attaches activities to each job in the list (?include=activities). For full pagination of a single job's activities use the get_job_activities tool."),
36
+ activities_limit_per_job: z.number().int().optional().describe("Max activities per job when include_activities=true. Range 1-100, default 15. Silently ignored when include_activities is false/omitted."),
37
+ activities_total_limit: z.number().int().optional().describe("Global cap on total activities returned across all jobs in the response when include_activities=true. Range 1-3000, default 500. Silently ignored when include_activities is false/omitted."),
38
+ activities_sort: z.string().optional().describe("Sort order for attached activities when include_activities=true. 'created_at' or '-created_at' (default). Silently ignored when include_activities is false/omitted.")
34
39
  }
35
40
  }, async (params) => {
36
41
  mcpDebugger.toolCall("list_jobs", params);
37
42
  const endpoint = `/services/agent-jobs`;
38
- // Build query parameters object from provided params
43
+ const { include_activities, activities_limit_per_job, activities_total_limit, activities_sort, ...rest } = params;
39
44
  const queryParams = {};
40
- for (const [key, value] of Object.entries(params)) {
45
+ for (const [key, value] of Object.entries(rest)) {
41
46
  if (value !== undefined) {
42
47
  queryParams[key] = value;
43
48
  }
44
49
  }
50
+ if (include_activities) {
51
+ // Per spec, range/enum on the overlay-only params are validated here
52
+ // (not in the Zod input schema) so that flag-off callers carrying stale
53
+ // configuration are silently tolerated. With the flag on, invalid
54
+ // values short-circuit before any HTTP call.
55
+ if (activities_limit_per_job !== undefined) {
56
+ if (!Number.isInteger(activities_limit_per_job) || activities_limit_per_job < 1 || activities_limit_per_job > 100) {
57
+ return {
58
+ content: [{ type: 'text', text: 'Error listing jobs: activities_limit_per_job must be an integer in [1, 100]' }],
59
+ };
60
+ }
61
+ }
62
+ if (activities_total_limit !== undefined) {
63
+ if (!Number.isInteger(activities_total_limit) || activities_total_limit < 1 || activities_total_limit > 3000) {
64
+ return {
65
+ content: [{ type: 'text', text: 'Error listing jobs: activities_total_limit must be an integer in [1, 3000]' }],
66
+ };
67
+ }
68
+ }
69
+ if (activities_sort !== undefined && !ACTIVITIES_SORT_VALUES.includes(activities_sort)) {
70
+ return {
71
+ content: [{ type: 'text', text: "Error listing jobs: activities_sort must be 'created_at' or '-created_at'" }],
72
+ };
73
+ }
74
+ queryParams.include = 'activities';
75
+ queryParams.activities_limit_per_job = activities_limit_per_job ?? 15;
76
+ queryParams.activities_total_limit = activities_total_limit ?? 500;
77
+ queryParams.activities_sort = activities_sort ?? '-created_at';
78
+ }
45
79
  mcpDebugger.debug("Built query parameters", { endpoint, queryParams });
46
80
  try {
47
81
  const apiResponse = await withTiming(() => agentJobsClient.getWithMeta(endpoint, queryParams), "list_jobs API call");
@@ -54,10 +88,11 @@ export default (server) => {
54
88
  meta,
55
89
  firstJob: jobs[0] || null
56
90
  });
91
+ const offset = typeof params.offset === 'number' ? params.offset : 0;
57
92
  const result = {
58
93
  content: [{
59
94
  type: "text",
60
- text: formatJobList(jobs, meta),
95
+ text: formatJobList(jobs, meta, offset, { includeActivities: include_activities === true }),
61
96
  }]
62
97
  };
63
98
  mcpDebugger.toolResponse("list_jobs", {