@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.
- package/dist/errors.d.ts +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/formatters.d.ts +0 -4
- package/dist/formatters.d.ts.map +1 -1
- package/dist/handlers/attachments.d.ts +5 -2
- package/dist/handlers/attachments.d.ts.map +1 -1
- package/dist/handlers/batch.d.ts +58 -0
- package/dist/handlers/batch.d.ts.map +1 -0
- package/dist/handlers/bookings.d.ts +5 -2
- package/dist/handlers/bookings.d.ts.map +1 -1
- package/dist/handlers/comments.d.ts +5 -2
- package/dist/handlers/comments.d.ts.map +1 -1
- package/dist/handlers/companies.d.ts +11 -5
- package/dist/handlers/companies.d.ts.map +1 -1
- package/dist/handlers/deals.d.ts +4 -5
- package/dist/handlers/deals.d.ts.map +1 -1
- package/dist/handlers/discussions.d.ts +5 -9
- package/dist/handlers/discussions.d.ts.map +1 -1
- package/dist/handlers/factory.d.ts +116 -0
- package/dist/handlers/factory.d.ts.map +1 -0
- package/dist/handlers/help.d.ts.map +1 -1
- package/dist/handlers/index.d.ts.map +1 -1
- package/dist/handlers/pages.d.ts +12 -9
- package/dist/handlers/pages.d.ts.map +1 -1
- package/dist/handlers/projects.d.ts +11 -5
- package/dist/handlers/projects.d.ts.map +1 -1
- package/dist/handlers/schema.d.ts +33 -0
- package/dist/handlers/schema.d.ts.map +1 -0
- package/dist/handlers/search.d.ts +28 -0
- package/dist/handlers/search.d.ts.map +1 -0
- package/dist/handlers/services.d.ts +12 -2
- package/dist/handlers/services.d.ts.map +1 -1
- package/dist/handlers/tasks.d.ts +4 -5
- package/dist/handlers/tasks.d.ts.map +1 -1
- package/dist/handlers/time.d.ts +4 -6
- package/dist/handlers/time.d.ts.map +1 -1
- package/dist/handlers/timers.d.ts +5 -2
- package/dist/handlers/timers.d.ts.map +1 -1
- package/dist/handlers/types.d.ts +6 -0
- package/dist/handlers/types.d.ts.map +1 -1
- package/dist/{handlers-D4tRd30c.js → handlers-DWowqxFA.js} +1247 -898
- package/dist/handlers-DWowqxFA.js.map +1 -0
- package/dist/handlers.js +1 -1
- package/dist/hints.d.ts +0 -4
- package/dist/hints.d.ts.map +1 -1
- package/dist/http.js +2 -2
- package/dist/index.js +2 -2
- package/dist/schema.d.ts +17 -2
- package/dist/schema.d.ts.map +1 -1
- package/dist/server.js +2 -2
- package/dist/stdio.js +1 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +63 -15
- package/dist/tools.js.map +1 -1
- package/dist/{version-IB2ulmSy.js → version-DoRPyhTL.js} +2 -2
- package/dist/{version-IB2ulmSy.js.map → version-DoRPyhTL.js.map} +1 -1
- package/package.json +3 -3
- package/skills/SKILL.md +35 -54
- package/dist/handlers/budgets.d.ts +0 -9
- package/dist/handlers/budgets.d.ts.map +0 -1
- package/dist/handlers-D4tRd30c.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ProductiveApi, formatAttachment, formatBooking,
|
|
2
|
-
import { RESOURCES, ResolveError, VALID_REPORT_TYPES, createBooking, createComment, createCompany, createDeal, createDiscussion, createPage, createTask, createTimeEntry, deleteAttachment, deleteDiscussion, deletePage, fromHandlerContext, getAttachment, getBooking,
|
|
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
|
-
*
|
|
840
|
+
* Merge user includes with defaults, ensuring no duplicates
|
|
1112
841
|
*/
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
];
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
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
|
-
|
|
1229
|
-
}, execCtx)
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
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
|
-
*
|
|
963
|
+
* Attachments MCP handler.
|
|
1257
964
|
*/
|
|
1258
|
-
|
|
1259
|
-
"
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
if (
|
|
1273
|
-
|
|
1274
|
-
if (
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
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
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
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 (
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
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
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
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
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
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
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
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
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
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
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
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
|
-
|
|
2192
|
-
}
|
|
2172
|
+
});
|
|
2193
2173
|
/**
|
|
2194
2174
|
* People MCP handler.
|
|
2195
2175
|
*/
|
|
2196
|
-
var VALID_ACTIONS$
|
|
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$
|
|
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
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
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
|
-
*
|
|
2660
|
+
* Get schema overview for all resources
|
|
2348
2661
|
*/
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
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
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2387
|
-
|
|
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
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2441
|
-
"
|
|
2442
|
-
"
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
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
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
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
|
-
|
|
2515
|
-
}
|
|
2861
|
+
});
|
|
2516
2862
|
/**
|
|
2517
2863
|
* Timers MCP handler.
|
|
2518
2864
|
*/
|
|
2519
|
-
|
|
2520
|
-
"
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
}
|
|
2549
|
-
}
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
}
|
|
2557
|
-
}
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
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
|
-
|
|
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-
|
|
3014
|
+
//# sourceMappingURL=handlers-DWowqxFA.js.map
|