@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.
@@ -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
- const options = createConfig.mapOptions(args);
908
- return jsonResult({
909
- success: true,
910
- ...formatter((await executors.create(options, execCtx)).data, formatOptions)
911
- });
912
- }
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));
1059
+ }],
1060
+ common_actions: [{
1061
+ action: "Resolve this discussion",
1062
+ example: {
1063
+ resource: "discussions",
1064
+ action: "resolve",
1065
+ id: discussionId
918
1066
  }
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);
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
- return inputErrorResult(ErrorMessages.invalidAction(action, resource, actions));
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$2 = [
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$2));
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$1 = ["get"];
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$1));
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": return jsonResult((await getMyDaySummary({}, execCtx)).data);
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
- required: [
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
- mapOptions: (args) => ({
2984
- personId: args.person_id,
2985
- serviceId: args.service_id,
2986
- time: args.time,
2987
- date: args.date,
2988
- note: args.note ?? void 0,
2989
- taskId: args.task_id,
2990
- projectId: args.project_id
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
- const resolveArgs = {
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-B8GRTaDu.js.map
3707
+ //# sourceMappingURL=handlers-CaOBYauF.js.map