@studiometa/productive-mcp 0.10.2 → 0.10.3
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 +25 -13
- package/dist/formatters.d.ts +1 -0
- package/dist/formatters.d.ts.map +1 -1
- package/dist/handlers/activities.d.ts +26 -0
- package/dist/handlers/activities.d.ts.map +1 -0
- package/dist/handlers/factory.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/services.d.ts.map +1 -1
- package/dist/handlers/summaries.d.ts.map +1 -1
- package/dist/handlers/time.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/valid-includes.d.ts +20 -0
- package/dist/handlers/valid-includes.d.ts.map +1 -0
- package/dist/handlers/workflows.d.ts +35 -0
- package/dist/handlers/workflows.d.ts.map +1 -0
- package/dist/{handlers-B8GRTaDu.js → handlers-CaOBYauF.js} +883 -335
- package/dist/handlers-CaOBYauF.js.map +1 -0
- package/dist/handlers.js +1 -1
- package/dist/hints.d.ts +9 -0
- package/dist/hints.d.ts.map +1 -1
- package/dist/http.js +2 -2
- package/dist/index.js +2 -2
- package/dist/schema.d.ts +10 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/server.js +2 -2
- package/dist/stdio.js +1 -1
- package/dist/suggestions.d.ts +44 -0
- package/dist/suggestions.d.ts.map +1 -0
- package/dist/{version-Bl0krPVf.js → version-BRTaTnUG.js} +2 -2
- package/dist/{version-Bl0krPVf.js.map → version-BRTaTnUG.js.map} +1 -1
- package/package.json +3 -3
- package/skills/SKILL.md +104 -0
- package/dist/handlers-B8GRTaDu.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ProductiveApi, formatAttachment, formatBooking, formatComment, formatCompany, formatDeal, formatDiscussion, formatListResponse, formatPage, formatPerson, formatProject, formatService, formatTask, formatTimeEntry, formatTimer } from "@studiometa/productive-api";
|
|
2
|
-
import { RESOURCES, ResolveError, VALID_REPORT_TYPES, createBooking, createComment, createCompany, createDeal, createDiscussion, createPage, createTask, createTimeEntry, deleteAttachment, deleteDiscussion, deletePage, deleteTimeEntry, fromHandlerContext, getAttachment, getBooking, getComment, getCompany, getDeal, getDealContext, getDiscussion, getMyDaySummary, getPage, getPerson, getProject, getProjectContext, getProjectHealthSummary, getReport, getTask, getTaskContext, getTeamPulseSummary, getTimeEntry, getTimer, listAttachments, listBookings, listComments, listCompanies, listDeals, listDiscussions, listPages, listPeople, listProjects, listServices, listTasks, listTimeEntries, listTimers, reopenDiscussion, resolveDiscussion, resolveResource, startTimer, stopTimer, updateBooking, updateComment, updateCompany, updateDeal, updateDiscussion, updatePage, updateTask, updateTimeEntry } from "@studiometa/productive-core";
|
|
1
|
+
import { ProductiveApi, formatActivity, formatAttachment, formatBooking, formatComment, formatCompany, 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, getDeal, getDealContext, getDiscussion, getMyDaySummary, getPage, getPerson, getProject, getProjectContext, getProjectHealthSummary, getReport, getTask, getTaskContext, getTeamPulseSummary, getTimeEntry, getTimer, listActivities, listAttachments, listBookings, listComments, listCompanies, 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
3
|
/**
|
|
4
4
|
* Custom error classes for MCP server
|
|
5
5
|
*
|
|
@@ -233,6 +233,12 @@ function formatDiscussion$1(discussion, options) {
|
|
|
233
233
|
if (options?.compact) return compactify(result, ["body"]);
|
|
234
234
|
return result;
|
|
235
235
|
}
|
|
236
|
+
function formatActivity$1(activity, options) {
|
|
237
|
+
return formatActivity(activity, {
|
|
238
|
+
...MCP_FORMAT_OPTIONS,
|
|
239
|
+
included: options?.included
|
|
240
|
+
});
|
|
241
|
+
}
|
|
236
242
|
/**
|
|
237
243
|
* Format list response with pagination
|
|
238
244
|
*
|
|
@@ -251,6 +257,337 @@ function formatListResponse$1(data, formatter, meta, options) {
|
|
|
251
257
|
});
|
|
252
258
|
}
|
|
253
259
|
/**
|
|
260
|
+
* Get today's date string in YYYY-MM-DD format
|
|
261
|
+
*/
|
|
262
|
+
function getToday() {
|
|
263
|
+
return (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Get suggestions for tasks.list response.
|
|
267
|
+
*
|
|
268
|
+
* - Warns about overdue tasks (due_date < today and not closed)
|
|
269
|
+
* - Informs about unassigned tasks
|
|
270
|
+
*/
|
|
271
|
+
function getTaskListSuggestions(tasks) {
|
|
272
|
+
const suggestions = [];
|
|
273
|
+
if (!tasks || tasks.length === 0) return suggestions;
|
|
274
|
+
const today = getToday();
|
|
275
|
+
let overdueCount = 0;
|
|
276
|
+
let unassignedCount = 0;
|
|
277
|
+
for (const task of tasks) {
|
|
278
|
+
const attrs = task.attributes;
|
|
279
|
+
const relationships = task.relationships;
|
|
280
|
+
const dueDate = attrs.due_date;
|
|
281
|
+
const closed = attrs.closed;
|
|
282
|
+
const closedAt = attrs.closed_at;
|
|
283
|
+
if (dueDate && dueDate < today && !closed && !closedAt) overdueCount++;
|
|
284
|
+
if (!(relationships?.assignee)?.data) unassignedCount++;
|
|
285
|
+
}
|
|
286
|
+
if (overdueCount > 0) suggestions.push(`⚠️ ${overdueCount} task(s) are overdue`);
|
|
287
|
+
if (unassignedCount > 0) suggestions.push(`ℹ️ ${unassignedCount} task(s) have no assignee`);
|
|
288
|
+
return suggestions;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Get suggestions for tasks.get response.
|
|
292
|
+
*
|
|
293
|
+
* - Warns if the single task is overdue (and by how many days)
|
|
294
|
+
* - Informs if no time has been logged (only when time_entries are included)
|
|
295
|
+
*/
|
|
296
|
+
function getTaskGetSuggestions(task, included) {
|
|
297
|
+
const suggestions = [];
|
|
298
|
+
if (!task) return suggestions;
|
|
299
|
+
const attrs = task.attributes;
|
|
300
|
+
const today = getToday();
|
|
301
|
+
const dueDate = attrs.due_date;
|
|
302
|
+
const closed = attrs.closed;
|
|
303
|
+
const closedAt = attrs.closed_at;
|
|
304
|
+
if (dueDate && dueDate < today && !closed && !closedAt) {
|
|
305
|
+
const due = new Date(dueDate);
|
|
306
|
+
const now = new Date(today);
|
|
307
|
+
const diffDays = Math.round((now.getTime() - due.getTime()) / (1e3 * 60 * 60 * 24));
|
|
308
|
+
suggestions.push(`⚠️ Task is ${diffDays} day(s) overdue`);
|
|
309
|
+
}
|
|
310
|
+
if (included && included.length > 0) {
|
|
311
|
+
const taskId = task.id;
|
|
312
|
+
if (included.filter((r) => {
|
|
313
|
+
if (r.type !== "time_entries") return false;
|
|
314
|
+
return ((r.relationships?.task)?.data)?.id === taskId;
|
|
315
|
+
}).length === 0) suggestions.push("ℹ️ No time entries on this task");
|
|
316
|
+
}
|
|
317
|
+
return suggestions;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Get suggestions for time.list response.
|
|
321
|
+
*
|
|
322
|
+
* - Shows total hours logged across all entries in the response.
|
|
323
|
+
* - If filtered by today, shows hours vs 8h target.
|
|
324
|
+
*/
|
|
325
|
+
function getTimeListSuggestions(entries, filter) {
|
|
326
|
+
const suggestions = [];
|
|
327
|
+
if (!entries || entries.length === 0) return suggestions;
|
|
328
|
+
const totalMinutes = entries.reduce((sum, entry) => {
|
|
329
|
+
return sum + (entry.attributes.time || 0);
|
|
330
|
+
}, 0);
|
|
331
|
+
const totalHours = +(totalMinutes / 60).toFixed(1);
|
|
332
|
+
const today = getToday();
|
|
333
|
+
if (filter?.after === today && filter?.before === today) suggestions.push(`📊 ${totalHours}h/8h logged today`);
|
|
334
|
+
else if (totalMinutes > 0) suggestions.push(`📊 Total: ${totalHours}h logged`);
|
|
335
|
+
return suggestions;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Get suggestions for summaries.my_day response.
|
|
339
|
+
*
|
|
340
|
+
* - Warns if no time has been logged today.
|
|
341
|
+
* - Warns if a timer has been running for more than 2 hours.
|
|
342
|
+
*/
|
|
343
|
+
function getMyDaySuggestions(data) {
|
|
344
|
+
const suggestions = [];
|
|
345
|
+
if (!data) return suggestions;
|
|
346
|
+
if (data.time.logged_today_minutes === 0 && data.time.entries_today === 0) suggestions.push("⚠️ No time logged today");
|
|
347
|
+
if (data.timers && data.timers.length > 0) {
|
|
348
|
+
for (const timer of data.timers) if (timer.total_time > 120) {
|
|
349
|
+
const hours = +(timer.total_time / 60).toFixed(1);
|
|
350
|
+
suggestions.push(`⏱️ Timer running for ${hours}h — remember to stop it`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return suggestions;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Helper to create a successful JSON response
|
|
357
|
+
*/
|
|
358
|
+
function jsonResult(data) {
|
|
359
|
+
return { content: [{
|
|
360
|
+
type: "text",
|
|
361
|
+
text: JSON.stringify(data, null, 2)
|
|
362
|
+
}] };
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Helper to create an error response from a string message
|
|
366
|
+
*/
|
|
367
|
+
function errorResult(message) {
|
|
368
|
+
return {
|
|
369
|
+
content: [{
|
|
370
|
+
type: "text",
|
|
371
|
+
text: `**Error:** ${message}`
|
|
372
|
+
}],
|
|
373
|
+
isError: true
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Helper to create an error response from a UserInputError
|
|
378
|
+
* Includes formatted hints for LLM consumption
|
|
379
|
+
*/
|
|
380
|
+
function inputErrorResult(error) {
|
|
381
|
+
return {
|
|
382
|
+
content: [{
|
|
383
|
+
type: "text",
|
|
384
|
+
text: error.toFormattedMessage()
|
|
385
|
+
}],
|
|
386
|
+
isError: true
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Helper to create an error response from any error type
|
|
391
|
+
* Automatically formats UserInputError with hints
|
|
392
|
+
*/
|
|
393
|
+
function formatError(error) {
|
|
394
|
+
if (isUserInputError(error)) return inputErrorResult(error);
|
|
395
|
+
return errorResult(error instanceof Error ? error.message : String(error));
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Convert unknown filter to string filter for API
|
|
399
|
+
*/
|
|
400
|
+
function toStringFilter(filter) {
|
|
401
|
+
if (!filter) return void 0;
|
|
402
|
+
const result = {};
|
|
403
|
+
for (const [key, value] of Object.entries(filter)) if (value !== void 0 && value !== null) result[key] = String(value);
|
|
404
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Resolve handler for MCP.
|
|
408
|
+
*
|
|
409
|
+
* Thin wrapper around core's resource resolver.
|
|
410
|
+
* Provides handleResolve for the MCP 'resolve' action.
|
|
411
|
+
*/
|
|
412
|
+
/**
|
|
413
|
+
* Handle resolve action for a resource.
|
|
414
|
+
*
|
|
415
|
+
* Delegates to core's resolveResource function and wraps
|
|
416
|
+
* errors in MCP-friendly format.
|
|
417
|
+
*/
|
|
418
|
+
async function handleResolve(args, ctx) {
|
|
419
|
+
const { query, type, project_id } = args;
|
|
420
|
+
if (!query) return errorResult("query is required for resolve action");
|
|
421
|
+
try {
|
|
422
|
+
const results = await resolveResource(ctx.executor().api, query, {
|
|
423
|
+
type,
|
|
424
|
+
projectId: project_id
|
|
425
|
+
});
|
|
426
|
+
return jsonResult({
|
|
427
|
+
query,
|
|
428
|
+
matches: results,
|
|
429
|
+
exact: results.length === 1 && results[0].exact
|
|
430
|
+
});
|
|
431
|
+
} catch (error) {
|
|
432
|
+
if (error instanceof ResolveError) return inputErrorResult(new UserInputError(error.message, [`Query: "${error.query}"`, ...error.type ? [`Type: ${error.type}`] : []]));
|
|
433
|
+
throw error;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Merge user includes with defaults, ensuring no duplicates
|
|
438
|
+
*/
|
|
439
|
+
function mergeIncludes(userInclude, defaults) {
|
|
440
|
+
if (!userInclude?.length && !defaults?.length) return void 0;
|
|
441
|
+
if (!userInclude?.length) return defaults;
|
|
442
|
+
if (!defaults?.length) return userInclude;
|
|
443
|
+
return [...new Set([...defaults, ...userInclude])];
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Create a resource handler function from configuration.
|
|
447
|
+
*
|
|
448
|
+
* @example
|
|
449
|
+
* ```typescript
|
|
450
|
+
* export const handleProjects = createResourceHandler({
|
|
451
|
+
* resource: 'projects',
|
|
452
|
+
* actions: ['list', 'get', 'resolve'],
|
|
453
|
+
* formatter: formatProject,
|
|
454
|
+
* hints: (data, id) => getProjectHints(id),
|
|
455
|
+
* supportsResolve: true,
|
|
456
|
+
* executors: {
|
|
457
|
+
* list: listProjects,
|
|
458
|
+
* get: getProject,
|
|
459
|
+
* },
|
|
460
|
+
* });
|
|
461
|
+
* ```
|
|
462
|
+
*/
|
|
463
|
+
function createResourceHandler(config) {
|
|
464
|
+
const { resource, displayName = resource, actions, formatter, hints, defaultInclude, supportsResolve, listFilterFromArgs, resolveArgsFromArgs, customActions, create: createConfig, update: updateConfig, executors } = config;
|
|
465
|
+
return async (action, args, ctx) => {
|
|
466
|
+
const { formatOptions, filter, page, perPage, include: userInclude } = ctx;
|
|
467
|
+
const { id, query, type } = args;
|
|
468
|
+
const execCtx = ctx.executor();
|
|
469
|
+
if (customActions?.[action]) return customActions[action](args, ctx, execCtx);
|
|
470
|
+
if (action === "resolve") {
|
|
471
|
+
if (!supportsResolve) return inputErrorResult(ErrorMessages.invalidAction(action, resource, actions));
|
|
472
|
+
return handleResolve({
|
|
473
|
+
query,
|
|
474
|
+
type,
|
|
475
|
+
...resolveArgsFromArgs?.(args)
|
|
476
|
+
}, ctx);
|
|
477
|
+
}
|
|
478
|
+
if (action === "get") {
|
|
479
|
+
if (!executors.get) return inputErrorResult(ErrorMessages.invalidAction(action, resource, actions));
|
|
480
|
+
if (!id) return inputErrorResult(ErrorMessages.missingId("get"));
|
|
481
|
+
const include = mergeIncludes(userInclude, defaultInclude?.get);
|
|
482
|
+
const result = await executors.get({
|
|
483
|
+
id,
|
|
484
|
+
include
|
|
485
|
+
}, execCtx);
|
|
486
|
+
const getResponseData = { ...formatter(result.data, {
|
|
487
|
+
...formatOptions,
|
|
488
|
+
included: result.included
|
|
489
|
+
}) };
|
|
490
|
+
if (ctx.includeHints) {
|
|
491
|
+
if (hints) getResponseData._hints = hints(result.data, id);
|
|
492
|
+
}
|
|
493
|
+
if (ctx.includeSuggestions !== false) {
|
|
494
|
+
let getSuggestions = [];
|
|
495
|
+
if (resource === "tasks") getSuggestions = getTaskGetSuggestions(result.data, result.included);
|
|
496
|
+
if (getSuggestions.length > 0) getResponseData._suggestions = getSuggestions;
|
|
497
|
+
}
|
|
498
|
+
return jsonResult(getResponseData);
|
|
499
|
+
}
|
|
500
|
+
if (action === "create") {
|
|
501
|
+
if (!executors.create || !createConfig) return inputErrorResult(ErrorMessages.invalidAction(action, resource, actions));
|
|
502
|
+
const missingFields = createConfig.required.filter((field) => !args[field]);
|
|
503
|
+
if (missingFields.length > 0) return inputErrorResult(ErrorMessages.missingRequiredFields(displayName, missingFields));
|
|
504
|
+
if (createConfig.validateArgs) {
|
|
505
|
+
const errorResult = createConfig.validateArgs(args);
|
|
506
|
+
if (errorResult) return errorResult;
|
|
507
|
+
}
|
|
508
|
+
const options = createConfig.mapOptions(args);
|
|
509
|
+
return jsonResult({
|
|
510
|
+
success: true,
|
|
511
|
+
...formatter((await executors.create(options, execCtx)).data, formatOptions)
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
if (action === "update") {
|
|
515
|
+
if (!executors.update || !updateConfig) return inputErrorResult(ErrorMessages.invalidAction(action, resource, actions));
|
|
516
|
+
if (!id) return inputErrorResult(ErrorMessages.missingId("update"));
|
|
517
|
+
if (updateConfig.allowedFields && updateConfig.allowedFields.length > 0) {
|
|
518
|
+
if (!updateConfig.allowedFields.some((field) => args[field] !== void 0)) return inputErrorResult(ErrorMessages.noUpdateFieldsSpecified(updateConfig.allowedFields));
|
|
519
|
+
}
|
|
520
|
+
const options = {
|
|
521
|
+
id,
|
|
522
|
+
...updateConfig.mapOptions(args)
|
|
523
|
+
};
|
|
524
|
+
return jsonResult({
|
|
525
|
+
success: true,
|
|
526
|
+
...formatter((await executors.update(options, execCtx)).data, formatOptions)
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
if (action === "delete") {
|
|
530
|
+
if (!executors.delete) return inputErrorResult(ErrorMessages.invalidAction(action, resource, actions));
|
|
531
|
+
if (!id) return inputErrorResult(ErrorMessages.missingId("delete"));
|
|
532
|
+
await executors.delete({ id }, execCtx);
|
|
533
|
+
return jsonResult({
|
|
534
|
+
success: true,
|
|
535
|
+
deleted: id
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
if (action === "list") {
|
|
539
|
+
const include = mergeIncludes(userInclude, defaultInclude?.list);
|
|
540
|
+
const additionalFilters = {
|
|
541
|
+
...filter,
|
|
542
|
+
...listFilterFromArgs?.(args)
|
|
543
|
+
};
|
|
544
|
+
const result = await executors.list({
|
|
545
|
+
page,
|
|
546
|
+
perPage,
|
|
547
|
+
additionalFilters,
|
|
548
|
+
include
|
|
549
|
+
}, execCtx);
|
|
550
|
+
const listResponseData = { ...formatListResponse$1(result.data, formatter, result.meta, {
|
|
551
|
+
...formatOptions,
|
|
552
|
+
included: result.included
|
|
553
|
+
}) };
|
|
554
|
+
if (result.resolved && Object.keys(result.resolved).length > 0) listResponseData._resolved = result.resolved;
|
|
555
|
+
if (ctx.includeSuggestions !== false) {
|
|
556
|
+
let listSuggestions = [];
|
|
557
|
+
if (resource === "tasks") listSuggestions = getTaskListSuggestions(result.data);
|
|
558
|
+
else if (resource === "time") listSuggestions = getTimeListSuggestions(result.data, additionalFilters);
|
|
559
|
+
if (listSuggestions.length > 0) listResponseData._suggestions = listSuggestions;
|
|
560
|
+
}
|
|
561
|
+
return jsonResult(listResponseData);
|
|
562
|
+
}
|
|
563
|
+
return inputErrorResult(ErrorMessages.invalidAction(action, resource, actions));
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Activities MCP handler.
|
|
568
|
+
*
|
|
569
|
+
* Uses the createResourceHandler factory for the common list pattern.
|
|
570
|
+
* Activities are read-only — only `list` is supported.
|
|
571
|
+
*/
|
|
572
|
+
/**
|
|
573
|
+
* Handle activities resource.
|
|
574
|
+
*
|
|
575
|
+
* Supports: list
|
|
576
|
+
*/
|
|
577
|
+
const handleActivities = createResourceHandler({
|
|
578
|
+
resource: "activities",
|
|
579
|
+
actions: ["list"],
|
|
580
|
+
formatter: formatActivity$1,
|
|
581
|
+
executors: { list: listActivities },
|
|
582
|
+
defaultInclude: { list: ["creator"] },
|
|
583
|
+
listFilterFromArgs: (args) => {
|
|
584
|
+
const filter = {};
|
|
585
|
+
if (args.after) filter.after = args.after;
|
|
586
|
+
if (args.event) filter.event = args.event;
|
|
587
|
+
return filter;
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
/**
|
|
254
591
|
* Generate hints for a task
|
|
255
592
|
*/
|
|
256
593
|
function getTaskHints(taskId, serviceId) {
|
|
@@ -470,6 +807,25 @@ function getPersonHints(personId) {
|
|
|
470
807
|
] };
|
|
471
808
|
}
|
|
472
809
|
/**
|
|
810
|
+
* Generate hints for a services list response.
|
|
811
|
+
*
|
|
812
|
+
* When the query doesn't already filter by deal_id, suggests filtering
|
|
813
|
+
* by deal_id to scope services to a specific budget/deal.
|
|
814
|
+
*
|
|
815
|
+
* @param currentFilter - The filter object currently applied to the list query
|
|
816
|
+
*/
|
|
817
|
+
function getServiceListHints(currentFilter) {
|
|
818
|
+
if (currentFilter?.deal_id) return null;
|
|
819
|
+
return { common_actions: [{
|
|
820
|
+
action: "Filter services by deal to see budget line items for a specific deal",
|
|
821
|
+
example: {
|
|
822
|
+
resource: "services",
|
|
823
|
+
action: "list",
|
|
824
|
+
filter: { deal_id: "<deal_id>" }
|
|
825
|
+
}
|
|
826
|
+
}] };
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
473
829
|
* Generate hints for a company
|
|
474
830
|
*/
|
|
475
831
|
function getCompanyHints(companyId) {
|
|
@@ -688,276 +1044,72 @@ function getPageHints(pageId) {
|
|
|
688
1044
|
};
|
|
689
1045
|
}
|
|
690
1046
|
/**
|
|
691
|
-
* Generate hints for a discussion
|
|
692
|
-
*/
|
|
693
|
-
function getDiscussionHints(discussionId, pageId) {
|
|
694
|
-
const hints = {
|
|
695
|
-
related_resources: [{
|
|
696
|
-
resource: "comments",
|
|
697
|
-
description: "Get comments on this discussion",
|
|
698
|
-
example: {
|
|
699
|
-
resource: "comments",
|
|
700
|
-
action: "list",
|
|
701
|
-
filter: { discussion_id: discussionId }
|
|
702
|
-
}
|
|
703
|
-
}],
|
|
704
|
-
common_actions: [{
|
|
705
|
-
action: "Resolve this discussion",
|
|
706
|
-
example: {
|
|
707
|
-
resource: "discussions",
|
|
708
|
-
action: "resolve",
|
|
709
|
-
id: discussionId
|
|
710
|
-
}
|
|
711
|
-
}, {
|
|
712
|
-
action: "Add a comment",
|
|
713
|
-
example: {
|
|
714
|
-
resource: "comments",
|
|
715
|
-
action: "create",
|
|
716
|
-
discussion_id: discussionId,
|
|
717
|
-
body: "<your comment>"
|
|
718
|
-
}
|
|
719
|
-
}]
|
|
720
|
-
};
|
|
721
|
-
if (pageId) hints.related_resources.push({
|
|
722
|
-
resource: "pages",
|
|
723
|
-
description: "Get the page this discussion is on",
|
|
724
|
-
example: {
|
|
725
|
-
resource: "pages",
|
|
726
|
-
action: "get",
|
|
727
|
-
id: pageId
|
|
728
|
-
}
|
|
729
|
-
});
|
|
730
|
-
return hints;
|
|
731
|
-
}
|
|
732
|
-
/**
|
|
733
|
-
* Generate hints for a timer
|
|
734
|
-
*/
|
|
735
|
-
function getTimerHints(timerId, serviceId) {
|
|
736
|
-
const hints = {
|
|
737
|
-
common_actions: [{
|
|
738
|
-
action: "Stop this timer",
|
|
739
|
-
example: {
|
|
740
|
-
resource: "timers",
|
|
741
|
-
action: "stop",
|
|
742
|
-
id: timerId
|
|
743
|
-
}
|
|
744
|
-
}],
|
|
745
|
-
related_resources: []
|
|
746
|
-
};
|
|
747
|
-
if (serviceId) hints.related_resources.push({
|
|
748
|
-
resource: "services",
|
|
749
|
-
description: "Get the service this timer is running on",
|
|
750
|
-
example: {
|
|
751
|
-
resource: "services",
|
|
752
|
-
action: "get",
|
|
753
|
-
id: serviceId
|
|
754
|
-
}
|
|
755
|
-
});
|
|
756
|
-
return hints;
|
|
757
|
-
}
|
|
758
|
-
/**
|
|
759
|
-
* Helper to create a successful JSON response
|
|
760
|
-
*/
|
|
761
|
-
function jsonResult(data) {
|
|
762
|
-
return { content: [{
|
|
763
|
-
type: "text",
|
|
764
|
-
text: JSON.stringify(data, null, 2)
|
|
765
|
-
}] };
|
|
766
|
-
}
|
|
767
|
-
/**
|
|
768
|
-
* Helper to create an error response from a string message
|
|
769
|
-
*/
|
|
770
|
-
function errorResult(message) {
|
|
771
|
-
return {
|
|
772
|
-
content: [{
|
|
773
|
-
type: "text",
|
|
774
|
-
text: `**Error:** ${message}`
|
|
775
|
-
}],
|
|
776
|
-
isError: true
|
|
777
|
-
};
|
|
778
|
-
}
|
|
779
|
-
/**
|
|
780
|
-
* Helper to create an error response from a UserInputError
|
|
781
|
-
* Includes formatted hints for LLM consumption
|
|
782
|
-
*/
|
|
783
|
-
function inputErrorResult(error) {
|
|
784
|
-
return {
|
|
785
|
-
content: [{
|
|
786
|
-
type: "text",
|
|
787
|
-
text: error.toFormattedMessage()
|
|
788
|
-
}],
|
|
789
|
-
isError: true
|
|
790
|
-
};
|
|
791
|
-
}
|
|
792
|
-
/**
|
|
793
|
-
* Helper to create an error response from any error type
|
|
794
|
-
* Automatically formats UserInputError with hints
|
|
795
|
-
*/
|
|
796
|
-
function formatError(error) {
|
|
797
|
-
if (isUserInputError(error)) return inputErrorResult(error);
|
|
798
|
-
return errorResult(error instanceof Error ? error.message : String(error));
|
|
799
|
-
}
|
|
800
|
-
/**
|
|
801
|
-
* Convert unknown filter to string filter for API
|
|
802
|
-
*/
|
|
803
|
-
function toStringFilter(filter) {
|
|
804
|
-
if (!filter) return void 0;
|
|
805
|
-
const result = {};
|
|
806
|
-
for (const [key, value] of Object.entries(filter)) if (value !== void 0 && value !== null) result[key] = String(value);
|
|
807
|
-
return Object.keys(result).length > 0 ? result : void 0;
|
|
808
|
-
}
|
|
809
|
-
/**
|
|
810
|
-
* Resolve handler for MCP.
|
|
811
|
-
*
|
|
812
|
-
* Thin wrapper around core's resource resolver.
|
|
813
|
-
* Provides handleResolve for the MCP 'resolve' action.
|
|
814
|
-
*/
|
|
815
|
-
/**
|
|
816
|
-
* Handle resolve action for a resource.
|
|
817
|
-
*
|
|
818
|
-
* Delegates to core's resolveResource function and wraps
|
|
819
|
-
* errors in MCP-friendly format.
|
|
820
|
-
*/
|
|
821
|
-
async function handleResolve(args, ctx) {
|
|
822
|
-
const { query, type, project_id } = args;
|
|
823
|
-
if (!query) return errorResult("query is required for resolve action");
|
|
824
|
-
try {
|
|
825
|
-
const results = await resolveResource(ctx.executor().api, query, {
|
|
826
|
-
type,
|
|
827
|
-
projectId: project_id
|
|
828
|
-
});
|
|
829
|
-
return jsonResult({
|
|
830
|
-
query,
|
|
831
|
-
matches: results,
|
|
832
|
-
exact: results.length === 1 && results[0].exact
|
|
833
|
-
});
|
|
834
|
-
} catch (error) {
|
|
835
|
-
if (error instanceof ResolveError) return inputErrorResult(new UserInputError(error.message, [`Query: "${error.query}"`, ...error.type ? [`Type: ${error.type}`] : []]));
|
|
836
|
-
throw error;
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
/**
|
|
840
|
-
* Merge user includes with defaults, ensuring no duplicates
|
|
841
|
-
*/
|
|
842
|
-
function mergeIncludes(userInclude, defaults) {
|
|
843
|
-
if (!userInclude?.length && !defaults?.length) return void 0;
|
|
844
|
-
if (!userInclude?.length) return defaults;
|
|
845
|
-
if (!defaults?.length) return userInclude;
|
|
846
|
-
return [...new Set([...defaults, ...userInclude])];
|
|
847
|
-
}
|
|
848
|
-
/**
|
|
849
|
-
* Create a resource handler function from configuration.
|
|
850
|
-
*
|
|
851
|
-
* @example
|
|
852
|
-
* ```typescript
|
|
853
|
-
* export const handleProjects = createResourceHandler({
|
|
854
|
-
* resource: 'projects',
|
|
855
|
-
* actions: ['list', 'get', 'resolve'],
|
|
856
|
-
* formatter: formatProject,
|
|
857
|
-
* hints: (data, id) => getProjectHints(id),
|
|
858
|
-
* supportsResolve: true,
|
|
859
|
-
* executors: {
|
|
860
|
-
* list: listProjects,
|
|
861
|
-
* get: getProject,
|
|
862
|
-
* },
|
|
863
|
-
* });
|
|
864
|
-
* ```
|
|
865
|
-
*/
|
|
866
|
-
function createResourceHandler(config) {
|
|
867
|
-
const { resource, displayName = resource, actions, formatter, hints, defaultInclude, supportsResolve, listFilterFromArgs, resolveArgsFromArgs, customActions, create: createConfig, update: updateConfig, executors } = config;
|
|
868
|
-
return async (action, args, ctx) => {
|
|
869
|
-
const { formatOptions, filter, page, perPage, include: userInclude } = ctx;
|
|
870
|
-
const { id, query, type } = args;
|
|
871
|
-
const execCtx = ctx.executor();
|
|
872
|
-
if (customActions?.[action]) return customActions[action](args, ctx, execCtx);
|
|
873
|
-
if (action === "resolve") {
|
|
874
|
-
if (!supportsResolve) return inputErrorResult(ErrorMessages.invalidAction(action, resource, actions));
|
|
875
|
-
return handleResolve({
|
|
876
|
-
query,
|
|
877
|
-
type,
|
|
878
|
-
...resolveArgsFromArgs?.(args)
|
|
879
|
-
}, ctx);
|
|
880
|
-
}
|
|
881
|
-
if (action === "get") {
|
|
882
|
-
if (!executors.get) return inputErrorResult(ErrorMessages.invalidAction(action, resource, actions));
|
|
883
|
-
if (!id) return inputErrorResult(ErrorMessages.missingId("get"));
|
|
884
|
-
const include = mergeIncludes(userInclude, defaultInclude?.get);
|
|
885
|
-
const result = await executors.get({
|
|
886
|
-
id,
|
|
887
|
-
include
|
|
888
|
-
}, execCtx);
|
|
889
|
-
const formatted = formatter(result.data, {
|
|
890
|
-
...formatOptions,
|
|
891
|
-
included: result.included
|
|
892
|
-
});
|
|
893
|
-
if (ctx.includeHints !== false && hints) return jsonResult({
|
|
894
|
-
...formatted,
|
|
895
|
-
_hints: hints(result.data, id)
|
|
896
|
-
});
|
|
897
|
-
return jsonResult(formatted);
|
|
898
|
-
}
|
|
899
|
-
if (action === "create") {
|
|
900
|
-
if (!executors.create || !createConfig) return inputErrorResult(ErrorMessages.invalidAction(action, resource, actions));
|
|
901
|
-
const missingFields = createConfig.required.filter((field) => !args[field]);
|
|
902
|
-
if (missingFields.length > 0) return inputErrorResult(ErrorMessages.missingRequiredFields(displayName, missingFields));
|
|
903
|
-
if (createConfig.validateArgs) {
|
|
904
|
-
const errorResult = createConfig.validateArgs(args);
|
|
905
|
-
if (errorResult) return errorResult;
|
|
1047
|
+
* Generate hints for a discussion
|
|
1048
|
+
*/
|
|
1049
|
+
function getDiscussionHints(discussionId, pageId) {
|
|
1050
|
+
const hints = {
|
|
1051
|
+
related_resources: [{
|
|
1052
|
+
resource: "comments",
|
|
1053
|
+
description: "Get comments on this discussion",
|
|
1054
|
+
example: {
|
|
1055
|
+
resource: "comments",
|
|
1056
|
+
action: "list",
|
|
1057
|
+
filter: { discussion_id: discussionId }
|
|
906
1058
|
}
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
if (!executors.update || !updateConfig) return inputErrorResult(ErrorMessages.invalidAction(action, resource, actions));
|
|
915
|
-
if (!id) return inputErrorResult(ErrorMessages.missingId("update"));
|
|
916
|
-
if (updateConfig.allowedFields && updateConfig.allowedFields.length > 0) {
|
|
917
|
-
if (!updateConfig.allowedFields.some((field) => args[field] !== void 0)) return inputErrorResult(ErrorMessages.noUpdateFieldsSpecified(updateConfig.allowedFields));
|
|
1059
|
+
}],
|
|
1060
|
+
common_actions: [{
|
|
1061
|
+
action: "Resolve this discussion",
|
|
1062
|
+
example: {
|
|
1063
|
+
resource: "discussions",
|
|
1064
|
+
action: "resolve",
|
|
1065
|
+
id: discussionId
|
|
918
1066
|
}
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
}
|
|
937
|
-
if (action === "list") {
|
|
938
|
-
const include = mergeIncludes(userInclude, defaultInclude?.list);
|
|
939
|
-
const additionalFilters = {
|
|
940
|
-
...filter,
|
|
941
|
-
...listFilterFromArgs?.(args)
|
|
942
|
-
};
|
|
943
|
-
const result = await executors.list({
|
|
944
|
-
page,
|
|
945
|
-
perPage,
|
|
946
|
-
additionalFilters,
|
|
947
|
-
include
|
|
948
|
-
}, execCtx);
|
|
949
|
-
const response = formatListResponse$1(result.data, formatter, result.meta, {
|
|
950
|
-
...formatOptions,
|
|
951
|
-
included: result.included
|
|
952
|
-
});
|
|
953
|
-
if (result.resolved && Object.keys(result.resolved).length > 0) return jsonResult({
|
|
954
|
-
...response,
|
|
955
|
-
_resolved: result.resolved
|
|
956
|
-
});
|
|
957
|
-
return jsonResult(response);
|
|
1067
|
+
}, {
|
|
1068
|
+
action: "Add a comment",
|
|
1069
|
+
example: {
|
|
1070
|
+
resource: "comments",
|
|
1071
|
+
action: "create",
|
|
1072
|
+
discussion_id: discussionId,
|
|
1073
|
+
body: "<your comment>"
|
|
1074
|
+
}
|
|
1075
|
+
}]
|
|
1076
|
+
};
|
|
1077
|
+
if (pageId) hints.related_resources.push({
|
|
1078
|
+
resource: "pages",
|
|
1079
|
+
description: "Get the page this discussion is on",
|
|
1080
|
+
example: {
|
|
1081
|
+
resource: "pages",
|
|
1082
|
+
action: "get",
|
|
1083
|
+
id: pageId
|
|
958
1084
|
}
|
|
959
|
-
|
|
1085
|
+
});
|
|
1086
|
+
return hints;
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Generate hints for a timer
|
|
1090
|
+
*/
|
|
1091
|
+
function getTimerHints(timerId, serviceId) {
|
|
1092
|
+
const hints = {
|
|
1093
|
+
common_actions: [{
|
|
1094
|
+
action: "Stop this timer",
|
|
1095
|
+
example: {
|
|
1096
|
+
resource: "timers",
|
|
1097
|
+
action: "stop",
|
|
1098
|
+
id: timerId
|
|
1099
|
+
}
|
|
1100
|
+
}],
|
|
1101
|
+
related_resources: []
|
|
960
1102
|
};
|
|
1103
|
+
if (serviceId) hints.related_resources.push({
|
|
1104
|
+
resource: "services",
|
|
1105
|
+
description: "Get the service this timer is running on",
|
|
1106
|
+
example: {
|
|
1107
|
+
resource: "services",
|
|
1108
|
+
action: "get",
|
|
1109
|
+
id: serviceId
|
|
1110
|
+
}
|
|
1111
|
+
});
|
|
1112
|
+
return hints;
|
|
961
1113
|
}
|
|
962
1114
|
/**
|
|
963
1115
|
* Attachments MCP handler.
|
|
@@ -1629,6 +1781,13 @@ var RESOURCE_HELP = {
|
|
|
1629
1781
|
worked_time: "Logged time in minutes"
|
|
1630
1782
|
},
|
|
1631
1783
|
examples: [{
|
|
1784
|
+
description: "List services for a deal (budget line items)",
|
|
1785
|
+
params: {
|
|
1786
|
+
resource: "services",
|
|
1787
|
+
action: "list",
|
|
1788
|
+
filter: { deal_id: "12345" }
|
|
1789
|
+
}
|
|
1790
|
+
}, {
|
|
1632
1791
|
description: "List services for a project",
|
|
1633
1792
|
params: {
|
|
1634
1793
|
resource: "services",
|
|
@@ -2103,6 +2262,137 @@ var RESOURCE_HELP = {
|
|
|
2103
2262
|
}
|
|
2104
2263
|
]
|
|
2105
2264
|
},
|
|
2265
|
+
workflows: {
|
|
2266
|
+
description: "Compound workflows that chain multiple resource operations into a single tool call. Use these for common multi-step patterns.",
|
|
2267
|
+
actions: {
|
|
2268
|
+
complete_task: "Mark a task closed, optionally post a comment, and stop running timers",
|
|
2269
|
+
log_day: "Create multiple time entries in parallel from a structured list",
|
|
2270
|
+
weekly_standup: "Aggregate completed tasks, time logged, and upcoming deadlines for a week"
|
|
2271
|
+
},
|
|
2272
|
+
fields: {
|
|
2273
|
+
task_id: "(complete_task) Required. Task ID to mark as complete",
|
|
2274
|
+
comment: "(complete_task) Optional. Completion comment text to post",
|
|
2275
|
+
stop_timer: "(complete_task) Optional. Whether to stop running timers (default: true)",
|
|
2276
|
+
entries: "(log_day) Required. Array of { project_id, service_id, duration_minutes, note?, date? }",
|
|
2277
|
+
date: "(log_day) Optional. Default date for all entries (YYYY-MM-DD, defaults to today)",
|
|
2278
|
+
person_id: "(log_day / weekly_standup) Optional. Person to act on (defaults to current user)",
|
|
2279
|
+
week_start: "(weekly_standup) Optional. Monday date of the target week (defaults to this Monday)"
|
|
2280
|
+
},
|
|
2281
|
+
examples: [
|
|
2282
|
+
{
|
|
2283
|
+
description: "Complete a task with a comment",
|
|
2284
|
+
params: {
|
|
2285
|
+
resource: "workflows",
|
|
2286
|
+
action: "complete_task",
|
|
2287
|
+
task_id: "12345",
|
|
2288
|
+
comment: "Done! All tests passing."
|
|
2289
|
+
}
|
|
2290
|
+
},
|
|
2291
|
+
{
|
|
2292
|
+
description: "Complete a task without stopping timers",
|
|
2293
|
+
params: {
|
|
2294
|
+
resource: "workflows",
|
|
2295
|
+
action: "complete_task",
|
|
2296
|
+
task_id: "12345",
|
|
2297
|
+
stop_timer: false
|
|
2298
|
+
}
|
|
2299
|
+
},
|
|
2300
|
+
{
|
|
2301
|
+
description: "Log a full day across multiple projects",
|
|
2302
|
+
params: {
|
|
2303
|
+
resource: "workflows",
|
|
2304
|
+
action: "log_day",
|
|
2305
|
+
date: "2024-01-16",
|
|
2306
|
+
entries: [
|
|
2307
|
+
{
|
|
2308
|
+
project_id: "100",
|
|
2309
|
+
service_id: "111",
|
|
2310
|
+
duration_minutes: 240,
|
|
2311
|
+
note: "Frontend dev"
|
|
2312
|
+
},
|
|
2313
|
+
{
|
|
2314
|
+
project_id: "200",
|
|
2315
|
+
service_id: "222",
|
|
2316
|
+
duration_minutes: 120,
|
|
2317
|
+
note: "Code review"
|
|
2318
|
+
},
|
|
2319
|
+
{
|
|
2320
|
+
project_id: "100",
|
|
2321
|
+
service_id: "333",
|
|
2322
|
+
duration_minutes: 60,
|
|
2323
|
+
note: "Meetings"
|
|
2324
|
+
}
|
|
2325
|
+
]
|
|
2326
|
+
}
|
|
2327
|
+
},
|
|
2328
|
+
{
|
|
2329
|
+
description: "Get this week standup",
|
|
2330
|
+
params: {
|
|
2331
|
+
resource: "workflows",
|
|
2332
|
+
action: "weekly_standup"
|
|
2333
|
+
}
|
|
2334
|
+
},
|
|
2335
|
+
{
|
|
2336
|
+
description: "Get standup for a specific past week",
|
|
2337
|
+
params: {
|
|
2338
|
+
resource: "workflows",
|
|
2339
|
+
action: "weekly_standup",
|
|
2340
|
+
week_start: "2024-01-15"
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
]
|
|
2344
|
+
},
|
|
2345
|
+
activities: {
|
|
2346
|
+
description: "Read-only activity feed — audit log of create/update/delete events across the organization",
|
|
2347
|
+
actions: { list: "List recent activities with optional filters" },
|
|
2348
|
+
filters: {
|
|
2349
|
+
event: "Filter by event type: create, update, delete",
|
|
2350
|
+
after: "Filter to activities after this ISO 8601 timestamp (e.g. 2026-01-01T00:00:00Z)",
|
|
2351
|
+
person_id: "Filter by creator person ID",
|
|
2352
|
+
project_id: "Filter by project ID"
|
|
2353
|
+
},
|
|
2354
|
+
includes: ["creator"],
|
|
2355
|
+
fields: {
|
|
2356
|
+
id: "Unique activity identifier",
|
|
2357
|
+
event: "Event type: create, update, or delete",
|
|
2358
|
+
changeset: "Human-readable summary of field changes (e.g. \"name: null → My Project\")",
|
|
2359
|
+
created_at: "When the activity occurred (ISO 8601)",
|
|
2360
|
+
creator_name: "Full name of the person who triggered the activity (when creator included)"
|
|
2361
|
+
},
|
|
2362
|
+
examples: [
|
|
2363
|
+
{
|
|
2364
|
+
description: "List recent activities",
|
|
2365
|
+
params: {
|
|
2366
|
+
resource: "activities",
|
|
2367
|
+
action: "list"
|
|
2368
|
+
}
|
|
2369
|
+
},
|
|
2370
|
+
{
|
|
2371
|
+
description: "List only create events",
|
|
2372
|
+
params: {
|
|
2373
|
+
resource: "activities",
|
|
2374
|
+
action: "list",
|
|
2375
|
+
filter: { event: "create" }
|
|
2376
|
+
}
|
|
2377
|
+
},
|
|
2378
|
+
{
|
|
2379
|
+
description: "List activities after a date",
|
|
2380
|
+
params: {
|
|
2381
|
+
resource: "activities",
|
|
2382
|
+
action: "list",
|
|
2383
|
+
filter: { after: "2026-02-01T00:00:00Z" }
|
|
2384
|
+
}
|
|
2385
|
+
},
|
|
2386
|
+
{
|
|
2387
|
+
description: "List activities by a specific person",
|
|
2388
|
+
params: {
|
|
2389
|
+
resource: "activities",
|
|
2390
|
+
action: "list",
|
|
2391
|
+
filter: { person_id: "12345" }
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
]
|
|
2395
|
+
},
|
|
2106
2396
|
reports: {
|
|
2107
2397
|
description: "Generate various reports (time, budget, project, etc.)",
|
|
2108
2398
|
actions: { get: "Generate a report (requires report_type)" },
|
|
@@ -2215,7 +2505,7 @@ const handlePages = createResourceHandler({
|
|
|
2215
2505
|
/**
|
|
2216
2506
|
* People MCP handler.
|
|
2217
2507
|
*/
|
|
2218
|
-
var VALID_ACTIONS$
|
|
2508
|
+
var VALID_ACTIONS$3 = [
|
|
2219
2509
|
"list",
|
|
2220
2510
|
"get",
|
|
2221
2511
|
"me",
|
|
@@ -2266,7 +2556,7 @@ async function handlePeople(action, args, ctx, credentials) {
|
|
|
2266
2556
|
});
|
|
2267
2557
|
return jsonResult(response);
|
|
2268
2558
|
}
|
|
2269
|
-
return inputErrorResult(ErrorMessages.invalidAction(action, "people", VALID_ACTIONS$
|
|
2559
|
+
return inputErrorResult(ErrorMessages.invalidAction(action, "people", VALID_ACTIONS$3));
|
|
2270
2560
|
}
|
|
2271
2561
|
/**
|
|
2272
2562
|
* Projects MCP handler.
|
|
@@ -2324,11 +2614,11 @@ function formatReportData(data) {
|
|
|
2324
2614
|
};
|
|
2325
2615
|
});
|
|
2326
2616
|
}
|
|
2327
|
-
var VALID_ACTIONS$
|
|
2617
|
+
var VALID_ACTIONS$2 = ["get"];
|
|
2328
2618
|
async function handleReports(action, args, ctx) {
|
|
2329
2619
|
const { filter, page, perPage } = ctx;
|
|
2330
2620
|
const { report_type, group, from, to, person_id, project_id, company_id, deal_id, status } = args;
|
|
2331
|
-
if (action !== "get") return inputErrorResult(ErrorMessages.invalidAction(action, "reports", VALID_ACTIONS$
|
|
2621
|
+
if (action !== "get") return inputErrorResult(ErrorMessages.invalidAction(action, "reports", VALID_ACTIONS$2));
|
|
2332
2622
|
if (!report_type) return inputErrorResult(ErrorMessages.missingReportType());
|
|
2333
2623
|
if (!VALID_REPORT_TYPES.includes(report_type)) return inputErrorResult(ErrorMessages.invalidReportType(report_type, [...VALID_REPORT_TYPES]));
|
|
2334
2624
|
const execCtx = ctx.executor();
|
|
@@ -2813,7 +3103,26 @@ const handleServices = createResourceHandler({
|
|
|
2813
3103
|
resource: "services",
|
|
2814
3104
|
actions: ["list"],
|
|
2815
3105
|
formatter: formatService$1,
|
|
2816
|
-
executors: { list: listServices }
|
|
3106
|
+
executors: { list: listServices },
|
|
3107
|
+
customActions: { list: async (args, ctx, execCtx) => {
|
|
3108
|
+
const { formatOptions, filter, page, perPage } = ctx;
|
|
3109
|
+
const additionalFilters = { ...filter };
|
|
3110
|
+
const result = await listServices({
|
|
3111
|
+
page,
|
|
3112
|
+
perPage,
|
|
3113
|
+
additionalFilters
|
|
3114
|
+
}, execCtx);
|
|
3115
|
+
const response = formatListResponse$1(result.data, formatService$1, result.meta, {
|
|
3116
|
+
...formatOptions,
|
|
3117
|
+
included: result.included
|
|
3118
|
+
});
|
|
3119
|
+
const hints = getServiceListHints(additionalFilters);
|
|
3120
|
+
if (hints) return jsonResult({
|
|
3121
|
+
...response,
|
|
3122
|
+
_hints: hints
|
|
3123
|
+
});
|
|
3124
|
+
return jsonResult(response);
|
|
3125
|
+
} }
|
|
2817
3126
|
});
|
|
2818
3127
|
/**
|
|
2819
3128
|
* Summaries MCP handler.
|
|
@@ -2821,7 +3130,7 @@ const handleServices = createResourceHandler({
|
|
|
2821
3130
|
* Custom handler for dashboard-style summaries (not using createResourceHandler).
|
|
2822
3131
|
* Routes actions to the appropriate summary executor.
|
|
2823
3132
|
*/
|
|
2824
|
-
var VALID_ACTIONS = [
|
|
3133
|
+
var VALID_ACTIONS$1 = [
|
|
2825
3134
|
"my_day",
|
|
2826
3135
|
"project_health",
|
|
2827
3136
|
"team_pulse",
|
|
@@ -2833,10 +3142,20 @@ var VALID_ACTIONS = [
|
|
|
2833
3142
|
* Supports: my_day, project_health, team_pulse
|
|
2834
3143
|
*/
|
|
2835
3144
|
async function handleSummaries(action, args, ctx) {
|
|
2836
|
-
if (!VALID_ACTIONS.includes(action)) return inputErrorResult(ErrorMessages.invalidAction(action, "summaries", VALID_ACTIONS));
|
|
3145
|
+
if (!VALID_ACTIONS$1.includes(action)) return inputErrorResult(ErrorMessages.invalidAction(action, "summaries", VALID_ACTIONS$1));
|
|
2837
3146
|
const execCtx = ctx.executor();
|
|
2838
3147
|
switch (action) {
|
|
2839
|
-
case "my_day":
|
|
3148
|
+
case "my_day": {
|
|
3149
|
+
const result = await getMyDaySummary({}, execCtx);
|
|
3150
|
+
if (ctx.includeSuggestions !== false) {
|
|
3151
|
+
const suggestions = getMyDaySuggestions(result.data);
|
|
3152
|
+
if (suggestions.length > 0) return jsonResult({
|
|
3153
|
+
...result.data,
|
|
3154
|
+
_suggestions: suggestions
|
|
3155
|
+
});
|
|
3156
|
+
}
|
|
3157
|
+
return jsonResult(result.data);
|
|
3158
|
+
}
|
|
2840
3159
|
case "project_health":
|
|
2841
3160
|
if (!args.project_id) return inputErrorResult(new UserInputError("project_id is required for project_health summary", [
|
|
2842
3161
|
"Provide the project_id parameter",
|
|
@@ -2878,7 +3197,7 @@ async function handleSummaries(action, args, ctx) {
|
|
|
2878
3197
|
}
|
|
2879
3198
|
}
|
|
2880
3199
|
});
|
|
2881
|
-
default: return inputErrorResult(ErrorMessages.invalidAction(action, "summaries", VALID_ACTIONS));
|
|
3200
|
+
default: return inputErrorResult(ErrorMessages.invalidAction(action, "summaries", VALID_ACTIONS$1));
|
|
2882
3201
|
}
|
|
2883
3202
|
}
|
|
2884
3203
|
/**
|
|
@@ -2973,23 +3292,28 @@ const handleTime = createResourceHandler({
|
|
|
2973
3292
|
},
|
|
2974
3293
|
supportsResolve: true,
|
|
2975
3294
|
resolveArgsFromArgs: (args) => ({ project_id: args.project_id }),
|
|
2976
|
-
create: {
|
|
2977
|
-
|
|
2978
|
-
"person_id",
|
|
3295
|
+
customActions: { create: async (args, ctx, execCtx) => {
|
|
3296
|
+
const missingFields = [
|
|
2979
3297
|
"service_id",
|
|
2980
3298
|
"time",
|
|
2981
3299
|
"date"
|
|
2982
|
-
]
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
3300
|
+
].filter((field) => !args[field]);
|
|
3301
|
+
if (missingFields.length > 0) return inputErrorResult(ErrorMessages.missingRequiredFields("time entry", missingFields));
|
|
3302
|
+
const personId = args.person_id ?? execCtx.config.userId;
|
|
3303
|
+
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"]));
|
|
3304
|
+
return jsonResult({
|
|
3305
|
+
success: true,
|
|
3306
|
+
...formatTimeEntry$1((await createTimeEntry({
|
|
3307
|
+
personId,
|
|
3308
|
+
serviceId: args.service_id,
|
|
3309
|
+
time: args.time,
|
|
3310
|
+
date: args.date,
|
|
3311
|
+
note: args.note ?? void 0,
|
|
3312
|
+
taskId: args.task_id,
|
|
3313
|
+
projectId: args.project_id
|
|
3314
|
+
}, execCtx)).data, ctx.formatOptions)
|
|
3315
|
+
});
|
|
3316
|
+
} },
|
|
2993
3317
|
update: { mapOptions: (args) => ({
|
|
2994
3318
|
time: args.time ?? void 0,
|
|
2995
3319
|
date: args.date ?? void 0,
|
|
@@ -3051,6 +3375,191 @@ const handleTimers = createResourceHandler({
|
|
|
3051
3375
|
}
|
|
3052
3376
|
});
|
|
3053
3377
|
/**
|
|
3378
|
+
* Valid include values per resource.
|
|
3379
|
+
*
|
|
3380
|
+
* Sourced from help.ts and schema.ts include lists.
|
|
3381
|
+
* Resources not listed here have no include validation (pass-through).
|
|
3382
|
+
*/
|
|
3383
|
+
const VALID_INCLUDES = {
|
|
3384
|
+
activities: ["creator"],
|
|
3385
|
+
tasks: [
|
|
3386
|
+
"project",
|
|
3387
|
+
"project.company",
|
|
3388
|
+
"assignee",
|
|
3389
|
+
"workflow_status",
|
|
3390
|
+
"comments",
|
|
3391
|
+
"attachments",
|
|
3392
|
+
"subtasks"
|
|
3393
|
+
],
|
|
3394
|
+
comments: [
|
|
3395
|
+
"creator",
|
|
3396
|
+
"task",
|
|
3397
|
+
"deal"
|
|
3398
|
+
],
|
|
3399
|
+
deals: [
|
|
3400
|
+
"company",
|
|
3401
|
+
"deal_status",
|
|
3402
|
+
"responsible",
|
|
3403
|
+
"project"
|
|
3404
|
+
],
|
|
3405
|
+
bookings: [
|
|
3406
|
+
"person",
|
|
3407
|
+
"service",
|
|
3408
|
+
"event"
|
|
3409
|
+
],
|
|
3410
|
+
time: [
|
|
3411
|
+
"person",
|
|
3412
|
+
"service",
|
|
3413
|
+
"task"
|
|
3414
|
+
]
|
|
3415
|
+
};
|
|
3416
|
+
/**
|
|
3417
|
+
* Known misleading include values and their suggestions.
|
|
3418
|
+
* These are values that agents commonly try that don't exist.
|
|
3419
|
+
*/
|
|
3420
|
+
var KNOWN_SUGGESTIONS = {
|
|
3421
|
+
notes: "Use resource=comments to fetch comments on a resource",
|
|
3422
|
+
services: "Use resource=services with filter.deal_id or filter.project_id to list services",
|
|
3423
|
+
time_entries: "Use resource=time with a filter (e.g. filter.task_id, filter.project_id) to list time entries",
|
|
3424
|
+
time: "Use resource=time with a filter (e.g. filter.task_id) to list time entries",
|
|
3425
|
+
user: "Use \"assignee\" or \"person\" instead",
|
|
3426
|
+
author: "Use \"creator\" instead",
|
|
3427
|
+
owner: "Use \"responsible\" or \"assignee\" instead",
|
|
3428
|
+
company: "Use \"project.company\" to include the project's company on tasks",
|
|
3429
|
+
status: "Use \"workflow_status\" to include the workflow/kanban status on tasks"
|
|
3430
|
+
};
|
|
3431
|
+
/**
|
|
3432
|
+
* Validate include values for a given resource.
|
|
3433
|
+
*
|
|
3434
|
+
* Returns the valid and invalid values, plus suggestions for invalid values.
|
|
3435
|
+
* If the resource is not in VALID_INCLUDES, skips validation (returns all as valid).
|
|
3436
|
+
*/
|
|
3437
|
+
function validateIncludes(resource, includes) {
|
|
3438
|
+
const validSet = VALID_INCLUDES[resource];
|
|
3439
|
+
if (!validSet) return null;
|
|
3440
|
+
const valid = [];
|
|
3441
|
+
const invalid = [];
|
|
3442
|
+
const suggestions = {};
|
|
3443
|
+
for (const inc of includes) if (validSet.includes(inc)) valid.push(inc);
|
|
3444
|
+
else {
|
|
3445
|
+
invalid.push(inc);
|
|
3446
|
+
if (KNOWN_SUGGESTIONS[inc]) suggestions[inc] = KNOWN_SUGGESTIONS[inc];
|
|
3447
|
+
else suggestions[inc] = `Valid includes for ${resource}: ${validSet.join(", ")}`;
|
|
3448
|
+
}
|
|
3449
|
+
return {
|
|
3450
|
+
valid,
|
|
3451
|
+
invalid,
|
|
3452
|
+
suggestions
|
|
3453
|
+
};
|
|
3454
|
+
}
|
|
3455
|
+
/**
|
|
3456
|
+
* Workflows MCP handler.
|
|
3457
|
+
*
|
|
3458
|
+
* Custom handler for compound workflows that chain multiple executors.
|
|
3459
|
+
* NOT using createResourceHandler — standalone routing like summaries.ts.
|
|
3460
|
+
*
|
|
3461
|
+
* Supported actions:
|
|
3462
|
+
* - complete_task: Mark task closed, optionally comment and stop timers
|
|
3463
|
+
* - log_day: Create multiple time entries in parallel from a structured list
|
|
3464
|
+
* - weekly_standup: Aggregate completed tasks, time logged, and upcoming deadlines
|
|
3465
|
+
*/
|
|
3466
|
+
var VALID_ACTIONS = [
|
|
3467
|
+
"complete_task",
|
|
3468
|
+
"log_day",
|
|
3469
|
+
"weekly_standup",
|
|
3470
|
+
"help"
|
|
3471
|
+
];
|
|
3472
|
+
/**
|
|
3473
|
+
* Handle workflows resource.
|
|
3474
|
+
*
|
|
3475
|
+
* Supports: complete_task, log_day, weekly_standup
|
|
3476
|
+
*/
|
|
3477
|
+
async function handleWorkflows(action, args, ctx) {
|
|
3478
|
+
if (!VALID_ACTIONS.includes(action)) return inputErrorResult(ErrorMessages.invalidAction(action, "workflows", VALID_ACTIONS));
|
|
3479
|
+
const execCtx = ctx.executor();
|
|
3480
|
+
switch (action) {
|
|
3481
|
+
case "complete_task":
|
|
3482
|
+
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\""]));
|
|
3483
|
+
return jsonResult((await completeTask({
|
|
3484
|
+
taskId: args.task_id,
|
|
3485
|
+
comment: args.comment,
|
|
3486
|
+
stopTimer: args.stop_timer
|
|
3487
|
+
}, execCtx)).data);
|
|
3488
|
+
case "log_day":
|
|
3489
|
+
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", [
|
|
3490
|
+
"Provide entries as an array of { project_id, service_id, duration_minutes, note?, date? }",
|
|
3491
|
+
"Example: { \"entries\": [{ \"project_id\": \"123\", \"service_id\": \"456\", \"duration_minutes\": 120, \"note\": \"Development\" }] }",
|
|
3492
|
+
"You can find service IDs using resource=\"services\" action=\"list\" with filter.project_id"
|
|
3493
|
+
]));
|
|
3494
|
+
return jsonResult((await logDay({
|
|
3495
|
+
entries: args.entries.map((e) => {
|
|
3496
|
+
const entry = e;
|
|
3497
|
+
return {
|
|
3498
|
+
project_id: String(entry.project_id),
|
|
3499
|
+
service_id: String(entry.service_id),
|
|
3500
|
+
duration_minutes: Number(entry.duration_minutes),
|
|
3501
|
+
note: entry.note != null ? String(entry.note) : void 0,
|
|
3502
|
+
date: entry.date != null ? String(entry.date) : void 0
|
|
3503
|
+
};
|
|
3504
|
+
}),
|
|
3505
|
+
date: args.date,
|
|
3506
|
+
personId: args.person_id
|
|
3507
|
+
}, execCtx)).data);
|
|
3508
|
+
case "weekly_standup": return jsonResult((await weeklyStandup({
|
|
3509
|
+
personId: args.person_id,
|
|
3510
|
+
weekStart: args.week_start
|
|
3511
|
+
}, execCtx)).data);
|
|
3512
|
+
case "help": return jsonResult({
|
|
3513
|
+
resource: "workflows",
|
|
3514
|
+
description: "Compound workflows that chain multiple resource operations into a single tool call",
|
|
3515
|
+
actions: {
|
|
3516
|
+
complete_task: {
|
|
3517
|
+
description: "Mark a task as complete, optionally post a comment and stop running timers",
|
|
3518
|
+
parameters: {
|
|
3519
|
+
task_id: "Required. The task ID to complete",
|
|
3520
|
+
comment: "Optional. A completion comment to post on the task",
|
|
3521
|
+
stop_timer: "Optional. Whether to stop running timers (default: true)"
|
|
3522
|
+
},
|
|
3523
|
+
returns: {
|
|
3524
|
+
task: "Updated task info (id, title, closed status)",
|
|
3525
|
+
comment_posted: "Whether the comment was posted",
|
|
3526
|
+
comment_id: "ID of the created comment (if posted)",
|
|
3527
|
+
timers_stopped: "Number of timers stopped",
|
|
3528
|
+
errors: "Any sub-step errors (partial results are still returned)"
|
|
3529
|
+
}
|
|
3530
|
+
},
|
|
3531
|
+
log_day: {
|
|
3532
|
+
description: "Create multiple time entries in parallel from a structured list",
|
|
3533
|
+
parameters: {
|
|
3534
|
+
entries: "Required. Array of { project_id, service_id, duration_minutes, note?, date? }",
|
|
3535
|
+
date: "Optional. Default date for all entries (YYYY-MM-DD, defaults to today)",
|
|
3536
|
+
person_id: "Optional. Person to log for (defaults to current user)"
|
|
3537
|
+
},
|
|
3538
|
+
returns: {
|
|
3539
|
+
entries: "Per-entry results with success/failure status",
|
|
3540
|
+
succeeded: "Number of entries successfully created",
|
|
3541
|
+
failed: "Number of entries that failed",
|
|
3542
|
+
total_minutes_logged: "Sum of minutes for successful entries"
|
|
3543
|
+
}
|
|
3544
|
+
},
|
|
3545
|
+
weekly_standup: {
|
|
3546
|
+
description: "Aggregate a weekly standup: completed tasks, time logged, and upcoming deadlines",
|
|
3547
|
+
parameters: {
|
|
3548
|
+
person_id: "Optional. Person to generate standup for (defaults to current user)",
|
|
3549
|
+
week_start: "Optional. ISO date for Monday of the week (defaults to this Monday)"
|
|
3550
|
+
},
|
|
3551
|
+
returns: {
|
|
3552
|
+
completed_tasks: "Tasks closed this week (count + list)",
|
|
3553
|
+
time_logged: "Total minutes and breakdown by project",
|
|
3554
|
+
upcoming_deadlines: "Open tasks due in the next 7 days"
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
});
|
|
3559
|
+
default: return inputErrorResult(ErrorMessages.invalidAction(action, "workflows", VALID_ACTIONS));
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3562
|
+
/**
|
|
3054
3563
|
* Tool execution handlers for Productive MCP server
|
|
3055
3564
|
* These are shared between stdio and HTTP transports
|
|
3056
3565
|
*
|
|
@@ -3062,12 +3571,67 @@ var VALID_RESOURCES = [...RESOURCES];
|
|
|
3062
3571
|
/** Default page size for MCP (smaller than CLI to reduce token usage) */
|
|
3063
3572
|
var DEFAULT_PER_PAGE = 20;
|
|
3064
3573
|
/**
|
|
3574
|
+
* Route to the appropriate resource handler.
|
|
3575
|
+
* Extracted from executeToolWithCredentials to keep cyclomatic complexity manageable.
|
|
3576
|
+
*/
|
|
3577
|
+
async function routeToHandler(resource, action, restArgs, resolveArgs, ctx, credentials) {
|
|
3578
|
+
switch (resource) {
|
|
3579
|
+
case "projects": return await handleProjects(action, {
|
|
3580
|
+
...restArgs,
|
|
3581
|
+
...resolveArgs
|
|
3582
|
+
}, ctx);
|
|
3583
|
+
case "time": return await handleTime(action, {
|
|
3584
|
+
...restArgs,
|
|
3585
|
+
...resolveArgs
|
|
3586
|
+
}, ctx);
|
|
3587
|
+
case "tasks": return await handleTasks(action, {
|
|
3588
|
+
...restArgs,
|
|
3589
|
+
...resolveArgs
|
|
3590
|
+
}, ctx);
|
|
3591
|
+
case "services": return await handleServices(action, restArgs, ctx);
|
|
3592
|
+
case "people": return await handlePeople(action, {
|
|
3593
|
+
...restArgs,
|
|
3594
|
+
...resolveArgs
|
|
3595
|
+
}, ctx, credentials);
|
|
3596
|
+
case "companies": return await handleCompanies(action, {
|
|
3597
|
+
...restArgs,
|
|
3598
|
+
...resolveArgs
|
|
3599
|
+
}, ctx);
|
|
3600
|
+
case "comments": return await handleComments(action, restArgs, ctx);
|
|
3601
|
+
case "attachments": return await handleAttachments(action, restArgs, ctx);
|
|
3602
|
+
case "timers": return await handleTimers(action, restArgs, ctx);
|
|
3603
|
+
case "deals": return await handleDeals(action, {
|
|
3604
|
+
...restArgs,
|
|
3605
|
+
...resolveArgs
|
|
3606
|
+
}, ctx);
|
|
3607
|
+
case "bookings": return await handleBookings(action, restArgs, ctx);
|
|
3608
|
+
case "pages": return await handlePages(action, restArgs, ctx);
|
|
3609
|
+
case "discussions": return await handleDiscussions(action, restArgs, ctx);
|
|
3610
|
+
case "activities": return await handleActivities(action, restArgs, ctx);
|
|
3611
|
+
case "reports": return await handleReports(action, restArgs, ctx);
|
|
3612
|
+
case "summaries": return await handleSummaries(action, restArgs, ctx);
|
|
3613
|
+
case "workflows": return await handleWorkflows(action, restArgs, ctx);
|
|
3614
|
+
case "budgets": return inputErrorResult(new UserInputError("The \"budgets\" resource has been removed. Budgets are deals with type=2.", [
|
|
3615
|
+
"Use resource=\"deals\" with filter[type]=\"2\" to list only budgets",
|
|
3616
|
+
"To create a budget: resource=\"deals\" action=\"create\" with budget=true",
|
|
3617
|
+
"Use action=\"help\" resource=\"deals\" for full documentation"
|
|
3618
|
+
]));
|
|
3619
|
+
case "docs": return inputErrorResult(new UserInputError("Unknown resource \"docs\". Did you mean \"pages\"?", [
|
|
3620
|
+
"Use resource=\"pages\" to access Productive pages/documents",
|
|
3621
|
+
"Use action=\"list\" to list all pages",
|
|
3622
|
+
"Use action=\"help\" resource=\"pages\" for full documentation"
|
|
3623
|
+
]));
|
|
3624
|
+
default: return inputErrorResult(ErrorMessages.unknownResource(resource, VALID_RESOURCES));
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
/**
|
|
3065
3628
|
* Execute a tool with the given credentials and arguments
|
|
3066
3629
|
*/
|
|
3067
3630
|
async function executeToolWithCredentials(name, args, credentials) {
|
|
3068
3631
|
if (name !== "productive") return errorResult(`Unknown tool: ${name}`);
|
|
3069
3632
|
const typedArgs = args;
|
|
3070
3633
|
if (typedArgs.resource === "batch") return handleBatch(typedArgs.operations, credentials, executeToolWithCredentials);
|
|
3634
|
+
if (args.params !== void 0) 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\""]));
|
|
3071
3635
|
const { resource, action, filter, page, per_page, compact, include, query, resources, no_hints, type, ...restArgs } = typedArgs;
|
|
3072
3636
|
if (resource === "search") return await handleSearch(query, resources, credentials, executeToolWithCredentials);
|
|
3073
3637
|
const api = new ProductiveApi({ config: {
|
|
@@ -3085,6 +3649,17 @@ async function executeToolWithCredentials(name, args, credentials) {
|
|
|
3085
3649
|
query
|
|
3086
3650
|
};
|
|
3087
3651
|
const includeHints = no_hints !== true && action === "get" && !isCompact;
|
|
3652
|
+
const includeSuggestions = no_hints !== true;
|
|
3653
|
+
if (include && include.length > 0) {
|
|
3654
|
+
const includeValidation = validateIncludes(resource, include);
|
|
3655
|
+
if (includeValidation && includeValidation.invalid.length > 0) {
|
|
3656
|
+
const { invalid, valid, suggestions } = includeValidation;
|
|
3657
|
+
const hintLines = [`Invalid include value${invalid.length > 1 ? "s" : ""}: ${invalid.join(", ")}`, `Valid includes for ${resource}: ${(VALID_INCLUDES[resource] ?? []).join(", ")}`];
|
|
3658
|
+
for (const [value, suggestion] of Object.entries(suggestions)) hintLines.push(`"${value}": ${suggestion}`);
|
|
3659
|
+
if (valid.length > 0) hintLines.push(`The following includes are valid and will be used if you remove the invalid ones: ${valid.join(", ")}`);
|
|
3660
|
+
return inputErrorResult(new UserInputError(`Invalid include value${invalid.length > 1 ? "s" : ""} for resource "${resource}": ${invalid.join(", ")}`, hintLines));
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3088
3663
|
const execCtx = fromHandlerContext({ api }, { userId: credentials.userId });
|
|
3089
3664
|
const ctx = {
|
|
3090
3665
|
formatOptions,
|
|
@@ -3093,56 +3668,29 @@ async function executeToolWithCredentials(name, args, credentials) {
|
|
|
3093
3668
|
perPage,
|
|
3094
3669
|
include,
|
|
3095
3670
|
includeHints,
|
|
3671
|
+
includeSuggestions,
|
|
3096
3672
|
executor: () => execCtx
|
|
3097
3673
|
};
|
|
3098
3674
|
try {
|
|
3675
|
+
if (action === "search" && resource !== "search") 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.`, [
|
|
3676
|
+
`Use resource="${resource}" action="list" with query="<your search terms>" to filter ${resource}`,
|
|
3677
|
+
"Use resource=\"search\" action=\"run\" with query=\"<your search terms>\" to search across all resources",
|
|
3678
|
+
`Use action="help" resource="${resource}" to see all supported actions and filters`
|
|
3679
|
+
]));
|
|
3680
|
+
if (action.startsWith("get_")) {
|
|
3681
|
+
const suggestedResource = action.replace(/^get_/, "").replace(/_/g, " ");
|
|
3682
|
+
return inputErrorResult(new UserInputError(`action="${action}" is not valid. Actions use simple verbs like "list", "get", "create", not function-style names.`, [
|
|
3683
|
+
`To retrieve a single item, use action="get" with an id parameter`,
|
|
3684
|
+
`To retrieve multiple items, use action="list" (e.g. resource="${resource || suggestedResource}" action="list")`,
|
|
3685
|
+
`Use action="help" resource="${resource || "tasks"}" to see all supported actions for a resource`
|
|
3686
|
+
]));
|
|
3687
|
+
}
|
|
3099
3688
|
if (action === "help" && resource !== "summaries") return resource ? handleHelp(resource) : handleHelpOverview();
|
|
3100
3689
|
if (action === "schema") return resource ? handleSchema(resource) : handleSchemaOverview();
|
|
3101
|
-
|
|
3690
|
+
return await routeToHandler(resource, action, restArgs, {
|
|
3102
3691
|
query,
|
|
3103
3692
|
type
|
|
3104
|
-
};
|
|
3105
|
-
switch (resource) {
|
|
3106
|
-
case "projects": return await handleProjects(action, {
|
|
3107
|
-
...restArgs,
|
|
3108
|
-
...resolveArgs
|
|
3109
|
-
}, ctx);
|
|
3110
|
-
case "time": return await handleTime(action, {
|
|
3111
|
-
...restArgs,
|
|
3112
|
-
...resolveArgs
|
|
3113
|
-
}, ctx);
|
|
3114
|
-
case "tasks": return await handleTasks(action, {
|
|
3115
|
-
...restArgs,
|
|
3116
|
-
...resolveArgs
|
|
3117
|
-
}, ctx);
|
|
3118
|
-
case "services": return await handleServices(action, restArgs, ctx);
|
|
3119
|
-
case "people": return await handlePeople(action, {
|
|
3120
|
-
...restArgs,
|
|
3121
|
-
...resolveArgs
|
|
3122
|
-
}, ctx, credentials);
|
|
3123
|
-
case "companies": return await handleCompanies(action, {
|
|
3124
|
-
...restArgs,
|
|
3125
|
-
...resolveArgs
|
|
3126
|
-
}, ctx);
|
|
3127
|
-
case "comments": return await handleComments(action, restArgs, ctx);
|
|
3128
|
-
case "attachments": return await handleAttachments(action, restArgs, ctx);
|
|
3129
|
-
case "timers": return await handleTimers(action, restArgs, ctx);
|
|
3130
|
-
case "deals": return await handleDeals(action, {
|
|
3131
|
-
...restArgs,
|
|
3132
|
-
...resolveArgs
|
|
3133
|
-
}, ctx);
|
|
3134
|
-
case "bookings": return await handleBookings(action, restArgs, ctx);
|
|
3135
|
-
case "pages": return await handlePages(action, restArgs, ctx);
|
|
3136
|
-
case "discussions": return await handleDiscussions(action, restArgs, ctx);
|
|
3137
|
-
case "reports": return await handleReports(action, restArgs, ctx);
|
|
3138
|
-
case "summaries": return await handleSummaries(action, restArgs, ctx);
|
|
3139
|
-
case "budgets": return inputErrorResult(new UserInputError("The \"budgets\" resource has been removed. Budgets are deals with type=2.", [
|
|
3140
|
-
"Use resource=\"deals\" with filter[type]=\"2\" to list only budgets",
|
|
3141
|
-
"To create a budget: resource=\"deals\" action=\"create\" with budget=true",
|
|
3142
|
-
"Use action=\"help\" resource=\"deals\" for full documentation"
|
|
3143
|
-
]));
|
|
3144
|
-
default: return inputErrorResult(ErrorMessages.unknownResource(resource, VALID_RESOURCES));
|
|
3145
|
-
}
|
|
3693
|
+
}, ctx, credentials);
|
|
3146
3694
|
} catch (error) {
|
|
3147
3695
|
if (isUserInputError(error)) return formatError(error);
|
|
3148
3696
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -3156,4 +3704,4 @@ async function executeToolWithCredentials(name, args, credentials) {
|
|
|
3156
3704
|
}
|
|
3157
3705
|
export { executeToolWithCredentials as t };
|
|
3158
3706
|
|
|
3159
|
-
//# sourceMappingURL=handlers-
|
|
3707
|
+
//# sourceMappingURL=handlers-CaOBYauF.js.map
|