@studiometa/productive-mcp 0.9.2 → 0.10.1

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 (61) hide show
  1. package/dist/errors.d.ts +1 -0
  2. package/dist/errors.d.ts.map +1 -1
  3. package/dist/formatters.d.ts +0 -4
  4. package/dist/formatters.d.ts.map +1 -1
  5. package/dist/handlers/attachments.d.ts +5 -2
  6. package/dist/handlers/attachments.d.ts.map +1 -1
  7. package/dist/handlers/batch.d.ts +58 -0
  8. package/dist/handlers/batch.d.ts.map +1 -0
  9. package/dist/handlers/bookings.d.ts +5 -2
  10. package/dist/handlers/bookings.d.ts.map +1 -1
  11. package/dist/handlers/comments.d.ts +5 -2
  12. package/dist/handlers/comments.d.ts.map +1 -1
  13. package/dist/handlers/companies.d.ts +11 -5
  14. package/dist/handlers/companies.d.ts.map +1 -1
  15. package/dist/handlers/deals.d.ts +4 -5
  16. package/dist/handlers/deals.d.ts.map +1 -1
  17. package/dist/handlers/discussions.d.ts +5 -9
  18. package/dist/handlers/discussions.d.ts.map +1 -1
  19. package/dist/handlers/factory.d.ts +116 -0
  20. package/dist/handlers/factory.d.ts.map +1 -0
  21. package/dist/handlers/help.d.ts.map +1 -1
  22. package/dist/handlers/index.d.ts.map +1 -1
  23. package/dist/handlers/pages.d.ts +12 -9
  24. package/dist/handlers/pages.d.ts.map +1 -1
  25. package/dist/handlers/projects.d.ts +11 -5
  26. package/dist/handlers/projects.d.ts.map +1 -1
  27. package/dist/handlers/schema.d.ts +33 -0
  28. package/dist/handlers/schema.d.ts.map +1 -0
  29. package/dist/handlers/search.d.ts +28 -0
  30. package/dist/handlers/search.d.ts.map +1 -0
  31. package/dist/handlers/services.d.ts +12 -2
  32. package/dist/handlers/services.d.ts.map +1 -1
  33. package/dist/handlers/tasks.d.ts +4 -5
  34. package/dist/handlers/tasks.d.ts.map +1 -1
  35. package/dist/handlers/time.d.ts +4 -6
  36. package/dist/handlers/time.d.ts.map +1 -1
  37. package/dist/handlers/timers.d.ts +5 -2
  38. package/dist/handlers/timers.d.ts.map +1 -1
  39. package/dist/handlers/types.d.ts +6 -0
  40. package/dist/handlers/types.d.ts.map +1 -1
  41. package/dist/{handlers-D4tRd30c.js → handlers-DWowqxFA.js} +1247 -898
  42. package/dist/handlers-DWowqxFA.js.map +1 -0
  43. package/dist/handlers.js +1 -1
  44. package/dist/hints.d.ts +0 -4
  45. package/dist/hints.d.ts.map +1 -1
  46. package/dist/http.js +2 -2
  47. package/dist/index.js +2 -2
  48. package/dist/schema.d.ts +17 -2
  49. package/dist/schema.d.ts.map +1 -1
  50. package/dist/server.js +2 -2
  51. package/dist/stdio.js +1 -1
  52. package/dist/tools.d.ts.map +1 -1
  53. package/dist/tools.js +63 -15
  54. package/dist/tools.js.map +1 -1
  55. package/dist/{version-IB2ulmSy.js → version-DoRPyhTL.js} +2 -2
  56. package/dist/{version-IB2ulmSy.js.map → version-DoRPyhTL.js.map} +1 -1
  57. package/package.json +3 -3
  58. package/skills/SKILL.md +35 -54
  59. package/dist/handlers/budgets.d.ts +0 -9
  60. package/dist/handlers/budgets.d.ts.map +0 -1
  61. package/dist/handlers-D4tRd30c.js.map +0 -1
@@ -1,5 +1,5 @@
1
- import { ProductiveApi, formatAttachment, formatBooking, formatBudget, 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, fromHandlerContext, getAttachment, getBooking, getBudget, getComment, getCompany, getDeal, getDiscussion, getPage, getPerson, getProject, getReport, getTask, getTimeEntry, getTimer, listAttachments, listBookings, listBudgets, 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, 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";
3
3
  /**
4
4
  * Custom error classes for MCP server
5
5
  *
@@ -42,6 +42,7 @@ const ErrorMessages = {
42
42
  noUserIdConfigured: () => new UserInputError("User ID not configured", ["The \"me\" action requires a user ID to be configured", "Use action=\"list\" to find people, or configure the user ID"]),
43
43
  missingCommentTarget: () => new UserInputError("A target is required for creating a comment", ["Provide one of: task_id, deal_id, or company_id", "Find targets using resource=\"tasks\", \"deals\", or \"companies\" with action=\"list\""]),
44
44
  missingBookingTarget: () => new UserInputError("A service or event is required for creating a booking", ["Provide either: service_id or event_id", "Find services using resource=\"services\" with action=\"list\""]),
45
+ noUpdateFieldsSpecified: (allowedFields) => new UserInputError(`No updates specified. Provide at least one of: ${allowedFields.join(", ")}`, ["Specify at least one field to update", `Updatable fields are: ${allowedFields.join(", ")}`]),
45
46
  apiError: (statusCode, message) => {
46
47
  const hints = [];
47
48
  if (statusCode === 401) {
@@ -156,14 +157,6 @@ function formatService$1(service, options) {
156
157
  return result;
157
158
  }
158
159
  /**
159
- * Format budget for agent consumption
160
- */
161
- function formatBudget$1(budget, options) {
162
- const result = formatBudget(budget, MCP_FORMAT_OPTIONS);
163
- if (options?.compact) return compactify(result, ["budget_type", "currency"]);
164
- return result;
165
- }
166
- /**
167
160
  * Format company for agent consumption
168
161
  */
169
162
  function formatCompany$1(company, options) {
@@ -434,40 +427,6 @@ function getDealHints(dealId) {
434
427
  };
435
428
  }
436
429
  /**
437
- * Generate hints for a budget
438
- */
439
- function getBudgetHints(budgetId) {
440
- return { related_resources: [
441
- {
442
- resource: "services",
443
- description: "Get services (budget lines) for this budget",
444
- example: {
445
- resource: "services",
446
- action: "list",
447
- filter: { budget_id: budgetId }
448
- }
449
- },
450
- {
451
- resource: "time",
452
- description: "Get time entries for this budget",
453
- example: {
454
- resource: "time",
455
- action: "list",
456
- filter: { budget_id: budgetId }
457
- }
458
- },
459
- {
460
- resource: "bookings",
461
- description: "Get bookings for this budget",
462
- example: {
463
- resource: "bookings",
464
- action: "list",
465
- filter: { budget_id: budgetId }
466
- }
467
- }
468
- ] };
469
- }
470
- /**
471
430
  * Generate hints for a person
472
431
  */
473
432
  function getPersonHints(personId) {
@@ -848,236 +807,6 @@ function toStringFilter(filter) {
848
807
  return Object.keys(result).length > 0 ? result : void 0;
849
808
  }
850
809
  /**
851
- * Attachments MCP handler.
852
- */
853
- var VALID_ACTIONS$14 = [
854
- "list",
855
- "get",
856
- "delete"
857
- ];
858
- async function handleAttachments(action, args, ctx) {
859
- const { formatOptions, filter, page, perPage } = ctx;
860
- const { id, task_id, comment_id, deal_id } = args;
861
- const execCtx = ctx.executor();
862
- if (action === "get") {
863
- if (!id) return inputErrorResult(ErrorMessages.missingId("get"));
864
- const result = await getAttachment({ id }, execCtx);
865
- const formatted = formatAttachment$1(result.data, formatOptions);
866
- if (ctx.includeHints !== false) {
867
- const attachableType = result.data.attributes?.attachable_type;
868
- return jsonResult({
869
- ...formatted,
870
- _hints: getAttachmentHints(id, attachableType)
871
- });
872
- }
873
- return jsonResult(formatted);
874
- }
875
- if (action === "delete") {
876
- if (!id) return inputErrorResult(ErrorMessages.missingId("delete"));
877
- await deleteAttachment({ id }, execCtx);
878
- return jsonResult({
879
- success: true,
880
- deleted: id
881
- });
882
- }
883
- if (action === "list") {
884
- const options = { ...filter };
885
- if (task_id) options.task_id = task_id;
886
- if (comment_id) options.comment_id = comment_id;
887
- if (deal_id) options.deal_id = deal_id;
888
- const result = await listAttachments({
889
- page,
890
- perPage,
891
- additionalFilters: options
892
- }, execCtx);
893
- return jsonResult(formatListResponse$1(result.data, formatAttachment$1, result.meta, formatOptions));
894
- }
895
- return inputErrorResult(ErrorMessages.invalidAction(action, "attachments", VALID_ACTIONS$14));
896
- }
897
- /**
898
- * Bookings MCP handler.
899
- */
900
- var VALID_ACTIONS$13 = [
901
- "list",
902
- "get",
903
- "create",
904
- "update"
905
- ];
906
- async function handleBookings(action, args, ctx) {
907
- const { formatOptions, filter, page, perPage, include: userInclude } = ctx;
908
- const { id, person_id, service_id, event_id, started_on, ended_on, time, note } = args;
909
- const include = userInclude?.length ? [...new Set([
910
- "person",
911
- "service",
912
- ...userInclude
913
- ])] : ["person", "service"];
914
- const execCtx = ctx.executor();
915
- if (action === "get") {
916
- if (!id) return inputErrorResult(ErrorMessages.missingId("get"));
917
- const result = await getBooking({
918
- id,
919
- include
920
- }, execCtx);
921
- const formatted = formatBooking$1(result.data, {
922
- ...formatOptions,
923
- included: result.included
924
- });
925
- if (ctx.includeHints !== false) {
926
- const personId = result.data.relationships?.person?.data?.id;
927
- return jsonResult({
928
- ...formatted,
929
- _hints: getBookingHints(id, personId)
930
- });
931
- }
932
- return jsonResult(formatted);
933
- }
934
- if (action === "create") {
935
- if (!person_id || !started_on || !ended_on) return inputErrorResult(ErrorMessages.missingRequiredFields("booking", [
936
- "person_id",
937
- "started_on",
938
- "ended_on"
939
- ]));
940
- if (!service_id && !event_id) return inputErrorResult(ErrorMessages.missingBookingTarget());
941
- return jsonResult({
942
- success: true,
943
- ...formatBooking$1((await createBooking({
944
- personId: person_id,
945
- serviceId: service_id ?? "",
946
- startedOn: started_on,
947
- endedOn: ended_on,
948
- time,
949
- note,
950
- eventId: event_id
951
- }, execCtx)).data, formatOptions)
952
- });
953
- }
954
- if (action === "update") {
955
- if (!id) return inputErrorResult(ErrorMessages.missingId("update"));
956
- return jsonResult({
957
- success: true,
958
- ...formatBooking$1((await updateBooking({
959
- id,
960
- startedOn: started_on,
961
- endedOn: ended_on,
962
- time,
963
- note
964
- }, execCtx)).data, formatOptions)
965
- });
966
- }
967
- if (action === "list") {
968
- const result = await listBookings({
969
- page,
970
- perPage,
971
- additionalFilters: filter,
972
- include
973
- }, execCtx);
974
- return jsonResult(formatListResponse$1(result.data, formatBooking$1, result.meta, formatOptions));
975
- }
976
- return inputErrorResult(ErrorMessages.invalidAction(action, "bookings", VALID_ACTIONS$13));
977
- }
978
- /**
979
- * Budgets MCP handler.
980
- *
981
- * Thin adapter that delegates business logic to core executors
982
- * and handles MCP-specific concerns (hints, error formatting, JSON results).
983
- */
984
- var VALID_ACTIONS$12 = ["list", "get"];
985
- async function handleBudgets(action, args, ctx) {
986
- const { formatOptions, filter, page, perPage } = ctx;
987
- const { id } = args;
988
- const execCtx = ctx.executor();
989
- if (action === "get") {
990
- if (!id) return inputErrorResult(ErrorMessages.missingId("get"));
991
- const formatted = formatBudget$1((await getBudget({ id }, execCtx)).data, formatOptions);
992
- if (ctx.includeHints !== false) return jsonResult({
993
- ...formatted,
994
- _hints: getBudgetHints(id)
995
- });
996
- return jsonResult(formatted);
997
- }
998
- if (action === "list") {
999
- const result = await listBudgets({
1000
- page,
1001
- perPage,
1002
- additionalFilters: filter
1003
- }, execCtx);
1004
- return jsonResult(formatListResponse$1(result.data, formatBudget$1, result.meta, formatOptions));
1005
- }
1006
- return inputErrorResult(ErrorMessages.invalidAction(action, "budgets", VALID_ACTIONS$12));
1007
- }
1008
- /**
1009
- * Comments MCP handler.
1010
- */
1011
- var VALID_ACTIONS$11 = [
1012
- "list",
1013
- "get",
1014
- "create",
1015
- "update"
1016
- ];
1017
- async function handleComments(action, args, ctx) {
1018
- const { formatOptions, filter, page, perPage, include: userInclude } = ctx;
1019
- const { id, body, task_id, deal_id, company_id } = args;
1020
- const include = userInclude?.length ? [...new Set(["creator", ...userInclude])] : ["creator"];
1021
- const execCtx = ctx.executor();
1022
- if (action === "get") {
1023
- if (!id) return inputErrorResult(ErrorMessages.missingId("get"));
1024
- const result = await getComment({
1025
- id,
1026
- include
1027
- }, execCtx);
1028
- const formatted = formatComment$1(result.data, {
1029
- ...formatOptions,
1030
- included: result.included
1031
- });
1032
- if (ctx.includeHints !== false) {
1033
- const commentableType = result.data.attributes?.commentable_type;
1034
- let commentableId;
1035
- if (commentableType === "task") commentableId = result.data.relationships?.task?.data?.id;
1036
- else if (commentableType === "deal") commentableId = result.data.relationships?.deal?.data?.id;
1037
- else if (commentableType === "company") commentableId = result.data.relationships?.company?.data?.id;
1038
- return jsonResult({
1039
- ...formatted,
1040
- _hints: getCommentHints(id, commentableType, commentableId)
1041
- });
1042
- }
1043
- return jsonResult(formatted);
1044
- }
1045
- if (action === "create") {
1046
- if (!body) return inputErrorResult(ErrorMessages.missingRequiredFields("comment", ["body"]));
1047
- if (!task_id && !deal_id && !company_id) return inputErrorResult(ErrorMessages.missingCommentTarget());
1048
- return jsonResult({
1049
- success: true,
1050
- ...formatComment$1((await createComment({
1051
- body,
1052
- taskId: task_id,
1053
- dealId: deal_id,
1054
- companyId: company_id
1055
- }, execCtx)).data, formatOptions)
1056
- });
1057
- }
1058
- if (action === "update") {
1059
- if (!id) return inputErrorResult(ErrorMessages.missingId("update"));
1060
- if (!body) return inputErrorResult(ErrorMessages.missingRequiredFields("comment update", ["body"]));
1061
- return jsonResult({
1062
- success: true,
1063
- ...formatComment$1((await updateComment({
1064
- id,
1065
- body
1066
- }, execCtx)).data, formatOptions)
1067
- });
1068
- }
1069
- if (action === "list") {
1070
- const result = await listComments({
1071
- page,
1072
- perPage,
1073
- additionalFilters: filter,
1074
- include
1075
- }, execCtx);
1076
- return jsonResult(formatListResponse$1(result.data, formatComment$1, result.meta, formatOptions));
1077
- }
1078
- return inputErrorResult(ErrorMessages.invalidAction(action, "comments", VALID_ACTIONS$11));
1079
- }
1080
- /**
1081
810
  * Resolve handler for MCP.
1082
811
  *
1083
812
  * Thin wrapper around core's resource resolver.
@@ -1108,242 +837,559 @@ async function handleResolve(args, ctx) {
1108
837
  }
1109
838
  }
1110
839
  /**
1111
- * Companies MCP handler.
840
+ * Merge user includes with defaults, ensuring no duplicates
1112
841
  */
1113
- var VALID_ACTIONS$10 = [
1114
- "list",
1115
- "get",
1116
- "create",
1117
- "update",
1118
- "resolve"
1119
- ];
1120
- async function handleCompanies(action, args, ctx) {
1121
- const { formatOptions, filter, page, perPage } = ctx;
1122
- const { id, name, query, type } = args;
1123
- if (action === "resolve") return handleResolve({
1124
- query,
1125
- type
1126
- }, ctx);
1127
- const execCtx = ctx.executor();
1128
- if (action === "get") {
1129
- if (!id) return inputErrorResult(ErrorMessages.missingId("get"));
1130
- const formatted = formatCompany$1((await getCompany({ id }, execCtx)).data, formatOptions);
1131
- if (ctx.includeHints !== false) return jsonResult({
1132
- ...formatted,
1133
- _hints: getCompanyHints(id)
1134
- });
1135
- return jsonResult(formatted);
1136
- }
1137
- if (action === "create") {
1138
- if (!name) return inputErrorResult(ErrorMessages.missingRequiredFields("company", ["name"]));
1139
- return jsonResult({
1140
- success: true,
1141
- ...formatCompany$1((await createCompany({ name }, execCtx)).data, formatOptions)
1142
- });
1143
- }
1144
- if (action === "update") {
1145
- if (!id) return inputErrorResult(ErrorMessages.missingId("update"));
1146
- return jsonResult({
1147
- success: true,
1148
- ...formatCompany$1((await updateCompany({
1149
- id,
1150
- name
1151
- }, execCtx)).data, formatOptions)
1152
- });
1153
- }
1154
- if (action === "list") {
1155
- const result = await listCompanies({
1156
- page,
1157
- perPage,
1158
- additionalFilters: filter
1159
- }, execCtx);
1160
- const response = formatListResponse$1(result.data, formatCompany$1, result.meta, formatOptions);
1161
- if (result.resolved && Object.keys(result.resolved).length > 0) return jsonResult({
1162
- ...response,
1163
- _resolved: result.resolved
1164
- });
1165
- return jsonResult(response);
1166
- }
1167
- return inputErrorResult(ErrorMessages.invalidAction(action, "companies", VALID_ACTIONS$10));
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])];
1168
847
  }
1169
848
  /**
1170
- * Deals MCP handler.
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
+ * ```
1171
865
  */
1172
- var VALID_ACTIONS$9 = [
1173
- "list",
1174
- "get",
1175
- "create",
1176
- "update",
1177
- "resolve"
1178
- ];
1179
- async function handleDeals(action, args, ctx) {
1180
- const { formatOptions, filter, page, perPage, include: userInclude } = ctx;
1181
- const { id, name, company_id, query, type } = args;
1182
- if (action === "resolve") return handleResolve({
1183
- query,
1184
- type
1185
- }, ctx);
1186
- const execCtx = ctx.executor();
1187
- if (action === "get") {
1188
- if (!id) return inputErrorResult(ErrorMessages.missingId("get"));
1189
- const result = await getDeal({
1190
- id,
1191
- include: userInclude?.length ? [...new Set([
1192
- "company",
1193
- "deal_status",
1194
- "responsible",
1195
- ...userInclude
1196
- ])] : [
1197
- "company",
1198
- "deal_status",
1199
- "responsible"
1200
- ]
1201
- }, execCtx);
1202
- const formatted = formatDeal$1(result.data, {
1203
- ...formatOptions,
1204
- included: result.included
1205
- });
1206
- if (ctx.includeHints !== false) return jsonResult({
1207
- ...formatted,
1208
- _hints: getDealHints(id)
1209
- });
1210
- return jsonResult(formatted);
1211
- }
1212
- if (action === "create") {
1213
- if (!name || !company_id) return inputErrorResult(ErrorMessages.missingRequiredFields("deal", ["name", "company_id"]));
1214
- return jsonResult({
1215
- success: true,
1216
- ...formatDeal$1((await createDeal({
1217
- name,
1218
- companyId: company_id
1219
- }, execCtx)).data, formatOptions)
1220
- });
1221
- }
1222
- if (action === "update") {
1223
- if (!id) return inputErrorResult(ErrorMessages.missingId("update"));
1224
- return jsonResult({
1225
- success: true,
1226
- ...formatDeal$1((await updateDeal({
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({
1227
886
  id,
1228
- name
1229
- }, execCtx)).data, formatOptions)
1230
- });
1231
- }
1232
- if (action === "list") {
1233
- const result = await listDeals({
1234
- page,
1235
- perPage,
1236
- additionalFilters: filter,
1237
- include: userInclude?.length ? [...new Set([
1238
- "company",
1239
- "deal_status",
1240
- ...userInclude
1241
- ])] : ["company", "deal_status"]
1242
- }, execCtx);
1243
- const response = formatListResponse$1(result.data, formatDeal$1, result.meta, {
1244
- ...formatOptions,
1245
- included: result.included
1246
- });
1247
- if (result.resolved && Object.keys(result.resolved).length > 0) return jsonResult({
1248
- ...response,
1249
- _resolved: result.resolved
1250
- });
1251
- return jsonResult(response);
1252
- }
1253
- return inputErrorResult(ErrorMessages.invalidAction(action, "deals", VALID_ACTIONS$9));
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
+ });
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));
918
+ }
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));
960
+ };
1254
961
  }
1255
962
  /**
1256
- * Discussions MCP handler.
963
+ * Attachments MCP handler.
1257
964
  */
1258
- var VALID_ACTIONS$8 = [
1259
- "list",
1260
- "get",
1261
- "create",
1262
- "update",
1263
- "delete",
1264
- "resolve",
1265
- "reopen"
1266
- ];
1267
- async function handleDiscussions(action, args, ctx) {
1268
- const { formatOptions, filter, page, perPage } = ctx;
1269
- const { id, title, body, page_id, status } = args;
1270
- const execCtx = ctx.executor();
1271
- if (action === "get") {
1272
- if (!id) return inputErrorResult(ErrorMessages.missingId("get"));
1273
- const formatted = formatDiscussion$1((await getDiscussion({ id }, execCtx)).data, formatOptions);
1274
- if (ctx.includeHints !== false) return jsonResult({
1275
- ...formatted,
1276
- _hints: getDiscussionHints(id, page_id)
1277
- });
1278
- return jsonResult(formatted);
965
+ const handleAttachments = createResourceHandler({
966
+ resource: "attachments",
967
+ actions: [
968
+ "list",
969
+ "get",
970
+ "delete"
971
+ ],
972
+ formatter: formatAttachment$1,
973
+ hints: (data, id) => {
974
+ const attachableType = data.attributes?.attachable_type;
975
+ return getAttachmentHints(id, attachableType);
976
+ },
977
+ listFilterFromArgs: (args) => {
978
+ const filters = {};
979
+ if (args.task_id) filters.task_id = args.task_id;
980
+ if (args.comment_id) filters.comment_id = args.comment_id;
981
+ if (args.deal_id) filters.deal_id = args.deal_id;
982
+ return filters;
983
+ },
984
+ executors: {
985
+ list: listAttachments,
986
+ get: getAttachment,
987
+ delete: deleteAttachment
1279
988
  }
1280
- if (action === "create") {
1281
- if (!body || !page_id) return inputErrorResult(ErrorMessages.missingRequiredFields("discussion", ["body", "page_id"]));
1282
- return jsonResult({
1283
- success: true,
1284
- ...formatDiscussion$1((await createDiscussion({
1285
- body,
1286
- pageId: page_id,
1287
- title
1288
- }, execCtx)).data, formatOptions)
1289
- });
989
+ });
990
+ /**
991
+ * Validate batch operations array
992
+ */
993
+ function validateOperations(operations) {
994
+ if (!Array.isArray(operations)) throw new UserInputError("operations must be an array", ["Provide an array of operation objects", "Each operation needs: { resource: \"...\", action: \"...\", ...params }"]);
995
+ if (operations.length === 0) throw new UserInputError("operations array cannot be empty", ["Provide at least one operation", "Example: operations: [{ resource: \"projects\", action: \"list\" }]"]);
996
+ if (operations.length > 10) throw new UserInputError(`operations array exceeds maximum size of 10`, [`Split your batch into chunks of 10 or fewer operations`, `You provided ${operations.length} operations`]);
997
+ const validatedOps = [];
998
+ const errors = [];
999
+ for (let i = 0; i < operations.length; i++) {
1000
+ const op = operations[i];
1001
+ if (typeof op !== "object" || op === null) {
1002
+ errors.push(`Operation at index ${i}: must be an object`);
1003
+ continue;
1004
+ }
1005
+ const { resource, action } = op;
1006
+ if (typeof resource !== "string" || resource.trim() === "") errors.push(`Operation at index ${i}: missing or invalid "resource" field`);
1007
+ if (typeof action !== "string" || action.trim() === "") errors.push(`Operation at index ${i}: missing or invalid "action" field`);
1008
+ if (errors.length === 0) validatedOps.push(op);
1290
1009
  }
1291
- if (action === "update") {
1292
- if (!id) return inputErrorResult(ErrorMessages.missingId("update"));
1293
- return jsonResult({
1294
- success: true,
1295
- ...formatDiscussion$1((await updateDiscussion({
1296
- id,
1297
- title,
1298
- body
1299
- }, execCtx)).data, formatOptions)
1300
- });
1010
+ if (errors.length > 0) throw new UserInputError("Invalid operations in batch", errors);
1011
+ return validatedOps;
1012
+ }
1013
+ /**
1014
+ * Execute a single operation and capture result
1015
+ */
1016
+ async function executeOperation(operation, index, credentials, execute) {
1017
+ const { resource, action, ...params } = operation;
1018
+ try {
1019
+ const result = await execute("productive", {
1020
+ resource,
1021
+ action,
1022
+ ...params
1023
+ }, credentials);
1024
+ const content = result.content[0];
1025
+ if (content?.type === "text") try {
1026
+ const data = JSON.parse(content.text);
1027
+ if (result.isError) return {
1028
+ resource,
1029
+ action,
1030
+ index,
1031
+ error: content.text
1032
+ };
1033
+ return {
1034
+ resource,
1035
+ action,
1036
+ index,
1037
+ data
1038
+ };
1039
+ } catch {
1040
+ if (result.isError) return {
1041
+ resource,
1042
+ action,
1043
+ index,
1044
+ error: content.text
1045
+ };
1046
+ return {
1047
+ resource,
1048
+ action,
1049
+ index,
1050
+ data: content.text
1051
+ };
1052
+ }
1053
+ return {
1054
+ resource,
1055
+ action,
1056
+ index,
1057
+ data: null
1058
+ };
1059
+ } catch (err) {
1060
+ return {
1061
+ resource,
1062
+ action,
1063
+ index,
1064
+ error: err instanceof Error ? err.message : String(err)
1065
+ };
1301
1066
  }
1302
- if (action === "delete") {
1303
- if (!id) return inputErrorResult(ErrorMessages.missingId("delete"));
1304
- await deleteDiscussion({ id }, execCtx);
1305
- return jsonResult({
1306
- success: true,
1307
- deleted: id
1308
- });
1067
+ }
1068
+ /**
1069
+ * Handle batch operation - execute multiple operations in parallel
1070
+ *
1071
+ * @param operations - Array of operations to execute
1072
+ * @param credentials - API credentials
1073
+ * @param execute - Function to execute individual operations (injected for testability)
1074
+ * @returns Batch response with summary and individual results
1075
+ */
1076
+ async function handleBatch(operations, credentials, execute) {
1077
+ let validatedOps;
1078
+ try {
1079
+ validatedOps = validateOperations(operations);
1080
+ } catch (err) {
1081
+ if (err instanceof UserInputError) return inputErrorResult(err);
1082
+ throw err;
1309
1083
  }
1310
- if (action === "resolve") {
1311
- if (!id) return inputErrorResult(ErrorMessages.missingId("resolve"));
1312
- return jsonResult({
1313
- success: true,
1314
- ...formatDiscussion$1((await resolveDiscussion({ id }, execCtx)).data, formatOptions)
1315
- });
1084
+ const results = await Promise.all(validatedOps.map((op, index) => executeOperation(op, index, credentials, execute)));
1085
+ const succeeded = results.filter((r) => r.data !== void 0 && r.error === void 0).length;
1086
+ const failed = results.filter((r) => r.error !== void 0).length;
1087
+ return jsonResult({
1088
+ _batch: {
1089
+ total: results.length,
1090
+ succeeded,
1091
+ failed
1092
+ },
1093
+ results
1094
+ });
1095
+ }
1096
+ /**
1097
+ * Bookings MCP handler.
1098
+ */
1099
+ const handleBookings = createResourceHandler({
1100
+ resource: "bookings",
1101
+ displayName: "booking",
1102
+ actions: [
1103
+ "list",
1104
+ "get",
1105
+ "create",
1106
+ "update"
1107
+ ],
1108
+ formatter: formatBooking$1,
1109
+ hints: (data, id) => {
1110
+ const personId = data.relationships?.person?.data?.id;
1111
+ return getBookingHints(id, personId);
1112
+ },
1113
+ defaultInclude: {
1114
+ list: ["person", "service"],
1115
+ get: ["person", "service"]
1116
+ },
1117
+ create: {
1118
+ required: [
1119
+ "person_id",
1120
+ "started_on",
1121
+ "ended_on"
1122
+ ],
1123
+ validateArgs: (args) => {
1124
+ if (!args.service_id && !args.event_id) return inputErrorResult(ErrorMessages.missingBookingTarget());
1125
+ },
1126
+ mapOptions: (args) => ({
1127
+ personId: args.person_id,
1128
+ serviceId: args.service_id ?? "",
1129
+ startedOn: args.started_on,
1130
+ endedOn: args.ended_on,
1131
+ time: args.time,
1132
+ note: args.note,
1133
+ eventId: args.event_id
1134
+ })
1135
+ },
1136
+ update: { mapOptions: (args) => ({
1137
+ startedOn: args.started_on,
1138
+ endedOn: args.ended_on,
1139
+ time: args.time,
1140
+ note: args.note
1141
+ }) },
1142
+ executors: {
1143
+ list: listBookings,
1144
+ get: getBooking,
1145
+ create: createBooking,
1146
+ update: updateBooking
1316
1147
  }
1317
- if (action === "reopen") {
1318
- if (!id) return inputErrorResult(ErrorMessages.missingId("reopen"));
1319
- return jsonResult({
1320
- success: true,
1321
- ...formatDiscussion$1((await reopenDiscussion({ id }, execCtx)).data, formatOptions)
1322
- });
1148
+ });
1149
+ /**
1150
+ * Comments MCP handler.
1151
+ */
1152
+ const handleComments = createResourceHandler({
1153
+ resource: "comments",
1154
+ actions: [
1155
+ "list",
1156
+ "get",
1157
+ "create",
1158
+ "update"
1159
+ ],
1160
+ formatter: formatComment$1,
1161
+ hints: (data, id) => {
1162
+ const commentableType = data.attributes?.commentable_type;
1163
+ let commentableId;
1164
+ if (commentableType === "task") commentableId = data.relationships?.task?.data?.id;
1165
+ else if (commentableType === "deal") commentableId = data.relationships?.deal?.data?.id;
1166
+ else if (commentableType === "company") commentableId = data.relationships?.company?.data?.id;
1167
+ return getCommentHints(id, commentableType, commentableId);
1168
+ },
1169
+ defaultInclude: {
1170
+ list: ["creator"],
1171
+ get: ["creator"]
1172
+ },
1173
+ create: {
1174
+ required: ["body"],
1175
+ validateArgs: (args) => {
1176
+ if (!args.task_id && !args.deal_id && !args.company_id) return inputErrorResult(ErrorMessages.missingCommentTarget());
1177
+ },
1178
+ mapOptions: (args) => ({
1179
+ body: args.body,
1180
+ taskId: args.task_id,
1181
+ dealId: args.deal_id,
1182
+ companyId: args.company_id
1183
+ })
1184
+ },
1185
+ update: {
1186
+ allowedFields: ["body"],
1187
+ mapOptions: (args) => ({ body: args.body })
1188
+ },
1189
+ executors: {
1190
+ list: listComments,
1191
+ get: getComment,
1192
+ create: createComment,
1193
+ update: updateComment
1323
1194
  }
1324
- if (action === "list") {
1325
- const listOptions = {
1326
- page,
1327
- perPage,
1328
- additionalFilters: filter
1329
- };
1330
- if (status) listOptions.status = status;
1331
- const result = await listDiscussions(listOptions, execCtx);
1332
- const response = formatListResponse$1(result.data, formatDiscussion$1, result.meta, formatOptions);
1333
- if (result.resolved && Object.keys(result.resolved).length > 0) return jsonResult({
1334
- ...response,
1335
- _resolved: result.resolved
1336
- });
1337
- return jsonResult(response);
1195
+ });
1196
+ /**
1197
+ * Companies MCP handler.
1198
+ *
1199
+ * Uses the createResourceHandler factory for the common list/get/create/update/resolve pattern.
1200
+ */
1201
+ /**
1202
+ * Handle companies resource.
1203
+ *
1204
+ * Supports: list, get, create, update, resolve
1205
+ */
1206
+ const handleCompanies = createResourceHandler({
1207
+ resource: "companies",
1208
+ actions: [
1209
+ "list",
1210
+ "get",
1211
+ "create",
1212
+ "update",
1213
+ "resolve"
1214
+ ],
1215
+ formatter: formatCompany$1,
1216
+ hints: (_data, id) => getCompanyHints(id),
1217
+ supportsResolve: true,
1218
+ create: {
1219
+ required: ["name"],
1220
+ mapOptions: (args) => ({ name: args.name })
1221
+ },
1222
+ update: { mapOptions: (args) => ({ name: args.name }) },
1223
+ executors: {
1224
+ list: listCompanies,
1225
+ get: getCompany,
1226
+ create: createCompany,
1227
+ update: updateCompany
1338
1228
  }
1339
- return inputErrorResult(ErrorMessages.invalidAction(action, "discussions", VALID_ACTIONS$8));
1340
- }
1229
+ });
1230
+ /**
1231
+ * Deals MCP handler.
1232
+ */
1233
+ const handleDeals = createResourceHandler({
1234
+ resource: "deals",
1235
+ displayName: "deal",
1236
+ actions: [
1237
+ "list",
1238
+ "get",
1239
+ "create",
1240
+ "update",
1241
+ "resolve"
1242
+ ],
1243
+ formatter: formatDeal$1,
1244
+ hints: (_data, id) => getDealHints(id),
1245
+ supportsResolve: true,
1246
+ defaultInclude: {
1247
+ list: ["company", "deal_status"],
1248
+ get: [
1249
+ "company",
1250
+ "deal_status",
1251
+ "responsible"
1252
+ ]
1253
+ },
1254
+ create: {
1255
+ required: ["name", "company_id"],
1256
+ mapOptions: (args) => ({
1257
+ name: args.name,
1258
+ companyId: args.company_id
1259
+ })
1260
+ },
1261
+ update: { mapOptions: (args) => ({ name: args.name }) },
1262
+ executors: {
1263
+ list: listDeals,
1264
+ get: getDeal,
1265
+ create: createDeal,
1266
+ update: updateDeal
1267
+ }
1268
+ });
1269
+ /**
1270
+ * Discussions MCP handler.
1271
+ */
1272
+ var STATUS_MAP = {
1273
+ active: "1",
1274
+ resolved: "2"
1275
+ };
1276
+ const handleDiscussions = createResourceHandler({
1277
+ resource: "discussions",
1278
+ actions: [
1279
+ "list",
1280
+ "get",
1281
+ "create",
1282
+ "update",
1283
+ "delete",
1284
+ "resolve",
1285
+ "reopen"
1286
+ ],
1287
+ formatter: formatDiscussion$1,
1288
+ hints: (data, id) => {
1289
+ const pageId = data.relationships?.page?.data?.id;
1290
+ return getDiscussionHints(id, pageId);
1291
+ },
1292
+ listFilterFromArgs: (args) => {
1293
+ const filters = {};
1294
+ if (args.status) {
1295
+ const mapped = STATUS_MAP[args.status.toLowerCase()];
1296
+ if (mapped) filters.status = mapped;
1297
+ }
1298
+ return filters;
1299
+ },
1300
+ create: {
1301
+ required: ["body", "page_id"],
1302
+ mapOptions: (args) => ({
1303
+ body: args.body,
1304
+ pageId: args.page_id,
1305
+ title: args.title
1306
+ })
1307
+ },
1308
+ update: { mapOptions: (args) => ({
1309
+ title: args.title,
1310
+ body: args.body
1311
+ }) },
1312
+ customActions: {
1313
+ resolve: async (args, ctx, execCtx) => {
1314
+ if (!args.id) return inputErrorResult(ErrorMessages.missingId("resolve"));
1315
+ return jsonResult({
1316
+ success: true,
1317
+ ...formatDiscussion$1((await resolveDiscussion({ id: args.id }, execCtx)).data, ctx.formatOptions)
1318
+ });
1319
+ },
1320
+ reopen: async (args, ctx, execCtx) => {
1321
+ if (!args.id) return inputErrorResult(ErrorMessages.missingId("reopen"));
1322
+ return jsonResult({
1323
+ success: true,
1324
+ ...formatDiscussion$1((await reopenDiscussion({ id: args.id }, execCtx)).data, ctx.formatOptions)
1325
+ });
1326
+ }
1327
+ },
1328
+ executors: {
1329
+ list: listDiscussions,
1330
+ get: getDiscussion,
1331
+ create: createDiscussion,
1332
+ update: updateDiscussion,
1333
+ delete: deleteDiscussion
1334
+ }
1335
+ });
1341
1336
  var RESOURCE_HELP = {
1337
+ batch: {
1338
+ description: "Execute multiple operations in a single call. Operations run in parallel via Promise.all, reducing round-trips for AI agents.",
1339
+ actions: { run: "Execute a batch of operations (max 10)" },
1340
+ fields: { operations: "Array of operation objects. Each must have \"resource\" and \"action\", plus any additional params for that resource." },
1341
+ examples: [{
1342
+ description: "Batch multiple queries",
1343
+ params: {
1344
+ resource: "batch",
1345
+ action: "run",
1346
+ operations: [
1347
+ {
1348
+ resource: "projects",
1349
+ action: "get",
1350
+ id: "123"
1351
+ },
1352
+ {
1353
+ resource: "time",
1354
+ action: "list",
1355
+ filter: { project_id: "123" }
1356
+ },
1357
+ {
1358
+ resource: "services",
1359
+ action: "list",
1360
+ filter: { project_id: "123" }
1361
+ }
1362
+ ]
1363
+ }
1364
+ }, {
1365
+ description: "Batch create time entries",
1366
+ params: {
1367
+ resource: "batch",
1368
+ action: "run",
1369
+ operations: [{
1370
+ resource: "time",
1371
+ action: "create",
1372
+ service_id: "111",
1373
+ date: "2024-01-15",
1374
+ time: 60,
1375
+ note: "Morning work"
1376
+ }, {
1377
+ resource: "time",
1378
+ action: "create",
1379
+ service_id: "111",
1380
+ date: "2024-01-15",
1381
+ time: 120,
1382
+ note: "Afternoon work"
1383
+ }]
1384
+ }
1385
+ }]
1386
+ },
1342
1387
  projects: {
1343
1388
  description: "Manage projects in Productive.io",
1344
1389
  actions: {
1345
1390
  list: "List all projects with optional filters",
1346
- get: "Get a single project by ID with full details"
1391
+ get: "Get a single project by ID (supports PRJ-123, P-123 format)",
1392
+ resolve: "Resolve by project number (PRJ-123, P-123)"
1347
1393
  },
1348
1394
  filters: {
1349
1395
  query: "Text search on project name",
@@ -1393,7 +1439,8 @@ var RESOURCE_HELP = {
1393
1439
  list: "List tasks with optional filters",
1394
1440
  get: "Get a single task by ID with full details (description, comments, etc.)",
1395
1441
  create: "Create a new task (requires title, project_id, task_list_id)",
1396
- update: "Update an existing task"
1442
+ update: "Update an existing task",
1443
+ resolve: "Resolve by text search"
1397
1444
  },
1398
1445
  filters: {
1399
1446
  query: "Text search on task title",
@@ -1478,7 +1525,9 @@ var RESOURCE_HELP = {
1478
1525
  list: "List time entries with optional filters",
1479
1526
  get: "Get a single time entry by ID",
1480
1527
  create: "Create a new time entry (requires person_id, service_id, date, time)",
1481
- update: "Update an existing time entry"
1528
+ update: "Update an existing time entry",
1529
+ delete: "Delete a time entry",
1530
+ resolve: "Resolve related resources (person, project, service)"
1482
1531
  },
1483
1532
  filters: {
1484
1533
  person_id: "Filter by person (use \"me\" for current user)",
@@ -1559,8 +1608,9 @@ var RESOURCE_HELP = {
1559
1608
  description: "Team members and contacts",
1560
1609
  actions: {
1561
1610
  list: "List people with optional filters",
1562
- get: "Get a single person by ID",
1563
- me: "Get the currently authenticated user"
1611
+ get: "Get a single person by ID (supports email address)",
1612
+ me: "Get the currently authenticated user",
1613
+ resolve: "Resolve by email address"
1564
1614
  },
1565
1615
  filters: {
1566
1616
  query: "Text search on name or email",
@@ -1610,9 +1660,10 @@ var RESOURCE_HELP = {
1610
1660
  description: "Client companies and organizations",
1611
1661
  actions: {
1612
1662
  list: "List companies with optional filters",
1613
- get: "Get a single company by ID",
1663
+ get: "Get a single company by ID (supports company name)",
1614
1664
  create: "Create a new company (requires name)",
1615
- update: "Update an existing company"
1665
+ update: "Update an existing company",
1666
+ resolve: "Resolve by company name"
1616
1667
  },
1617
1668
  filters: {
1618
1669
  query: "Text search on company name",
@@ -1776,12 +1827,13 @@ var RESOURCE_HELP = {
1776
1827
  ]
1777
1828
  },
1778
1829
  deals: {
1779
- description: "Sales deals and opportunities",
1830
+ description: "Sales deals, opportunities, and budgets. Budgets are deals with budget=true — use filter[type]=2 to list only budgets.",
1780
1831
  actions: {
1781
1832
  list: "List deals with optional filters",
1782
- get: "Get a single deal by ID",
1833
+ get: "Get a single deal by ID (supports D-123, DEAL-123 format)",
1783
1834
  create: "Create a new deal (requires name, company_id)",
1784
- update: "Update an existing deal"
1835
+ update: "Update an existing deal",
1836
+ resolve: "Resolve by deal number (D-123, DEAL-123)"
1785
1837
  },
1786
1838
  filters: {
1787
1839
  query: "Text search on deal name",
@@ -1804,23 +1856,35 @@ var RESOURCE_HELP = {
1804
1856
  name: "Deal name",
1805
1857
  number: "Deal number",
1806
1858
  date: "Deal date",
1859
+ budget: "Whether this deal is a budget (true/false)",
1807
1860
  status: "Current status (from deal_status)"
1808
1861
  },
1809
- examples: [{
1810
- description: "Search deals",
1811
- params: {
1812
- resource: "deals",
1813
- action: "list",
1814
- query: "website redesign"
1815
- }
1816
- }, {
1817
- description: "List deals for a company",
1818
- params: {
1819
- resource: "deals",
1820
- action: "list",
1821
- filter: { company_id: "12345" }
1862
+ examples: [
1863
+ {
1864
+ description: "Search deals",
1865
+ params: {
1866
+ resource: "deals",
1867
+ action: "list",
1868
+ query: "website redesign"
1869
+ }
1870
+ },
1871
+ {
1872
+ description: "List deals for a company",
1873
+ params: {
1874
+ resource: "deals",
1875
+ action: "list",
1876
+ filter: { company_id: "12345" }
1877
+ }
1878
+ },
1879
+ {
1880
+ description: "List only budgets",
1881
+ params: {
1882
+ resource: "deals",
1883
+ action: "list",
1884
+ filter: { type: "2" }
1885
+ }
1822
1886
  }
1823
- }]
1887
+ ]
1824
1888
  },
1825
1889
  bookings: {
1826
1890
  description: "Resource scheduling and capacity planning",
@@ -1863,66 +1927,6 @@ var RESOURCE_HELP = {
1863
1927
  }
1864
1928
  }]
1865
1929
  },
1866
- budgets: {
1867
- description: "Budget tracking and financial overview",
1868
- actions: {
1869
- list: "List budgets with optional filters",
1870
- get: "Get a single budget by ID with full details"
1871
- },
1872
- filters: {
1873
- project_id: "Filter by project",
1874
- company_id: "Filter by company",
1875
- deal_id: "Filter by deal",
1876
- billable: "Filter by billable status (true/false)",
1877
- budget_type: "Filter by budget type"
1878
- },
1879
- fields: {
1880
- id: "Unique budget identifier",
1881
- name: "Budget name",
1882
- budget_type: "Type of budget",
1883
- billable: "Whether the budget is billable",
1884
- started_on: "Budget start date (YYYY-MM-DD)",
1885
- ended_on: "Budget end date (YYYY-MM-DD)",
1886
- currency: "Budget currency code",
1887
- total_time_budget: "Total time budget in minutes",
1888
- remaining_time_budget: "Remaining time budget in minutes",
1889
- total_monetary_budget: "Total monetary budget",
1890
- remaining_monetary_budget: "Remaining monetary budget"
1891
- },
1892
- examples: [
1893
- {
1894
- description: "List all budgets",
1895
- params: {
1896
- resource: "budgets",
1897
- action: "list"
1898
- }
1899
- },
1900
- {
1901
- description: "List budgets for a project",
1902
- params: {
1903
- resource: "budgets",
1904
- action: "list",
1905
- filter: { project_id: "12345" }
1906
- }
1907
- },
1908
- {
1909
- description: "Get budget details",
1910
- params: {
1911
- resource: "budgets",
1912
- action: "get",
1913
- id: "67890"
1914
- }
1915
- },
1916
- {
1917
- description: "List billable budgets",
1918
- params: {
1919
- resource: "budgets",
1920
- action: "list",
1921
- filter: { billable: "true" }
1922
- }
1923
- }
1924
- ]
1925
- },
1926
1930
  pages: {
1927
1931
  description: "Manage pages (wiki/docs) within projects",
1928
1932
  actions: {
@@ -2101,7 +2105,8 @@ function handleHelp(resource) {
2101
2105
  const help = RESOURCE_HELP[resource];
2102
2106
  if (!help) return jsonResult({
2103
2107
  error: `Unknown resource: ${resource}`,
2104
- available_resources: Object.keys(RESOURCE_HELP)
2108
+ available_resources: Object.keys(RESOURCE_HELP),
2109
+ _tip: "Call { action: 'help' } without a resource to see all available resources."
2105
2110
  });
2106
2111
  return jsonResult({
2107
2112
  resource,
@@ -2118,82 +2123,57 @@ function handleHelpOverview() {
2118
2123
  resource,
2119
2124
  description: help.description,
2120
2125
  actions: Object.keys(help.actions)
2121
- }))
2126
+ })),
2127
+ _tip: "Always call { action: 'help', resource: '<name>' } before your first interaction with any resource to learn valid filters, required fields, and examples."
2122
2128
  });
2123
2129
  }
2124
2130
  /**
2125
2131
  * Pages MCP handler.
2132
+ *
2133
+ * Uses the createResourceHandler factory for the common list/get/create/update/delete pattern.
2126
2134
  */
2127
- var VALID_ACTIONS$7 = [
2128
- "list",
2129
- "get",
2130
- "create",
2131
- "update",
2132
- "delete"
2133
- ];
2134
- async function handlePages(action, args, ctx) {
2135
- const { formatOptions, filter, page, perPage } = ctx;
2136
- const { id, title, body, project_id, parent_page_id } = args;
2137
- const execCtx = ctx.executor();
2138
- if (action === "get") {
2139
- if (!id) return inputErrorResult(ErrorMessages.missingId("get"));
2140
- const formatted = formatPage$1((await getPage({ id }, execCtx)).data, formatOptions);
2141
- if (ctx.includeHints !== false) return jsonResult({
2142
- ...formatted,
2143
- _hints: getPageHints(id)
2144
- });
2145
- return jsonResult(formatted);
2146
- }
2147
- if (action === "create") {
2148
- if (!title || !project_id) return inputErrorResult(ErrorMessages.missingRequiredFields("page", ["title", "project_id"]));
2149
- return jsonResult({
2150
- success: true,
2151
- ...formatPage$1((await createPage({
2152
- title,
2153
- projectId: project_id,
2154
- body,
2155
- parentPageId: parent_page_id
2156
- }, execCtx)).data, formatOptions)
2157
- });
2158
- }
2159
- if (action === "update") {
2160
- if (!id) return inputErrorResult(ErrorMessages.missingId("update"));
2161
- return jsonResult({
2162
- success: true,
2163
- ...formatPage$1((await updatePage({
2164
- id,
2165
- title,
2166
- body
2167
- }, execCtx)).data, formatOptions)
2168
- });
2169
- }
2170
- if (action === "delete") {
2171
- if (!id) return inputErrorResult(ErrorMessages.missingId("delete"));
2172
- await deletePage({ id }, execCtx);
2173
- return jsonResult({
2174
- success: true,
2175
- deleted: id
2176
- });
2177
- }
2178
- if (action === "list") {
2179
- const result = await listPages({
2180
- page,
2181
- perPage,
2182
- additionalFilters: filter
2183
- }, execCtx);
2184
- const response = formatListResponse$1(result.data, formatPage$1, result.meta, formatOptions);
2185
- if (result.resolved && Object.keys(result.resolved).length > 0) return jsonResult({
2186
- ...response,
2187
- _resolved: result.resolved
2188
- });
2189
- return jsonResult(response);
2135
+ /**
2136
+ * Handle pages resource.
2137
+ *
2138
+ * Supports: list, get, create, update, delete
2139
+ */
2140
+ const handlePages = createResourceHandler({
2141
+ resource: "pages",
2142
+ actions: [
2143
+ "list",
2144
+ "get",
2145
+ "create",
2146
+ "update",
2147
+ "delete"
2148
+ ],
2149
+ formatter: formatPage$1,
2150
+ hints: (_data, id) => getPageHints(id),
2151
+ supportsResolve: false,
2152
+ create: {
2153
+ required: ["title", "project_id"],
2154
+ mapOptions: (args) => ({
2155
+ title: args.title,
2156
+ projectId: args.project_id,
2157
+ body: args.body,
2158
+ parentPageId: args.parent_page_id
2159
+ })
2160
+ },
2161
+ update: { mapOptions: (args) => ({
2162
+ title: args.title,
2163
+ body: args.body
2164
+ }) },
2165
+ executors: {
2166
+ list: listPages,
2167
+ get: getPage,
2168
+ create: createPage,
2169
+ update: updatePage,
2170
+ delete: deletePage
2190
2171
  }
2191
- return inputErrorResult(ErrorMessages.invalidAction(action, "pages", VALID_ACTIONS$7));
2192
- }
2172
+ });
2193
2173
  /**
2194
2174
  * People MCP handler.
2195
2175
  */
2196
- var VALID_ACTIONS$6 = [
2176
+ var VALID_ACTIONS$1 = [
2197
2177
  "list",
2198
2178
  "get",
2199
2179
  "me",
@@ -2244,48 +2224,33 @@ async function handlePeople(action, args, ctx, credentials) {
2244
2224
  });
2245
2225
  return jsonResult(response);
2246
2226
  }
2247
- return inputErrorResult(ErrorMessages.invalidAction(action, "people", VALID_ACTIONS$6));
2227
+ return inputErrorResult(ErrorMessages.invalidAction(action, "people", VALID_ACTIONS$1));
2248
2228
  }
2249
2229
  /**
2250
2230
  * Projects MCP handler.
2231
+ *
2232
+ * Uses the createResourceHandler factory for the common list/get/resolve pattern.
2251
2233
  */
2252
- var VALID_ACTIONS$5 = [
2253
- "list",
2254
- "get",
2255
- "resolve"
2256
- ];
2257
- async function handleProjects(action, args, ctx) {
2258
- const { formatOptions, filter, page, perPage } = ctx;
2259
- const { id, query, type } = args;
2260
- if (action === "resolve") return handleResolve({
2261
- query,
2262
- type
2263
- }, ctx);
2264
- const execCtx = ctx.executor();
2265
- if (action === "get") {
2266
- if (!id) return inputErrorResult(ErrorMessages.missingId("get"));
2267
- const formatted = formatProject$1((await getProject({ id }, execCtx)).data, formatOptions);
2268
- if (ctx.includeHints !== false) return jsonResult({
2269
- ...formatted,
2270
- _hints: getProjectHints(id)
2271
- });
2272
- return jsonResult(formatted);
2273
- }
2274
- if (action === "list") {
2275
- const result = await listProjects({
2276
- page,
2277
- perPage,
2278
- additionalFilters: filter
2279
- }, execCtx);
2280
- const response = formatListResponse$1(result.data, formatProject$1, result.meta, formatOptions);
2281
- if (result.resolved && Object.keys(result.resolved).length > 0) return jsonResult({
2282
- ...response,
2283
- _resolved: result.resolved
2284
- });
2285
- return jsonResult(response);
2234
+ /**
2235
+ * Handle projects resource.
2236
+ *
2237
+ * Supports: list, get, resolve
2238
+ */
2239
+ const handleProjects = createResourceHandler({
2240
+ resource: "projects",
2241
+ actions: [
2242
+ "list",
2243
+ "get",
2244
+ "resolve"
2245
+ ],
2246
+ formatter: formatProject$1,
2247
+ hints: (_data, id) => getProjectHints(id),
2248
+ supportsResolve: true,
2249
+ executors: {
2250
+ list: listProjects,
2251
+ get: getProject
2286
2252
  }
2287
- return inputErrorResult(ErrorMessages.invalidAction(action, "projects", VALID_ACTIONS$5));
2288
- }
2253
+ });
2289
2254
  /**
2290
2255
  * Reports MCP handler.
2291
2256
  */
@@ -2299,11 +2264,11 @@ function formatReportData(data) {
2299
2264
  };
2300
2265
  });
2301
2266
  }
2302
- var VALID_ACTIONS$4 = ["get"];
2267
+ var VALID_ACTIONS = ["get"];
2303
2268
  async function handleReports(action, args, ctx) {
2304
2269
  const { filter, page, perPage } = ctx;
2305
2270
  const { report_type, group, from, to, person_id, project_id, company_id, deal_id, status } = args;
2306
- if (action !== "get") return inputErrorResult(ErrorMessages.invalidAction(action, "reports", VALID_ACTIONS$4));
2271
+ if (action !== "get") return inputErrorResult(ErrorMessages.invalidAction(action, "reports", VALID_ACTIONS));
2307
2272
  if (!report_type) return inputErrorResult(ErrorMessages.missingReportType());
2308
2273
  if (!VALID_REPORT_TYPES.includes(report_type)) return inputErrorResult(ErrorMessages.invalidReportType(report_type, [...VALID_REPORT_TYPES]));
2309
2274
  const execCtx = ctx.executor();
@@ -2327,245 +2292,620 @@ async function handleReports(action, args, ctx) {
2327
2292
  });
2328
2293
  }
2329
2294
  /**
2330
- * Services MCP handler.
2295
+ * Schema definitions for all resources.
2296
+ *
2297
+ * This provides a compact, machine-readable specification of each resource's
2298
+ * capabilities. For detailed documentation with examples, use action=help.
2331
2299
  */
2332
- var VALID_ACTIONS$3 = ["list"];
2333
- async function handleServices(action, _args, ctx) {
2334
- const { formatOptions, filter, page, perPage } = ctx;
2335
- if (action === "list") {
2336
- const execCtx = ctx.executor();
2337
- const result = await listServices({
2338
- page,
2339
- perPage,
2340
- additionalFilters: filter
2341
- }, execCtx);
2342
- return jsonResult(formatListResponse$1(result.data, formatService$1, result.meta, formatOptions));
2343
- }
2344
- return inputErrorResult(ErrorMessages.invalidAction(action, "services", VALID_ACTIONS$3));
2300
+ var RESOURCE_SCHEMAS = {
2301
+ projects: {
2302
+ actions: [
2303
+ "list",
2304
+ "get",
2305
+ "resolve"
2306
+ ],
2307
+ filters: {
2308
+ query: "string — text search on project name",
2309
+ project_type: "1=internal|2=client",
2310
+ company_id: "string",
2311
+ responsible_id: "string",
2312
+ person_id: "string",
2313
+ status: "1=active|2=archived"
2314
+ }
2315
+ },
2316
+ time: {
2317
+ actions: [
2318
+ "list",
2319
+ "get",
2320
+ "create",
2321
+ "update",
2322
+ "delete"
2323
+ ],
2324
+ filters: {
2325
+ person_id: "string — use 'me' for current user",
2326
+ after: "date YYYY-MM-DD",
2327
+ before: "date YYYY-MM-DD",
2328
+ project_id: "string",
2329
+ service_id: "string",
2330
+ task_id: "string",
2331
+ status: "1=approved|2=unapproved|3=rejected"
2332
+ },
2333
+ create: {
2334
+ person_id: {
2335
+ required: true,
2336
+ type: "string"
2337
+ },
2338
+ service_id: {
2339
+ required: true,
2340
+ type: "string"
2341
+ },
2342
+ date: {
2343
+ required: true,
2344
+ type: "date YYYY-MM-DD"
2345
+ },
2346
+ time: {
2347
+ required: true,
2348
+ type: "minutes integer"
2349
+ },
2350
+ note: {
2351
+ required: false,
2352
+ type: "string"
2353
+ },
2354
+ task_id: {
2355
+ required: false,
2356
+ type: "string"
2357
+ }
2358
+ },
2359
+ includes: [
2360
+ "person",
2361
+ "service",
2362
+ "task"
2363
+ ]
2364
+ },
2365
+ tasks: {
2366
+ actions: [
2367
+ "list",
2368
+ "get",
2369
+ "create",
2370
+ "update",
2371
+ "resolve"
2372
+ ],
2373
+ filters: {
2374
+ query: "string — text search on task title",
2375
+ project_id: "string",
2376
+ assignee_id: "string",
2377
+ status: "1=open|2=closed (or \"open\", \"closed\", \"all\")",
2378
+ task_list_id: "string",
2379
+ workflow_status_id: "string — kanban column"
2380
+ },
2381
+ create: {
2382
+ title: {
2383
+ required: true,
2384
+ type: "string"
2385
+ },
2386
+ project_id: {
2387
+ required: true,
2388
+ type: "string"
2389
+ },
2390
+ task_list_id: {
2391
+ required: true,
2392
+ type: "string"
2393
+ },
2394
+ description: {
2395
+ required: false,
2396
+ type: "string"
2397
+ },
2398
+ assignee_id: {
2399
+ required: false,
2400
+ type: "string"
2401
+ }
2402
+ },
2403
+ includes: [
2404
+ "project",
2405
+ "assignee",
2406
+ "comments",
2407
+ "subtasks",
2408
+ "workflow_status"
2409
+ ]
2410
+ },
2411
+ services: {
2412
+ actions: ["list", "get"],
2413
+ filters: {
2414
+ project_id: "string",
2415
+ deal_id: "string",
2416
+ task_id: "string",
2417
+ budget_status: "1=open|2=delivered"
2418
+ }
2419
+ },
2420
+ people: {
2421
+ actions: [
2422
+ "list",
2423
+ "get",
2424
+ "me",
2425
+ "resolve"
2426
+ ],
2427
+ filters: {
2428
+ query: "string — text search on name or email",
2429
+ status: "1=active|2=deactivated",
2430
+ company_id: "string"
2431
+ }
2432
+ },
2433
+ companies: {
2434
+ actions: [
2435
+ "list",
2436
+ "get",
2437
+ "create",
2438
+ "update",
2439
+ "resolve"
2440
+ ],
2441
+ filters: {
2442
+ query: "string — text search on company name",
2443
+ archived: "boolean"
2444
+ },
2445
+ create: { name: {
2446
+ required: true,
2447
+ type: "string"
2448
+ } }
2449
+ },
2450
+ comments: {
2451
+ actions: [
2452
+ "list",
2453
+ "get",
2454
+ "create",
2455
+ "update"
2456
+ ],
2457
+ filters: {
2458
+ task_id: "string",
2459
+ deal_id: "string",
2460
+ page_id: "string",
2461
+ discussion_id: "string"
2462
+ },
2463
+ create: {
2464
+ body: {
2465
+ required: true,
2466
+ type: "string"
2467
+ },
2468
+ task_id: {
2469
+ required: false,
2470
+ type: "string — one of task_id, deal_id required"
2471
+ },
2472
+ deal_id: {
2473
+ required: false,
2474
+ type: "string — one of task_id, deal_id required"
2475
+ }
2476
+ },
2477
+ includes: ["creator"]
2478
+ },
2479
+ attachments: {
2480
+ actions: [
2481
+ "list",
2482
+ "get",
2483
+ "delete"
2484
+ ],
2485
+ filters: {
2486
+ task_id: "string",
2487
+ comment_id: "string",
2488
+ deal_id: "string",
2489
+ page_id: "string"
2490
+ }
2491
+ },
2492
+ timers: {
2493
+ actions: [
2494
+ "list",
2495
+ "get",
2496
+ "start",
2497
+ "stop"
2498
+ ],
2499
+ filters: {
2500
+ person_id: "string",
2501
+ time_entry_id: "string"
2502
+ }
2503
+ },
2504
+ deals: {
2505
+ actions: [
2506
+ "list",
2507
+ "get",
2508
+ "create",
2509
+ "update",
2510
+ "resolve"
2511
+ ],
2512
+ filters: {
2513
+ query: "string — text search on deal name",
2514
+ company_id: "string",
2515
+ type: "1=deal|2=budget",
2516
+ stage_status_id: "1=open|2=won|3=lost"
2517
+ },
2518
+ create: {
2519
+ name: {
2520
+ required: true,
2521
+ type: "string"
2522
+ },
2523
+ company_id: {
2524
+ required: true,
2525
+ type: "string"
2526
+ }
2527
+ },
2528
+ includes: ["company", "deal_status"]
2529
+ },
2530
+ bookings: {
2531
+ actions: [
2532
+ "list",
2533
+ "get",
2534
+ "create",
2535
+ "update"
2536
+ ],
2537
+ filters: {
2538
+ person_id: "string",
2539
+ after: "date YYYY-MM-DD",
2540
+ before: "date YYYY-MM-DD",
2541
+ service_id: "string"
2542
+ },
2543
+ create: {
2544
+ person_id: {
2545
+ required: true,
2546
+ type: "string"
2547
+ },
2548
+ started_on: {
2549
+ required: true,
2550
+ type: "date YYYY-MM-DD"
2551
+ },
2552
+ ended_on: {
2553
+ required: true,
2554
+ type: "date YYYY-MM-DD"
2555
+ },
2556
+ service_id: {
2557
+ required: false,
2558
+ type: "string — one of service_id, event_id required"
2559
+ },
2560
+ event_id: {
2561
+ required: false,
2562
+ type: "string — one of service_id, event_id required"
2563
+ }
2564
+ }
2565
+ },
2566
+ pages: {
2567
+ actions: [
2568
+ "list",
2569
+ "get",
2570
+ "create",
2571
+ "update",
2572
+ "delete"
2573
+ ],
2574
+ filters: { project_id: "string" },
2575
+ create: {
2576
+ title: {
2577
+ required: true,
2578
+ type: "string"
2579
+ },
2580
+ project_id: {
2581
+ required: true,
2582
+ type: "string"
2583
+ },
2584
+ body: {
2585
+ required: false,
2586
+ type: "string"
2587
+ },
2588
+ parent_page_id: {
2589
+ required: false,
2590
+ type: "string"
2591
+ }
2592
+ }
2593
+ },
2594
+ discussions: {
2595
+ actions: [
2596
+ "list",
2597
+ "get",
2598
+ "create",
2599
+ "update",
2600
+ "delete",
2601
+ "resolve",
2602
+ "reopen"
2603
+ ],
2604
+ filters: {
2605
+ page_id: "string",
2606
+ status: "1=active|2=resolved"
2607
+ },
2608
+ create: {
2609
+ body: {
2610
+ required: true,
2611
+ type: "string"
2612
+ },
2613
+ page_id: {
2614
+ required: true,
2615
+ type: "string"
2616
+ }
2617
+ }
2618
+ },
2619
+ reports: {
2620
+ actions: ["get"],
2621
+ filters: {
2622
+ person_id: "string",
2623
+ project_id: "string",
2624
+ company_id: "string",
2625
+ after: "date YYYY-MM-DD",
2626
+ before: "date YYYY-MM-DD"
2627
+ },
2628
+ create: {
2629
+ report_type: {
2630
+ required: true,
2631
+ type: "time_reports|project_reports|budget_reports|..."
2632
+ },
2633
+ from: {
2634
+ required: false,
2635
+ type: "date YYYY-MM-DD"
2636
+ },
2637
+ to: {
2638
+ required: false,
2639
+ type: "date YYYY-MM-DD"
2640
+ },
2641
+ group: {
2642
+ required: false,
2643
+ type: "string — grouping dimension"
2644
+ }
2645
+ }
2646
+ }
2647
+ };
2648
+ /**
2649
+ * Handle schema action - returns compact specification for a specific resource
2650
+ */
2651
+ function handleSchema(resource) {
2652
+ const schema = RESOURCE_SCHEMAS[resource];
2653
+ if (!schema) return errorResult(`Unknown resource: ${resource}. Valid resources: ${Object.keys(RESOURCE_SCHEMAS).join(", ")}`);
2654
+ return jsonResult({
2655
+ resource,
2656
+ ...schema
2657
+ });
2345
2658
  }
2346
2659
  /**
2347
- * Tasks MCP handler.
2660
+ * Get schema overview for all resources
2348
2661
  */
2349
- var DEFAULT_TASK_INCLUDE = ["project", "project.company"];
2350
- var VALID_ACTIONS$2 = [
2351
- "list",
2352
- "get",
2353
- "create",
2354
- "update",
2355
- "resolve"
2662
+ function handleSchemaOverview() {
2663
+ return jsonResult({
2664
+ _tip: "Use action=\"schema\" with a specific resource for full filter/create/includes spec",
2665
+ resources: Object.entries(RESOURCE_SCHEMAS).map(([resource, schema]) => ({
2666
+ resource,
2667
+ actions: schema.actions
2668
+ }))
2669
+ });
2670
+ }
2671
+ /**
2672
+ * Resources that support the query filter for text search
2673
+ */
2674
+ const SEARCHABLE_RESOURCES = [
2675
+ "projects",
2676
+ "companies",
2677
+ "people",
2678
+ "tasks",
2679
+ "deals"
2356
2680
  ];
2357
- async function handleTasks(action, args, ctx) {
2358
- const { formatOptions, filter, page, perPage, include: userInclude } = ctx;
2359
- const { id, title, project_id, task_list_id, description, assignee_id, query, type } = args;
2360
- const include = userInclude?.length ? [...new Set([...DEFAULT_TASK_INCLUDE, ...userInclude])] : DEFAULT_TASK_INCLUDE;
2361
- if (action === "resolve") return handleResolve({
2362
- query,
2363
- type,
2364
- project_id
2365
- }, ctx);
2366
- const execCtx = ctx.executor();
2367
- if (action === "get") {
2368
- if (!id) return inputErrorResult(ErrorMessages.missingId("get"));
2369
- const result = await getTask({
2370
- id,
2371
- include
2372
- }, execCtx);
2373
- const formatted = formatTask$1(result.data, {
2374
- ...formatOptions,
2375
- included: result.included
2376
- });
2377
- if (ctx.includeHints !== false) {
2378
- const serviceId = result.data.relationships?.service?.data?.id;
2379
- return jsonResult({
2380
- ...formatted,
2381
- _hints: getTaskHints(id, serviceId)
2382
- });
2681
+ /**
2682
+ * Default resources to search when not specified
2683
+ */
2684
+ var DEFAULT_SEARCH_RESOURCES = [
2685
+ "projects",
2686
+ "companies",
2687
+ "people",
2688
+ "tasks"
2689
+ ];
2690
+ /**
2691
+ * Handle cross-resource search.
2692
+ *
2693
+ * @param query - Search query text (required)
2694
+ * @param resources - Resource types to search (optional, defaults to DEFAULT_SEARCH_RESOURCES)
2695
+ * @param credentials - Productive API credentials
2696
+ * @param execute - Function to execute tool calls (injected for delegation and testing)
2697
+ * @returns Grouped search results across all requested resources
2698
+ */
2699
+ async function handleSearch(query, resources, credentials, execute) {
2700
+ if (!query || query.trim() === "") return errorResult("Missing required parameter: query. Provide a non-empty search string.");
2701
+ const trimmedQuery = query.trim();
2702
+ const resourcesToSearch = resources && resources.length > 0 ? resources : DEFAULT_SEARCH_RESOURCES;
2703
+ const invalidResources = resourcesToSearch.filter((r) => !SEARCHABLE_RESOURCES.includes(r));
2704
+ if (invalidResources.length > 0) return errorResult(`Invalid searchable resources: ${invalidResources.join(", ")}. Valid searchable resources: ${SEARCHABLE_RESOURCES.join(", ")}.`);
2705
+ const searchPromises = resourcesToSearch.map(async (resource) => {
2706
+ try {
2707
+ const textContent = (await execute("productive", {
2708
+ resource,
2709
+ action: "list",
2710
+ query: trimmedQuery,
2711
+ compact: true,
2712
+ per_page: 10
2713
+ }, credentials)).content.find((c) => c.type === "text");
2714
+ if (!textContent || textContent.type !== "text") return [resource, { error: "No content in response" }];
2715
+ try {
2716
+ const parsed = JSON.parse(textContent.text);
2717
+ const items = parsed.items ?? parsed.data ?? [];
2718
+ return [resource, { items: Array.isArray(items) ? items : [] }];
2719
+ } catch {
2720
+ return [resource, { error: "Failed to parse response JSON" }];
2721
+ }
2722
+ } catch (err) {
2723
+ return [resource, { error: err instanceof Error ? err.message : String(err) }];
2383
2724
  }
2384
- return jsonResult(formatted);
2725
+ });
2726
+ const searchResults = await Promise.all(searchPromises);
2727
+ const results = {};
2728
+ let totalResults = 0;
2729
+ for (const [resource, result] of searchResults) if (result.error) results[resource] = { error: result.error };
2730
+ else {
2731
+ const items = result.items ?? [];
2732
+ results[resource] = items;
2733
+ totalResults += items.length;
2385
2734
  }
2386
- if (action === "create") {
2387
- if (!title || !project_id || !task_list_id) return inputErrorResult(ErrorMessages.missingRequiredFields("task", [
2735
+ return jsonResult({
2736
+ query: trimmedQuery,
2737
+ resources_searched: resourcesToSearch,
2738
+ results,
2739
+ total_results: totalResults
2740
+ });
2741
+ }
2742
+ /**
2743
+ * Services MCP handler.
2744
+ *
2745
+ * Uses the createResourceHandler factory for the common list pattern.
2746
+ */
2747
+ /**
2748
+ * Handle services resource.
2749
+ *
2750
+ * Supports: list
2751
+ */
2752
+ const handleServices = createResourceHandler({
2753
+ resource: "services",
2754
+ actions: ["list"],
2755
+ formatter: formatService$1,
2756
+ executors: { list: listServices }
2757
+ });
2758
+ /**
2759
+ * Tasks MCP handler.
2760
+ */
2761
+ const handleTasks = createResourceHandler({
2762
+ resource: "tasks",
2763
+ displayName: "task",
2764
+ actions: [
2765
+ "list",
2766
+ "get",
2767
+ "create",
2768
+ "update",
2769
+ "resolve"
2770
+ ],
2771
+ formatter: formatTask$1,
2772
+ hints: (data, id) => {
2773
+ const serviceId = data.relationships?.service?.data?.id;
2774
+ return getTaskHints(id, serviceId);
2775
+ },
2776
+ supportsResolve: true,
2777
+ resolveArgsFromArgs: (args) => ({ project_id: args.project_id }),
2778
+ defaultInclude: {
2779
+ list: ["project", "project.company"],
2780
+ get: ["project", "project.company"]
2781
+ },
2782
+ create: {
2783
+ required: [
2388
2784
  "title",
2389
2785
  "project_id",
2390
2786
  "task_list_id"
2391
- ]));
2392
- return jsonResult({
2393
- success: true,
2394
- ...formatTask$1((await createTask({
2395
- title,
2396
- projectId: project_id,
2397
- taskListId: task_list_id,
2398
- assigneeId: assignee_id,
2399
- description
2400
- }, execCtx)).data, formatOptions)
2401
- });
2402
- }
2403
- if (action === "update") {
2404
- if (!id) return inputErrorResult(ErrorMessages.missingId("update"));
2405
- return jsonResult({
2406
- success: true,
2407
- ...formatTask$1((await updateTask({
2408
- id,
2409
- title,
2410
- description,
2411
- assigneeId: assignee_id
2412
- }, execCtx)).data, formatOptions)
2413
- });
2414
- }
2415
- if (action === "list") {
2416
- const result = await listTasks({
2417
- page,
2418
- perPage,
2419
- additionalFilters: filter,
2420
- include
2421
- }, execCtx);
2422
- const response = formatListResponse$1(result.data, formatTask$1, result.meta, {
2423
- ...formatOptions,
2424
- included: result.included
2425
- });
2426
- if (result.resolved && Object.keys(result.resolved).length > 0) return jsonResult({
2427
- ...response,
2428
- _resolved: result.resolved
2429
- });
2430
- return jsonResult(response);
2787
+ ],
2788
+ mapOptions: (args) => ({
2789
+ title: args.title,
2790
+ projectId: args.project_id,
2791
+ taskListId: args.task_list_id,
2792
+ assigneeId: args.assignee_id,
2793
+ description: args.description
2794
+ })
2795
+ },
2796
+ update: { mapOptions: (args) => ({
2797
+ title: args.title,
2798
+ description: args.description,
2799
+ assigneeId: args.assignee_id
2800
+ }) },
2801
+ executors: {
2802
+ list: listTasks,
2803
+ get: getTask,
2804
+ create: createTask,
2805
+ update: updateTask
2431
2806
  }
2432
- return inputErrorResult(ErrorMessages.invalidAction(action, "tasks", VALID_ACTIONS$2));
2433
- }
2807
+ });
2434
2808
  /**
2435
2809
  * Time entries MCP handler.
2436
2810
  *
2437
2811
  * Thin adapter that delegates business logic to core executors
2438
2812
  * and handles MCP-specific concerns (hints, error formatting, JSON results).
2439
2813
  */
2440
- var VALID_ACTIONS$1 = [
2441
- "list",
2442
- "get",
2443
- "create",
2444
- "update",
2445
- "resolve"
2446
- ];
2447
- async function handleTime(action, args, ctx) {
2448
- const { formatOptions, filter, page, perPage } = ctx;
2449
- const { id, person_id, service_id, task_id, time, date, note, query, type, project_id } = args;
2450
- if (action === "resolve") return handleResolve({
2451
- query,
2452
- type,
2453
- project_id
2454
- }, ctx);
2455
- const execCtx = ctx.executor();
2456
- if (action === "get") {
2457
- if (!id) return inputErrorResult(ErrorMessages.missingId("get"));
2458
- const result = await getTimeEntry({ id }, execCtx);
2459
- const formatted = formatTimeEntry$1(result.data, formatOptions);
2460
- if (ctx.includeHints !== false) {
2461
- const serviceId = result.data.relationships?.service?.data?.id;
2462
- return jsonResult({
2463
- ...formatted,
2464
- _hints: getTimeEntryHints(id, void 0, serviceId)
2465
- });
2466
- }
2467
- return jsonResult(formatted);
2468
- }
2469
- if (action === "create") {
2470
- if (!person_id || !service_id || !time || !date) return inputErrorResult(ErrorMessages.missingRequiredFields("time entry", [
2814
+ const handleTime = createResourceHandler({
2815
+ resource: "time",
2816
+ displayName: "time entry",
2817
+ actions: [
2818
+ "list",
2819
+ "get",
2820
+ "create",
2821
+ "update",
2822
+ "delete",
2823
+ "resolve"
2824
+ ],
2825
+ formatter: formatTimeEntry$1,
2826
+ hints: (data, id) => {
2827
+ const serviceId = data.relationships?.service?.data?.id;
2828
+ return getTimeEntryHints(id, void 0, serviceId);
2829
+ },
2830
+ supportsResolve: true,
2831
+ resolveArgsFromArgs: (args) => ({ project_id: args.project_id }),
2832
+ create: {
2833
+ required: [
2471
2834
  "person_id",
2472
2835
  "service_id",
2473
2836
  "time",
2474
2837
  "date"
2475
- ]));
2476
- return jsonResult({
2477
- success: true,
2478
- ...formatTimeEntry$1((await createTimeEntry({
2479
- personId: person_id,
2480
- serviceId: service_id,
2481
- time,
2482
- date,
2483
- note: note ?? void 0,
2484
- taskId: task_id,
2485
- projectId: project_id
2486
- }, execCtx)).data, formatOptions)
2487
- });
2488
- }
2489
- if (action === "update") {
2490
- if (!id) return inputErrorResult(ErrorMessages.missingId("update"));
2491
- return jsonResult({
2492
- success: true,
2493
- ...formatTimeEntry$1((await updateTimeEntry({
2494
- id,
2495
- time: time ?? void 0,
2496
- date: date ?? void 0,
2497
- note: note ?? void 0
2498
- }, execCtx)).data, formatOptions)
2499
- });
2500
- }
2501
- if (action === "list") {
2502
- const result = await listTimeEntries({
2503
- page,
2504
- perPage,
2505
- additionalFilters: filter
2506
- }, execCtx);
2507
- const response = formatListResponse$1(result.data, formatTimeEntry$1, result.meta, formatOptions);
2508
- if (result.resolved && Object.keys(result.resolved).length > 0) return jsonResult({
2509
- ...response,
2510
- _resolved: result.resolved
2511
- });
2512
- return jsonResult(response);
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
+ },
2849
+ update: { mapOptions: (args) => ({
2850
+ time: args.time ?? void 0,
2851
+ date: args.date ?? void 0,
2852
+ note: args.note ?? void 0
2853
+ }) },
2854
+ executors: {
2855
+ list: listTimeEntries,
2856
+ get: getTimeEntry,
2857
+ create: createTimeEntry,
2858
+ update: updateTimeEntry,
2859
+ delete: deleteTimeEntry
2513
2860
  }
2514
- return inputErrorResult(ErrorMessages.invalidAction(action, "time", VALID_ACTIONS$1));
2515
- }
2861
+ });
2516
2862
  /**
2517
2863
  * Timers MCP handler.
2518
2864
  */
2519
- var VALID_ACTIONS = [
2520
- "list",
2521
- "get",
2522
- "start",
2523
- "stop"
2524
- ];
2525
- async function handleTimers(action, args, ctx) {
2526
- const { formatOptions, filter, page, perPage, include } = ctx;
2527
- const { id, service_id, time_entry_id } = args;
2528
- const execCtx = ctx.executor();
2529
- if (action === "get") {
2530
- if (!id) return inputErrorResult(ErrorMessages.missingId("get"));
2531
- const formatted = formatTimer$1((await getTimer({
2532
- id,
2533
- include
2534
- }, execCtx)).data, formatOptions);
2535
- if (ctx.includeHints !== false) return jsonResult({
2536
- ...formatted,
2537
- _hints: getTimerHints(id)
2538
- });
2539
- return jsonResult(formatted);
2540
- }
2541
- if (action === "start" || action === "create") {
2542
- if (!service_id && !time_entry_id) return inputErrorResult(ErrorMessages.missingServiceForTimer());
2543
- return jsonResult({
2544
- success: true,
2545
- ...formatTimer$1((await startTimer({
2546
- serviceId: service_id,
2547
- timeEntryId: time_entry_id
2548
- }, execCtx)).data, formatOptions)
2549
- });
2550
- }
2551
- if (action === "stop") {
2552
- if (!id) return inputErrorResult(ErrorMessages.missingId("stop"));
2553
- return jsonResult({
2554
- success: true,
2555
- ...formatTimer$1((await stopTimer({ id }, execCtx)).data, formatOptions)
2556
- });
2557
- }
2558
- if (action === "list") {
2559
- const result = await listTimers({
2560
- page,
2561
- perPage,
2562
- additionalFilters: filter,
2563
- include
2564
- }, execCtx);
2565
- return jsonResult(formatListResponse$1(result.data, formatTimer$1, result.meta, formatOptions));
2865
+ const handleTimers = createResourceHandler({
2866
+ resource: "timers",
2867
+ actions: [
2868
+ "list",
2869
+ "get",
2870
+ "start",
2871
+ "stop"
2872
+ ],
2873
+ formatter: formatTimer$1,
2874
+ hints: (_data, id) => getTimerHints(id),
2875
+ customActions: {
2876
+ start: async (args, ctx, execCtx) => {
2877
+ if (!args.service_id && !args.time_entry_id) return inputErrorResult(ErrorMessages.missingServiceForTimer());
2878
+ return jsonResult({
2879
+ success: true,
2880
+ ...formatTimer$1((await startTimer({
2881
+ serviceId: args.service_id,
2882
+ timeEntryId: args.time_entry_id
2883
+ }, execCtx)).data, ctx.formatOptions)
2884
+ });
2885
+ },
2886
+ create: async (args, ctx, execCtx) => {
2887
+ if (!args.service_id && !args.time_entry_id) return inputErrorResult(ErrorMessages.missingServiceForTimer());
2888
+ return jsonResult({
2889
+ success: true,
2890
+ ...formatTimer$1((await startTimer({
2891
+ serviceId: args.service_id,
2892
+ timeEntryId: args.time_entry_id
2893
+ }, execCtx)).data, ctx.formatOptions)
2894
+ });
2895
+ },
2896
+ stop: async (args, ctx, execCtx) => {
2897
+ if (!args.id) return inputErrorResult(ErrorMessages.missingId("stop"));
2898
+ return jsonResult({
2899
+ success: true,
2900
+ ...formatTimer$1((await stopTimer({ id: args.id }, execCtx)).data, ctx.formatOptions)
2901
+ });
2902
+ }
2903
+ },
2904
+ executors: {
2905
+ list: listTimers,
2906
+ get: getTimer
2566
2907
  }
2567
- return inputErrorResult(ErrorMessages.invalidAction(action, "timers", VALID_ACTIONS));
2568
- }
2908
+ });
2569
2909
  /**
2570
2910
  * Tool execution handlers for Productive MCP server
2571
2911
  * These are shared between stdio and HTTP transports
@@ -2581,13 +2921,17 @@ var DEFAULT_PER_PAGE = 20;
2581
2921
  * Execute a tool with the given credentials and arguments
2582
2922
  */
2583
2923
  async function executeToolWithCredentials(name, args, credentials) {
2924
+ if (name !== "productive") return errorResult(`Unknown tool: ${name}`);
2925
+ const typedArgs = args;
2926
+ if (typedArgs.resource === "batch") return handleBatch(typedArgs.operations, credentials, executeToolWithCredentials);
2927
+ const { resource, action, filter, page, per_page, compact, include, query, resources, no_hints, type, ...restArgs } = typedArgs;
2928
+ if (resource === "search") return await handleSearch(query, resources, credentials, executeToolWithCredentials);
2584
2929
  const api = new ProductiveApi({ config: {
2585
2930
  apiToken: credentials.apiToken,
2586
2931
  organizationId: credentials.organizationId,
2587
- userId: credentials.userId
2932
+ userId: credentials.userId,
2933
+ baseUrl: process.env.PRODUCTIVE_BASE_URL
2588
2934
  } });
2589
- if (name !== "productive") return errorResult(`Unknown tool: ${name}`);
2590
- const { resource, action, filter, page, per_page, compact, include, query, no_hints, type, ...restArgs } = args;
2591
2935
  const isCompact = compact ?? action !== "get";
2592
2936
  const formatOptions = { compact: isCompact };
2593
2937
  let stringFilter = toStringFilter(filter);
@@ -2609,6 +2953,7 @@ async function executeToolWithCredentials(name, args, credentials) {
2609
2953
  };
2610
2954
  try {
2611
2955
  if (action === "help") return resource ? handleHelp(resource) : handleHelpOverview();
2956
+ if (action === "schema") return resource ? handleSchema(resource) : handleSchemaOverview();
2612
2957
  const resolveArgs = {
2613
2958
  query,
2614
2959
  type
@@ -2643,10 +2988,14 @@ async function executeToolWithCredentials(name, args, credentials) {
2643
2988
  ...resolveArgs
2644
2989
  }, ctx);
2645
2990
  case "bookings": return await handleBookings(action, restArgs, ctx);
2646
- case "budgets": return await handleBudgets(action, restArgs, ctx);
2647
2991
  case "pages": return await handlePages(action, restArgs, ctx);
2648
2992
  case "discussions": return await handleDiscussions(action, restArgs, ctx);
2649
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
+ ]));
2650
2999
  default: return inputErrorResult(ErrorMessages.unknownResource(resource, VALID_RESOURCES));
2651
3000
  }
2652
3001
  } catch (error) {
@@ -2662,4 +3011,4 @@ async function executeToolWithCredentials(name, args, credentials) {
2662
3011
  }
2663
3012
  export { executeToolWithCredentials as t };
2664
3013
 
2665
- //# sourceMappingURL=handlers-D4tRd30c.js.map
3014
+ //# sourceMappingURL=handlers-DWowqxFA.js.map