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