@studiometa/productive-mcp 0.10.10 → 0.10.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +120 -0
  2. package/dist/api-reference/generated.d.ts +3 -0
  3. package/dist/api-reference/generated.d.ts.map +1 -0
  4. package/dist/api-reference/types.d.ts +31 -0
  5. package/dist/api-reference/types.d.ts.map +1 -0
  6. package/dist/auth.js.map +1 -1
  7. package/dist/crypto.js +3 -3
  8. package/dist/crypto.js.map +1 -1
  9. package/dist/errors.d.ts.map +1 -1
  10. package/dist/handlers/activities.d.ts +3 -99
  11. package/dist/handlers/activities.d.ts.map +1 -1
  12. package/dist/handlers/api-read.d.ts +14 -0
  13. package/dist/handlers/api-read.d.ts.map +1 -0
  14. package/dist/handlers/api-utils.d.ts +27 -0
  15. package/dist/handlers/api-utils.d.ts.map +1 -0
  16. package/dist/handlers/api-write.d.ts +10 -0
  17. package/dist/handlers/api-write.d.ts.map +1 -0
  18. package/dist/handlers/attachments.d.ts +3 -99
  19. package/dist/handlers/attachments.d.ts.map +1 -1
  20. package/dist/handlers/bookings.d.ts +3 -99
  21. package/dist/handlers/bookings.d.ts.map +1 -1
  22. package/dist/handlers/comments.d.ts +3 -99
  23. package/dist/handlers/comments.d.ts.map +1 -1
  24. package/dist/handlers/companies.d.ts +3 -99
  25. package/dist/handlers/companies.d.ts.map +1 -1
  26. package/dist/handlers/custom-fields.d.ts +3 -99
  27. package/dist/handlers/custom-fields.d.ts.map +1 -1
  28. package/dist/handlers/deals.d.ts +3 -99
  29. package/dist/handlers/deals.d.ts.map +1 -1
  30. package/dist/handlers/discussions.d.ts +3 -99
  31. package/dist/handlers/discussions.d.ts.map +1 -1
  32. package/dist/handlers/index.d.ts.map +1 -1
  33. package/dist/handlers/pages.d.ts +3 -99
  34. package/dist/handlers/pages.d.ts.map +1 -1
  35. package/dist/handlers/projects.d.ts +3 -99
  36. package/dist/handlers/projects.d.ts.map +1 -1
  37. package/dist/handlers/search.d.ts +1 -1
  38. package/dist/handlers/search.d.ts.map +1 -1
  39. package/dist/handlers/services.d.ts +3 -99
  40. package/dist/handlers/services.d.ts.map +1 -1
  41. package/dist/handlers/tasks.d.ts +3 -99
  42. package/dist/handlers/tasks.d.ts.map +1 -1
  43. package/dist/handlers/time.d.ts +3 -99
  44. package/dist/handlers/time.d.ts.map +1 -1
  45. package/dist/handlers/timers.d.ts +3 -99
  46. package/dist/handlers/timers.d.ts.map +1 -1
  47. package/dist/handlers-DonE83xo.js +41289 -0
  48. package/dist/handlers-DonE83xo.js.map +1 -0
  49. package/dist/handlers.js +1 -1
  50. package/dist/http-QQVUnV2e.js +3238 -0
  51. package/dist/http-QQVUnV2e.js.map +1 -0
  52. package/dist/http.d.ts.map +1 -1
  53. package/dist/http.js +1 -1
  54. package/dist/index.js +2 -2
  55. package/dist/index.js.map +1 -1
  56. package/dist/oauth.js.map +1 -1
  57. package/dist/schema.d.ts +32 -1
  58. package/dist/schema.d.ts.map +1 -1
  59. package/dist/server.js +2 -2
  60. package/dist/server.js.map +1 -1
  61. package/dist/{stdio-BFK9AcdQ.js → stdio-CRD2nJPs.js} +2 -2
  62. package/dist/{stdio-BFK9AcdQ.js.map → stdio-CRD2nJPs.js.map} +1 -1
  63. package/dist/stdio.js +1 -1
  64. package/dist/tools.d.ts.map +1 -1
  65. package/dist/tools.js +193 -119
  66. package/dist/tools.js.map +1 -1
  67. package/dist/{version-Cy8UEAT1.js → version-DMEaGciu.js} +3 -3
  68. package/dist/{version-Cy8UEAT1.js.map → version-DMEaGciu.js.map} +1 -1
  69. package/package.json +6 -6
  70. package/skills/SKILL.md +113 -1
  71. package/dist/handlers-vtRpc-Lx.js +0 -4301
  72. package/dist/handlers-vtRpc-Lx.js.map +0 -1
  73. package/dist/http-CVE4qtko.js +0 -6541
  74. package/dist/http-CVE4qtko.js.map +0 -1
@@ -1,4301 +0,0 @@
1
- import { ProductiveApi, formatActivity, formatAttachment, formatBooking, formatComment, formatCompany, formatCustomField, formatDeal, formatDiscussion, formatListResponse, formatPage, formatPerson, formatProject, formatService, formatTask, formatTimeEntry, formatTimer } from "@studiometa/productive-api";
2
- import { RESOURCES, ResolveError, VALID_REPORT_TYPES, completeTask, createBooking, createComment, createCompany, createDeal, createDiscussion, createPage, createTask, createTimeEntry, deleteAttachment, deleteDiscussion, deletePage, deleteTimeEntry, fromHandlerContext, getAttachment, getBooking, getComment, getCompany, getCustomField, getDeal, getDealContext, getDiscussion, getMyDaySummary, getPage, getPerson, getProject, getProjectContext, getProjectHealthSummary, getReport, getService, getTask, getTaskContext, getTeamPulseSummary, getTimeEntry, getTimer, listActivities, listAttachments, listBookings, listComments, listCompanies, listCustomFields, listDeals, listDiscussions, listPages, listPeople, listProjects, listServices, listTasks, listTimeEntries, listTimers, logDay, reopenDiscussion, resolveDiscussion, resolveResource, startTimer, stopTimer, updateBooking, updateComment, updateCompany, updateDeal, updateDiscussion, updatePage, updateTask, updateTimeEntry, weeklyStandup } from "@studiometa/productive-core";
3
- //#region src/errors.ts
4
- /**
5
- * Custom error classes for MCP server
6
- *
7
- * These provide structured error handling with LLM-friendly messages
8
- * that include guidance on how to resolve issues.
9
- */
10
- /**
11
- * Error thrown when user input validation fails.
12
- * These errors should be returned to the user directly.
13
- *
14
- * Includes optional hints for how to resolve the issue.
15
- */
16
- var UserInputError = class extends Error {
17
- hints;
18
- constructor(message, hints) {
19
- super(message);
20
- this.name = "UserInputError";
21
- this.hints = hints;
22
- }
23
- /**
24
- * Format error message with hints for LLM consumption
25
- */
26
- toFormattedMessage() {
27
- let msg = `**Input Error:** ${this.message}`;
28
- if (this.hints && this.hints.length > 0) msg += "\n\n**Hints:**\n" + this.hints.map((h) => `- ${h}`).join("\n");
29
- return msg;
30
- }
31
- };
32
- /**
33
- * Error messages with guidance for common validation failures
34
- */
35
- var ErrorMessages = {
36
- missingId: (action) => new UserInputError(`id is required for ${action} action`, [`Use action="list" first to find the resource ID`, `Then use action="${action}" with the id parameter`]),
37
- missingRequiredFields: (resource, fields) => new UserInputError(`${fields.join(", ")} ${fields.length === 1 ? "is" : "are"} required for creating ${resource}`, [`Provide all required fields: ${fields.join(", ")}`, `Use action="help" for detailed documentation on ${resource}`]),
38
- invalidAction: (action, resource, validActions) => new UserInputError(`Invalid action "${action}" for ${resource}`, [`Valid actions are: ${validActions.join(", ")}`, `Use action="help" with resource="${resource}" for detailed documentation`]),
39
- unknownResource: (resource, validResources) => new UserInputError(`Unknown resource: ${resource}`, [`Valid resources are: ${validResources.join(", ")}`, `Use action="help" without a resource for an overview of all resources`]),
40
- missingReportType: () => new UserInputError("report_type is required for reports", ["Specify report_type parameter (e.g., \"time_reports\", \"project_reports\")", "Use action=\"help\" with resource=\"reports\" for available report types"]),
41
- invalidReportType: (reportType, validTypes) => new UserInputError(`Invalid report_type: ${reportType}`, [`Valid report types are: ${validTypes.join(", ")}`, "Use action=\"help\" with resource=\"reports\" for detailed documentation"]),
42
- missingServiceForTimer: () => new UserInputError("service_id is required to start a timer", ["First find a service using resource=\"services\" action=\"list\"", "Then start the timer with the service_id"]),
43
- noUserIdConfigured: () => new UserInputError("User ID not configured", ["The \"me\" action requires a user ID to be configured", "Use action=\"list\" to find people, or configure the user ID"]),
44
- missingCommentTarget: () => new UserInputError("A target is required for creating a comment", ["Provide one of: task_id, deal_id, or company_id", "Find targets using resource=\"tasks\", \"deals\", or \"companies\" with action=\"list\""]),
45
- missingBookingTarget: () => new UserInputError("A service or event is required for creating a booking", ["Provide either: service_id or event_id", "Find services using resource=\"services\" with action=\"list\""]),
46
- noUpdateFieldsSpecified: (allowedFields) => new UserInputError(`No updates specified. Provide at least one of: ${allowedFields.join(", ")}`, ["Specify at least one field to update", `Updatable fields are: ${allowedFields.join(", ")}`]),
47
- apiError: (statusCode, message) => {
48
- const hints = [];
49
- if (statusCode === 401) {
50
- hints.push("Check that your API token is valid and not expired");
51
- hints.push("Verify the organization ID is correct");
52
- } else if (statusCode === 403) {
53
- hints.push("You may not have permission to access this resource");
54
- hints.push("Check your API token permissions");
55
- } else if (statusCode === 404) {
56
- hints.push("The resource may not exist or you may not have access");
57
- hints.push("Verify the resource ID is correct");
58
- hints.push("Use action=\"list\" to find valid resource IDs");
59
- } else if (statusCode === 422) {
60
- hints.push("The request data may be invalid");
61
- hints.push("Check the field values and types");
62
- hints.push("Use action=\"help\" for field documentation");
63
- } else if (statusCode >= 500) hints.push("This is a server error - try again later");
64
- return new UserInputError(`API error (${statusCode}): ${message}`, hints);
65
- }
66
- };
67
- /**
68
- * Check if an error is a UserInputError
69
- */
70
- function isUserInputError(error) {
71
- return error instanceof UserInputError;
72
- }
73
- //#endregion
74
- //#region src/formatters.ts
75
- /**
76
- * Response formatters for agent-friendly output
77
- *
78
- * This module re-exports formatters from @studiometa/productive-api
79
- * with MCP-specific defaults (no relationship IDs, no timestamps).
80
- *
81
- * Supports compact mode to reduce token usage by omitting verbose fields
82
- * like descriptions and notes from list responses.
83
- */
84
- /**
85
- * MCP-specific format options
86
- * - No relationship IDs (cleaner output for agents)
87
- * - No timestamps (reduce noise)
88
- * - HTML stripping enabled
89
- */
90
- var MCP_FORMAT_OPTIONS = {
91
- includeRelationshipIds: false,
92
- includeTimestamps: false,
93
- stripHtml: true
94
- };
95
- /**
96
- * Remove verbose fields from an object for compact output
97
- */
98
- function compactify(obj, fieldsToRemove) {
99
- const result = { ...obj };
100
- for (const field of fieldsToRemove) delete result[field];
101
- return result;
102
- }
103
- /**
104
- * Format time entry for agent consumption
105
- */
106
- function formatTimeEntry$1(entry, options) {
107
- const result = formatTimeEntry(entry, MCP_FORMAT_OPTIONS);
108
- if (options?.compact) return compactify(result, [
109
- "note",
110
- "billable_time",
111
- "approved"
112
- ]);
113
- return result;
114
- }
115
- /**
116
- * Format project for agent consumption
117
- */
118
- function formatProject$1(project, options) {
119
- const result = formatProject(project, MCP_FORMAT_OPTIONS);
120
- if (options?.compact) return compactify(result, ["budget"]);
121
- return result;
122
- }
123
- /**
124
- * Format task for agent consumption
125
- * Tasks use included resources to resolve project/company names
126
- */
127
- function formatTask$1(task, options) {
128
- const result = formatTask(task, {
129
- ...MCP_FORMAT_OPTIONS,
130
- included: options?.included
131
- });
132
- if (options?.compact) return compactify(result, [
133
- "description",
134
- "initial_estimate",
135
- "worked_time",
136
- "remaining_time",
137
- "project",
138
- "company"
139
- ]);
140
- return result;
141
- }
142
- /**
143
- * Format person for agent consumption
144
- */
145
- function formatPerson$1(person, options) {
146
- const result = formatPerson(person, MCP_FORMAT_OPTIONS);
147
- if (options?.compact) return compactify(result, [
148
- "title",
149
- "first_name",
150
- "last_name"
151
- ]);
152
- return result;
153
- }
154
- /**
155
- * Format service for agent consumption
156
- */
157
- function formatService$1(service, options) {
158
- const result = formatService(service, MCP_FORMAT_OPTIONS);
159
- if (options?.compact) return compactify(result, ["budgeted_time", "worked_time"]);
160
- return result;
161
- }
162
- /**
163
- * Format company for agent consumption
164
- */
165
- function formatCompany$1(company, options) {
166
- const result = formatCompany(company, MCP_FORMAT_OPTIONS);
167
- if (options?.compact) return compactify(result, [
168
- "billing_name",
169
- "domain",
170
- "due_days"
171
- ]);
172
- return result;
173
- }
174
- /**
175
- * Format comment for agent consumption
176
- */
177
- function formatComment$1(comment, options) {
178
- return formatComment(comment, {
179
- ...MCP_FORMAT_OPTIONS,
180
- included: options?.included
181
- });
182
- }
183
- /**
184
- * Format timer for agent consumption
185
- */
186
- function formatTimer$1(timer, _options) {
187
- return formatTimer(timer, MCP_FORMAT_OPTIONS);
188
- }
189
- /**
190
- * Format deal for agent consumption
191
- */
192
- function formatDeal$1(deal, options) {
193
- const result = formatDeal(deal, {
194
- ...MCP_FORMAT_OPTIONS,
195
- included: options?.included
196
- });
197
- if (options?.compact) return compactify(result, ["won_at", "lost_at"]);
198
- return result;
199
- }
200
- /**
201
- * Format booking for agent consumption
202
- */
203
- function formatBooking$1(booking, options) {
204
- const result = formatBooking(booking, {
205
- ...MCP_FORMAT_OPTIONS,
206
- included: options?.included
207
- });
208
- if (options?.compact) return compactify(result, [
209
- "approved_at",
210
- "rejected_at",
211
- "rejected_reason"
212
- ]);
213
- return result;
214
- }
215
- /**
216
- * Format attachment for agent consumption
217
- */
218
- function formatAttachment$1(attachment, options) {
219
- const result = formatAttachment(attachment, MCP_FORMAT_OPTIONS);
220
- if (options?.compact) return compactify(result, ["url"]);
221
- return result;
222
- }
223
- /**
224
- * Format page for agent consumption
225
- */
226
- function formatPage$1(page, options) {
227
- const result = formatPage(page, MCP_FORMAT_OPTIONS);
228
- if (options?.compact) return compactify(result, ["body", "version_number"]);
229
- return result;
230
- }
231
- /**
232
- * Format discussion for agent consumption
233
- */
234
- function formatDiscussion$1(discussion, options) {
235
- const result = formatDiscussion(discussion, MCP_FORMAT_OPTIONS);
236
- if (options?.compact) return compactify(result, ["body"]);
237
- return result;
238
- }
239
- function formatActivity$1(activity, options) {
240
- return formatActivity(activity, {
241
- ...MCP_FORMAT_OPTIONS,
242
- included: options?.included
243
- });
244
- }
245
- /**
246
- * Format custom field for agent consumption
247
- */
248
- function formatCustomField$1(field, options) {
249
- return formatCustomField(field, {
250
- ...MCP_FORMAT_OPTIONS,
251
- included: options?.included
252
- });
253
- }
254
- /**
255
- * Format list response with pagination
256
- *
257
- * @param data - Array of JSON:API resources
258
- * @param formatter - Formatter function (item, options?) => T
259
- * @param meta - Pagination metadata
260
- * @param options - MCP format options (compact, included)
261
- */
262
- function formatListResponse$1(data, formatter, meta, options) {
263
- const wrappedFormatter = (item, _cliOptions) => {
264
- return formatter(item, options);
265
- };
266
- return formatListResponse(data, wrappedFormatter, meta, {
267
- ...MCP_FORMAT_OPTIONS,
268
- included: options?.included
269
- });
270
- }
271
- //#endregion
272
- //#region src/hints.ts
273
- /**
274
- * Generate hints for a task
275
- */
276
- function getTaskHints(taskId, serviceId) {
277
- const hints = {
278
- related_resources: [
279
- {
280
- resource: "comments",
281
- description: "Get comments on this task",
282
- example: {
283
- resource: "comments",
284
- action: "list",
285
- filter: { task_id: taskId }
286
- }
287
- },
288
- {
289
- resource: "time",
290
- description: "Get time entries logged on this task",
291
- example: {
292
- resource: "time",
293
- action: "list",
294
- filter: { task_id: taskId }
295
- }
296
- },
297
- {
298
- resource: "tasks",
299
- description: "Get subtasks of this task",
300
- example: {
301
- resource: "tasks",
302
- action: "list",
303
- filter: { parent_task_id: taskId }
304
- }
305
- },
306
- {
307
- resource: "custom_fields",
308
- description: "List custom field definitions for tasks (to resolve custom_fields values)",
309
- example: {
310
- resource: "custom_fields",
311
- action: "list",
312
- filter: { customizable_type: "Task" }
313
- }
314
- }
315
- ],
316
- common_actions: [{
317
- action: "Add a comment",
318
- example: {
319
- resource: "comments",
320
- action: "create",
321
- task_id: taskId,
322
- body: "<your comment>"
323
- }
324
- }]
325
- };
326
- if (serviceId) hints.common_actions.push({
327
- action: "Log time on this task",
328
- example: {
329
- resource: "time",
330
- action: "create",
331
- service_id: serviceId,
332
- task_id: taskId,
333
- date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
334
- time: 60,
335
- note: "<description of work>"
336
- }
337
- });
338
- return hints;
339
- }
340
- /**
341
- * Generate hints for a project
342
- */
343
- function getProjectHints(projectId) {
344
- return {
345
- related_resources: [
346
- {
347
- resource: "tasks",
348
- description: "Get tasks in this project",
349
- example: {
350
- resource: "tasks",
351
- action: "list",
352
- filter: { project_id: projectId }
353
- }
354
- },
355
- {
356
- resource: "services",
357
- description: "Get services (budget lines) for this project",
358
- example: {
359
- resource: "services",
360
- action: "list",
361
- filter: { project_id: projectId }
362
- }
363
- },
364
- {
365
- resource: "time",
366
- description: "Get time entries for this project",
367
- example: {
368
- resource: "time",
369
- action: "list",
370
- filter: { project_id: projectId }
371
- }
372
- },
373
- {
374
- resource: "comments",
375
- description: "Get comments on this project",
376
- example: {
377
- resource: "comments",
378
- action: "list",
379
- filter: { project_id: projectId }
380
- }
381
- },
382
- {
383
- resource: "deals",
384
- description: "Get deals/budgets for this project",
385
- example: {
386
- resource: "deals",
387
- action: "list",
388
- filter: { project_id: projectId }
389
- }
390
- }
391
- ],
392
- common_actions: [{
393
- action: "Create a task",
394
- example: {
395
- resource: "tasks",
396
- action: "create",
397
- project_id: projectId,
398
- task_list_id: "<task_list_id>",
399
- title: "<task title>"
400
- }
401
- }]
402
- };
403
- }
404
- /**
405
- * Generate hints for a deal/budget
406
- */
407
- function getDealHints(dealId) {
408
- return {
409
- related_resources: [
410
- {
411
- resource: "comments",
412
- description: "Get comments on this deal/budget",
413
- example: {
414
- resource: "comments",
415
- action: "list",
416
- filter: { deal_id: dealId }
417
- }
418
- },
419
- {
420
- resource: "services",
421
- description: "Get services (budget lines) for this deal",
422
- example: {
423
- resource: "services",
424
- action: "list",
425
- filter: { deal_id: dealId }
426
- }
427
- },
428
- {
429
- resource: "time",
430
- description: "Get time entries for this deal/budget",
431
- example: {
432
- resource: "time",
433
- action: "list",
434
- filter: { deal_id: dealId }
435
- }
436
- },
437
- {
438
- resource: "bookings",
439
- description: "Get resource bookings for this deal",
440
- example: {
441
- resource: "bookings",
442
- action: "list",
443
- filter: { deal_id: dealId }
444
- }
445
- },
446
- {
447
- resource: "custom_fields",
448
- description: "List custom field definitions for deals (to resolve custom_fields values)",
449
- example: {
450
- resource: "custom_fields",
451
- action: "list",
452
- filter: { customizable_type: "Deal" }
453
- }
454
- }
455
- ],
456
- common_actions: [{
457
- action: "Add a comment",
458
- example: {
459
- resource: "comments",
460
- action: "create",
461
- deal_id: dealId,
462
- body: "<your comment>"
463
- }
464
- }]
465
- };
466
- }
467
- /**
468
- * Generate hints for a person
469
- */
470
- function getPersonHints(personId) {
471
- return { related_resources: [
472
- {
473
- resource: "tasks",
474
- description: "Get tasks assigned to this person",
475
- example: {
476
- resource: "tasks",
477
- action: "list",
478
- filter: { assignee_id: personId }
479
- }
480
- },
481
- {
482
- resource: "time",
483
- description: "Get time entries by this person",
484
- example: {
485
- resource: "time",
486
- action: "list",
487
- filter: { person_id: personId }
488
- }
489
- },
490
- {
491
- resource: "bookings",
492
- description: "Get bookings for this person",
493
- example: {
494
- resource: "bookings",
495
- action: "list",
496
- filter: { person_id: personId }
497
- }
498
- },
499
- {
500
- resource: "timers",
501
- description: "Get active timers for this person",
502
- example: {
503
- resource: "timers",
504
- action: "list",
505
- filter: { person_id: personId }
506
- }
507
- }
508
- ] };
509
- }
510
- /**
511
- * Generate hints for a services list response.
512
- *
513
- * When the query doesn't already filter by deal_id, suggests filtering
514
- * by deal_id to scope services to a specific budget/deal.
515
- *
516
- * @param currentFilter - The filter object currently applied to the list query
517
- */
518
- function getServiceListHints(currentFilter) {
519
- if (currentFilter?.deal_id) return null;
520
- return { common_actions: [{
521
- action: "Filter services by deal to see budget line items for a specific deal",
522
- example: {
523
- resource: "services",
524
- action: "list",
525
- filter: { deal_id: "<deal_id>" }
526
- }
527
- }] };
528
- }
529
- /**
530
- * Generate hints for a company
531
- */
532
- function getCompanyHints(companyId) {
533
- return { related_resources: [
534
- {
535
- resource: "projects",
536
- description: "Get projects for this company",
537
- example: {
538
- resource: "projects",
539
- action: "list",
540
- filter: { company_id: companyId }
541
- }
542
- },
543
- {
544
- resource: "deals",
545
- description: "Get deals for this company",
546
- example: {
547
- resource: "deals",
548
- action: "list",
549
- filter: { company_id: companyId }
550
- }
551
- },
552
- {
553
- resource: "tasks",
554
- description: "Get tasks for this company",
555
- example: {
556
- resource: "tasks",
557
- action: "list",
558
- filter: { company_id: companyId }
559
- }
560
- },
561
- {
562
- resource: "people",
563
- description: "Get contacts at this company",
564
- example: {
565
- resource: "people",
566
- action: "list",
567
- filter: { company_id: companyId }
568
- }
569
- },
570
- {
571
- resource: "custom_fields",
572
- description: "List custom field definitions for companies (to resolve custom_fields values)",
573
- example: {
574
- resource: "custom_fields",
575
- action: "list",
576
- filter: { customizable_type: "Company" }
577
- }
578
- }
579
- ] };
580
- }
581
- /**
582
- * Generate hints for a time entry
583
- */
584
- function getTimeEntryHints(timeEntryId, taskId, serviceId) {
585
- const hints = {
586
- related_resources: [],
587
- common_actions: [{
588
- action: "Update this time entry",
589
- example: {
590
- resource: "time",
591
- action: "update",
592
- id: timeEntryId,
593
- time: 120,
594
- note: "<updated note>"
595
- }
596
- }]
597
- };
598
- if (taskId) hints.related_resources.push({
599
- resource: "tasks",
600
- description: "Get the associated task",
601
- example: {
602
- resource: "tasks",
603
- action: "get",
604
- id: taskId
605
- }
606
- });
607
- if (serviceId) hints.related_resources.push({
608
- resource: "services",
609
- description: "Get the associated service",
610
- example: {
611
- resource: "services",
612
- action: "get",
613
- id: serviceId
614
- }
615
- });
616
- return hints;
617
- }
618
- /**
619
- * Generate hints for a comment
620
- */
621
- function getCommentHints(_commentId, commentableType, commentableId) {
622
- const hints = { related_resources: [] };
623
- if (commentableType && commentableId) {
624
- const resource = {
625
- task: "tasks",
626
- deal: "deals",
627
- project: "projects",
628
- company: "companies"
629
- }[commentableType];
630
- if (resource) hints.related_resources.push({
631
- resource,
632
- description: `Get the ${commentableType} this comment is on`,
633
- example: {
634
- resource,
635
- action: "get",
636
- id: commentableId
637
- }
638
- });
639
- }
640
- return hints;
641
- }
642
- /**
643
- * Generate hints for an attachment
644
- */
645
- function getAttachmentHints(_attachmentId, attachableType) {
646
- const hints = {
647
- related_resources: [],
648
- common_actions: [{
649
- action: "Delete this attachment",
650
- example: {
651
- resource: "attachments",
652
- action: "delete",
653
- id: _attachmentId
654
- }
655
- }]
656
- };
657
- if (attachableType) {
658
- const resource = {
659
- Task: "tasks",
660
- Comment: "comments",
661
- Deal: "deals",
662
- Page: "projects"
663
- }[attachableType];
664
- if (resource) hints.related_resources.push({
665
- resource,
666
- description: `View the ${attachableType.toLowerCase()} this attachment belongs to`,
667
- example: {
668
- resource,
669
- action: "list"
670
- }
671
- });
672
- }
673
- return hints;
674
- }
675
- /**
676
- * Generate hints for a booking
677
- */
678
- function getBookingHints(bookingId, personId) {
679
- const hints = {
680
- related_resources: [],
681
- common_actions: [{
682
- action: "Update this booking",
683
- example: {
684
- resource: "bookings",
685
- action: "update",
686
- id: bookingId,
687
- time: 480
688
- }
689
- }]
690
- };
691
- if (personId) hints.related_resources.push({
692
- resource: "people",
693
- description: "Get the person this booking is for",
694
- example: {
695
- resource: "people",
696
- action: "get",
697
- id: personId
698
- }
699
- });
700
- return hints;
701
- }
702
- /**
703
- * Generate hints for a page
704
- */
705
- function getPageHints(pageId) {
706
- return {
707
- related_resources: [
708
- {
709
- resource: "discussions",
710
- description: "Get discussions on this page",
711
- example: {
712
- resource: "discussions",
713
- action: "list",
714
- filter: { page_id: pageId }
715
- }
716
- },
717
- {
718
- resource: "comments",
719
- description: "Get comments on this page",
720
- example: {
721
- resource: "comments",
722
- action: "list",
723
- filter: { page_id: pageId }
724
- }
725
- },
726
- {
727
- resource: "pages",
728
- description: "Get sub-pages of this page",
729
- example: {
730
- resource: "pages",
731
- action: "list",
732
- filter: { parent_page_id: pageId }
733
- }
734
- }
735
- ],
736
- common_actions: [{
737
- action: "Create a discussion",
738
- example: {
739
- resource: "discussions",
740
- action: "create",
741
- page_id: pageId,
742
- body: "<your discussion>"
743
- }
744
- }, {
745
- action: "Create a sub-page",
746
- example: {
747
- resource: "pages",
748
- action: "create",
749
- parent_page_id: pageId,
750
- title: "<sub-page title>",
751
- project_id: "<project_id>"
752
- }
753
- }]
754
- };
755
- }
756
- /**
757
- * Generate hints for a discussion
758
- */
759
- function getDiscussionHints(discussionId, pageId) {
760
- const hints = {
761
- related_resources: [{
762
- resource: "comments",
763
- description: "Get comments on this discussion",
764
- example: {
765
- resource: "comments",
766
- action: "list",
767
- filter: { discussion_id: discussionId }
768
- }
769
- }],
770
- common_actions: [{
771
- action: "Resolve this discussion",
772
- example: {
773
- resource: "discussions",
774
- action: "resolve",
775
- id: discussionId
776
- }
777
- }, {
778
- action: "Add a comment",
779
- example: {
780
- resource: "comments",
781
- action: "create",
782
- discussion_id: discussionId,
783
- body: "<your comment>"
784
- }
785
- }]
786
- };
787
- if (pageId) hints.related_resources.push({
788
- resource: "pages",
789
- description: "Get the page this discussion is on",
790
- example: {
791
- resource: "pages",
792
- action: "get",
793
- id: pageId
794
- }
795
- });
796
- return hints;
797
- }
798
- /**
799
- * Generate hints for a custom field definition
800
- */
801
- function getCustomFieldHints(fieldId) {
802
- return {
803
- related_resources: [{
804
- resource: "custom_fields",
805
- description: "List all custom field definitions for a resource type",
806
- example: {
807
- resource: "custom_fields",
808
- action: "list",
809
- filter: { customizable_type: "Task" }
810
- }
811
- }],
812
- common_actions: [{
813
- action: "Get this custom field with its options",
814
- example: {
815
- resource: "custom_fields",
816
- action: "get",
817
- id: fieldId,
818
- include: ["options"]
819
- }
820
- }]
821
- };
822
- }
823
- /**
824
- * Generate hints for a timer
825
- */
826
- function getTimerHints(timerId, serviceId) {
827
- const hints = {
828
- common_actions: [{
829
- action: "Stop this timer",
830
- example: {
831
- resource: "timers",
832
- action: "stop",
833
- id: timerId
834
- }
835
- }],
836
- related_resources: []
837
- };
838
- if (serviceId) hints.related_resources.push({
839
- resource: "services",
840
- description: "Get the service this timer is running on",
841
- example: {
842
- resource: "services",
843
- action: "get",
844
- id: serviceId
845
- }
846
- });
847
- return hints;
848
- }
849
- //#endregion
850
- //#region src/suggestions.ts
851
- /**
852
- * Get today's date string in YYYY-MM-DD format
853
- */
854
- function getToday() {
855
- return (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
856
- }
857
- /**
858
- * Get suggestions for tasks.list response.
859
- *
860
- * - Warns about overdue tasks (due_date < today and not closed)
861
- * - Informs about unassigned tasks
862
- */
863
- function getTaskListSuggestions(tasks) {
864
- const suggestions = [];
865
- if (!tasks || tasks.length === 0) return suggestions;
866
- const today = getToday();
867
- let overdueCount = 0;
868
- let unassignedCount = 0;
869
- for (const task of tasks) {
870
- const attrs = task.attributes;
871
- const relationships = task.relationships;
872
- const dueDate = attrs.due_date;
873
- const closed = attrs.closed;
874
- const closedAt = attrs.closed_at;
875
- if (dueDate && dueDate < today && !closed && !closedAt) overdueCount++;
876
- if (!(relationships?.assignee)?.data) unassignedCount++;
877
- }
878
- if (overdueCount > 0) suggestions.push(`⚠️ ${overdueCount} task(s) are overdue`);
879
- if (unassignedCount > 0) suggestions.push(`ℹ️ ${unassignedCount} task(s) have no assignee`);
880
- return suggestions;
881
- }
882
- /**
883
- * Get suggestions for tasks.get response.
884
- *
885
- * - Warns if the single task is overdue (and by how many days)
886
- * - Informs if no time has been logged (only when time_entries are included)
887
- */
888
- function getTaskGetSuggestions(task, included) {
889
- const suggestions = [];
890
- if (!task) return suggestions;
891
- const attrs = task.attributes;
892
- const today = getToday();
893
- const dueDate = attrs.due_date;
894
- const closed = attrs.closed;
895
- const closedAt = attrs.closed_at;
896
- if (dueDate && dueDate < today && !closed && !closedAt) {
897
- const due = new Date(dueDate);
898
- const now = new Date(today);
899
- const diffDays = Math.round((now.getTime() - due.getTime()) / (1e3 * 60 * 60 * 24));
900
- suggestions.push(`⚠️ Task is ${diffDays} day(s) overdue`);
901
- }
902
- if (included && included.length > 0) {
903
- const taskId = task.id;
904
- if (included.filter((r) => {
905
- if (r.type !== "time_entries") return false;
906
- return ((r.relationships?.task)?.data)?.id === taskId;
907
- }).length === 0) suggestions.push("ℹ️ No time entries on this task");
908
- }
909
- return suggestions;
910
- }
911
- /**
912
- * Get suggestions for time.list response.
913
- *
914
- * - Shows total hours logged across all entries in the response.
915
- * - If filtered by today, shows hours vs 8h target.
916
- */
917
- function getTimeListSuggestions(entries, filter) {
918
- const suggestions = [];
919
- if (!entries || entries.length === 0) return suggestions;
920
- const totalMinutes = entries.reduce((sum, entry) => {
921
- return sum + (entry.attributes.time || 0);
922
- }, 0);
923
- const totalHours = +(totalMinutes / 60).toFixed(1);
924
- const today = getToday();
925
- if (filter?.after === today && filter?.before === today) suggestions.push(`📊 ${totalHours}h/8h logged today`);
926
- else if (totalMinutes > 0) suggestions.push(`📊 Total: ${totalHours}h logged`);
927
- return suggestions;
928
- }
929
- /**
930
- * Get suggestions for summaries.my_day response.
931
- *
932
- * - Warns if no time has been logged today.
933
- * - Warns if a timer has been running for more than 2 hours.
934
- */
935
- function getMyDaySuggestions(data) {
936
- const suggestions = [];
937
- if (!data) return suggestions;
938
- if (data.time.logged_today_minutes === 0 && data.time.entries_today === 0) suggestions.push("⚠️ No time logged today");
939
- if (data.timers && data.timers.length > 0) {
940
- for (const timer of data.timers) if (timer.total_time > 120) {
941
- const hours = +(timer.total_time / 60).toFixed(1);
942
- suggestions.push(`⏱️ Timer running for ${hours}h — remember to stop it`);
943
- }
944
- }
945
- return suggestions;
946
- }
947
- //#endregion
948
- //#region src/handlers/utils.ts
949
- /**
950
- * Helper to create a successful JSON response
951
- */
952
- function jsonResult(data) {
953
- return { content: [{
954
- type: "text",
955
- text: JSON.stringify(data, null, 2)
956
- }] };
957
- }
958
- /**
959
- * Helper to create an error response from a string message
960
- */
961
- function errorResult(message) {
962
- return {
963
- content: [{
964
- type: "text",
965
- text: `**Error:** ${message}`
966
- }],
967
- isError: true
968
- };
969
- }
970
- /**
971
- * Helper to create an error response from a UserInputError
972
- * Includes formatted hints for LLM consumption
973
- */
974
- function inputErrorResult(error) {
975
- return {
976
- content: [{
977
- type: "text",
978
- text: error.toFormattedMessage()
979
- }],
980
- isError: true
981
- };
982
- }
983
- /**
984
- * Helper to create an error response from any error type
985
- * Automatically formats UserInputError with hints
986
- */
987
- function formatError(error) {
988
- if (isUserInputError(error)) return inputErrorResult(error);
989
- return errorResult(error instanceof Error ? error.message : String(error));
990
- }
991
- /**
992
- * Convert unknown filter to string filter for API
993
- */
994
- function toStringFilter(filter) {
995
- if (!filter) return void 0;
996
- const result = {};
997
- for (const [key, value] of Object.entries(filter)) if (value !== void 0 && value !== null) result[key] = String(value);
998
- return Object.keys(result).length > 0 ? result : void 0;
999
- }
1000
- //#endregion
1001
- //#region src/handlers/resolve.ts
1002
- /**
1003
- * Resolve handler for MCP.
1004
- *
1005
- * Thin wrapper around core's resource resolver.
1006
- * Provides handleResolve for the MCP 'resolve' action.
1007
- */
1008
- /**
1009
- * Handle resolve action for a resource.
1010
- *
1011
- * Delegates to core's resolveResource function and wraps
1012
- * errors in MCP-friendly format.
1013
- */
1014
- async function handleResolve(args, ctx) {
1015
- const { query, type, project_id } = args;
1016
- if (!query) return errorResult("query is required for resolve action");
1017
- try {
1018
- const results = await resolveResource(ctx.executor().api, query, {
1019
- type,
1020
- projectId: project_id
1021
- });
1022
- return jsonResult({
1023
- query,
1024
- matches: results,
1025
- exact: results.length === 1 && results[0].exact
1026
- });
1027
- } catch (error) {
1028
- if (error instanceof ResolveError) return inputErrorResult(new UserInputError(error.message, [`Query: "${error.query}"`, ...error.type ? [`Type: ${error.type}`] : []]));
1029
- throw error;
1030
- }
1031
- }
1032
- //#endregion
1033
- //#region src/handlers/factory.ts
1034
- /**
1035
- * Merge user includes with defaults, ensuring no duplicates
1036
- */
1037
- function mergeIncludes(userInclude, defaults) {
1038
- if (!userInclude?.length && !defaults?.length) return void 0;
1039
- if (!userInclude?.length) return defaults;
1040
- if (!defaults?.length) return userInclude;
1041
- return [...new Set([...defaults, ...userInclude])];
1042
- }
1043
- /**
1044
- * Create a resource handler function from configuration.
1045
- *
1046
- * @example
1047
- * ```typescript
1048
- * export const handleProjects = createResourceHandler({
1049
- * resource: 'projects',
1050
- * actions: ['list', 'get', 'resolve'],
1051
- * formatter: formatProject,
1052
- * hints: (data, id) => getProjectHints(id),
1053
- * supportsResolve: true,
1054
- * executors: {
1055
- * list: listProjects,
1056
- * get: getProject,
1057
- * },
1058
- * });
1059
- * ```
1060
- */
1061
- function createResourceHandler(config) {
1062
- const { resource, displayName = resource, actions, formatter, hints, defaultInclude, supportsResolve, listFilterFromArgs, resolveArgsFromArgs, customActions, create: createConfig, update: updateConfig, executors } = config;
1063
- return async (action, args, ctx) => {
1064
- const { formatOptions, filter, page, perPage, include: userInclude } = ctx;
1065
- const { id, query, type } = args;
1066
- const execCtx = ctx.executor();
1067
- if (customActions?.[action]) return customActions[action](args, ctx, execCtx);
1068
- if (action === "resolve") {
1069
- if (!supportsResolve) return inputErrorResult(ErrorMessages.invalidAction(action, resource, actions));
1070
- return handleResolve({
1071
- query,
1072
- type,
1073
- ...resolveArgsFromArgs?.(args)
1074
- }, ctx);
1075
- }
1076
- if (action === "get") {
1077
- if (!executors.get) return inputErrorResult(ErrorMessages.invalidAction(action, resource, actions));
1078
- if (!id) return inputErrorResult(ErrorMessages.missingId("get"));
1079
- const include = mergeIncludes(userInclude, defaultInclude?.get);
1080
- const result = await executors.get({
1081
- id,
1082
- include
1083
- }, execCtx);
1084
- const getResponseData = { ...formatter(result.data, {
1085
- ...formatOptions,
1086
- included: result.included
1087
- }) };
1088
- if (ctx.includeHints) {
1089
- if (hints) getResponseData._hints = hints(result.data, id);
1090
- }
1091
- if (ctx.includeSuggestions !== false) {
1092
- let getSuggestions = [];
1093
- if (resource === "tasks") getSuggestions = getTaskGetSuggestions(result.data, result.included);
1094
- if (getSuggestions.length > 0) getResponseData._suggestions = getSuggestions;
1095
- }
1096
- return jsonResult(getResponseData);
1097
- }
1098
- if (action === "create") {
1099
- if (!executors.create || !createConfig) return inputErrorResult(ErrorMessages.invalidAction(action, resource, actions));
1100
- const missingFields = createConfig.required.filter((field) => !args[field]);
1101
- if (missingFields.length > 0) return inputErrorResult(ErrorMessages.missingRequiredFields(displayName, missingFields));
1102
- if (createConfig.validateArgs) {
1103
- const errorResult = createConfig.validateArgs(args);
1104
- if (errorResult) return errorResult;
1105
- }
1106
- const options = createConfig.mapOptions(args);
1107
- return jsonResult({
1108
- success: true,
1109
- ...formatter((await executors.create(options, execCtx)).data, formatOptions)
1110
- });
1111
- }
1112
- if (action === "update") {
1113
- if (!executors.update || !updateConfig) return inputErrorResult(ErrorMessages.invalidAction(action, resource, actions));
1114
- if (!id) return inputErrorResult(ErrorMessages.missingId("update"));
1115
- if (updateConfig.allowedFields && updateConfig.allowedFields.length > 0) {
1116
- if (!updateConfig.allowedFields.some((field) => args[field] !== void 0)) return inputErrorResult(ErrorMessages.noUpdateFieldsSpecified(updateConfig.allowedFields));
1117
- }
1118
- const options = {
1119
- id,
1120
- ...updateConfig.mapOptions(args)
1121
- };
1122
- return jsonResult({
1123
- success: true,
1124
- ...formatter((await executors.update(options, execCtx)).data, formatOptions)
1125
- });
1126
- }
1127
- if (action === "delete") {
1128
- if (!executors.delete) return inputErrorResult(ErrorMessages.invalidAction(action, resource, actions));
1129
- if (!id) return inputErrorResult(ErrorMessages.missingId("delete"));
1130
- await executors.delete({ id }, execCtx);
1131
- return jsonResult({
1132
- success: true,
1133
- deleted: id
1134
- });
1135
- }
1136
- if (action === "list") {
1137
- const include = mergeIncludes(userInclude, defaultInclude?.list);
1138
- const additionalFilters = {
1139
- ...filter,
1140
- ...listFilterFromArgs?.(args)
1141
- };
1142
- const result = await executors.list({
1143
- page,
1144
- perPage,
1145
- additionalFilters,
1146
- include
1147
- }, execCtx);
1148
- const listResponseData = { ...formatListResponse$1(result.data, formatter, result.meta, {
1149
- ...formatOptions,
1150
- included: result.included
1151
- }) };
1152
- if (result.resolved && Object.keys(result.resolved).length > 0) listResponseData._resolved = result.resolved;
1153
- if (ctx.includeSuggestions !== false) {
1154
- let listSuggestions = [];
1155
- if (resource === "tasks") listSuggestions = getTaskListSuggestions(result.data);
1156
- else if (resource === "time") listSuggestions = getTimeListSuggestions(result.data, additionalFilters);
1157
- if (listSuggestions.length > 0) listResponseData._suggestions = listSuggestions;
1158
- }
1159
- return jsonResult(listResponseData);
1160
- }
1161
- return inputErrorResult(ErrorMessages.invalidAction(action, resource, actions));
1162
- };
1163
- }
1164
- //#endregion
1165
- //#region src/handlers/deals.ts
1166
- /**
1167
- * Deals MCP handler.
1168
- */
1169
- var handleDeals = createResourceHandler({
1170
- resource: "deals",
1171
- displayName: "deal",
1172
- actions: [
1173
- "list",
1174
- "get",
1175
- "create",
1176
- "update",
1177
- "resolve",
1178
- "context"
1179
- ],
1180
- formatter: formatDeal$1,
1181
- hints: (_data, id) => getDealHints(id),
1182
- supportsResolve: true,
1183
- defaultInclude: {
1184
- list: ["company", "deal_status"],
1185
- get: [
1186
- "company",
1187
- "deal_status",
1188
- "responsible"
1189
- ]
1190
- },
1191
- create: {
1192
- required: ["name", "company_id"],
1193
- mapOptions: (args) => ({
1194
- name: args.name,
1195
- companyId: args.company_id
1196
- })
1197
- },
1198
- update: { mapOptions: (args) => ({ name: args.name }) },
1199
- customActions: { context: async (args, ctx, execCtx) => {
1200
- if (!args.id) return inputErrorResult(ErrorMessages.missingId("context"));
1201
- const result = await getDealContext({ id: args.id }, execCtx);
1202
- const formatOptions = {
1203
- ...ctx.formatOptions,
1204
- included: result.included
1205
- };
1206
- return jsonResult({
1207
- ...formatDeal$1(result.data.deal, formatOptions),
1208
- services: result.data.services.map((s) => formatService$1(s, { compact: true })),
1209
- comments: result.data.comments.map((c) => formatComment$1(c, { compact: true })),
1210
- time_entries: result.data.time_entries.map((t) => formatTimeEntry$1(t, { compact: true }))
1211
- });
1212
- } },
1213
- executors: {
1214
- list: listDeals,
1215
- get: getDeal,
1216
- create: createDeal,
1217
- update: updateDeal
1218
- }
1219
- });
1220
- //#endregion
1221
- //#region src/handlers/people.ts
1222
- /**
1223
- * People MCP handler.
1224
- */
1225
- var VALID_ACTIONS$3 = [
1226
- "list",
1227
- "get",
1228
- "me",
1229
- "resolve"
1230
- ];
1231
- async function handlePeople(action, args, ctx, credentials) {
1232
- const { formatOptions, filter, page, perPage } = ctx;
1233
- const { id, query, type } = args;
1234
- if (action === "resolve") return handleResolve({
1235
- query,
1236
- type
1237
- }, ctx);
1238
- const execCtx = ctx.executor();
1239
- if (action === "get") {
1240
- if (!id) return inputErrorResult(ErrorMessages.missingId("get"));
1241
- const formatted = formatPerson$1((await getPerson({ id }, execCtx)).data, formatOptions);
1242
- if (ctx.includeHints !== false) return jsonResult({
1243
- ...formatted,
1244
- _hints: getPersonHints(id)
1245
- });
1246
- return jsonResult(formatted);
1247
- }
1248
- if (action === "me") {
1249
- if (credentials.userId) {
1250
- const formatted = formatPerson$1((await getPerson({ id: credentials.userId }, execCtx)).data, formatOptions);
1251
- if (ctx.includeHints !== false) return jsonResult({
1252
- ...formatted,
1253
- _hints: getPersonHints(credentials.userId)
1254
- });
1255
- return jsonResult(formatted);
1256
- }
1257
- return jsonResult({
1258
- message: "User ID not configured. Set userId in credentials to use this action.",
1259
- hint: "Use action=\"list\" to find people, or configure the user ID in your credentials.",
1260
- organizationId: credentials.organizationId
1261
- });
1262
- }
1263
- if (action === "list") {
1264
- const result = await listPeople({
1265
- page,
1266
- perPage,
1267
- additionalFilters: filter
1268
- }, execCtx);
1269
- const response = formatListResponse$1(result.data, formatPerson$1, result.meta, formatOptions);
1270
- if (result.resolved && Object.keys(result.resolved).length > 0) return jsonResult({
1271
- ...response,
1272
- _resolved: result.resolved
1273
- });
1274
- return jsonResult(response);
1275
- }
1276
- return inputErrorResult(ErrorMessages.invalidAction(action, "people", VALID_ACTIONS$3));
1277
- }
1278
- //#endregion
1279
- //#region src/handlers/projects.ts
1280
- /**
1281
- * Projects MCP handler.
1282
- *
1283
- * Uses the createResourceHandler factory for the common list/get/resolve pattern.
1284
- */
1285
- /**
1286
- * Handle projects resource.
1287
- *
1288
- * Supports: list, get, resolve, context
1289
- */
1290
- var handleProjects = createResourceHandler({
1291
- resource: "projects",
1292
- actions: [
1293
- "list",
1294
- "get",
1295
- "resolve",
1296
- "context"
1297
- ],
1298
- formatter: formatProject$1,
1299
- hints: (_data, id) => getProjectHints(id),
1300
- supportsResolve: true,
1301
- customActions: { context: async (args, ctx, execCtx) => {
1302
- if (!args.id) return inputErrorResult(ErrorMessages.missingId("context"));
1303
- const result = await getProjectContext({ id: args.id }, execCtx);
1304
- const formatOptions = {
1305
- ...ctx.formatOptions,
1306
- included: result.included
1307
- };
1308
- return jsonResult({
1309
- ...formatProject$1(result.data.project, ctx.formatOptions),
1310
- tasks: result.data.tasks.map((t) => formatTask$1(t, {
1311
- ...formatOptions,
1312
- compact: true
1313
- })),
1314
- services: result.data.services.map((s) => formatService$1(s, { compact: true })),
1315
- time_entries: result.data.time_entries.map((t) => formatTimeEntry$1(t, { compact: true }))
1316
- });
1317
- } },
1318
- executors: {
1319
- list: listProjects,
1320
- get: getProject
1321
- }
1322
- });
1323
- //#endregion
1324
- //#region src/handlers/schema.ts
1325
- /**
1326
- * Schema definitions for all resources.
1327
- *
1328
- * This provides a compact, machine-readable specification of each resource's
1329
- * capabilities. For detailed documentation with examples, use action=help.
1330
- */
1331
- var RESOURCE_SCHEMAS = {
1332
- projects: {
1333
- actions: [
1334
- "list",
1335
- "get",
1336
- "resolve"
1337
- ],
1338
- filters: {
1339
- query: "string — text search on project name",
1340
- project_type: "1=internal|2=client",
1341
- company_id: "string",
1342
- responsible_id: "string",
1343
- person_id: "string",
1344
- status: "1=active|2=archived"
1345
- }
1346
- },
1347
- time: {
1348
- actions: [
1349
- "list",
1350
- "get",
1351
- "create",
1352
- "update",
1353
- "delete"
1354
- ],
1355
- filters: {
1356
- person_id: "string|array — use 'me' for current user",
1357
- after: "date YYYY-MM-DD",
1358
- before: "date YYYY-MM-DD",
1359
- date: "date YYYY-MM-DD — exact date",
1360
- project_id: "string|array",
1361
- service_id: "string|array",
1362
- task_id: "string|array",
1363
- company_id: "string|array",
1364
- deal_id: "string|array",
1365
- budget_id: "string|array",
1366
- status: "1=approved|2=unapproved|3=rejected",
1367
- billing_type_id: "1=fixed|2=actuals|3=non_billable",
1368
- invoicing_status: "1=not_invoiced|2=drafted|3=finalized",
1369
- invoiced: "boolean",
1370
- creator_id: "string|array",
1371
- approver_id: "string|array",
1372
- booking_id: "string|array",
1373
- autotracked: "boolean"
1374
- },
1375
- create: {
1376
- person_id: {
1377
- required: true,
1378
- type: "string"
1379
- },
1380
- service_id: {
1381
- required: true,
1382
- type: "string"
1383
- },
1384
- date: {
1385
- required: true,
1386
- type: "date YYYY-MM-DD"
1387
- },
1388
- time: {
1389
- required: true,
1390
- type: "minutes integer"
1391
- },
1392
- note: {
1393
- required: false,
1394
- type: "string"
1395
- },
1396
- task_id: {
1397
- required: false,
1398
- type: "string"
1399
- }
1400
- },
1401
- update: [
1402
- "time",
1403
- "billable_time",
1404
- "date",
1405
- "note"
1406
- ],
1407
- includes: [
1408
- "person",
1409
- "service",
1410
- "task"
1411
- ]
1412
- },
1413
- tasks: {
1414
- actions: [
1415
- "list",
1416
- "get",
1417
- "create",
1418
- "update",
1419
- "resolve"
1420
- ],
1421
- filters: {
1422
- query: "string — text search on task title",
1423
- project_id: "string|array",
1424
- company_id: "string|array",
1425
- assignee_id: "string|array",
1426
- creator_id: "string|array",
1427
- status: "1=open|2=closed (or \"open\", \"closed\", \"all\")",
1428
- task_list_id: "string|array",
1429
- task_list_status: "1=open|2=closed",
1430
- board_id: "string|array",
1431
- workflow_status_id: "string|array — kanban column",
1432
- workflow_status_category_id: "1=not started|2=started|3=closed",
1433
- workflow_id: "string|array",
1434
- parent_task_id: "string|array — for subtasks",
1435
- task_type: "1=parent task|2=subtask",
1436
- overdue_status: "1=not overdue|2=overdue",
1437
- due_date_on: "date YYYY-MM-DD",
1438
- due_date_before: "date YYYY-MM-DD",
1439
- due_date_after: "date YYYY-MM-DD",
1440
- start_date_before: "date YYYY-MM-DD",
1441
- start_date_after: "date YYYY-MM-DD",
1442
- after: "date YYYY-MM-DD — created after",
1443
- before: "date YYYY-MM-DD — created before",
1444
- closed_after: "date YYYY-MM-DD",
1445
- closed_before: "date YYYY-MM-DD",
1446
- project_manager_id: "string|array",
1447
- subscriber_id: "string|array",
1448
- tags: "string"
1449
- },
1450
- create: {
1451
- title: {
1452
- required: true,
1453
- type: "string"
1454
- },
1455
- project_id: {
1456
- required: true,
1457
- type: "string"
1458
- },
1459
- task_list_id: {
1460
- required: true,
1461
- type: "string"
1462
- },
1463
- description: {
1464
- required: false,
1465
- type: "string"
1466
- },
1467
- assignee_id: {
1468
- required: false,
1469
- type: "string"
1470
- }
1471
- },
1472
- includes: [
1473
- "project",
1474
- "assignee",
1475
- "comments",
1476
- "subtasks",
1477
- "workflow_status"
1478
- ]
1479
- },
1480
- services: {
1481
- actions: ["list", "get"],
1482
- filters: {
1483
- project_id: "string|array",
1484
- deal_id: "string|array",
1485
- task_id: "string|array",
1486
- person_id: "string|array",
1487
- name: "string — text match",
1488
- budget_status: "1=open|2=delivered",
1489
- stage_status_id: "1=open|2=won|3=lost|4=delivered (array)",
1490
- billing_type: "1=fixed|2=actuals|3=none",
1491
- unit: "1=hour|2=piece|3=day",
1492
- time_tracking_enabled: "boolean",
1493
- expense_tracking_enabled: "boolean",
1494
- trackable_by_person_id: "string",
1495
- after: "date YYYY-MM-DD",
1496
- before: "date YYYY-MM-DD"
1497
- }
1498
- },
1499
- people: {
1500
- actions: [
1501
- "list",
1502
- "get",
1503
- "me",
1504
- "resolve"
1505
- ],
1506
- filters: {
1507
- query: "string — text search on name or email",
1508
- email: "string — exact email address",
1509
- status: "1=active|2=deactivated",
1510
- person_type: "1=user|2=contact|3=placeholder",
1511
- company_id: "string|array",
1512
- project_id: "string",
1513
- role_id: "string|array",
1514
- team: "string",
1515
- manager_id: "string",
1516
- custom_role_id: "string",
1517
- tags: "string"
1518
- }
1519
- },
1520
- companies: {
1521
- actions: [
1522
- "list",
1523
- "get",
1524
- "create",
1525
- "update",
1526
- "resolve"
1527
- ],
1528
- filters: {
1529
- query: "string — text search on company name",
1530
- name: "string — exact name match",
1531
- company_code: "string",
1532
- billing_name: "string",
1533
- vat: "string",
1534
- status: "integer",
1535
- archived: "boolean",
1536
- project_id: "string|array",
1537
- subsidiary_id: "string|array",
1538
- default_currency: "string — e.g. USD, EUR"
1539
- },
1540
- create: { name: {
1541
- required: true,
1542
- type: "string"
1543
- } }
1544
- },
1545
- comments: {
1546
- actions: [
1547
- "list",
1548
- "get",
1549
- "create",
1550
- "update"
1551
- ],
1552
- filters: {
1553
- task_id: "string",
1554
- project_id: "string|array",
1555
- page_id: "string|array",
1556
- discussion_id: "string",
1557
- draft: "boolean",
1558
- workflow_status_category_id: "string|array"
1559
- },
1560
- create: {
1561
- body: {
1562
- required: true,
1563
- type: "string"
1564
- },
1565
- hidden: {
1566
- required: false,
1567
- type: "boolean — true to hide from client"
1568
- },
1569
- task_id: {
1570
- required: false,
1571
- type: "string — one of task_id, deal_id required"
1572
- },
1573
- deal_id: {
1574
- required: false,
1575
- type: "string — one of task_id, deal_id required"
1576
- }
1577
- },
1578
- update: ["body", "hidden"],
1579
- includes: ["creator"]
1580
- },
1581
- attachments: {
1582
- actions: [
1583
- "list",
1584
- "get",
1585
- "delete"
1586
- ],
1587
- filters: {
1588
- task_id: "string|array",
1589
- comment_id: "string|array",
1590
- page_id: "string|array"
1591
- }
1592
- },
1593
- timers: {
1594
- actions: [
1595
- "list",
1596
- "get",
1597
- "start",
1598
- "stop"
1599
- ],
1600
- filters: {
1601
- person_id: "string",
1602
- time_entry_id: "string",
1603
- started_at: "date ISO 8601",
1604
- stopped_at: "date ISO 8601"
1605
- }
1606
- },
1607
- deals: {
1608
- actions: [
1609
- "list",
1610
- "get",
1611
- "create",
1612
- "update",
1613
- "resolve"
1614
- ],
1615
- filters: {
1616
- query: "string — text search on deal name",
1617
- number: "string — deal number",
1618
- company_id: "string|array",
1619
- project_id: "string|array",
1620
- responsible_id: "string|array",
1621
- creator_id: "string|array",
1622
- pipeline_id: "string|array",
1623
- status_id: "string|array",
1624
- stage_status_id: "1=open|2=won|3=lost (array)",
1625
- type: "1=deal|2=budget",
1626
- deal_type_id: "1=internal|2=client",
1627
- budget_status: "1=open|2=closed",
1628
- project_type: "1=internal project|2=client project",
1629
- subsidiary_id: "string|array",
1630
- tags: "string",
1631
- recurring: "boolean",
1632
- needs_invoicing: "boolean",
1633
- time_approval: "boolean"
1634
- },
1635
- create: {
1636
- name: {
1637
- required: true,
1638
- type: "string"
1639
- },
1640
- company_id: {
1641
- required: true,
1642
- type: "string"
1643
- }
1644
- },
1645
- includes: ["company", "deal_status"]
1646
- },
1647
- bookings: {
1648
- actions: [
1649
- "list",
1650
- "get",
1651
- "create",
1652
- "update"
1653
- ],
1654
- filters: {
1655
- person_id: "string|array",
1656
- service_id: "string",
1657
- project_id: "string|array",
1658
- company_id: "string|array",
1659
- event_id: "string|array",
1660
- task_id: "string|array",
1661
- approver_id: "string|array",
1662
- after: "date YYYY-MM-DD",
1663
- before: "date YYYY-MM-DD",
1664
- started_on: "date YYYY-MM-DD",
1665
- ended_on: "date YYYY-MM-DD",
1666
- booking_type: "event|service",
1667
- draft: "boolean — tentative only",
1668
- with_draft: "boolean — include tentative",
1669
- status: "string|array — approval status alias",
1670
- approval_status: "string|array",
1671
- billing_type_id: "1=fixed|2=actuals|3=none (array)",
1672
- person_type: "1=user|2=contact|3=placeholder",
1673
- canceled: "boolean"
1674
- },
1675
- create: {
1676
- person_id: {
1677
- required: true,
1678
- type: "string"
1679
- },
1680
- started_on: {
1681
- required: true,
1682
- type: "date YYYY-MM-DD"
1683
- },
1684
- ended_on: {
1685
- required: true,
1686
- type: "date YYYY-MM-DD"
1687
- },
1688
- service_id: {
1689
- required: false,
1690
- type: "string — one of service_id, event_id required"
1691
- },
1692
- event_id: {
1693
- required: false,
1694
- type: "string — one of service_id, event_id required"
1695
- }
1696
- }
1697
- },
1698
- pages: {
1699
- actions: [
1700
- "list",
1701
- "get",
1702
- "create",
1703
- "update",
1704
- "delete"
1705
- ],
1706
- filters: {
1707
- project_id: "string|array",
1708
- creator_id: "string",
1709
- edited_at: "date ISO 8601"
1710
- },
1711
- create: {
1712
- title: {
1713
- required: true,
1714
- type: "string"
1715
- },
1716
- project_id: {
1717
- required: true,
1718
- type: "string"
1719
- },
1720
- body: {
1721
- required: false,
1722
- type: "string"
1723
- },
1724
- parent_page_id: {
1725
- required: false,
1726
- type: "string"
1727
- }
1728
- }
1729
- },
1730
- discussions: {
1731
- actions: [
1732
- "list",
1733
- "get",
1734
- "create",
1735
- "update",
1736
- "delete",
1737
- "resolve",
1738
- "reopen"
1739
- ],
1740
- filters: {
1741
- page_id: "string",
1742
- status: "1=active|2=resolved"
1743
- },
1744
- create: {
1745
- body: {
1746
- required: true,
1747
- type: "string"
1748
- },
1749
- page_id: {
1750
- required: true,
1751
- type: "string"
1752
- }
1753
- }
1754
- },
1755
- custom_fields: {
1756
- actions: ["list", "get"],
1757
- filters: {
1758
- customizable_type: "string — Task, Deal, Company, Project, Booking, Service, etc.",
1759
- archived: "boolean",
1760
- name: "string",
1761
- project_id: "string",
1762
- global: "boolean"
1763
- },
1764
- includes: ["options"]
1765
- },
1766
- activities: {
1767
- actions: ["list"],
1768
- filters: {
1769
- event: "string — create, copy, update, delete, etc.",
1770
- type: "1=Comment|2=Changeset|3=Email",
1771
- after: "date ISO 8601",
1772
- before: "date ISO 8601",
1773
- person_id: "string|array",
1774
- project_id: "string|array",
1775
- company_id: "string|array",
1776
- task_id: "string|array",
1777
- deal_id: "string|array",
1778
- discussion_id: "string|array",
1779
- booking_id: "string|array",
1780
- invoice_id: "string|array",
1781
- item_type: "string — Task, Page, Deal, Workspace, etc.",
1782
- parent_type: "string — Task, Page, Deal, etc.",
1783
- root_type: "string — Workspace, Page, Person, etc.",
1784
- participant_id: "string",
1785
- has_attachments: "boolean",
1786
- pinned: "boolean"
1787
- },
1788
- includes: ["creator"]
1789
- },
1790
- reports: {
1791
- actions: ["get"],
1792
- filters: {
1793
- person_id: "string",
1794
- project_id: "string",
1795
- company_id: "string",
1796
- after: "date YYYY-MM-DD",
1797
- before: "date YYYY-MM-DD"
1798
- },
1799
- create: {
1800
- report_type: {
1801
- required: true,
1802
- type: "time_reports|project_reports|budget_reports|..."
1803
- },
1804
- from: {
1805
- required: false,
1806
- type: "date YYYY-MM-DD"
1807
- },
1808
- to: {
1809
- required: false,
1810
- type: "date YYYY-MM-DD"
1811
- },
1812
- group: {
1813
- required: false,
1814
- type: "string — grouping dimension"
1815
- }
1816
- }
1817
- }
1818
- };
1819
- /**
1820
- * Handle schema action - returns compact specification for a specific resource
1821
- */
1822
- function handleSchema(resource) {
1823
- const schema = RESOURCE_SCHEMAS[resource];
1824
- if (!schema) return errorResult(`Unknown resource: ${resource}. Valid resources: ${Object.keys(RESOURCE_SCHEMAS).join(", ")}`);
1825
- return jsonResult({
1826
- resource,
1827
- ...schema
1828
- });
1829
- }
1830
- /**
1831
- * Get schema overview for all resources
1832
- */
1833
- function handleSchemaOverview() {
1834
- return jsonResult({
1835
- _tip: "Use action=\"schema\" with a specific resource for full filter/create/includes spec",
1836
- resources: Object.entries(RESOURCE_SCHEMAS).map(([resource, schema]) => ({
1837
- resource,
1838
- actions: schema.actions
1839
- }))
1840
- });
1841
- }
1842
- //#endregion
1843
- //#region src/handlers/services.ts
1844
- /**
1845
- * Services MCP handler.
1846
- *
1847
- * Uses the createResourceHandler factory for the common list pattern.
1848
- */
1849
- /**
1850
- * Handle services resource.
1851
- *
1852
- * Supports: list
1853
- */
1854
- var handleServices = createResourceHandler({
1855
- resource: "services",
1856
- actions: ["list", "get"],
1857
- formatter: formatService$1,
1858
- executors: {
1859
- list: listServices,
1860
- get: getService
1861
- },
1862
- customActions: { list: async (args, ctx, execCtx) => {
1863
- const { formatOptions, filter, page, perPage } = ctx;
1864
- const additionalFilters = { ...filter };
1865
- const result = await listServices({
1866
- page,
1867
- perPage,
1868
- additionalFilters
1869
- }, execCtx);
1870
- const response = formatListResponse$1(result.data, formatService$1, result.meta, {
1871
- ...formatOptions,
1872
- included: result.included
1873
- });
1874
- const hints = getServiceListHints(additionalFilters);
1875
- if (hints) return jsonResult({
1876
- ...response,
1877
- _hints: hints
1878
- });
1879
- return jsonResult(response);
1880
- } }
1881
- });
1882
- //#endregion
1883
- //#region src/handlers/summaries.ts
1884
- /**
1885
- * Summaries MCP handler.
1886
- *
1887
- * Custom handler for dashboard-style summaries (not using createResourceHandler).
1888
- * Routes actions to the appropriate summary executor.
1889
- */
1890
- var VALID_ACTIONS$2 = [
1891
- "my_day",
1892
- "project_health",
1893
- "team_pulse",
1894
- "help"
1895
- ];
1896
- /**
1897
- * Handle summaries resource.
1898
- *
1899
- * Supports: my_day, project_health, team_pulse
1900
- */
1901
- async function handleSummaries(action, args, ctx) {
1902
- if (!VALID_ACTIONS$2.includes(action)) return inputErrorResult(ErrorMessages.invalidAction(action, "summaries", VALID_ACTIONS$2));
1903
- const execCtx = ctx.executor();
1904
- switch (action) {
1905
- case "my_day": {
1906
- const result = await getMyDaySummary({}, execCtx);
1907
- if (ctx.includeSuggestions !== false) {
1908
- const suggestions = getMyDaySuggestions(result.data);
1909
- if (suggestions.length > 0) return jsonResult({
1910
- ...result.data,
1911
- _suggestions: suggestions
1912
- });
1913
- }
1914
- return jsonResult(result.data);
1915
- }
1916
- case "project_health":
1917
- if (!args.project_id) return inputErrorResult(new UserInputError("project_id is required for project_health summary", [
1918
- "Provide the project_id parameter",
1919
- "You can find project IDs using resource=\"projects\" action=\"list\"",
1920
- "Or use a project number like \"PRJ-123\""
1921
- ]));
1922
- return jsonResult((await getProjectHealthSummary({ projectId: args.project_id }, execCtx)).data);
1923
- case "team_pulse": return jsonResult((await getTeamPulseSummary({}, execCtx)).data);
1924
- case "help": return jsonResult({
1925
- resource: "summaries",
1926
- description: "Dashboard-style summaries that aggregate data from multiple resources",
1927
- actions: {
1928
- my_day: {
1929
- description: "Personal dashboard for the current user",
1930
- parameters: {},
1931
- returns: {
1932
- tasks: "Open and overdue tasks assigned to you",
1933
- time: "Time entries logged today",
1934
- timers: "Currently running timers"
1935
- }
1936
- },
1937
- project_health: {
1938
- description: "Project status with budget burn and task stats",
1939
- parameters: { project_id: "Required. Project ID or project number (e.g., PRJ-123)" },
1940
- returns: {
1941
- project: "Project details",
1942
- tasks: "Open and overdue task counts",
1943
- budget: "Budget burn rate by service",
1944
- recent_activity: "Time tracking activity in last 7 days"
1945
- }
1946
- },
1947
- team_pulse: {
1948
- description: "Team-wide time tracking activity for today",
1949
- parameters: {},
1950
- returns: {
1951
- team: "Counts of active users, those tracking time, and with timers",
1952
- people: "Per-person breakdown of time logged and active timers"
1953
- }
1954
- }
1955
- }
1956
- });
1957
- default: return inputErrorResult(ErrorMessages.invalidAction(action, "summaries", VALID_ACTIONS$2));
1958
- }
1959
- }
1960
- //#endregion
1961
- //#region src/handlers/tasks.ts
1962
- /**
1963
- * Tasks MCP handler.
1964
- */
1965
- var handleTasks = createResourceHandler({
1966
- resource: "tasks",
1967
- displayName: "task",
1968
- actions: [
1969
- "list",
1970
- "get",
1971
- "create",
1972
- "update",
1973
- "resolve",
1974
- "context"
1975
- ],
1976
- formatter: formatTask$1,
1977
- hints: (data, id) => {
1978
- const serviceId = data.relationships?.service?.data?.id;
1979
- return getTaskHints(id, serviceId);
1980
- },
1981
- supportsResolve: true,
1982
- resolveArgsFromArgs: (args) => ({ project_id: args.project_id }),
1983
- defaultInclude: {
1984
- list: ["project", "project.company"],
1985
- get: ["project", "project.company"]
1986
- },
1987
- create: {
1988
- required: [
1989
- "title",
1990
- "project_id",
1991
- "task_list_id"
1992
- ],
1993
- mapOptions: (args) => ({
1994
- title: args.title,
1995
- projectId: args.project_id,
1996
- taskListId: args.task_list_id,
1997
- assigneeId: args.assignee_id,
1998
- description: args.description
1999
- })
2000
- },
2001
- update: { mapOptions: (args) => ({
2002
- title: args.title,
2003
- description: args.description,
2004
- assigneeId: args.assignee_id
2005
- }) },
2006
- customActions: { context: async (args, ctx, execCtx) => {
2007
- if (!args.id) return inputErrorResult(ErrorMessages.missingId("context"));
2008
- const result = await getTaskContext({ id: args.id }, execCtx);
2009
- const formatOptions = {
2010
- ...ctx.formatOptions,
2011
- included: result.included
2012
- };
2013
- return jsonResult({
2014
- ...formatTask$1(result.data.task, formatOptions),
2015
- comments: result.data.comments.map((c) => formatComment$1(c, { compact: true })),
2016
- time_entries: result.data.time_entries.map((t) => formatTimeEntry$1(t, { compact: true })),
2017
- subtasks: result.data.subtasks.map((s) => formatTask$1(s, {
2018
- ...formatOptions,
2019
- compact: true
2020
- }))
2021
- });
2022
- } },
2023
- executors: {
2024
- list: listTasks,
2025
- get: getTask,
2026
- create: createTask,
2027
- update: updateTask
2028
- }
2029
- });
2030
- //#endregion
2031
- //#region src/handlers/activities.ts
2032
- /**
2033
- * Activities MCP handler.
2034
- *
2035
- * Uses the createResourceHandler factory for the common list pattern.
2036
- * Activities are read-only — only `list` is supported.
2037
- */
2038
- /**
2039
- * Handle activities resource.
2040
- *
2041
- * Supports: list
2042
- */
2043
- var handleActivities = createResourceHandler({
2044
- resource: "activities",
2045
- actions: ["list"],
2046
- formatter: formatActivity$1,
2047
- executors: { list: listActivities },
2048
- defaultInclude: { list: ["creator"] },
2049
- listFilterFromArgs: (args) => {
2050
- const filter = {};
2051
- if (args.after) filter.after = args.after;
2052
- if (args.event) filter.event = args.event;
2053
- return filter;
2054
- }
2055
- });
2056
- //#endregion
2057
- //#region src/handlers/attachments.ts
2058
- /**
2059
- * Attachments MCP handler.
2060
- */
2061
- var handleAttachments = createResourceHandler({
2062
- resource: "attachments",
2063
- actions: [
2064
- "list",
2065
- "get",
2066
- "delete"
2067
- ],
2068
- formatter: formatAttachment$1,
2069
- hints: (data, id) => {
2070
- const attachableType = data.attributes?.attachable_type;
2071
- return getAttachmentHints(id, attachableType);
2072
- },
2073
- listFilterFromArgs: (args) => {
2074
- const filters = {};
2075
- if (args.task_id) filters.task_id = args.task_id;
2076
- if (args.comment_id) filters.comment_id = args.comment_id;
2077
- if (args.deal_id) filters.deal_id = args.deal_id;
2078
- return filters;
2079
- },
2080
- executors: {
2081
- list: listAttachments,
2082
- get: getAttachment,
2083
- delete: deleteAttachment
2084
- }
2085
- });
2086
- /**
2087
- * Validate batch operations array
2088
- */
2089
- function validateOperations(operations) {
2090
- if (!Array.isArray(operations)) throw new UserInputError("operations must be an array", ["Provide an array of operation objects", "Each operation needs: { resource: \"...\", action: \"...\", ...params }"]);
2091
- if (operations.length === 0) throw new UserInputError("operations array cannot be empty", ["Provide at least one operation", "Example: operations: [{ resource: \"projects\", action: \"list\" }]"]);
2092
- if (operations.length > 10) throw new UserInputError(`operations array exceeds maximum size of 10`, [`Split your batch into chunks of 10 or fewer operations`, `You provided ${operations.length} operations`]);
2093
- const validatedOps = [];
2094
- const errors = [];
2095
- for (let i = 0; i < operations.length; i++) {
2096
- const op = operations[i];
2097
- if (typeof op !== "object" || op === null) {
2098
- errors.push(`Operation at index ${i}: must be an object`);
2099
- continue;
2100
- }
2101
- const { resource, action } = op;
2102
- if (typeof resource !== "string" || resource.trim() === "") errors.push(`Operation at index ${i}: missing or invalid "resource" field`);
2103
- if (typeof action !== "string" || action.trim() === "") errors.push(`Operation at index ${i}: missing or invalid "action" field`);
2104
- if (errors.length === 0) validatedOps.push(op);
2105
- }
2106
- if (errors.length > 0) throw new UserInputError("Invalid operations in batch", errors);
2107
- return validatedOps;
2108
- }
2109
- /**
2110
- * Execute a single operation and capture result
2111
- */
2112
- async function executeOperation(operation, index, credentials, execute) {
2113
- const { resource, action, ...params } = operation;
2114
- try {
2115
- const result = await execute("productive", {
2116
- resource,
2117
- action,
2118
- ...params
2119
- }, credentials);
2120
- const content = result.content[0];
2121
- if (content?.type === "text") try {
2122
- const data = JSON.parse(content.text);
2123
- if (result.isError) return {
2124
- resource,
2125
- action,
2126
- index,
2127
- error: content.text
2128
- };
2129
- return {
2130
- resource,
2131
- action,
2132
- index,
2133
- data
2134
- };
2135
- } catch {
2136
- if (result.isError) return {
2137
- resource,
2138
- action,
2139
- index,
2140
- error: content.text
2141
- };
2142
- return {
2143
- resource,
2144
- action,
2145
- index,
2146
- data: content.text
2147
- };
2148
- }
2149
- return {
2150
- resource,
2151
- action,
2152
- index,
2153
- data: null
2154
- };
2155
- } catch (err) {
2156
- return {
2157
- resource,
2158
- action,
2159
- index,
2160
- error: err instanceof Error ? err.message : String(err)
2161
- };
2162
- }
2163
- }
2164
- /**
2165
- * Handle batch operation - execute multiple operations in parallel
2166
- *
2167
- * @param operations - Array of operations to execute
2168
- * @param credentials - API credentials
2169
- * @param execute - Function to execute individual operations (injected for testability)
2170
- * @returns Batch response with summary and individual results
2171
- */
2172
- async function handleBatch(operations, credentials, execute) {
2173
- let validatedOps;
2174
- try {
2175
- validatedOps = validateOperations(operations);
2176
- } catch (err) {
2177
- if (err instanceof UserInputError) return inputErrorResult(err);
2178
- throw err;
2179
- }
2180
- const results = await Promise.all(validatedOps.map((op, index) => executeOperation(op, index, credentials, execute)));
2181
- const succeeded = results.filter((r) => r.data !== void 0 && r.error === void 0).length;
2182
- const failed = results.filter((r) => r.error !== void 0).length;
2183
- return jsonResult({
2184
- _batch: {
2185
- total: results.length,
2186
- succeeded,
2187
- failed
2188
- },
2189
- results
2190
- });
2191
- }
2192
- //#endregion
2193
- //#region src/handlers/bookings.ts
2194
- /**
2195
- * Bookings MCP handler.
2196
- */
2197
- var handleBookings = createResourceHandler({
2198
- resource: "bookings",
2199
- displayName: "booking",
2200
- actions: [
2201
- "list",
2202
- "get",
2203
- "create",
2204
- "update"
2205
- ],
2206
- formatter: formatBooking$1,
2207
- hints: (data, id) => {
2208
- const personId = data.relationships?.person?.data?.id;
2209
- return getBookingHints(id, personId);
2210
- },
2211
- defaultInclude: {
2212
- list: ["person", "service"],
2213
- get: ["person", "service"]
2214
- },
2215
- create: {
2216
- required: [
2217
- "person_id",
2218
- "started_on",
2219
- "ended_on"
2220
- ],
2221
- validateArgs: (args) => {
2222
- if (!args.service_id && !args.event_id) return inputErrorResult(ErrorMessages.missingBookingTarget());
2223
- },
2224
- mapOptions: (args) => ({
2225
- personId: args.person_id,
2226
- serviceId: args.service_id ?? "",
2227
- startedOn: args.started_on,
2228
- endedOn: args.ended_on,
2229
- time: args.time,
2230
- note: args.note,
2231
- eventId: args.event_id
2232
- })
2233
- },
2234
- update: { mapOptions: (args) => ({
2235
- startedOn: args.started_on,
2236
- endedOn: args.ended_on,
2237
- time: args.time,
2238
- note: args.note
2239
- }) },
2240
- executors: {
2241
- list: listBookings,
2242
- get: getBooking,
2243
- create: createBooking,
2244
- update: updateBooking
2245
- }
2246
- });
2247
- //#endregion
2248
- //#region src/handlers/comments.ts
2249
- /**
2250
- * Comments MCP handler.
2251
- */
2252
- var handleComments = createResourceHandler({
2253
- resource: "comments",
2254
- actions: [
2255
- "list",
2256
- "get",
2257
- "create",
2258
- "update"
2259
- ],
2260
- formatter: formatComment$1,
2261
- hints: (data, id) => {
2262
- const commentableType = data.attributes?.commentable_type;
2263
- let commentableId;
2264
- if (commentableType === "task") commentableId = data.relationships?.task?.data?.id;
2265
- else if (commentableType === "deal") commentableId = data.relationships?.deal?.data?.id;
2266
- else if (commentableType === "company") commentableId = data.relationships?.company?.data?.id;
2267
- return getCommentHints(id, commentableType, commentableId);
2268
- },
2269
- defaultInclude: {
2270
- list: ["creator"],
2271
- get: ["creator"]
2272
- },
2273
- create: {
2274
- required: ["body"],
2275
- validateArgs: (args) => {
2276
- if (!args.task_id && !args.deal_id && !args.company_id) return inputErrorResult(ErrorMessages.missingCommentTarget());
2277
- },
2278
- mapOptions: (args) => ({
2279
- body: args.body,
2280
- hidden: args.hidden,
2281
- taskId: args.task_id,
2282
- dealId: args.deal_id,
2283
- companyId: args.company_id
2284
- })
2285
- },
2286
- update: {
2287
- allowedFields: ["body", "hidden"],
2288
- mapOptions: (args) => ({
2289
- body: args.body,
2290
- hidden: args.hidden
2291
- })
2292
- },
2293
- executors: {
2294
- list: listComments,
2295
- get: getComment,
2296
- create: createComment,
2297
- update: updateComment
2298
- }
2299
- });
2300
- //#endregion
2301
- //#region src/handlers/companies.ts
2302
- /**
2303
- * Companies MCP handler.
2304
- *
2305
- * Uses the createResourceHandler factory for the common list/get/create/update/resolve pattern.
2306
- */
2307
- /**
2308
- * Handle companies resource.
2309
- *
2310
- * Supports: list, get, create, update, resolve
2311
- */
2312
- var handleCompanies = createResourceHandler({
2313
- resource: "companies",
2314
- actions: [
2315
- "list",
2316
- "get",
2317
- "create",
2318
- "update",
2319
- "resolve"
2320
- ],
2321
- formatter: formatCompany$1,
2322
- hints: (_data, id) => getCompanyHints(id),
2323
- supportsResolve: true,
2324
- create: {
2325
- required: ["name"],
2326
- mapOptions: (args) => ({ name: args.name })
2327
- },
2328
- update: { mapOptions: (args) => ({ name: args.name }) },
2329
- executors: {
2330
- list: listCompanies,
2331
- get: getCompany,
2332
- create: createCompany,
2333
- update: updateCompany
2334
- }
2335
- });
2336
- //#endregion
2337
- //#region src/handlers/custom-fields.ts
2338
- /**
2339
- * Custom Fields MCP handler.
2340
- *
2341
- * Uses the createResourceHandler factory for list/get.
2342
- * Custom fields are read-only — only `list` and `get` are supported.
2343
- */
2344
- /**
2345
- * Handle custom_fields resource.
2346
- *
2347
- * Supports: list, get
2348
- */
2349
- var handleCustomFields = createResourceHandler({
2350
- resource: "custom_fields",
2351
- displayName: "custom field",
2352
- actions: ["list", "get"],
2353
- formatter: formatCustomField$1,
2354
- hints: (_data, id) => getCustomFieldHints(id),
2355
- executors: {
2356
- list: listCustomFields,
2357
- get: getCustomField
2358
- },
2359
- defaultInclude: { get: ["options"] },
2360
- listFilterFromArgs: (args) => {
2361
- const filter = {};
2362
- if (args.customizable_type) filter.customizable_type = args.customizable_type;
2363
- if (args.archived) filter.archived = args.archived;
2364
- return filter;
2365
- }
2366
- });
2367
- //#endregion
2368
- //#region src/handlers/discussions.ts
2369
- /**
2370
- * Discussions MCP handler.
2371
- */
2372
- var STATUS_MAP = {
2373
- active: "1",
2374
- resolved: "2"
2375
- };
2376
- var handleDiscussions = createResourceHandler({
2377
- resource: "discussions",
2378
- actions: [
2379
- "list",
2380
- "get",
2381
- "create",
2382
- "update",
2383
- "delete",
2384
- "resolve",
2385
- "reopen"
2386
- ],
2387
- formatter: formatDiscussion$1,
2388
- hints: (data, id) => {
2389
- const pageId = data.relationships?.page?.data?.id;
2390
- return getDiscussionHints(id, pageId);
2391
- },
2392
- listFilterFromArgs: (args) => {
2393
- const filters = {};
2394
- if (args.status) {
2395
- const mapped = STATUS_MAP[args.status.toLowerCase()];
2396
- if (mapped) filters.status = mapped;
2397
- }
2398
- return filters;
2399
- },
2400
- create: {
2401
- required: ["body", "page_id"],
2402
- mapOptions: (args) => ({
2403
- body: args.body,
2404
- pageId: args.page_id,
2405
- title: args.title
2406
- })
2407
- },
2408
- update: { mapOptions: (args) => ({
2409
- title: args.title,
2410
- body: args.body
2411
- }) },
2412
- customActions: {
2413
- resolve: async (args, ctx, execCtx) => {
2414
- if (!args.id) return inputErrorResult(ErrorMessages.missingId("resolve"));
2415
- return jsonResult({
2416
- success: true,
2417
- ...formatDiscussion$1((await resolveDiscussion({ id: args.id }, execCtx)).data, ctx.formatOptions)
2418
- });
2419
- },
2420
- reopen: async (args, ctx, execCtx) => {
2421
- if (!args.id) return inputErrorResult(ErrorMessages.missingId("reopen"));
2422
- return jsonResult({
2423
- success: true,
2424
- ...formatDiscussion$1((await reopenDiscussion({ id: args.id }, execCtx)).data, ctx.formatOptions)
2425
- });
2426
- }
2427
- },
2428
- executors: {
2429
- list: listDiscussions,
2430
- get: getDiscussion,
2431
- create: createDiscussion,
2432
- update: updateDiscussion,
2433
- delete: deleteDiscussion
2434
- }
2435
- });
2436
- //#endregion
2437
- //#region src/handlers/help.ts
2438
- var RESOURCE_HELP = {
2439
- batch: {
2440
- description: "Execute multiple operations in a single call. Operations run in parallel via Promise.all, reducing round-trips for AI agents.",
2441
- actions: { run: "Execute a batch of operations (max 10)" },
2442
- fields: { operations: "Array of operation objects. Each must have \"resource\" and \"action\", plus any additional params for that resource." },
2443
- examples: [{
2444
- description: "Batch multiple queries",
2445
- params: {
2446
- resource: "batch",
2447
- action: "run",
2448
- operations: [
2449
- {
2450
- resource: "projects",
2451
- action: "get",
2452
- id: "123"
2453
- },
2454
- {
2455
- resource: "time",
2456
- action: "list",
2457
- filter: { project_id: "123" }
2458
- },
2459
- {
2460
- resource: "services",
2461
- action: "list",
2462
- filter: { project_id: "123" }
2463
- }
2464
- ]
2465
- }
2466
- }, {
2467
- description: "Batch create time entries",
2468
- params: {
2469
- resource: "batch",
2470
- action: "run",
2471
- operations: [{
2472
- resource: "time",
2473
- action: "create",
2474
- service_id: "111",
2475
- date: "2024-01-15",
2476
- time: 60,
2477
- note: "Morning work"
2478
- }, {
2479
- resource: "time",
2480
- action: "create",
2481
- service_id: "111",
2482
- date: "2024-01-15",
2483
- time: 120,
2484
- note: "Afternoon work"
2485
- }]
2486
- }
2487
- }]
2488
- },
2489
- projects: {
2490
- description: "Manage projects in Productive.io",
2491
- actions: {
2492
- list: "List all projects with optional filters",
2493
- get: "Get a single project by ID (supports PRJ-123, P-123 format)",
2494
- resolve: "Resolve by project number (PRJ-123, P-123)",
2495
- context: "Get full project context in one call: project details + open tasks + services + recent time entries"
2496
- },
2497
- filters: {
2498
- query: "Text search on project name",
2499
- project_type: "Filter by project type: 1=internal, 2=client",
2500
- company_id: "Filter by company",
2501
- responsible_id: "Filter by project manager",
2502
- person_id: "Filter by team member",
2503
- status: "Filter by status: 1=active, 2=archived"
2504
- },
2505
- fields: {
2506
- id: "Unique project identifier",
2507
- name: "Project name",
2508
- project_number: "Project reference number",
2509
- archived: "Whether the project is archived",
2510
- budget: "Project budget amount"
2511
- },
2512
- examples: [
2513
- {
2514
- description: "Search projects by name",
2515
- params: {
2516
- resource: "projects",
2517
- action: "list",
2518
- query: "website"
2519
- }
2520
- },
2521
- {
2522
- description: "Search projects by name (filter passthrough)",
2523
- params: {
2524
- resource: "projects",
2525
- action: "list",
2526
- filter: { query: "website" }
2527
- }
2528
- },
2529
- {
2530
- description: "List active client projects",
2531
- params: {
2532
- resource: "projects",
2533
- action: "list",
2534
- filter: {
2535
- status: "1",
2536
- project_type: "2"
2537
- }
2538
- }
2539
- },
2540
- {
2541
- description: "List active projects",
2542
- params: {
2543
- resource: "projects",
2544
- action: "list",
2545
- filter: { archived: "false" }
2546
- }
2547
- },
2548
- {
2549
- description: "Get project details",
2550
- params: {
2551
- resource: "projects",
2552
- action: "get",
2553
- id: "12345"
2554
- }
2555
- },
2556
- {
2557
- description: "Get full project context",
2558
- params: {
2559
- resource: "projects",
2560
- action: "context",
2561
- id: "12345"
2562
- }
2563
- }
2564
- ]
2565
- },
2566
- tasks: {
2567
- description: "Manage tasks within projects",
2568
- actions: {
2569
- list: "List tasks with optional filters",
2570
- get: "Get a single task by ID with full details (description, comments, etc.)",
2571
- create: "Create a new task (requires title, project_id, task_list_id)",
2572
- update: "Update an existing task",
2573
- resolve: "Resolve by text search",
2574
- context: "Get full task context in one call: task details + comments + time entries + subtasks"
2575
- },
2576
- filters: {
2577
- query: "Text search on task title",
2578
- project_id: "Filter by project (array)",
2579
- company_id: "Filter by company (array)",
2580
- assignee_id: "Filter by assigned person (array)",
2581
- creator_id: "Filter by task creator (array)",
2582
- status: "Filter by status: 1=open, 2=closed (or \"open\", \"closed\", \"all\")",
2583
- task_list_id: "Filter by task list (array)",
2584
- task_list_status: "Filter by task list status: 1=open, 2=closed",
2585
- task_list_name: "Filter by task list name (text match)",
2586
- board_id: "Filter by board (array)",
2587
- board_name: "Filter by board name (text match)",
2588
- board_status: "Filter by board status: 1=active, 2=archived",
2589
- workflow_status_id: "Filter by workflow status/kanban column (array)",
2590
- workflow_status_category_id: "Filter by workflow status category: 1=not started, 2=started, 3=closed",
2591
- workflow_id: "Filter by workflow (array)",
2592
- parent_task_id: "Filter by parent task (for subtasks) (array)",
2593
- task_type: "Filter by task type: 1=parent task, 2=subtask",
2594
- task_number: "Filter by task number within project",
2595
- overdue_status: "Filter by overdue: 1=not overdue, 2=overdue",
2596
- due_date: "Filter by due date: 1=any, 2=overdue",
2597
- due_date_on: "Filter by exact due date (YYYY-MM-DD)",
2598
- due_date_before: "Filter by due date before (YYYY-MM-DD)",
2599
- due_date_after: "Filter by due date after (YYYY-MM-DD)",
2600
- start_date: "Filter by exact start date (YYYY-MM-DD)",
2601
- start_date_before: "Filter by start date before (YYYY-MM-DD)",
2602
- start_date_after: "Filter by start date after (YYYY-MM-DD)",
2603
- after: "Filter tasks created after date (YYYY-MM-DD)",
2604
- before: "Filter tasks created before date (YYYY-MM-DD)",
2605
- closed_after: "Filter tasks closed after date (YYYY-MM-DD)",
2606
- closed_before: "Filter tasks closed before date (YYYY-MM-DD)",
2607
- project_manager_id: "Filter by project manager (array)",
2608
- project_type: "Filter by project type: 1=internal project, 2=client project",
2609
- subscriber_id: "Filter by subscriber/watcher (array)",
2610
- last_actor_id: "Filter by last person who acted on the task (array)",
2611
- tags: "Filter by tags",
2612
- repeating: "Filter repeating tasks (boolean)"
2613
- },
2614
- includes: [
2615
- "project",
2616
- "project.company",
2617
- "assignee",
2618
- "workflow_status",
2619
- "comments",
2620
- "attachments",
2621
- "subtasks"
2622
- ],
2623
- fields: {
2624
- id: "Unique task identifier",
2625
- title: "Task title",
2626
- description: "Full task description (HTML)",
2627
- number: "Task number within project",
2628
- due_date: "Due date (YYYY-MM-DD)",
2629
- initial_estimate: "Estimated time in minutes",
2630
- worked_time: "Logged time in minutes",
2631
- remaining_time: "Remaining time in minutes",
2632
- closed: "Whether the task is closed"
2633
- },
2634
- examples: [
2635
- {
2636
- description: "Search tasks by title",
2637
- params: {
2638
- resource: "tasks",
2639
- action: "list",
2640
- query: "bug fix"
2641
- }
2642
- },
2643
- {
2644
- description: "Search tasks by title (filter passthrough)",
2645
- params: {
2646
- resource: "tasks",
2647
- action: "list",
2648
- filter: { query: "bug fix" }
2649
- }
2650
- },
2651
- {
2652
- description: "List open tasks for a project",
2653
- params: {
2654
- resource: "tasks",
2655
- action: "list",
2656
- filter: {
2657
- project_id: "12345",
2658
- status: "open"
2659
- }
2660
- }
2661
- },
2662
- {
2663
- description: "List overdue tasks for a project",
2664
- params: {
2665
- resource: "tasks",
2666
- action: "list",
2667
- filter: {
2668
- project_id: "12345",
2669
- overdue_status: "2"
2670
- }
2671
- }
2672
- },
2673
- {
2674
- description: "List subtasks of a parent task",
2675
- params: {
2676
- resource: "tasks",
2677
- action: "list",
2678
- filter: { parent_task_id: "12345" }
2679
- }
2680
- },
2681
- {
2682
- description: "Get task with comments",
2683
- params: {
2684
- resource: "tasks",
2685
- action: "get",
2686
- id: "67890",
2687
- include: ["comments", "assignee"]
2688
- }
2689
- },
2690
- {
2691
- description: "Create a task",
2692
- params: {
2693
- resource: "tasks",
2694
- action: "create",
2695
- title: "New task",
2696
- project_id: "12345",
2697
- task_list_id: "111"
2698
- }
2699
- },
2700
- {
2701
- description: "Get full task context",
2702
- params: {
2703
- resource: "tasks",
2704
- action: "context",
2705
- id: "67890"
2706
- }
2707
- }
2708
- ]
2709
- },
2710
- time: {
2711
- description: "Track time entries against services/tasks",
2712
- actions: {
2713
- list: "List time entries with optional filters",
2714
- get: "Get a single time entry by ID",
2715
- create: "Create a new time entry (requires person_id, service_id, date, time)",
2716
- update: "Update an existing time entry",
2717
- delete: "Delete a time entry",
2718
- resolve: "Resolve related resources (person, project, service)"
2719
- },
2720
- filters: {
2721
- person_id: "Filter by person (use \"me\" for current user) (array)",
2722
- service_id: "Filter by service (array)",
2723
- project_id: "Filter by project (array)",
2724
- task_id: "Filter by task (array)",
2725
- company_id: "Filter by company (array)",
2726
- deal_id: "Filter by deal (array)",
2727
- budget_id: "Filter by budget (array)",
2728
- after: "Filter entries after date (YYYY-MM-DD)",
2729
- before: "Filter entries before date (YYYY-MM-DD)",
2730
- date: "Filter by exact date (YYYY-MM-DD)",
2731
- status: "Filter by approval status: 1=approved, 2=unapproved, 3=rejected (5=submitted, 6=draft if timesheet feature enabled)",
2732
- billing_type_id: "Filter by billing type: 1=fixed, 2=actuals, 3=non_billable",
2733
- invoicing_status: "Filter by invoicing: 1=not_invoiced, 2=drafted, 3=finalized",
2734
- invoiced: "Filter by invoiced status (boolean)",
2735
- creator_id: "Filter by creator (array)",
2736
- approver_id: "Filter by approver (array)",
2737
- responsible_id: "Filter by responsible person (array)",
2738
- booking_id: "Filter by booking (array)",
2739
- invoice_id: "Filter by invoice (array)",
2740
- autotracked: "Filter auto-tracked entries (boolean)"
2741
- },
2742
- fields: {
2743
- id: "Unique time entry identifier",
2744
- date: "Date of the entry (YYYY-MM-DD)",
2745
- time: "Time in minutes",
2746
- note: "Description of work done",
2747
- billable_time: "Billable time in minutes",
2748
- approved: "Whether the entry is approved",
2749
- overhead: "Whether the entry is overhead time",
2750
- started_at: "Start time for timer-based entries (ISO 8601)"
2751
- },
2752
- examples: [{
2753
- description: "List my time entries this week",
2754
- params: {
2755
- resource: "time",
2756
- action: "list",
2757
- filter: {
2758
- person_id: "me",
2759
- after: "2024-01-15",
2760
- before: "2024-01-21"
2761
- }
2762
- }
2763
- }, {
2764
- description: "Log 2 hours",
2765
- params: {
2766
- resource: "time",
2767
- action: "create",
2768
- service_id: "12345",
2769
- date: "2024-01-16",
2770
- time: 120,
2771
- note: "Development work"
2772
- }
2773
- }]
2774
- },
2775
- services: {
2776
- description: "Budget line items within projects",
2777
- actions: {
2778
- list: "List services with optional filters",
2779
- get: "Get a single service by ID"
2780
- },
2781
- filters: {
2782
- project_id: "Filter by project (array)",
2783
- deal_id: "Filter by deal (array)",
2784
- task_id: "Filter by task (array)",
2785
- person_id: "Filter by person (trackable by) (array)",
2786
- name: "Filter by service name (text match)",
2787
- budget_status: "Filter by budget status: 1=open, 2=delivered",
2788
- stage_status_id: "Filter by stage status: 1=open, 2=won, 3=lost, 4=delivered (array)",
2789
- billing_type: "Filter by billing type: 1=fixed, 2=actuals, 3=none",
2790
- unit: "Filter by unit: 1=hour, 2=piece, 3=day",
2791
- time_tracking_enabled: "Filter by time tracking enabled: true/false",
2792
- expense_tracking_enabled: "Filter by expense tracking enabled: true/false",
2793
- trackable_by_person_id: "Filter services trackable by a specific person",
2794
- after: "Filter by service date after (YYYY-MM-DD)",
2795
- before: "Filter by service date before (YYYY-MM-DD)"
2796
- },
2797
- fields: {
2798
- id: "Unique service identifier",
2799
- name: "Service name",
2800
- budgeted_time: "Budgeted time in minutes",
2801
- worked_time: "Logged time in minutes",
2802
- billing_type_id: "Billing type: 1=fixed, 2=actuals, 3=non_billable"
2803
- },
2804
- examples: [{
2805
- description: "List services for a deal (budget line items)",
2806
- params: {
2807
- resource: "services",
2808
- action: "list",
2809
- filter: { deal_id: "12345" }
2810
- }
2811
- }, {
2812
- description: "List services for a project",
2813
- params: {
2814
- resource: "services",
2815
- action: "list",
2816
- filter: { project_id: "12345" }
2817
- }
2818
- }]
2819
- },
2820
- people: {
2821
- description: "Team members and contacts",
2822
- actions: {
2823
- list: "List people with optional filters",
2824
- get: "Get a single person by ID (supports email address)",
2825
- me: "Get the currently authenticated user",
2826
- resolve: "Resolve by email address"
2827
- },
2828
- filters: {
2829
- query: "Text search on name or email",
2830
- email: "Filter by exact email address",
2831
- status: "Filter by status: 1=active, 2=deactivated",
2832
- person_type: "Filter by type: 1=user, 2=contact, 3=placeholder",
2833
- company_id: "Filter by company (array)",
2834
- project_id: "Filter by project",
2835
- role_id: "Filter by role (array)",
2836
- team: "Filter by team name",
2837
- manager_id: "Filter by manager",
2838
- custom_role_id: "Filter by custom role",
2839
- tags: "Filter by tags"
2840
- },
2841
- fields: {
2842
- id: "Unique person identifier",
2843
- name: "Full name",
2844
- first_name: "First name",
2845
- last_name: "Last name",
2846
- email: "Email address",
2847
- title: "Job title",
2848
- active: "Whether the person is active",
2849
- custom_fields: "Employee custom fields (when Employee Fields are enabled)"
2850
- },
2851
- examples: [
2852
- {
2853
- description: "Get current user",
2854
- params: {
2855
- resource: "people",
2856
- action: "me"
2857
- }
2858
- },
2859
- {
2860
- description: "Search people by name",
2861
- params: {
2862
- resource: "people",
2863
- action: "list",
2864
- query: "john"
2865
- }
2866
- },
2867
- {
2868
- description: "List active team members",
2869
- params: {
2870
- resource: "people",
2871
- action: "list",
2872
- filter: { status: "active" }
2873
- }
2874
- }
2875
- ]
2876
- },
2877
- companies: {
2878
- description: "Client companies and organizations",
2879
- actions: {
2880
- list: "List companies with optional filters",
2881
- get: "Get a single company by ID (supports company name)",
2882
- create: "Create a new company (requires name)",
2883
- update: "Update an existing company",
2884
- resolve: "Resolve by company name"
2885
- },
2886
- filters: {
2887
- query: "Text search on company name",
2888
- name: "Filter by exact company name",
2889
- company_code: "Filter by company code",
2890
- billing_name: "Filter by billing/legal name",
2891
- vat: "Filter by VAT number",
2892
- status: "Filter by status (integer)",
2893
- archived: "Filter by archived status (true/false)",
2894
- project_id: "Filter by project (array)",
2895
- subsidiary_id: "Filter by subsidiary (array)",
2896
- default_currency: "Filter by default currency code (e.g. USD, EUR)"
2897
- },
2898
- fields: {
2899
- id: "Unique company identifier",
2900
- name: "Company name",
2901
- billing_name: "Legal/billing name",
2902
- domain: "Website domain",
2903
- vat: "VAT number"
2904
- },
2905
- examples: [{
2906
- description: "Search companies",
2907
- params: {
2908
- resource: "companies",
2909
- action: "list",
2910
- query: "acme"
2911
- }
2912
- }, {
2913
- description: "List active companies",
2914
- params: {
2915
- resource: "companies",
2916
- action: "list",
2917
- filter: { archived: "false" }
2918
- }
2919
- }]
2920
- },
2921
- attachments: {
2922
- description: "File attachments on tasks, comments, and pages",
2923
- actions: {
2924
- list: "List attachments with optional filters",
2925
- get: "Get a single attachment by ID",
2926
- delete: "Delete an attachment by ID"
2927
- },
2928
- filters: {
2929
- task_id: "Filter by task (array)",
2930
- comment_id: "Filter by comment (array)",
2931
- page_id: "Filter by page (array)"
2932
- },
2933
- fields: {
2934
- id: "Unique attachment identifier",
2935
- name: "File name",
2936
- content_type: "MIME type (e.g., image/png, application/pdf)",
2937
- size: "File size in bytes",
2938
- size_human: "Human-readable file size (e.g., 1.5 MB)",
2939
- url: "Download URL",
2940
- attachable_type: "Parent resource type (Task, Comment, Deal, Page)"
2941
- },
2942
- examples: [
2943
- {
2944
- description: "List attachments on a task",
2945
- params: {
2946
- resource: "attachments",
2947
- action: "list",
2948
- filter: { task_id: "12345" }
2949
- }
2950
- },
2951
- {
2952
- description: "Get attachment details",
2953
- params: {
2954
- resource: "attachments",
2955
- action: "get",
2956
- id: "67890"
2957
- }
2958
- },
2959
- {
2960
- description: "Delete an attachment",
2961
- params: {
2962
- resource: "attachments",
2963
- action: "delete",
2964
- id: "67890"
2965
- }
2966
- }
2967
- ]
2968
- },
2969
- comments: {
2970
- description: "Comments on tasks, deals, and other resources",
2971
- actions: {
2972
- list: "List comments with optional filters",
2973
- get: "Get a single comment by ID",
2974
- create: "Create a new comment (requires body and one of: task_id, deal_id, company_id)",
2975
- update: "Update an existing comment"
2976
- },
2977
- filters: {
2978
- task_id: "Filter by task",
2979
- project_id: "Filter by project (array)",
2980
- page_id: "Filter by page (array)",
2981
- discussion_id: "Filter by discussion",
2982
- draft: "Filter draft comments: true/false",
2983
- workflow_status_category_id: "Filter by workflow status category (array)"
2984
- },
2985
- includes: [
2986
- "creator",
2987
- "task",
2988
- "deal"
2989
- ],
2990
- fields: {
2991
- id: "Unique comment identifier",
2992
- body: "Comment text (may contain HTML)",
2993
- hidden: "Boolean — true if hidden from client (default: false)",
2994
- creator: "Person who created the comment"
2995
- },
2996
- examples: [
2997
- {
2998
- description: "List comments on a task",
2999
- params: {
3000
- resource: "comments",
3001
- action: "list",
3002
- filter: { task_id: "12345" }
3003
- }
3004
- },
3005
- {
3006
- description: "Add a comment",
3007
- params: {
3008
- resource: "comments",
3009
- action: "create",
3010
- task_id: "12345",
3011
- body: "Looking good!"
3012
- }
3013
- },
3014
- {
3015
- description: "Add a hidden comment (hidden from client)",
3016
- params: {
3017
- resource: "comments",
3018
- action: "create",
3019
- task_id: "12345",
3020
- body: "Internal note",
3021
- hidden: true
3022
- }
3023
- }
3024
- ]
3025
- },
3026
- timers: {
3027
- description: "Active time tracking timers",
3028
- actions: {
3029
- list: "List active timers",
3030
- get: "Get a single timer by ID",
3031
- start: "Start a new timer (requires service_id or time_entry_id)",
3032
- stop: "Stop an active timer by ID"
3033
- },
3034
- filters: {
3035
- person_id: "Filter by person",
3036
- time_entry_id: "Filter by time entry",
3037
- started_at: "Filter timers started after date (ISO 8601)",
3038
- stopped_at: "Filter timers stopped after date (ISO 8601)"
3039
- },
3040
- fields: {
3041
- id: "Unique timer identifier",
3042
- started_at: "When the timer started (ISO 8601)",
3043
- total_time: "Elapsed time in seconds"
3044
- },
3045
- examples: [
3046
- {
3047
- description: "List active timers",
3048
- params: {
3049
- resource: "timers",
3050
- action: "list"
3051
- }
3052
- },
3053
- {
3054
- description: "Start timer on service",
3055
- params: {
3056
- resource: "timers",
3057
- action: "start",
3058
- service_id: "12345"
3059
- }
3060
- },
3061
- {
3062
- description: "Stop timer",
3063
- params: {
3064
- resource: "timers",
3065
- action: "stop",
3066
- id: "67890"
3067
- }
3068
- }
3069
- ]
3070
- },
3071
- deals: {
3072
- description: "Sales deals, opportunities, and budgets. Budgets are deals with budget=true — use filter[type]=2 to list only budgets.",
3073
- actions: {
3074
- list: "List deals with optional filters",
3075
- get: "Get a single deal by ID (supports D-123, DEAL-123 format)",
3076
- create: "Create a new deal (requires name, company_id)",
3077
- update: "Update an existing deal",
3078
- resolve: "Resolve by deal number (D-123, DEAL-123)",
3079
- context: "Get full deal context in one call: deal details + services + comments + time entries"
3080
- },
3081
- filters: {
3082
- query: "Text search on deal name",
3083
- number: "Filter by deal number",
3084
- company_id: "Filter by company (array)",
3085
- project_id: "Filter by project (array)",
3086
- responsible_id: "Filter by responsible person (array)",
3087
- creator_id: "Filter by creator (array)",
3088
- pipeline_id: "Filter by pipeline (array)",
3089
- stage_status_id: "Filter by stage: 1=open, 2=won, 3=lost (array)",
3090
- status_id: "Filter by deal status (array)",
3091
- type: "Filter by type: 1=deal, 2=budget",
3092
- deal_type_id: "Filter by deal type: 1=internal, 2=client",
3093
- budget_status: "Filter by budget status: 1=open, 2=closed",
3094
- project_type: "Filter by project type: 1=internal project, 2=client project",
3095
- subsidiary_id: "Filter by subsidiary (array)",
3096
- tags: "Filter by tags",
3097
- recurring: "Filter recurring deals (boolean)",
3098
- needs_invoicing: "Filter deals that need invoicing (boolean)",
3099
- time_approval: "Filter by time approval enabled (boolean)"
3100
- },
3101
- includes: [
3102
- "company",
3103
- "deal_status",
3104
- "responsible",
3105
- "project"
3106
- ],
3107
- fields: {
3108
- id: "Unique deal identifier",
3109
- name: "Deal name",
3110
- number: "Deal number",
3111
- date: "Deal date",
3112
- budget: "Whether this deal is a budget (true/false)",
3113
- status: "Current status (from deal_status)"
3114
- },
3115
- examples: [
3116
- {
3117
- description: "Search deals",
3118
- params: {
3119
- resource: "deals",
3120
- action: "list",
3121
- query: "website redesign"
3122
- }
3123
- },
3124
- {
3125
- description: "List deals for a company",
3126
- params: {
3127
- resource: "deals",
3128
- action: "list",
3129
- filter: { company_id: "12345" }
3130
- }
3131
- },
3132
- {
3133
- description: "List only budgets",
3134
- params: {
3135
- resource: "deals",
3136
- action: "list",
3137
- filter: { type: "2" }
3138
- }
3139
- },
3140
- {
3141
- description: "Get full deal context",
3142
- params: {
3143
- resource: "deals",
3144
- action: "context",
3145
- id: "12345"
3146
- }
3147
- }
3148
- ]
3149
- },
3150
- bookings: {
3151
- description: "Resource scheduling and capacity planning",
3152
- actions: {
3153
- list: "List bookings with optional filters",
3154
- get: "Get a single booking by ID",
3155
- create: "Create a new booking (requires person_id, started_on, ended_on, and service_id or event_id)",
3156
- update: "Update an existing booking"
3157
- },
3158
- filters: {
3159
- person_id: "Filter by person (array)",
3160
- service_id: "Filter by service",
3161
- project_id: "Filter by project (array)",
3162
- company_id: "Filter by company (array)",
3163
- event_id: "Filter by event/absence (array)",
3164
- task_id: "Filter by task (array)",
3165
- approver_id: "Filter by approver (array)",
3166
- after: "Filter bookings after date (YYYY-MM-DD)",
3167
- before: "Filter bookings before date (YYYY-MM-DD)",
3168
- started_on: "Filter by exact start date (YYYY-MM-DD)",
3169
- ended_on: "Filter by exact end date (YYYY-MM-DD)",
3170
- booking_type: "Filter by type: event (absence) or service (budget)",
3171
- draft: "Filter tentative bookings only: true/false",
3172
- with_draft: "Include tentative bookings in results: true/false",
3173
- status: "Filter by approval status (alias for approval_status)",
3174
- approval_status: "Filter by approval status (array)",
3175
- billing_type_id: "Filter by billing type: 1=fixed, 2=actuals, 3=none (array)",
3176
- person_type: "Filter by person type: 1=user, 2=contact, 3=placeholder",
3177
- project_type: "Filter by project type (array)",
3178
- tags: "Filter by tags (array)",
3179
- canceled: "Filter canceled bookings (boolean)"
3180
- },
3181
- includes: [
3182
- "person",
3183
- "service",
3184
- "event"
3185
- ],
3186
- fields: {
3187
- id: "Unique booking identifier",
3188
- started_on: "Start date (YYYY-MM-DD)",
3189
- ended_on: "End date (YYYY-MM-DD)",
3190
- time: "Time per day in minutes",
3191
- total_time: "Total booked time in minutes",
3192
- note: "Booking note"
3193
- },
3194
- examples: [{
3195
- description: "List my bookings",
3196
- params: {
3197
- resource: "bookings",
3198
- action: "list",
3199
- filter: { person_id: "me" }
3200
- }
3201
- }]
3202
- },
3203
- pages: {
3204
- description: "Manage pages (wiki/docs) within projects",
3205
- actions: {
3206
- list: "List pages with optional filters",
3207
- get: "Get a single page by ID with full details",
3208
- create: "Create a new page (requires title, project_id)",
3209
- update: "Update an existing page",
3210
- delete: "Delete a page"
3211
- },
3212
- filters: {
3213
- project_id: "Filter by project (array)",
3214
- creator_id: "Filter by creator",
3215
- parent_page_id: "Filter by parent page (for sub-pages)",
3216
- edited_at: "Filter by last edited date (ISO 8601)"
3217
- },
3218
- fields: {
3219
- id: "Unique page identifier",
3220
- title: "Page title",
3221
- body: "Page body content (HTML)",
3222
- public: "Whether the page is publicly accessible",
3223
- version_number: "Current version number",
3224
- parent_page_id: "Parent page ID (for sub-pages)"
3225
- },
3226
- examples: [
3227
- {
3228
- description: "List pages for a project",
3229
- params: {
3230
- resource: "pages",
3231
- action: "list",
3232
- filter: { project_id: "12345" }
3233
- }
3234
- },
3235
- {
3236
- description: "Get page details",
3237
- params: {
3238
- resource: "pages",
3239
- action: "get",
3240
- id: "67890"
3241
- }
3242
- },
3243
- {
3244
- description: "Create a page",
3245
- params: {
3246
- resource: "pages",
3247
- action: "create",
3248
- title: "Getting Started",
3249
- project_id: "12345"
3250
- }
3251
- },
3252
- {
3253
- description: "Create a sub-page",
3254
- params: {
3255
- resource: "pages",
3256
- action: "create",
3257
- title: "Sub-section",
3258
- project_id: "12345",
3259
- parent_page_id: "67890"
3260
- }
3261
- },
3262
- {
3263
- description: "Delete a page",
3264
- params: {
3265
- resource: "pages",
3266
- action: "delete",
3267
- id: "67890"
3268
- }
3269
- }
3270
- ]
3271
- },
3272
- discussions: {
3273
- description: "Manage discussions (comment threads on highlighted page content)",
3274
- actions: {
3275
- list: "List discussions with optional filters",
3276
- get: "Get a single discussion by ID",
3277
- create: "Create a new discussion (requires body, page_id)",
3278
- update: "Update an existing discussion",
3279
- delete: "Delete a discussion",
3280
- resolve: "Resolve a discussion (mark as resolved)",
3281
- reopen: "Reopen a resolved discussion"
3282
- },
3283
- filters: {
3284
- page_id: "Filter by page",
3285
- status: "Filter by status: 1=active, 2=resolved"
3286
- },
3287
- fields: {
3288
- id: "Unique discussion identifier",
3289
- title: "Discussion title",
3290
- body: "Discussion body (HTML)",
3291
- status: "Status: active or resolved",
3292
- resolved_at: "When the discussion was resolved"
3293
- },
3294
- examples: [
3295
- {
3296
- description: "List discussions on a page",
3297
- params: {
3298
- resource: "discussions",
3299
- action: "list",
3300
- filter: { page_id: "12345" }
3301
- }
3302
- },
3303
- {
3304
- description: "List active discussions",
3305
- params: {
3306
- resource: "discussions",
3307
- action: "list",
3308
- status: "active"
3309
- }
3310
- },
3311
- {
3312
- description: "Create a discussion",
3313
- params: {
3314
- resource: "discussions",
3315
- action: "create",
3316
- page_id: "12345",
3317
- body: "Review this section"
3318
- }
3319
- },
3320
- {
3321
- description: "Resolve a discussion",
3322
- params: {
3323
- resource: "discussions",
3324
- action: "resolve",
3325
- id: "67890"
3326
- }
3327
- },
3328
- {
3329
- description: "Reopen a discussion",
3330
- params: {
3331
- resource: "discussions",
3332
- action: "reopen",
3333
- id: "67890"
3334
- }
3335
- }
3336
- ]
3337
- },
3338
- workflows: {
3339
- description: "Compound workflows that chain multiple resource operations into a single tool call. Use these for common multi-step patterns.",
3340
- actions: {
3341
- complete_task: "Mark a task closed, optionally post a comment, and stop running timers",
3342
- log_day: "Create multiple time entries in parallel from a structured list",
3343
- weekly_standup: "Aggregate completed tasks, time logged, and upcoming deadlines for a week"
3344
- },
3345
- fields: {
3346
- task_id: "(complete_task) Required. Task ID to mark as complete",
3347
- comment: "(complete_task) Optional. Completion comment text to post",
3348
- stop_timer: "(complete_task) Optional. Whether to stop running timers (default: true)",
3349
- entries: "(log_day) Required. Array of { project_id, service_id, duration_minutes, note?, date? }",
3350
- date: "(log_day) Optional. Default date for all entries (YYYY-MM-DD, defaults to today)",
3351
- person_id: "(log_day / weekly_standup) Optional. Person to act on (defaults to current user)",
3352
- week_start: "(weekly_standup) Optional. Monday date of the target week (defaults to this Monday)"
3353
- },
3354
- examples: [
3355
- {
3356
- description: "Complete a task with a comment",
3357
- params: {
3358
- resource: "workflows",
3359
- action: "complete_task",
3360
- task_id: "12345",
3361
- comment: "Done! All tests passing."
3362
- }
3363
- },
3364
- {
3365
- description: "Complete a task without stopping timers",
3366
- params: {
3367
- resource: "workflows",
3368
- action: "complete_task",
3369
- task_id: "12345",
3370
- stop_timer: false
3371
- }
3372
- },
3373
- {
3374
- description: "Log a full day across multiple projects",
3375
- params: {
3376
- resource: "workflows",
3377
- action: "log_day",
3378
- date: "2024-01-16",
3379
- entries: [
3380
- {
3381
- project_id: "100",
3382
- service_id: "111",
3383
- duration_minutes: 240,
3384
- note: "Frontend dev"
3385
- },
3386
- {
3387
- project_id: "200",
3388
- service_id: "222",
3389
- duration_minutes: 120,
3390
- note: "Code review"
3391
- },
3392
- {
3393
- project_id: "100",
3394
- service_id: "333",
3395
- duration_minutes: 60,
3396
- note: "Meetings"
3397
- }
3398
- ]
3399
- }
3400
- },
3401
- {
3402
- description: "Get this week standup",
3403
- params: {
3404
- resource: "workflows",
3405
- action: "weekly_standup"
3406
- }
3407
- },
3408
- {
3409
- description: "Get standup for a specific past week",
3410
- params: {
3411
- resource: "workflows",
3412
- action: "weekly_standup",
3413
- week_start: "2024-01-15"
3414
- }
3415
- }
3416
- ]
3417
- },
3418
- custom_fields: {
3419
- description: "Custom field definitions — list and inspect custom fields configured in your organization. Custom fields appear as raw ID hashes on tasks, deals, companies, etc. Use this resource to discover field names, data types, and option values for resolution.",
3420
- actions: {
3421
- list: "List custom field definitions (filter by customizable_type to scope to a resource)",
3422
- get: "Get a single custom field definition with its options (include: options)"
3423
- },
3424
- filters: {
3425
- customizable_type: "Filter by resource type: Task, Deal, Company, Project, Booking, Service, etc.",
3426
- archived: "Filter by archived status (boolean)",
3427
- name: "Filter by field name",
3428
- project_id: "Filter by project ID",
3429
- global: "Filter global custom fields (boolean)"
3430
- },
3431
- includes: ["options"],
3432
- fields: {
3433
- id: "Unique custom field identifier (used as key in custom_fields hash)",
3434
- name: "Human-readable field name",
3435
- data_type: "Field type: text, number, select, date, multi-select, person, attachment",
3436
- data_type_id: "Numeric type: 1=Text, 2=Number, 3=Select, 4=Date, 5=Multi-select, 6=Person, 7=Attachment",
3437
- customizable_type: "Resource type this field applies to (e.g. Task, Deal)",
3438
- archived: "Whether the field is archived",
3439
- required: "Whether the field is required",
3440
- description: "Optional description of the field",
3441
- options: "For select/multi-select: array of {id, value, archived} (when include=options)"
3442
- },
3443
- examples: [
3444
- {
3445
- description: "List custom fields for tasks",
3446
- params: {
3447
- resource: "custom_fields",
3448
- action: "list",
3449
- filter: { customizable_type: "Task" }
3450
- }
3451
- },
3452
- {
3453
- description: "Get a custom field with its options",
3454
- params: {
3455
- resource: "custom_fields",
3456
- action: "get",
3457
- id: "42236",
3458
- include: ["options"]
3459
- }
3460
- },
3461
- {
3462
- description: "List all non-archived custom fields",
3463
- params: {
3464
- resource: "custom_fields",
3465
- action: "list",
3466
- filter: { archived: "false" }
3467
- }
3468
- }
3469
- ]
3470
- },
3471
- activities: {
3472
- description: "Read-only activity feed — audit log of create/update/delete events across the organization",
3473
- actions: { list: "List recent activities with optional filters" },
3474
- filters: {
3475
- event: "Filter by event type: create, copy, update, delete, etc.",
3476
- type: "Filter by activity type: 1=Comment, 2=Changeset, 3=Email",
3477
- after: "Filter to activities after this ISO 8601 timestamp (e.g. 2026-01-01T00:00:00Z)",
3478
- before: "Filter to activities before this ISO 8601 timestamp",
3479
- person_id: "Filter by creator person ID (array)",
3480
- project_id: "Filter by project ID (array)",
3481
- company_id: "Filter by company ID (array)",
3482
- task_id: "Filter by task ID (array)",
3483
- deal_id: "Filter by deal ID (array)",
3484
- discussion_id: "Filter by discussion ID (array)",
3485
- booking_id: "Filter by booking ID (array)",
3486
- invoice_id: "Filter by invoice ID (array)",
3487
- item_type: "Filter by resource type (e.g. Task, Page, Deal, Workspace)",
3488
- parent_type: "Filter by parent resource type (e.g. Task, Page, Deal)",
3489
- root_type: "Filter by root resource type (e.g. Workspace, Page, Person)",
3490
- participant_id: "Filter by participant person ID",
3491
- has_attachments: "Filter activities with attachments (boolean)",
3492
- pinned: "Filter pinned activities (boolean)"
3493
- },
3494
- includes: ["creator"],
3495
- fields: {
3496
- id: "Unique activity identifier",
3497
- event: "Event type: create, update, or delete",
3498
- changeset: "Human-readable summary of field changes (e.g. \"name: null → My Project\")",
3499
- created_at: "When the activity occurred (ISO 8601)",
3500
- creator_name: "Full name of the person who triggered the activity (when creator included)"
3501
- },
3502
- examples: [
3503
- {
3504
- description: "List recent activities",
3505
- params: {
3506
- resource: "activities",
3507
- action: "list"
3508
- }
3509
- },
3510
- {
3511
- description: "List only create events",
3512
- params: {
3513
- resource: "activities",
3514
- action: "list",
3515
- filter: { event: "create" }
3516
- }
3517
- },
3518
- {
3519
- description: "List activities in a date range",
3520
- params: {
3521
- resource: "activities",
3522
- action: "list",
3523
- filter: {
3524
- after: "2026-02-01T00:00:00Z",
3525
- before: "2026-03-01T00:00:00Z"
3526
- }
3527
- }
3528
- },
3529
- {
3530
- description: "List activities by a specific person",
3531
- params: {
3532
- resource: "activities",
3533
- action: "list",
3534
- filter: { person_id: "12345" }
3535
- }
3536
- },
3537
- {
3538
- description: "List task-related activities for a project",
3539
- params: {
3540
- resource: "activities",
3541
- action: "list",
3542
- filter: {
3543
- project_id: "12345",
3544
- item_type: "Task"
3545
- }
3546
- }
3547
- }
3548
- ]
3549
- },
3550
- reports: {
3551
- description: "Generate various reports (time, budget, project, etc.)",
3552
- actions: { get: "Generate a report (requires report_type)" },
3553
- filters: {
3554
- person_id: "Filter by person",
3555
- project_id: "Filter by project",
3556
- company_id: "Filter by company",
3557
- after: "Filter from date (YYYY-MM-DD)",
3558
- before: "Filter to date (YYYY-MM-DD)"
3559
- },
3560
- fields: {
3561
- report_type: "Type of report: time_reports, project_reports, budget_reports, person_reports, invoice_reports, payment_reports, service_reports, task_reports, company_reports, deal_reports, timesheet_reports",
3562
- group: "Grouping dimension (varies by report type)",
3563
- from: "Start date for date range",
3564
- to: "End date for date range"
3565
- },
3566
- examples: [{
3567
- description: "Time report by person",
3568
- params: {
3569
- resource: "reports",
3570
- action: "get",
3571
- report_type: "time_reports",
3572
- group: "person",
3573
- from: "2024-01-01",
3574
- to: "2024-01-31"
3575
- }
3576
- }, {
3577
- description: "Project budget report",
3578
- params: {
3579
- resource: "reports",
3580
- action: "get",
3581
- report_type: "budget_reports",
3582
- filter: { project_id: "12345" }
3583
- }
3584
- }]
3585
- }
3586
- };
3587
- /**
3588
- * Handle help action - returns documentation for a specific resource
3589
- */
3590
- function handleHelp(resource) {
3591
- const help = RESOURCE_HELP[resource];
3592
- if (!help) return jsonResult({
3593
- error: `Unknown resource: ${resource}`,
3594
- available_resources: Object.keys(RESOURCE_HELP),
3595
- _tip: "Call { action: 'help' } without a resource to see all available resources."
3596
- });
3597
- return jsonResult({
3598
- resource,
3599
- ...help
3600
- });
3601
- }
3602
- /**
3603
- * Get help for all resources (overview)
3604
- */
3605
- function handleHelpOverview() {
3606
- return jsonResult({
3607
- message: "Use action=\"help\" with a specific resource for detailed documentation",
3608
- resources: Object.entries(RESOURCE_HELP).map(([resource, help]) => ({
3609
- resource,
3610
- description: help.description,
3611
- actions: Object.keys(help.actions)
3612
- })),
3613
- _tip: "Always call { action: 'help', resource: '<name>' } before your first interaction with any resource to learn valid filters, required fields, and examples."
3614
- });
3615
- }
3616
- //#endregion
3617
- //#region src/handlers/pages.ts
3618
- /**
3619
- * Pages MCP handler.
3620
- *
3621
- * Uses the createResourceHandler factory for the common list/get/create/update/delete pattern.
3622
- */
3623
- /**
3624
- * Handle pages resource.
3625
- *
3626
- * Supports: list, get, create, update, delete
3627
- */
3628
- var handlePages = createResourceHandler({
3629
- resource: "pages",
3630
- actions: [
3631
- "list",
3632
- "get",
3633
- "create",
3634
- "update",
3635
- "delete"
3636
- ],
3637
- formatter: formatPage$1,
3638
- hints: (_data, id) => getPageHints(id),
3639
- supportsResolve: false,
3640
- create: {
3641
- required: ["title", "project_id"],
3642
- mapOptions: (args) => ({
3643
- title: args.title,
3644
- projectId: args.project_id,
3645
- body: args.body,
3646
- parentPageId: args.parent_page_id
3647
- })
3648
- },
3649
- update: { mapOptions: (args) => ({
3650
- title: args.title,
3651
- body: args.body
3652
- }) },
3653
- executors: {
3654
- list: listPages,
3655
- get: getPage,
3656
- create: createPage,
3657
- update: updatePage,
3658
- delete: deletePage
3659
- }
3660
- });
3661
- //#endregion
3662
- //#region src/handlers/pre-validation-guards.ts
3663
- /**
3664
- * Detects the classic `params` mistake.
3665
- *
3666
- * Agents familiar with REST conventions often pass `params` as a top-level
3667
- * field. Zod's `.strip()` silently removes it before any post-parse check
3668
- * could fire, so this must run on the raw args.
3669
- */
3670
- function detectParamsField(args) {
3671
- if (args.params === void 0) return null;
3672
- return inputErrorResult(new UserInputError("Unknown field \"params\". Use \"filter\" instead.", ["Example: { \"filter\": { \"assignee_id\": \"me\" } }", "The MCP tool uses \"filter\" for query parameters, not \"params\""]));
3673
- }
3674
- /**
3675
- * Detects `resource="budgets"` and redirects to `deals` with a type filter.
3676
- *
3677
- * The "budgets" resource was removed. Budgets are deals with `type=2`.
3678
- */
3679
- function detectBudgetsResource(args) {
3680
- if (args.resource !== "budgets") return null;
3681
- return inputErrorResult(new UserInputError("The \"budgets\" resource has been removed. Budgets are deals with type=2.", [
3682
- "Use resource=\"deals\" with filter[type]=\"2\" to list only budgets",
3683
- "To create a budget: resource=\"deals\" action=\"create\" with budget=true",
3684
- "Use action=\"help\" resource=\"deals\" for full documentation"
3685
- ]));
3686
- }
3687
- /**
3688
- * Detects `resource="docs"` and redirects to `pages`.
3689
- */
3690
- function detectDocsResource(args) {
3691
- if (args.resource !== "docs") return null;
3692
- return inputErrorResult(new UserInputError("Unknown resource \"docs\". Did you mean \"pages\"?", [
3693
- "Use resource=\"pages\" to access Productive pages/documents",
3694
- "Use action=\"list\" to list all pages",
3695
- "Use action=\"help\" resource=\"pages\" for full documentation"
3696
- ]));
3697
- }
3698
- /**
3699
- * Detects `action="search"` used on a specific resource.
3700
- *
3701
- * Agents should use `action="list"` with a `query` parameter for text
3702
- * filtering within a single resource, or `resource="search"` for
3703
- * cross-resource search. This guard only fires when `resource` is not
3704
- * already `"search"` to avoid blocking the legitimate search resource.
3705
- */
3706
- function detectSearchAction(args) {
3707
- if (args.action !== "search") return null;
3708
- if (args.resource === "search") return null;
3709
- const resource = typeof args.resource === "string" ? args.resource : "tasks";
3710
- return inputErrorResult(new UserInputError(`action="search" is not supported on resource="${resource}". Use action="list" with a query parameter for text filtering, or use resource="search" for cross-resource search.`, [
3711
- `Use resource="${resource}" action="list" with query="<your search terms>" to filter ${resource}`,
3712
- "Use resource=\"search\" action=\"run\" with query=\"<your search terms>\" to search across all resources",
3713
- `Use action="help" resource="${resource}" to see all supported actions and filters`
3714
- ]));
3715
- }
3716
- /**
3717
- * Detects `action` values that start with `get_`.
3718
- *
3719
- * Agents using function-style naming (e.g. `get_tasks`, `get_projects`)
3720
- * should use plain verbs: `action="get"` for a single item or
3721
- * `action="list"` for multiple items.
3722
- */
3723
- function detectGetUnderscoreAction(args) {
3724
- if (typeof args.action !== "string") return null;
3725
- if (!args.action.startsWith("get_")) return null;
3726
- const suggestedResource = args.action.replace(/^get_/, "").replace(/_/g, " ");
3727
- const resource = typeof args.resource === "string" && args.resource ? args.resource : suggestedResource;
3728
- return inputErrorResult(new UserInputError(`action="${args.action}" is not valid. Actions use simple verbs like "list", "get", "create", not function-style names.`, [
3729
- "To retrieve a single item, use action=\"get\" with an id parameter",
3730
- `To retrieve multiple items, use action="list" (e.g. resource="${resource}" action="list")`,
3731
- `Use action="help" resource="${resource}" to see all supported actions for a resource`
3732
- ]));
3733
- }
3734
- /**
3735
- * Ordered list of pre-validation guards.
3736
- * Guards are evaluated in order; the first match short-circuits the pipeline.
3737
- */
3738
- var PRE_VALIDATION_GUARDS = [
3739
- detectParamsField,
3740
- detectBudgetsResource,
3741
- detectDocsResource,
3742
- detectSearchAction,
3743
- detectGetUnderscoreAction
3744
- ];
3745
- /**
3746
- * Run all pre-validation guards against raw args.
3747
- *
3748
- * Returns the first guard's ToolResult on a match, or `null` if all guards
3749
- * pass (meaning the args look structurally sound and Zod parsing can proceed).
3750
- */
3751
- function runPreValidationGuards(args) {
3752
- for (const guard of PRE_VALIDATION_GUARDS) {
3753
- const result = guard(args);
3754
- if (result !== null) return result;
3755
- }
3756
- return null;
3757
- }
3758
- //#endregion
3759
- //#region src/handlers/reports.ts
3760
- /**
3761
- * Reports MCP handler.
3762
- */
3763
- function formatReportData(data) {
3764
- return data.map((item) => {
3765
- const record = item;
3766
- return {
3767
- id: record.id,
3768
- type: record.type,
3769
- ...record.attributes
3770
- };
3771
- });
3772
- }
3773
- var VALID_ACTIONS$1 = ["get"];
3774
- async function handleReports(action, args, ctx) {
3775
- const { filter, page, perPage } = ctx;
3776
- const { report_type, group, from, to, person_id, project_id, company_id, deal_id, status } = args;
3777
- if (action !== "get") return inputErrorResult(ErrorMessages.invalidAction(action, "reports", VALID_ACTIONS$1));
3778
- if (!report_type) return inputErrorResult(ErrorMessages.missingReportType());
3779
- if (!VALID_REPORT_TYPES.includes(report_type)) return inputErrorResult(ErrorMessages.invalidReportType(report_type, [...VALID_REPORT_TYPES]));
3780
- const execCtx = ctx.executor();
3781
- const result = await getReport({
3782
- reportType: report_type,
3783
- page,
3784
- perPage,
3785
- group,
3786
- from,
3787
- to,
3788
- personId: person_id,
3789
- projectId: project_id,
3790
- companyId: company_id,
3791
- dealId: deal_id,
3792
- status,
3793
- additionalFilters: filter
3794
- }, execCtx);
3795
- return jsonResult({
3796
- data: formatReportData(result.data),
3797
- meta: result.meta
3798
- });
3799
- }
3800
- //#endregion
3801
- //#region src/handlers/search.ts
3802
- /**
3803
- * Resources that support the query filter for text search
3804
- */
3805
- var SEARCHABLE_RESOURCES = [
3806
- "projects",
3807
- "companies",
3808
- "people",
3809
- "tasks",
3810
- "deals"
3811
- ];
3812
- /**
3813
- * Default resources to search when not specified
3814
- */
3815
- var DEFAULT_SEARCH_RESOURCES = [
3816
- "projects",
3817
- "companies",
3818
- "people",
3819
- "tasks"
3820
- ];
3821
- /**
3822
- * Handle cross-resource search.
3823
- *
3824
- * @param query - Search query text (required)
3825
- * @param resources - Resource types to search (optional, defaults to DEFAULT_SEARCH_RESOURCES)
3826
- * @param credentials - Productive API credentials
3827
- * @param execute - Function to execute tool calls (injected for delegation and testing)
3828
- * @returns Grouped search results across all requested resources
3829
- */
3830
- async function handleSearch(query, resources, credentials, execute) {
3831
- if (!query || query.trim() === "") return errorResult("Missing required parameter: query. Provide a non-empty search string.");
3832
- const trimmedQuery = query.trim();
3833
- const resourcesToSearch = resources && resources.length > 0 ? resources : DEFAULT_SEARCH_RESOURCES;
3834
- const invalidResources = resourcesToSearch.filter((r) => !SEARCHABLE_RESOURCES.includes(r));
3835
- if (invalidResources.length > 0) return errorResult(`Invalid searchable resources: ${invalidResources.join(", ")}. Valid searchable resources: ${SEARCHABLE_RESOURCES.join(", ")}.`);
3836
- const searchPromises = resourcesToSearch.map(async (resource) => {
3837
- try {
3838
- const textContent = (await execute("productive", {
3839
- resource,
3840
- action: "list",
3841
- query: trimmedQuery,
3842
- compact: true,
3843
- per_page: 10
3844
- }, credentials)).content.find((c) => c.type === "text");
3845
- if (!textContent || textContent.type !== "text") return [resource, { error: "No content in response" }];
3846
- try {
3847
- const parsed = JSON.parse(textContent.text);
3848
- const items = parsed.items ?? parsed.data ?? [];
3849
- return [resource, { items: Array.isArray(items) ? items : [] }];
3850
- } catch {
3851
- return [resource, { error: "Failed to parse response JSON" }];
3852
- }
3853
- } catch (err) {
3854
- return [resource, { error: err instanceof Error ? err.message : String(err) }];
3855
- }
3856
- });
3857
- const searchResults = await Promise.all(searchPromises);
3858
- const results = {};
3859
- let totalResults = 0;
3860
- for (const [resource, result] of searchResults) if (result.error) results[resource] = { error: result.error };
3861
- else {
3862
- const items = result.items ?? [];
3863
- results[resource] = items;
3864
- totalResults += items.length;
3865
- }
3866
- return jsonResult({
3867
- query: trimmedQuery,
3868
- resources_searched: resourcesToSearch,
3869
- results,
3870
- total_results: totalResults
3871
- });
3872
- }
3873
- //#endregion
3874
- //#region src/handlers/time.ts
3875
- /**
3876
- * Time entries MCP handler.
3877
- *
3878
- * Thin adapter that delegates business logic to core executors
3879
- * and handles MCP-specific concerns (hints, error formatting, JSON results).
3880
- */
3881
- var handleTime = createResourceHandler({
3882
- resource: "time",
3883
- displayName: "time entry",
3884
- actions: [
3885
- "list",
3886
- "get",
3887
- "create",
3888
- "update",
3889
- "delete",
3890
- "resolve"
3891
- ],
3892
- formatter: formatTimeEntry$1,
3893
- hints: (data, id) => {
3894
- const serviceId = data.relationships?.service?.data?.id;
3895
- return getTimeEntryHints(id, void 0, serviceId);
3896
- },
3897
- supportsResolve: true,
3898
- resolveArgsFromArgs: (args) => ({ project_id: args.project_id }),
3899
- customActions: { create: async (args, ctx, execCtx) => {
3900
- const missingFields = [
3901
- "service_id",
3902
- "time",
3903
- "date"
3904
- ].filter((field) => !args[field]);
3905
- if (missingFields.length > 0) return inputErrorResult(ErrorMessages.missingRequiredFields("time entry", missingFields));
3906
- const personId = args.person_id ?? execCtx.config.userId;
3907
- if (!personId) return inputErrorResult(new UserInputError("person_id is required (could not auto-resolve: userId not configured)", ["Provide person_id explicitly", "Or configure userId in your credentials"]));
3908
- return jsonResult({
3909
- success: true,
3910
- ...formatTimeEntry$1((await createTimeEntry({
3911
- personId,
3912
- serviceId: args.service_id,
3913
- time: args.time,
3914
- date: args.date,
3915
- note: args.note ?? void 0,
3916
- taskId: args.task_id,
3917
- projectId: args.project_id
3918
- }, execCtx)).data, ctx.formatOptions)
3919
- });
3920
- } },
3921
- update: { mapOptions: (args) => ({
3922
- time: args.time ?? void 0,
3923
- billable_time: args.billable_time ?? void 0,
3924
- date: args.date ?? void 0,
3925
- note: args.note ?? void 0
3926
- }) },
3927
- executors: {
3928
- list: listTimeEntries,
3929
- get: getTimeEntry,
3930
- create: createTimeEntry,
3931
- update: updateTimeEntry,
3932
- delete: deleteTimeEntry
3933
- }
3934
- });
3935
- //#endregion
3936
- //#region src/handlers/timers.ts
3937
- /**
3938
- * Timers MCP handler.
3939
- */
3940
- var handleTimers = createResourceHandler({
3941
- resource: "timers",
3942
- actions: [
3943
- "list",
3944
- "get",
3945
- "start",
3946
- "stop"
3947
- ],
3948
- formatter: formatTimer$1,
3949
- hints: (_data, id) => getTimerHints(id),
3950
- customActions: {
3951
- start: async (args, ctx, execCtx) => {
3952
- if (!args.service_id && !args.time_entry_id) return inputErrorResult(ErrorMessages.missingServiceForTimer());
3953
- return jsonResult({
3954
- success: true,
3955
- ...formatTimer$1((await startTimer({
3956
- serviceId: args.service_id,
3957
- timeEntryId: args.time_entry_id
3958
- }, execCtx)).data, ctx.formatOptions)
3959
- });
3960
- },
3961
- create: async (args, ctx, execCtx) => {
3962
- if (!args.service_id && !args.time_entry_id) return inputErrorResult(ErrorMessages.missingServiceForTimer());
3963
- return jsonResult({
3964
- success: true,
3965
- ...formatTimer$1((await startTimer({
3966
- serviceId: args.service_id,
3967
- timeEntryId: args.time_entry_id
3968
- }, execCtx)).data, ctx.formatOptions)
3969
- });
3970
- },
3971
- stop: async (args, ctx, execCtx) => {
3972
- if (!args.id) return inputErrorResult(ErrorMessages.missingId("stop"));
3973
- return jsonResult({
3974
- success: true,
3975
- ...formatTimer$1((await stopTimer({ id: args.id }, execCtx)).data, ctx.formatOptions)
3976
- });
3977
- }
3978
- },
3979
- executors: {
3980
- list: listTimers,
3981
- get: getTimer
3982
- }
3983
- });
3984
- //#endregion
3985
- //#region src/handlers/valid-includes.ts
3986
- /**
3987
- * Valid include values per resource.
3988
- *
3989
- * Sourced from help.ts and schema.ts include lists.
3990
- * Resources not listed here have no include validation (pass-through).
3991
- */
3992
- var VALID_INCLUDES = {
3993
- activities: ["creator"],
3994
- custom_fields: ["options"],
3995
- tasks: [
3996
- "project",
3997
- "project.company",
3998
- "assignee",
3999
- "workflow_status",
4000
- "comments",
4001
- "attachments",
4002
- "subtasks"
4003
- ],
4004
- comments: [
4005
- "creator",
4006
- "task",
4007
- "deal"
4008
- ],
4009
- deals: [
4010
- "company",
4011
- "deal_status",
4012
- "responsible",
4013
- "project"
4014
- ],
4015
- bookings: [
4016
- "person",
4017
- "service",
4018
- "event"
4019
- ],
4020
- time: [
4021
- "person",
4022
- "service",
4023
- "task"
4024
- ]
4025
- };
4026
- /**
4027
- * Known misleading include values and their suggestions.
4028
- * These are values that agents commonly try that don't exist.
4029
- */
4030
- var KNOWN_SUGGESTIONS = {
4031
- notes: "Use resource=comments to fetch comments on a resource",
4032
- services: "Use resource=services with filter.deal_id or filter.project_id to list services",
4033
- time_entries: "Use resource=time with a filter (e.g. filter.task_id, filter.project_id) to list time entries",
4034
- time: "Use resource=time with a filter (e.g. filter.task_id) to list time entries",
4035
- user: "Use \"assignee\" or \"person\" instead",
4036
- author: "Use \"creator\" instead",
4037
- owner: "Use \"responsible\" or \"assignee\" instead",
4038
- company: "Use \"project.company\" to include the project's company on tasks",
4039
- status: "Use \"workflow_status\" to include the workflow/kanban status on tasks"
4040
- };
4041
- /**
4042
- * Validate include values for a given resource.
4043
- *
4044
- * Returns the valid and invalid values, plus suggestions for invalid values.
4045
- * If the resource is not in VALID_INCLUDES, skips validation (returns all as valid).
4046
- */
4047
- function validateIncludes(resource, includes) {
4048
- const validSet = VALID_INCLUDES[resource];
4049
- if (!validSet) return null;
4050
- const valid = [];
4051
- const invalid = [];
4052
- const suggestions = {};
4053
- for (const inc of includes) if (validSet.includes(inc)) valid.push(inc);
4054
- else {
4055
- invalid.push(inc);
4056
- if (KNOWN_SUGGESTIONS[inc]) suggestions[inc] = KNOWN_SUGGESTIONS[inc];
4057
- else suggestions[inc] = `Valid includes for ${resource}: ${validSet.join(", ")}`;
4058
- }
4059
- return {
4060
- valid,
4061
- invalid,
4062
- suggestions
4063
- };
4064
- }
4065
- //#endregion
4066
- //#region src/handlers/workflows.ts
4067
- /**
4068
- * Workflows MCP handler.
4069
- *
4070
- * Custom handler for compound workflows that chain multiple executors.
4071
- * NOT using createResourceHandler — standalone routing like summaries.ts.
4072
- *
4073
- * Supported actions:
4074
- * - complete_task: Mark task closed, optionally comment and stop timers
4075
- * - log_day: Create multiple time entries in parallel from a structured list
4076
- * - weekly_standup: Aggregate completed tasks, time logged, and upcoming deadlines
4077
- */
4078
- var VALID_ACTIONS = [
4079
- "complete_task",
4080
- "log_day",
4081
- "weekly_standup",
4082
- "help"
4083
- ];
4084
- /**
4085
- * Handle workflows resource.
4086
- *
4087
- * Supports: complete_task, log_day, weekly_standup
4088
- */
4089
- async function handleWorkflows(action, args, ctx) {
4090
- if (!VALID_ACTIONS.includes(action)) return inputErrorResult(ErrorMessages.invalidAction(action, "workflows", VALID_ACTIONS));
4091
- const execCtx = ctx.executor();
4092
- switch (action) {
4093
- case "complete_task":
4094
- if (!args.task_id) return inputErrorResult(new UserInputError("task_id is required for complete_task workflow", ["Provide the task_id parameter (numeric task ID)", "You can find task IDs using resource=\"tasks\" action=\"list\""]));
4095
- return jsonResult((await completeTask({
4096
- taskId: args.task_id,
4097
- comment: args.comment,
4098
- stopTimer: args.stop_timer
4099
- }, execCtx)).data);
4100
- case "log_day":
4101
- if (!args.entries || !Array.isArray(args.entries) || args.entries.length === 0) return inputErrorResult(new UserInputError("entries is required and must be a non-empty array for log_day workflow", [
4102
- "Provide entries as an array of { project_id, service_id, duration_minutes, note?, date? }",
4103
- "Example: { \"entries\": [{ \"project_id\": \"123\", \"service_id\": \"456\", \"duration_minutes\": 120, \"note\": \"Development\" }] }",
4104
- "You can find service IDs using resource=\"services\" action=\"list\" with filter.project_id"
4105
- ]));
4106
- return jsonResult((await logDay({
4107
- entries: args.entries.map((e) => {
4108
- const entry = e;
4109
- return {
4110
- project_id: String(entry.project_id),
4111
- service_id: String(entry.service_id),
4112
- duration_minutes: Number(entry.duration_minutes),
4113
- note: entry.note != null ? String(entry.note) : void 0,
4114
- date: entry.date != null ? String(entry.date) : void 0
4115
- };
4116
- }),
4117
- date: args.date,
4118
- personId: args.person_id
4119
- }, execCtx)).data);
4120
- case "weekly_standup": return jsonResult((await weeklyStandup({
4121
- personId: args.person_id,
4122
- weekStart: args.week_start
4123
- }, execCtx)).data);
4124
- case "help": return jsonResult({
4125
- resource: "workflows",
4126
- description: "Compound workflows that chain multiple resource operations into a single tool call",
4127
- actions: {
4128
- complete_task: {
4129
- description: "Mark a task as complete, optionally post a comment and stop running timers",
4130
- parameters: {
4131
- task_id: "Required. The task ID to complete",
4132
- comment: "Optional. A completion comment to post on the task",
4133
- stop_timer: "Optional. Whether to stop running timers (default: true)"
4134
- },
4135
- returns: {
4136
- task: "Updated task info (id, title, closed status)",
4137
- comment_posted: "Whether the comment was posted",
4138
- comment_id: "ID of the created comment (if posted)",
4139
- timers_stopped: "Number of timers stopped",
4140
- errors: "Any sub-step errors (partial results are still returned)"
4141
- }
4142
- },
4143
- log_day: {
4144
- description: "Create multiple time entries in parallel from a structured list",
4145
- parameters: {
4146
- entries: "Required. Array of { project_id, service_id, duration_minutes, note?, date? }",
4147
- date: "Optional. Default date for all entries (YYYY-MM-DD, defaults to today)",
4148
- person_id: "Optional. Person to log for (defaults to current user)"
4149
- },
4150
- returns: {
4151
- entries: "Per-entry results with success/failure status",
4152
- succeeded: "Number of entries successfully created",
4153
- failed: "Number of entries that failed",
4154
- total_minutes_logged: "Sum of minutes for successful entries"
4155
- }
4156
- },
4157
- weekly_standup: {
4158
- description: "Aggregate a weekly standup: completed tasks, time logged, and upcoming deadlines",
4159
- parameters: {
4160
- person_id: "Optional. Person to generate standup for (defaults to current user)",
4161
- week_start: "Optional. ISO date for Monday of the week (defaults to this Monday)"
4162
- },
4163
- returns: {
4164
- completed_tasks: "Tasks closed this week (count + list)",
4165
- time_logged: "Total minutes and breakdown by project",
4166
- upcoming_deadlines: "Open tasks due in the next 7 days"
4167
- }
4168
- }
4169
- }
4170
- });
4171
- default: return inputErrorResult(ErrorMessages.invalidAction(action, "workflows", VALID_ACTIONS));
4172
- }
4173
- }
4174
- //#endregion
4175
- //#region src/handlers/index.ts
4176
- /**
4177
- * Tool execution handlers for Productive MCP server
4178
- * These are shared between stdio and HTTP transports
4179
- *
4180
- * Single consolidated tool for minimal token overhead:
4181
- * - productive: resource + action based API
4182
- */
4183
- /** Valid resources for the productive tool (derived from core constants) */
4184
- var VALID_RESOURCES = [...RESOURCES];
4185
- /** Default page size for MCP (smaller than CLI to reduce token usage) */
4186
- var DEFAULT_PER_PAGE = 20;
4187
- /**
4188
- * Route to the appropriate resource handler.
4189
- * Extracted from executeToolWithCredentials to keep cyclomatic complexity manageable.
4190
- */
4191
- async function routeToHandler(resource, action, restArgs, resolveArgs, ctx, credentials) {
4192
- switch (resource) {
4193
- case "projects": return await handleProjects(action, {
4194
- ...restArgs,
4195
- ...resolveArgs
4196
- }, ctx);
4197
- case "time": return await handleTime(action, {
4198
- ...restArgs,
4199
- ...resolveArgs
4200
- }, ctx);
4201
- case "tasks": return await handleTasks(action, {
4202
- ...restArgs,
4203
- ...resolveArgs
4204
- }, ctx);
4205
- case "services": return await handleServices(action, restArgs, ctx);
4206
- case "people": return await handlePeople(action, {
4207
- ...restArgs,
4208
- ...resolveArgs
4209
- }, ctx, credentials);
4210
- case "companies": return await handleCompanies(action, {
4211
- ...restArgs,
4212
- ...resolveArgs
4213
- }, ctx);
4214
- case "comments": return await handleComments(action, restArgs, ctx);
4215
- case "attachments": return await handleAttachments(action, restArgs, ctx);
4216
- case "timers": return await handleTimers(action, restArgs, ctx);
4217
- case "deals": return await handleDeals(action, {
4218
- ...restArgs,
4219
- ...resolveArgs
4220
- }, ctx);
4221
- case "bookings": return await handleBookings(action, restArgs, ctx);
4222
- case "pages": return await handlePages(action, restArgs, ctx);
4223
- case "discussions": return await handleDiscussions(action, restArgs, ctx);
4224
- case "activities": return await handleActivities(action, restArgs, ctx);
4225
- case "custom_fields": return await handleCustomFields(action, restArgs, ctx);
4226
- case "reports": return await handleReports(action, restArgs, ctx);
4227
- case "summaries": return await handleSummaries(action, restArgs, ctx);
4228
- case "workflows": return await handleWorkflows(action, restArgs, ctx);
4229
- default: return inputErrorResult(ErrorMessages.unknownResource(resource, VALID_RESOURCES));
4230
- }
4231
- }
4232
- /**
4233
- * Execute a tool with the given credentials and arguments
4234
- */
4235
- async function executeToolWithCredentials(name, args, credentials) {
4236
- if (name !== "productive") return errorResult(`Unknown tool: ${name}`);
4237
- const typedArgs = args;
4238
- if (typedArgs.resource === "batch") return handleBatch(typedArgs.operations, credentials, executeToolWithCredentials);
4239
- const guardResult = runPreValidationGuards(args);
4240
- if (guardResult) return guardResult;
4241
- const { resource, action, filter, page, per_page, compact, include, query, resources, no_hints, type, ...restArgs } = typedArgs;
4242
- if (resource === "search") return await handleSearch(query, resources, credentials, executeToolWithCredentials);
4243
- const api = new ProductiveApi({ config: {
4244
- apiToken: credentials.apiToken,
4245
- organizationId: credentials.organizationId,
4246
- userId: credentials.userId,
4247
- baseUrl: process.env.PRODUCTIVE_BASE_URL
4248
- } });
4249
- const isCompact = compact ?? action !== "get";
4250
- const formatOptions = { compact: isCompact };
4251
- let stringFilter = toStringFilter(filter);
4252
- const perPage = per_page ?? DEFAULT_PER_PAGE;
4253
- if (query) stringFilter = {
4254
- ...stringFilter,
4255
- query
4256
- };
4257
- const includeHints = no_hints !== true && action === "get" && !isCompact;
4258
- const includeSuggestions = no_hints !== true;
4259
- if (include && include.length > 0) {
4260
- const includeValidation = validateIncludes(resource, include);
4261
- if (includeValidation && includeValidation.invalid.length > 0) {
4262
- const { invalid, valid, suggestions } = includeValidation;
4263
- const hintLines = [`Invalid include value${invalid.length > 1 ? "s" : ""}: ${invalid.join(", ")}`, `Valid includes for ${resource}: ${(VALID_INCLUDES[resource] ?? []).join(", ")}`];
4264
- for (const [value, suggestion] of Object.entries(suggestions)) hintLines.push(`"${value}": ${suggestion}`);
4265
- if (valid.length > 0) hintLines.push(`The following includes are valid and will be used if you remove the invalid ones: ${valid.join(", ")}`);
4266
- return inputErrorResult(new UserInputError(`Invalid include value${invalid.length > 1 ? "s" : ""} for resource "${resource}": ${invalid.join(", ")}`, hintLines));
4267
- }
4268
- }
4269
- const execCtx = fromHandlerContext({ api }, { userId: credentials.userId });
4270
- const ctx = {
4271
- formatOptions,
4272
- filter: stringFilter,
4273
- page,
4274
- perPage,
4275
- include,
4276
- includeHints,
4277
- includeSuggestions,
4278
- executor: () => execCtx
4279
- };
4280
- try {
4281
- if (action === "help" && resource !== "summaries") return resource ? handleHelp(resource) : handleHelpOverview();
4282
- if (action === "schema") return resource ? handleSchema(resource) : handleSchemaOverview();
4283
- return await routeToHandler(resource, action, restArgs, {
4284
- query,
4285
- type
4286
- }, ctx, credentials);
4287
- } catch (error) {
4288
- if (isUserInputError(error)) return formatError(error);
4289
- const message = error instanceof Error ? error.message : String(error);
4290
- const statusMatch = message.match(/(\d{3})/);
4291
- if (statusMatch) {
4292
- const statusCode = Number.parseInt(statusMatch[1], 10);
4293
- return inputErrorResult(ErrorMessages.apiError(statusCode, message));
4294
- }
4295
- return errorResult(message);
4296
- }
4297
- }
4298
- //#endregion
4299
- export { handleSchemaOverview as a, handleDeals as c, handleServices as i, handleTasks as n, handleProjects as o, handleSummaries as r, handlePeople as s, executeToolWithCredentials as t };
4300
-
4301
- //# sourceMappingURL=handlers-vtRpc-Lx.js.map