@studiometa/productive-mcp 0.10.1 → 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.
Files changed (43) hide show
  1. package/README.md +25 -13
  2. package/dist/formatters.d.ts +1 -0
  3. package/dist/formatters.d.ts.map +1 -1
  4. package/dist/handlers/activities.d.ts +26 -0
  5. package/dist/handlers/activities.d.ts.map +1 -0
  6. package/dist/handlers/deals.d.ts.map +1 -1
  7. package/dist/handlers/factory.d.ts.map +1 -1
  8. package/dist/handlers/help.d.ts.map +1 -1
  9. package/dist/handlers/index.d.ts.map +1 -1
  10. package/dist/handlers/projects.d.ts +1 -1
  11. package/dist/handlers/projects.d.ts.map +1 -1
  12. package/dist/handlers/services.d.ts.map +1 -1
  13. package/dist/handlers/summaries.d.ts +18 -0
  14. package/dist/handlers/summaries.d.ts.map +1 -0
  15. package/dist/handlers/tasks.d.ts.map +1 -1
  16. package/dist/handlers/time.d.ts.map +1 -1
  17. package/dist/handlers/types.d.ts +1 -0
  18. package/dist/handlers/types.d.ts.map +1 -1
  19. package/dist/handlers/valid-includes.d.ts +20 -0
  20. package/dist/handlers/valid-includes.d.ts.map +1 -0
  21. package/dist/handlers/workflows.d.ts +35 -0
  22. package/dist/handlers/workflows.d.ts.map +1 -0
  23. package/dist/{handlers-DWowqxFA.js → handlers-CaOBYauF.js} +997 -304
  24. package/dist/handlers-CaOBYauF.js.map +1 -0
  25. package/dist/handlers.js +1 -1
  26. package/dist/hints.d.ts +9 -0
  27. package/dist/hints.d.ts.map +1 -1
  28. package/dist/http.js +2 -2
  29. package/dist/index.js +2 -2
  30. package/dist/schema.d.ts +20 -0
  31. package/dist/schema.d.ts.map +1 -1
  32. package/dist/server.js +2 -2
  33. package/dist/stdio.js +1 -1
  34. package/dist/suggestions.d.ts +44 -0
  35. package/dist/suggestions.d.ts.map +1 -0
  36. package/dist/tools.d.ts.map +1 -1
  37. package/dist/tools.js +2 -1
  38. package/dist/tools.js.map +1 -1
  39. package/dist/{version-DoRPyhTL.js → version-BRTaTnUG.js} +2 -2
  40. package/dist/{version-DoRPyhTL.js.map → version-BRTaTnUG.js.map} +1 -1
  41. package/package.json +3 -3
  42. package/skills/SKILL.md +139 -16
  43. package/dist/handlers-DWowqxFA.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, getDiscussion, getPage, getPerson, getProject, getReport, getTask, 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) {
@@ -724,240 +1080,36 @@ function getDiscussionHints(discussionId, pageId) {
724
1080
  example: {
725
1081
  resource: "pages",
726
1082
  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;
906
- }
907
- const options = createConfig.mapOptions(args);
908
- return jsonResult({
909
- success: true,
910
- ...formatter((await executors.create(options, execCtx)).data, formatOptions)
911
- });
1083
+ id: pageId
912
1084
  }
913
- if (action === "update") {
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));
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
918
1099
  }
919
- const options = {
920
- id,
921
- ...updateConfig.mapOptions(args)
922
- };
923
- return jsonResult({
924
- success: true,
925
- ...formatter((await executors.update(options, execCtx)).data, formatOptions)
926
- });
927
- }
928
- if (action === "delete") {
929
- if (!executors.delete) return inputErrorResult(ErrorMessages.invalidAction(action, resource, actions));
930
- if (!id) return inputErrorResult(ErrorMessages.missingId("delete"));
931
- await executors.delete({ id }, execCtx);
932
- return jsonResult({
933
- success: true,
934
- deleted: id
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);
958
- }
959
- return inputErrorResult(ErrorMessages.invalidAction(action, resource, actions));
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.
@@ -1238,7 +1390,8 @@ const handleDeals = createResourceHandler({
1238
1390
  "get",
1239
1391
  "create",
1240
1392
  "update",
1241
- "resolve"
1393
+ "resolve",
1394
+ "context"
1242
1395
  ],
1243
1396
  formatter: formatDeal$1,
1244
1397
  hints: (_data, id) => getDealHints(id),
@@ -1259,6 +1412,20 @@ const handleDeals = createResourceHandler({
1259
1412
  })
1260
1413
  },
1261
1414
  update: { mapOptions: (args) => ({ name: args.name }) },
1415
+ customActions: { context: async (args, ctx, execCtx) => {
1416
+ if (!args.id) return inputErrorResult(ErrorMessages.missingId("context"));
1417
+ const result = await getDealContext({ id: args.id }, execCtx);
1418
+ const formatOptions = {
1419
+ ...ctx.formatOptions,
1420
+ included: result.included
1421
+ };
1422
+ return jsonResult({
1423
+ ...formatDeal$1(result.data.deal, formatOptions),
1424
+ services: result.data.services.map((s) => formatService$1(s, { compact: true })),
1425
+ comments: result.data.comments.map((c) => formatComment$1(c, { compact: true })),
1426
+ time_entries: result.data.time_entries.map((t) => formatTimeEntry$1(t, { compact: true }))
1427
+ });
1428
+ } },
1262
1429
  executors: {
1263
1430
  list: listDeals,
1264
1431
  get: getDeal,
@@ -1389,7 +1556,8 @@ var RESOURCE_HELP = {
1389
1556
  actions: {
1390
1557
  list: "List all projects with optional filters",
1391
1558
  get: "Get a single project by ID (supports PRJ-123, P-123 format)",
1392
- resolve: "Resolve by project number (PRJ-123, P-123)"
1559
+ resolve: "Resolve by project number (PRJ-123, P-123)",
1560
+ context: "Get full project context in one call: project details + open tasks + services + recent time entries"
1393
1561
  },
1394
1562
  filters: {
1395
1563
  query: "Text search on project name",
@@ -1430,6 +1598,14 @@ var RESOURCE_HELP = {
1430
1598
  action: "get",
1431
1599
  id: "12345"
1432
1600
  }
1601
+ },
1602
+ {
1603
+ description: "Get full project context",
1604
+ params: {
1605
+ resource: "projects",
1606
+ action: "context",
1607
+ id: "12345"
1608
+ }
1433
1609
  }
1434
1610
  ]
1435
1611
  },
@@ -1440,7 +1616,8 @@ var RESOURCE_HELP = {
1440
1616
  get: "Get a single task by ID with full details (description, comments, etc.)",
1441
1617
  create: "Create a new task (requires title, project_id, task_list_id)",
1442
1618
  update: "Update an existing task",
1443
- resolve: "Resolve by text search"
1619
+ resolve: "Resolve by text search",
1620
+ context: "Get full task context in one call: task details + comments + time entries + subtasks"
1444
1621
  },
1445
1622
  filters: {
1446
1623
  query: "Text search on task title",
@@ -1516,6 +1693,14 @@ var RESOURCE_HELP = {
1516
1693
  project_id: "12345",
1517
1694
  task_list_id: "111"
1518
1695
  }
1696
+ },
1697
+ {
1698
+ description: "Get full task context",
1699
+ params: {
1700
+ resource: "tasks",
1701
+ action: "context",
1702
+ id: "67890"
1703
+ }
1519
1704
  }
1520
1705
  ]
1521
1706
  },
@@ -1596,6 +1781,13 @@ var RESOURCE_HELP = {
1596
1781
  worked_time: "Logged time in minutes"
1597
1782
  },
1598
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
+ }, {
1599
1791
  description: "List services for a project",
1600
1792
  params: {
1601
1793
  resource: "services",
@@ -1833,7 +2025,8 @@ var RESOURCE_HELP = {
1833
2025
  get: "Get a single deal by ID (supports D-123, DEAL-123 format)",
1834
2026
  create: "Create a new deal (requires name, company_id)",
1835
2027
  update: "Update an existing deal",
1836
- resolve: "Resolve by deal number (D-123, DEAL-123)"
2028
+ resolve: "Resolve by deal number (D-123, DEAL-123)",
2029
+ context: "Get full deal context in one call: deal details + services + comments + time entries"
1837
2030
  },
1838
2031
  filters: {
1839
2032
  query: "Text search on deal name",
@@ -1883,6 +2076,14 @@ var RESOURCE_HELP = {
1883
2076
  action: "list",
1884
2077
  filter: { type: "2" }
1885
2078
  }
2079
+ },
2080
+ {
2081
+ description: "Get full deal context",
2082
+ params: {
2083
+ resource: "deals",
2084
+ action: "context",
2085
+ id: "12345"
2086
+ }
1886
2087
  }
1887
2088
  ]
1888
2089
  },
@@ -2061,6 +2262,137 @@ var RESOURCE_HELP = {
2061
2262
  }
2062
2263
  ]
2063
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
+ },
2064
2396
  reports: {
2065
2397
  description: "Generate various reports (time, budget, project, etc.)",
2066
2398
  actions: { get: "Generate a report (requires report_type)" },
@@ -2173,7 +2505,7 @@ const handlePages = createResourceHandler({
2173
2505
  /**
2174
2506
  * People MCP handler.
2175
2507
  */
2176
- var VALID_ACTIONS$1 = [
2508
+ var VALID_ACTIONS$3 = [
2177
2509
  "list",
2178
2510
  "get",
2179
2511
  "me",
@@ -2224,7 +2556,7 @@ async function handlePeople(action, args, ctx, credentials) {
2224
2556
  });
2225
2557
  return jsonResult(response);
2226
2558
  }
2227
- return inputErrorResult(ErrorMessages.invalidAction(action, "people", VALID_ACTIONS$1));
2559
+ return inputErrorResult(ErrorMessages.invalidAction(action, "people", VALID_ACTIONS$3));
2228
2560
  }
2229
2561
  /**
2230
2562
  * Projects MCP handler.
@@ -2234,18 +2566,36 @@ async function handlePeople(action, args, ctx, credentials) {
2234
2566
  /**
2235
2567
  * Handle projects resource.
2236
2568
  *
2237
- * Supports: list, get, resolve
2569
+ * Supports: list, get, resolve, context
2238
2570
  */
2239
2571
  const handleProjects = createResourceHandler({
2240
2572
  resource: "projects",
2241
2573
  actions: [
2242
2574
  "list",
2243
2575
  "get",
2244
- "resolve"
2576
+ "resolve",
2577
+ "context"
2245
2578
  ],
2246
2579
  formatter: formatProject$1,
2247
2580
  hints: (_data, id) => getProjectHints(id),
2248
2581
  supportsResolve: true,
2582
+ customActions: { context: async (args, ctx, execCtx) => {
2583
+ if (!args.id) return inputErrorResult(ErrorMessages.missingId("context"));
2584
+ const result = await getProjectContext({ id: args.id }, execCtx);
2585
+ const formatOptions = {
2586
+ ...ctx.formatOptions,
2587
+ included: result.included
2588
+ };
2589
+ return jsonResult({
2590
+ ...formatProject$1(result.data.project, ctx.formatOptions),
2591
+ tasks: result.data.tasks.map((t) => formatTask$1(t, {
2592
+ ...formatOptions,
2593
+ compact: true
2594
+ })),
2595
+ services: result.data.services.map((s) => formatService$1(s, { compact: true })),
2596
+ time_entries: result.data.time_entries.map((t) => formatTimeEntry$1(t, { compact: true }))
2597
+ });
2598
+ } },
2249
2599
  executors: {
2250
2600
  list: listProjects,
2251
2601
  get: getProject
@@ -2264,11 +2614,11 @@ function formatReportData(data) {
2264
2614
  };
2265
2615
  });
2266
2616
  }
2267
- var VALID_ACTIONS = ["get"];
2617
+ var VALID_ACTIONS$2 = ["get"];
2268
2618
  async function handleReports(action, args, ctx) {
2269
2619
  const { filter, page, perPage } = ctx;
2270
2620
  const { report_type, group, from, to, person_id, project_id, company_id, deal_id, status } = args;
2271
- if (action !== "get") return inputErrorResult(ErrorMessages.invalidAction(action, "reports", VALID_ACTIONS));
2621
+ if (action !== "get") return inputErrorResult(ErrorMessages.invalidAction(action, "reports", VALID_ACTIONS$2));
2272
2622
  if (!report_type) return inputErrorResult(ErrorMessages.missingReportType());
2273
2623
  if (!VALID_REPORT_TYPES.includes(report_type)) return inputErrorResult(ErrorMessages.invalidReportType(report_type, [...VALID_REPORT_TYPES]));
2274
2624
  const execCtx = ctx.executor();
@@ -2753,9 +3103,104 @@ const handleServices = createResourceHandler({
2753
3103
  resource: "services",
2754
3104
  actions: ["list"],
2755
3105
  formatter: formatService$1,
2756
- 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
+ } }
2757
3126
  });
2758
3127
  /**
3128
+ * Summaries MCP handler.
3129
+ *
3130
+ * Custom handler for dashboard-style summaries (not using createResourceHandler).
3131
+ * Routes actions to the appropriate summary executor.
3132
+ */
3133
+ var VALID_ACTIONS$1 = [
3134
+ "my_day",
3135
+ "project_health",
3136
+ "team_pulse",
3137
+ "help"
3138
+ ];
3139
+ /**
3140
+ * Handle summaries resource.
3141
+ *
3142
+ * Supports: my_day, project_health, team_pulse
3143
+ */
3144
+ async function handleSummaries(action, args, ctx) {
3145
+ if (!VALID_ACTIONS$1.includes(action)) return inputErrorResult(ErrorMessages.invalidAction(action, "summaries", VALID_ACTIONS$1));
3146
+ const execCtx = ctx.executor();
3147
+ switch (action) {
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
+ }
3159
+ case "project_health":
3160
+ if (!args.project_id) return inputErrorResult(new UserInputError("project_id is required for project_health summary", [
3161
+ "Provide the project_id parameter",
3162
+ "You can find project IDs using resource=\"projects\" action=\"list\"",
3163
+ "Or use a project number like \"PRJ-123\""
3164
+ ]));
3165
+ return jsonResult((await getProjectHealthSummary({ projectId: args.project_id }, execCtx)).data);
3166
+ case "team_pulse": return jsonResult((await getTeamPulseSummary({}, execCtx)).data);
3167
+ case "help": return jsonResult({
3168
+ resource: "summaries",
3169
+ description: "Dashboard-style summaries that aggregate data from multiple resources",
3170
+ actions: {
3171
+ my_day: {
3172
+ description: "Personal dashboard for the current user",
3173
+ parameters: {},
3174
+ returns: {
3175
+ tasks: "Open and overdue tasks assigned to you",
3176
+ time: "Time entries logged today",
3177
+ timers: "Currently running timers"
3178
+ }
3179
+ },
3180
+ project_health: {
3181
+ description: "Project status with budget burn and task stats",
3182
+ parameters: { project_id: "Required. Project ID or project number (e.g., PRJ-123)" },
3183
+ returns: {
3184
+ project: "Project details",
3185
+ tasks: "Open and overdue task counts",
3186
+ budget: "Budget burn rate by service",
3187
+ recent_activity: "Time tracking activity in last 7 days"
3188
+ }
3189
+ },
3190
+ team_pulse: {
3191
+ description: "Team-wide time tracking activity for today",
3192
+ parameters: {},
3193
+ returns: {
3194
+ team: "Counts of active users, those tracking time, and with timers",
3195
+ people: "Per-person breakdown of time logged and active timers"
3196
+ }
3197
+ }
3198
+ }
3199
+ });
3200
+ default: return inputErrorResult(ErrorMessages.invalidAction(action, "summaries", VALID_ACTIONS$1));
3201
+ }
3202
+ }
3203
+ /**
2759
3204
  * Tasks MCP handler.
2760
3205
  */
2761
3206
  const handleTasks = createResourceHandler({
@@ -2766,7 +3211,8 @@ const handleTasks = createResourceHandler({
2766
3211
  "get",
2767
3212
  "create",
2768
3213
  "update",
2769
- "resolve"
3214
+ "resolve",
3215
+ "context"
2770
3216
  ],
2771
3217
  formatter: formatTask$1,
2772
3218
  hints: (data, id) => {
@@ -2798,6 +3244,23 @@ const handleTasks = createResourceHandler({
2798
3244
  description: args.description,
2799
3245
  assigneeId: args.assignee_id
2800
3246
  }) },
3247
+ customActions: { context: async (args, ctx, execCtx) => {
3248
+ if (!args.id) return inputErrorResult(ErrorMessages.missingId("context"));
3249
+ const result = await getTaskContext({ id: args.id }, execCtx);
3250
+ const formatOptions = {
3251
+ ...ctx.formatOptions,
3252
+ included: result.included
3253
+ };
3254
+ return jsonResult({
3255
+ ...formatTask$1(result.data.task, formatOptions),
3256
+ comments: result.data.comments.map((c) => formatComment$1(c, { compact: true })),
3257
+ time_entries: result.data.time_entries.map((t) => formatTimeEntry$1(t, { compact: true })),
3258
+ subtasks: result.data.subtasks.map((s) => formatTask$1(s, {
3259
+ ...formatOptions,
3260
+ compact: true
3261
+ }))
3262
+ });
3263
+ } },
2801
3264
  executors: {
2802
3265
  list: listTasks,
2803
3266
  get: getTask,
@@ -2829,23 +3292,28 @@ const handleTime = createResourceHandler({
2829
3292
  },
2830
3293
  supportsResolve: true,
2831
3294
  resolveArgsFromArgs: (args) => ({ project_id: args.project_id }),
2832
- create: {
2833
- required: [
2834
- "person_id",
3295
+ customActions: { create: async (args, ctx, execCtx) => {
3296
+ const missingFields = [
2835
3297
  "service_id",
2836
3298
  "time",
2837
3299
  "date"
2838
- ],
2839
- mapOptions: (args) => ({
2840
- personId: args.person_id,
2841
- serviceId: args.service_id,
2842
- time: args.time,
2843
- date: args.date,
2844
- note: args.note ?? void 0,
2845
- taskId: args.task_id,
2846
- projectId: args.project_id
2847
- })
2848
- },
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
+ } },
2849
3317
  update: { mapOptions: (args) => ({
2850
3318
  time: args.time ?? void 0,
2851
3319
  date: args.date ?? void 0,
@@ -2907,6 +3375,191 @@ const handleTimers = createResourceHandler({
2907
3375
  }
2908
3376
  });
2909
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
+ /**
2910
3563
  * Tool execution handlers for Productive MCP server
2911
3564
  * These are shared between stdio and HTTP transports
2912
3565
  *
@@ -2918,12 +3571,67 @@ var VALID_RESOURCES = [...RESOURCES];
2918
3571
  /** Default page size for MCP (smaller than CLI to reduce token usage) */
2919
3572
  var DEFAULT_PER_PAGE = 20;
2920
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
+ /**
2921
3628
  * Execute a tool with the given credentials and arguments
2922
3629
  */
2923
3630
  async function executeToolWithCredentials(name, args, credentials) {
2924
3631
  if (name !== "productive") return errorResult(`Unknown tool: ${name}`);
2925
3632
  const typedArgs = args;
2926
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\""]));
2927
3635
  const { resource, action, filter, page, per_page, compact, include, query, resources, no_hints, type, ...restArgs } = typedArgs;
2928
3636
  if (resource === "search") return await handleSearch(query, resources, credentials, executeToolWithCredentials);
2929
3637
  const api = new ProductiveApi({ config: {
@@ -2941,7 +3649,18 @@ async function executeToolWithCredentials(name, args, credentials) {
2941
3649
  query
2942
3650
  };
2943
3651
  const includeHints = no_hints !== true && action === "get" && !isCompact;
2944
- const execCtx = fromHandlerContext({ api });
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
+ }
3663
+ const execCtx = fromHandlerContext({ api }, { userId: credentials.userId });
2945
3664
  const ctx = {
2946
3665
  formatOptions,
2947
3666
  filter: stringFilter,
@@ -2949,55 +3668,29 @@ async function executeToolWithCredentials(name, args, credentials) {
2949
3668
  perPage,
2950
3669
  include,
2951
3670
  includeHints,
3671
+ includeSuggestions,
2952
3672
  executor: () => execCtx
2953
3673
  };
2954
3674
  try {
2955
- if (action === "help") return resource ? handleHelp(resource) : handleHelpOverview();
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
+ }
3688
+ if (action === "help" && resource !== "summaries") return resource ? handleHelp(resource) : handleHelpOverview();
2956
3689
  if (action === "schema") return resource ? handleSchema(resource) : handleSchemaOverview();
2957
- const resolveArgs = {
3690
+ return await routeToHandler(resource, action, restArgs, {
2958
3691
  query,
2959
3692
  type
2960
- };
2961
- switch (resource) {
2962
- case "projects": return await handleProjects(action, {
2963
- ...restArgs,
2964
- ...resolveArgs
2965
- }, ctx);
2966
- case "time": return await handleTime(action, {
2967
- ...restArgs,
2968
- ...resolveArgs
2969
- }, ctx);
2970
- case "tasks": return await handleTasks(action, {
2971
- ...restArgs,
2972
- ...resolveArgs
2973
- }, ctx);
2974
- case "services": return await handleServices(action, restArgs, ctx);
2975
- case "people": return await handlePeople(action, {
2976
- ...restArgs,
2977
- ...resolveArgs
2978
- }, ctx, credentials);
2979
- case "companies": return await handleCompanies(action, {
2980
- ...restArgs,
2981
- ...resolveArgs
2982
- }, ctx);
2983
- case "comments": return await handleComments(action, restArgs, ctx);
2984
- case "attachments": return await handleAttachments(action, restArgs, ctx);
2985
- case "timers": return await handleTimers(action, restArgs, ctx);
2986
- case "deals": return await handleDeals(action, {
2987
- ...restArgs,
2988
- ...resolveArgs
2989
- }, ctx);
2990
- case "bookings": return await handleBookings(action, restArgs, ctx);
2991
- case "pages": return await handlePages(action, restArgs, ctx);
2992
- case "discussions": return await handleDiscussions(action, restArgs, ctx);
2993
- case "reports": return await handleReports(action, restArgs, ctx);
2994
- case "budgets": return inputErrorResult(new UserInputError("The \"budgets\" resource has been removed. Budgets are deals with type=2.", [
2995
- "Use resource=\"deals\" with filter[type]=\"2\" to list only budgets",
2996
- "To create a budget: resource=\"deals\" action=\"create\" with budget=true",
2997
- "Use action=\"help\" resource=\"deals\" for full documentation"
2998
- ]));
2999
- default: return inputErrorResult(ErrorMessages.unknownResource(resource, VALID_RESOURCES));
3000
- }
3693
+ }, ctx, credentials);
3001
3694
  } catch (error) {
3002
3695
  if (isUserInputError(error)) return formatError(error);
3003
3696
  const message = error instanceof Error ? error.message : String(error);
@@ -3011,4 +3704,4 @@ async function executeToolWithCredentials(name, args, credentials) {
3011
3704
  }
3012
3705
  export { executeToolWithCredentials as t };
3013
3706
 
3014
- //# sourceMappingURL=handlers-DWowqxFA.js.map
3707
+ //# sourceMappingURL=handlers-CaOBYauF.js.map