@studiometa/productive-mcp 0.9.2 → 0.10.0

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 (52) 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/bookings.d.ts +5 -2
  8. package/dist/handlers/bookings.d.ts.map +1 -1
  9. package/dist/handlers/comments.d.ts +5 -2
  10. package/dist/handlers/comments.d.ts.map +1 -1
  11. package/dist/handlers/companies.d.ts +11 -5
  12. package/dist/handlers/companies.d.ts.map +1 -1
  13. package/dist/handlers/deals.d.ts +4 -5
  14. package/dist/handlers/deals.d.ts.map +1 -1
  15. package/dist/handlers/discussions.d.ts +5 -9
  16. package/dist/handlers/discussions.d.ts.map +1 -1
  17. package/dist/handlers/factory.d.ts +116 -0
  18. package/dist/handlers/factory.d.ts.map +1 -0
  19. package/dist/handlers/help.d.ts.map +1 -1
  20. package/dist/handlers/index.d.ts.map +1 -1
  21. package/dist/handlers/pages.d.ts +12 -9
  22. package/dist/handlers/pages.d.ts.map +1 -1
  23. package/dist/handlers/projects.d.ts +11 -5
  24. package/dist/handlers/projects.d.ts.map +1 -1
  25. package/dist/handlers/services.d.ts +12 -2
  26. package/dist/handlers/services.d.ts.map +1 -1
  27. package/dist/handlers/tasks.d.ts +4 -5
  28. package/dist/handlers/tasks.d.ts.map +1 -1
  29. package/dist/handlers/time.d.ts +4 -6
  30. package/dist/handlers/time.d.ts.map +1 -1
  31. package/dist/handlers/timers.d.ts +5 -2
  32. package/dist/handlers/timers.d.ts.map +1 -1
  33. package/dist/handlers/types.d.ts +6 -0
  34. package/dist/handlers/types.d.ts.map +1 -1
  35. package/dist/{handlers-D4tRd30c.js → handlers-BE3CYyVX.js} +636 -897
  36. package/dist/handlers-BE3CYyVX.js.map +1 -0
  37. package/dist/handlers.js +1 -1
  38. package/dist/hints.d.ts +0 -4
  39. package/dist/hints.d.ts.map +1 -1
  40. package/dist/http.js +2 -2
  41. package/dist/index.js +2 -2
  42. package/dist/schema.d.ts +0 -2
  43. package/dist/schema.d.ts.map +1 -1
  44. package/dist/server.js +2 -2
  45. package/dist/stdio.js +1 -1
  46. package/dist/{version-IB2ulmSy.js → version-BKUpCCHx.js} +2 -2
  47. package/dist/{version-IB2ulmSy.js.map → version-BKUpCCHx.js.map} +1 -1
  48. package/package.json +3 -3
  49. package/skills/SKILL.md +31 -54
  50. package/dist/handlers/budgets.d.ts +0 -9
  51. package/dist/handlers/budgets.d.ts.map +0 -1
  52. 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,403 @@ 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);
1279
- }
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
- });
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
1290
988
  }
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
- });
989
+ });
990
+ /**
991
+ * Bookings MCP handler.
992
+ */
993
+ const handleBookings = createResourceHandler({
994
+ resource: "bookings",
995
+ displayName: "booking",
996
+ actions: [
997
+ "list",
998
+ "get",
999
+ "create",
1000
+ "update"
1001
+ ],
1002
+ formatter: formatBooking$1,
1003
+ hints: (data, id) => {
1004
+ const personId = data.relationships?.person?.data?.id;
1005
+ return getBookingHints(id, personId);
1006
+ },
1007
+ defaultInclude: {
1008
+ list: ["person", "service"],
1009
+ get: ["person", "service"]
1010
+ },
1011
+ create: {
1012
+ required: [
1013
+ "person_id",
1014
+ "started_on",
1015
+ "ended_on"
1016
+ ],
1017
+ validateArgs: (args) => {
1018
+ if (!args.service_id && !args.event_id) return inputErrorResult(ErrorMessages.missingBookingTarget());
1019
+ },
1020
+ mapOptions: (args) => ({
1021
+ personId: args.person_id,
1022
+ serviceId: args.service_id ?? "",
1023
+ startedOn: args.started_on,
1024
+ endedOn: args.ended_on,
1025
+ time: args.time,
1026
+ note: args.note,
1027
+ eventId: args.event_id
1028
+ })
1029
+ },
1030
+ update: { mapOptions: (args) => ({
1031
+ startedOn: args.started_on,
1032
+ endedOn: args.ended_on,
1033
+ time: args.time,
1034
+ note: args.note
1035
+ }) },
1036
+ executors: {
1037
+ list: listBookings,
1038
+ get: getBooking,
1039
+ create: createBooking,
1040
+ update: updateBooking
1301
1041
  }
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
- });
1042
+ });
1043
+ /**
1044
+ * Comments MCP handler.
1045
+ */
1046
+ const handleComments = createResourceHandler({
1047
+ resource: "comments",
1048
+ actions: [
1049
+ "list",
1050
+ "get",
1051
+ "create",
1052
+ "update"
1053
+ ],
1054
+ formatter: formatComment$1,
1055
+ hints: (data, id) => {
1056
+ const commentableType = data.attributes?.commentable_type;
1057
+ let commentableId;
1058
+ if (commentableType === "task") commentableId = data.relationships?.task?.data?.id;
1059
+ else if (commentableType === "deal") commentableId = data.relationships?.deal?.data?.id;
1060
+ else if (commentableType === "company") commentableId = data.relationships?.company?.data?.id;
1061
+ return getCommentHints(id, commentableType, commentableId);
1062
+ },
1063
+ defaultInclude: {
1064
+ list: ["creator"],
1065
+ get: ["creator"]
1066
+ },
1067
+ create: {
1068
+ required: ["body"],
1069
+ validateArgs: (args) => {
1070
+ if (!args.task_id && !args.deal_id && !args.company_id) return inputErrorResult(ErrorMessages.missingCommentTarget());
1071
+ },
1072
+ mapOptions: (args) => ({
1073
+ body: args.body,
1074
+ taskId: args.task_id,
1075
+ dealId: args.deal_id,
1076
+ companyId: args.company_id
1077
+ })
1078
+ },
1079
+ update: {
1080
+ allowedFields: ["body"],
1081
+ mapOptions: (args) => ({ body: args.body })
1082
+ },
1083
+ executors: {
1084
+ list: listComments,
1085
+ get: getComment,
1086
+ create: createComment,
1087
+ update: updateComment
1309
1088
  }
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
- });
1089
+ });
1090
+ /**
1091
+ * Companies MCP handler.
1092
+ *
1093
+ * Uses the createResourceHandler factory for the common list/get/create/update/resolve pattern.
1094
+ */
1095
+ /**
1096
+ * Handle companies resource.
1097
+ *
1098
+ * Supports: list, get, create, update, resolve
1099
+ */
1100
+ const handleCompanies = createResourceHandler({
1101
+ resource: "companies",
1102
+ actions: [
1103
+ "list",
1104
+ "get",
1105
+ "create",
1106
+ "update",
1107
+ "resolve"
1108
+ ],
1109
+ formatter: formatCompany$1,
1110
+ hints: (_data, id) => getCompanyHints(id),
1111
+ supportsResolve: true,
1112
+ create: {
1113
+ required: ["name"],
1114
+ mapOptions: (args) => ({ name: args.name })
1115
+ },
1116
+ update: { mapOptions: (args) => ({ name: args.name }) },
1117
+ executors: {
1118
+ list: listCompanies,
1119
+ get: getCompany,
1120
+ create: createCompany,
1121
+ update: updateCompany
1316
1122
  }
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
- });
1123
+ });
1124
+ /**
1125
+ * Deals MCP handler.
1126
+ */
1127
+ const handleDeals = createResourceHandler({
1128
+ resource: "deals",
1129
+ displayName: "deal",
1130
+ actions: [
1131
+ "list",
1132
+ "get",
1133
+ "create",
1134
+ "update",
1135
+ "resolve"
1136
+ ],
1137
+ formatter: formatDeal$1,
1138
+ hints: (_data, id) => getDealHints(id),
1139
+ supportsResolve: true,
1140
+ defaultInclude: {
1141
+ list: ["company", "deal_status"],
1142
+ get: [
1143
+ "company",
1144
+ "deal_status",
1145
+ "responsible"
1146
+ ]
1147
+ },
1148
+ create: {
1149
+ required: ["name", "company_id"],
1150
+ mapOptions: (args) => ({
1151
+ name: args.name,
1152
+ companyId: args.company_id
1153
+ })
1154
+ },
1155
+ update: { mapOptions: (args) => ({ name: args.name }) },
1156
+ executors: {
1157
+ list: listDeals,
1158
+ get: getDeal,
1159
+ create: createDeal,
1160
+ update: updateDeal
1323
1161
  }
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);
1162
+ });
1163
+ /**
1164
+ * Discussions MCP handler.
1165
+ */
1166
+ var STATUS_MAP = {
1167
+ active: "1",
1168
+ resolved: "2"
1169
+ };
1170
+ const handleDiscussions = createResourceHandler({
1171
+ resource: "discussions",
1172
+ actions: [
1173
+ "list",
1174
+ "get",
1175
+ "create",
1176
+ "update",
1177
+ "delete",
1178
+ "resolve",
1179
+ "reopen"
1180
+ ],
1181
+ formatter: formatDiscussion$1,
1182
+ hints: (data, id) => {
1183
+ const pageId = data.relationships?.page?.data?.id;
1184
+ return getDiscussionHints(id, pageId);
1185
+ },
1186
+ listFilterFromArgs: (args) => {
1187
+ const filters = {};
1188
+ if (args.status) {
1189
+ const mapped = STATUS_MAP[args.status.toLowerCase()];
1190
+ if (mapped) filters.status = mapped;
1191
+ }
1192
+ return filters;
1193
+ },
1194
+ create: {
1195
+ required: ["body", "page_id"],
1196
+ mapOptions: (args) => ({
1197
+ body: args.body,
1198
+ pageId: args.page_id,
1199
+ title: args.title
1200
+ })
1201
+ },
1202
+ update: { mapOptions: (args) => ({
1203
+ title: args.title,
1204
+ body: args.body
1205
+ }) },
1206
+ customActions: {
1207
+ resolve: async (args, ctx, execCtx) => {
1208
+ if (!args.id) return inputErrorResult(ErrorMessages.missingId("resolve"));
1209
+ return jsonResult({
1210
+ success: true,
1211
+ ...formatDiscussion$1((await resolveDiscussion({ id: args.id }, execCtx)).data, ctx.formatOptions)
1212
+ });
1213
+ },
1214
+ reopen: async (args, ctx, execCtx) => {
1215
+ if (!args.id) return inputErrorResult(ErrorMessages.missingId("reopen"));
1216
+ return jsonResult({
1217
+ success: true,
1218
+ ...formatDiscussion$1((await reopenDiscussion({ id: args.id }, execCtx)).data, ctx.formatOptions)
1219
+ });
1220
+ }
1221
+ },
1222
+ executors: {
1223
+ list: listDiscussions,
1224
+ get: getDiscussion,
1225
+ create: createDiscussion,
1226
+ update: updateDiscussion,
1227
+ delete: deleteDiscussion
1338
1228
  }
1339
- return inputErrorResult(ErrorMessages.invalidAction(action, "discussions", VALID_ACTIONS$8));
1340
- }
1229
+ });
1341
1230
  var RESOURCE_HELP = {
1342
1231
  projects: {
1343
1232
  description: "Manage projects in Productive.io",
1344
1233
  actions: {
1345
1234
  list: "List all projects with optional filters",
1346
- get: "Get a single project by ID with full details"
1235
+ get: "Get a single project by ID (supports PRJ-123, P-123 format)",
1236
+ resolve: "Resolve by project number (PRJ-123, P-123)"
1347
1237
  },
1348
1238
  filters: {
1349
1239
  query: "Text search on project name",
@@ -1393,7 +1283,8 @@ var RESOURCE_HELP = {
1393
1283
  list: "List tasks with optional filters",
1394
1284
  get: "Get a single task by ID with full details (description, comments, etc.)",
1395
1285
  create: "Create a new task (requires title, project_id, task_list_id)",
1396
- update: "Update an existing task"
1286
+ update: "Update an existing task",
1287
+ resolve: "Resolve by text search"
1397
1288
  },
1398
1289
  filters: {
1399
1290
  query: "Text search on task title",
@@ -1478,7 +1369,9 @@ var RESOURCE_HELP = {
1478
1369
  list: "List time entries with optional filters",
1479
1370
  get: "Get a single time entry by ID",
1480
1371
  create: "Create a new time entry (requires person_id, service_id, date, time)",
1481
- update: "Update an existing time entry"
1372
+ update: "Update an existing time entry",
1373
+ delete: "Delete a time entry",
1374
+ resolve: "Resolve related resources (person, project, service)"
1482
1375
  },
1483
1376
  filters: {
1484
1377
  person_id: "Filter by person (use \"me\" for current user)",
@@ -1559,8 +1452,9 @@ var RESOURCE_HELP = {
1559
1452
  description: "Team members and contacts",
1560
1453
  actions: {
1561
1454
  list: "List people with optional filters",
1562
- get: "Get a single person by ID",
1563
- me: "Get the currently authenticated user"
1455
+ get: "Get a single person by ID (supports email address)",
1456
+ me: "Get the currently authenticated user",
1457
+ resolve: "Resolve by email address"
1564
1458
  },
1565
1459
  filters: {
1566
1460
  query: "Text search on name or email",
@@ -1610,9 +1504,10 @@ var RESOURCE_HELP = {
1610
1504
  description: "Client companies and organizations",
1611
1505
  actions: {
1612
1506
  list: "List companies with optional filters",
1613
- get: "Get a single company by ID",
1507
+ get: "Get a single company by ID (supports company name)",
1614
1508
  create: "Create a new company (requires name)",
1615
- update: "Update an existing company"
1509
+ update: "Update an existing company",
1510
+ resolve: "Resolve by company name"
1616
1511
  },
1617
1512
  filters: {
1618
1513
  query: "Text search on company name",
@@ -1776,12 +1671,13 @@ var RESOURCE_HELP = {
1776
1671
  ]
1777
1672
  },
1778
1673
  deals: {
1779
- description: "Sales deals and opportunities",
1674
+ description: "Sales deals, opportunities, and budgets. Budgets are deals with budget=true — use filter[type]=2 to list only budgets.",
1780
1675
  actions: {
1781
1676
  list: "List deals with optional filters",
1782
- get: "Get a single deal by ID",
1677
+ get: "Get a single deal by ID (supports D-123, DEAL-123 format)",
1783
1678
  create: "Create a new deal (requires name, company_id)",
1784
- update: "Update an existing deal"
1679
+ update: "Update an existing deal",
1680
+ resolve: "Resolve by deal number (D-123, DEAL-123)"
1785
1681
  },
1786
1682
  filters: {
1787
1683
  query: "Text search on deal name",
@@ -1804,23 +1700,35 @@ var RESOURCE_HELP = {
1804
1700
  name: "Deal name",
1805
1701
  number: "Deal number",
1806
1702
  date: "Deal date",
1703
+ budget: "Whether this deal is a budget (true/false)",
1807
1704
  status: "Current status (from deal_status)"
1808
1705
  },
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" }
1706
+ examples: [
1707
+ {
1708
+ description: "Search deals",
1709
+ params: {
1710
+ resource: "deals",
1711
+ action: "list",
1712
+ query: "website redesign"
1713
+ }
1714
+ },
1715
+ {
1716
+ description: "List deals for a company",
1717
+ params: {
1718
+ resource: "deals",
1719
+ action: "list",
1720
+ filter: { company_id: "12345" }
1721
+ }
1722
+ },
1723
+ {
1724
+ description: "List only budgets",
1725
+ params: {
1726
+ resource: "deals",
1727
+ action: "list",
1728
+ filter: { type: "2" }
1729
+ }
1822
1730
  }
1823
- }]
1731
+ ]
1824
1732
  },
1825
1733
  bookings: {
1826
1734
  description: "Resource scheduling and capacity planning",
@@ -1863,66 +1771,6 @@ var RESOURCE_HELP = {
1863
1771
  }
1864
1772
  }]
1865
1773
  },
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
1774
  pages: {
1927
1775
  description: "Manage pages (wiki/docs) within projects",
1928
1776
  actions: {
@@ -2123,77 +1971,51 @@ function handleHelpOverview() {
2123
1971
  }
2124
1972
  /**
2125
1973
  * Pages MCP handler.
1974
+ *
1975
+ * Uses the createResourceHandler factory for the common list/get/create/update/delete pattern.
2126
1976
  */
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);
1977
+ /**
1978
+ * Handle pages resource.
1979
+ *
1980
+ * Supports: list, get, create, update, delete
1981
+ */
1982
+ const handlePages = createResourceHandler({
1983
+ resource: "pages",
1984
+ actions: [
1985
+ "list",
1986
+ "get",
1987
+ "create",
1988
+ "update",
1989
+ "delete"
1990
+ ],
1991
+ formatter: formatPage$1,
1992
+ hints: (_data, id) => getPageHints(id),
1993
+ supportsResolve: false,
1994
+ create: {
1995
+ required: ["title", "project_id"],
1996
+ mapOptions: (args) => ({
1997
+ title: args.title,
1998
+ projectId: args.project_id,
1999
+ body: args.body,
2000
+ parentPageId: args.parent_page_id
2001
+ })
2002
+ },
2003
+ update: { mapOptions: (args) => ({
2004
+ title: args.title,
2005
+ body: args.body
2006
+ }) },
2007
+ executors: {
2008
+ list: listPages,
2009
+ get: getPage,
2010
+ create: createPage,
2011
+ update: updatePage,
2012
+ delete: deletePage
2190
2013
  }
2191
- return inputErrorResult(ErrorMessages.invalidAction(action, "pages", VALID_ACTIONS$7));
2192
- }
2014
+ });
2193
2015
  /**
2194
2016
  * People MCP handler.
2195
2017
  */
2196
- var VALID_ACTIONS$6 = [
2018
+ var VALID_ACTIONS$1 = [
2197
2019
  "list",
2198
2020
  "get",
2199
2021
  "me",
@@ -2244,48 +2066,33 @@ async function handlePeople(action, args, ctx, credentials) {
2244
2066
  });
2245
2067
  return jsonResult(response);
2246
2068
  }
2247
- return inputErrorResult(ErrorMessages.invalidAction(action, "people", VALID_ACTIONS$6));
2069
+ return inputErrorResult(ErrorMessages.invalidAction(action, "people", VALID_ACTIONS$1));
2248
2070
  }
2249
2071
  /**
2250
2072
  * Projects MCP handler.
2073
+ *
2074
+ * Uses the createResourceHandler factory for the common list/get/resolve pattern.
2251
2075
  */
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);
2076
+ /**
2077
+ * Handle projects resource.
2078
+ *
2079
+ * Supports: list, get, resolve
2080
+ */
2081
+ const handleProjects = createResourceHandler({
2082
+ resource: "projects",
2083
+ actions: [
2084
+ "list",
2085
+ "get",
2086
+ "resolve"
2087
+ ],
2088
+ formatter: formatProject$1,
2089
+ hints: (_data, id) => getProjectHints(id),
2090
+ supportsResolve: true,
2091
+ executors: {
2092
+ list: listProjects,
2093
+ get: getProject
2286
2094
  }
2287
- return inputErrorResult(ErrorMessages.invalidAction(action, "projects", VALID_ACTIONS$5));
2288
- }
2095
+ });
2289
2096
  /**
2290
2097
  * Reports MCP handler.
2291
2098
  */
@@ -2299,11 +2106,11 @@ function formatReportData(data) {
2299
2106
  };
2300
2107
  });
2301
2108
  }
2302
- var VALID_ACTIONS$4 = ["get"];
2109
+ var VALID_ACTIONS = ["get"];
2303
2110
  async function handleReports(action, args, ctx) {
2304
2111
  const { filter, page, perPage } = ctx;
2305
2112
  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));
2113
+ if (action !== "get") return inputErrorResult(ErrorMessages.invalidAction(action, "reports", VALID_ACTIONS));
2307
2114
  if (!report_type) return inputErrorResult(ErrorMessages.missingReportType());
2308
2115
  if (!VALID_REPORT_TYPES.includes(report_type)) return inputErrorResult(ErrorMessages.invalidReportType(report_type, [...VALID_REPORT_TYPES]));
2309
2116
  const execCtx = ctx.executor();
@@ -2328,244 +2135,171 @@ async function handleReports(action, args, ctx) {
2328
2135
  }
2329
2136
  /**
2330
2137
  * Services MCP handler.
2138
+ *
2139
+ * Uses the createResourceHandler factory for the common list pattern.
2331
2140
  */
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));
2345
- }
2141
+ /**
2142
+ * Handle services resource.
2143
+ *
2144
+ * Supports: list
2145
+ */
2146
+ const handleServices = createResourceHandler({
2147
+ resource: "services",
2148
+ actions: ["list"],
2149
+ formatter: formatService$1,
2150
+ executors: { list: listServices }
2151
+ });
2346
2152
  /**
2347
2153
  * Tasks MCP handler.
2348
2154
  */
2349
- var DEFAULT_TASK_INCLUDE = ["project", "project.company"];
2350
- var VALID_ACTIONS$2 = [
2351
- "list",
2352
- "get",
2353
- "create",
2354
- "update",
2355
- "resolve"
2356
- ];
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
- });
2383
- }
2384
- return jsonResult(formatted);
2385
- }
2386
- if (action === "create") {
2387
- if (!title || !project_id || !task_list_id) return inputErrorResult(ErrorMessages.missingRequiredFields("task", [
2155
+ const handleTasks = createResourceHandler({
2156
+ resource: "tasks",
2157
+ displayName: "task",
2158
+ actions: [
2159
+ "list",
2160
+ "get",
2161
+ "create",
2162
+ "update",
2163
+ "resolve"
2164
+ ],
2165
+ formatter: formatTask$1,
2166
+ hints: (data, id) => {
2167
+ const serviceId = data.relationships?.service?.data?.id;
2168
+ return getTaskHints(id, serviceId);
2169
+ },
2170
+ supportsResolve: true,
2171
+ resolveArgsFromArgs: (args) => ({ project_id: args.project_id }),
2172
+ defaultInclude: {
2173
+ list: ["project", "project.company"],
2174
+ get: ["project", "project.company"]
2175
+ },
2176
+ create: {
2177
+ required: [
2388
2178
  "title",
2389
2179
  "project_id",
2390
2180
  "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);
2181
+ ],
2182
+ mapOptions: (args) => ({
2183
+ title: args.title,
2184
+ projectId: args.project_id,
2185
+ taskListId: args.task_list_id,
2186
+ assigneeId: args.assignee_id,
2187
+ description: args.description
2188
+ })
2189
+ },
2190
+ update: { mapOptions: (args) => ({
2191
+ title: args.title,
2192
+ description: args.description,
2193
+ assigneeId: args.assignee_id
2194
+ }) },
2195
+ executors: {
2196
+ list: listTasks,
2197
+ get: getTask,
2198
+ create: createTask,
2199
+ update: updateTask
2431
2200
  }
2432
- return inputErrorResult(ErrorMessages.invalidAction(action, "tasks", VALID_ACTIONS$2));
2433
- }
2201
+ });
2434
2202
  /**
2435
2203
  * Time entries MCP handler.
2436
2204
  *
2437
2205
  * Thin adapter that delegates business logic to core executors
2438
2206
  * and handles MCP-specific concerns (hints, error formatting, JSON results).
2439
2207
  */
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", [
2208
+ const handleTime = createResourceHandler({
2209
+ resource: "time",
2210
+ displayName: "time entry",
2211
+ actions: [
2212
+ "list",
2213
+ "get",
2214
+ "create",
2215
+ "update",
2216
+ "delete",
2217
+ "resolve"
2218
+ ],
2219
+ formatter: formatTimeEntry$1,
2220
+ hints: (data, id) => {
2221
+ const serviceId = data.relationships?.service?.data?.id;
2222
+ return getTimeEntryHints(id, void 0, serviceId);
2223
+ },
2224
+ supportsResolve: true,
2225
+ resolveArgsFromArgs: (args) => ({ project_id: args.project_id }),
2226
+ create: {
2227
+ required: [
2471
2228
  "person_id",
2472
2229
  "service_id",
2473
2230
  "time",
2474
2231
  "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);
2232
+ ],
2233
+ mapOptions: (args) => ({
2234
+ personId: args.person_id,
2235
+ serviceId: args.service_id,
2236
+ time: args.time,
2237
+ date: args.date,
2238
+ note: args.note ?? void 0,
2239
+ taskId: args.task_id,
2240
+ projectId: args.project_id
2241
+ })
2242
+ },
2243
+ update: { mapOptions: (args) => ({
2244
+ time: args.time ?? void 0,
2245
+ date: args.date ?? void 0,
2246
+ note: args.note ?? void 0
2247
+ }) },
2248
+ executors: {
2249
+ list: listTimeEntries,
2250
+ get: getTimeEntry,
2251
+ create: createTimeEntry,
2252
+ update: updateTimeEntry,
2253
+ delete: deleteTimeEntry
2513
2254
  }
2514
- return inputErrorResult(ErrorMessages.invalidAction(action, "time", VALID_ACTIONS$1));
2515
- }
2255
+ });
2516
2256
  /**
2517
2257
  * Timers MCP handler.
2518
2258
  */
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));
2259
+ const handleTimers = createResourceHandler({
2260
+ resource: "timers",
2261
+ actions: [
2262
+ "list",
2263
+ "get",
2264
+ "start",
2265
+ "stop"
2266
+ ],
2267
+ formatter: formatTimer$1,
2268
+ hints: (_data, id) => getTimerHints(id),
2269
+ customActions: {
2270
+ start: async (args, ctx, execCtx) => {
2271
+ if (!args.service_id && !args.time_entry_id) return inputErrorResult(ErrorMessages.missingServiceForTimer());
2272
+ return jsonResult({
2273
+ success: true,
2274
+ ...formatTimer$1((await startTimer({
2275
+ serviceId: args.service_id,
2276
+ timeEntryId: args.time_entry_id
2277
+ }, execCtx)).data, ctx.formatOptions)
2278
+ });
2279
+ },
2280
+ create: async (args, ctx, execCtx) => {
2281
+ if (!args.service_id && !args.time_entry_id) return inputErrorResult(ErrorMessages.missingServiceForTimer());
2282
+ return jsonResult({
2283
+ success: true,
2284
+ ...formatTimer$1((await startTimer({
2285
+ serviceId: args.service_id,
2286
+ timeEntryId: args.time_entry_id
2287
+ }, execCtx)).data, ctx.formatOptions)
2288
+ });
2289
+ },
2290
+ stop: async (args, ctx, execCtx) => {
2291
+ if (!args.id) return inputErrorResult(ErrorMessages.missingId("stop"));
2292
+ return jsonResult({
2293
+ success: true,
2294
+ ...formatTimer$1((await stopTimer({ id: args.id }, execCtx)).data, ctx.formatOptions)
2295
+ });
2296
+ }
2297
+ },
2298
+ executors: {
2299
+ list: listTimers,
2300
+ get: getTimer
2566
2301
  }
2567
- return inputErrorResult(ErrorMessages.invalidAction(action, "timers", VALID_ACTIONS));
2568
- }
2302
+ });
2569
2303
  /**
2570
2304
  * Tool execution handlers for Productive MCP server
2571
2305
  * These are shared between stdio and HTTP transports
@@ -2584,7 +2318,8 @@ async function executeToolWithCredentials(name, args, credentials) {
2584
2318
  const api = new ProductiveApi({ config: {
2585
2319
  apiToken: credentials.apiToken,
2586
2320
  organizationId: credentials.organizationId,
2587
- userId: credentials.userId
2321
+ userId: credentials.userId,
2322
+ baseUrl: process.env.PRODUCTIVE_BASE_URL
2588
2323
  } });
2589
2324
  if (name !== "productive") return errorResult(`Unknown tool: ${name}`);
2590
2325
  const { resource, action, filter, page, per_page, compact, include, query, no_hints, type, ...restArgs } = args;
@@ -2643,10 +2378,14 @@ async function executeToolWithCredentials(name, args, credentials) {
2643
2378
  ...resolveArgs
2644
2379
  }, ctx);
2645
2380
  case "bookings": return await handleBookings(action, restArgs, ctx);
2646
- case "budgets": return await handleBudgets(action, restArgs, ctx);
2647
2381
  case "pages": return await handlePages(action, restArgs, ctx);
2648
2382
  case "discussions": return await handleDiscussions(action, restArgs, ctx);
2649
2383
  case "reports": return await handleReports(action, restArgs, ctx);
2384
+ case "budgets": return inputErrorResult(new UserInputError("The \"budgets\" resource has been removed. Budgets are deals with type=2.", [
2385
+ "Use resource=\"deals\" with filter[type]=\"2\" to list only budgets",
2386
+ "To create a budget: resource=\"deals\" action=\"create\" with budget=true",
2387
+ "Use action=\"help\" resource=\"deals\" for full documentation"
2388
+ ]));
2650
2389
  default: return inputErrorResult(ErrorMessages.unknownResource(resource, VALID_RESOURCES));
2651
2390
  }
2652
2391
  } catch (error) {
@@ -2662,4 +2401,4 @@ async function executeToolWithCredentials(name, args, credentials) {
2662
2401
  }
2663
2402
  export { executeToolWithCredentials as t };
2664
2403
 
2665
- //# sourceMappingURL=handlers-D4tRd30c.js.map
2404
+ //# sourceMappingURL=handlers-BE3CYyVX.js.map