@studiometa/productive-mcp 0.10.1 → 0.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -13
- package/dist/formatters.d.ts +1 -0
- package/dist/formatters.d.ts.map +1 -1
- package/dist/handlers/activities.d.ts +26 -0
- package/dist/handlers/activities.d.ts.map +1 -0
- package/dist/handlers/deals.d.ts.map +1 -1
- package/dist/handlers/factory.d.ts.map +1 -1
- package/dist/handlers/help.d.ts.map +1 -1
- package/dist/handlers/index.d.ts.map +1 -1
- package/dist/handlers/projects.d.ts +1 -1
- package/dist/handlers/projects.d.ts.map +1 -1
- package/dist/handlers/services.d.ts.map +1 -1
- package/dist/handlers/summaries.d.ts +18 -0
- package/dist/handlers/summaries.d.ts.map +1 -0
- package/dist/handlers/tasks.d.ts.map +1 -1
- package/dist/handlers/time.d.ts.map +1 -1
- package/dist/handlers/types.d.ts +1 -0
- package/dist/handlers/types.d.ts.map +1 -1
- package/dist/handlers/valid-includes.d.ts +20 -0
- package/dist/handlers/valid-includes.d.ts.map +1 -0
- package/dist/handlers/workflows.d.ts +35 -0
- package/dist/handlers/workflows.d.ts.map +1 -0
- package/dist/{handlers-DWowqxFA.js → handlers-CaOBYauF.js} +997 -304
- package/dist/handlers-CaOBYauF.js.map +1 -0
- package/dist/handlers.js +1 -1
- package/dist/hints.d.ts +9 -0
- 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 +20 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/server.js +2 -2
- package/dist/stdio.js +1 -1
- package/dist/suggestions.d.ts +44 -0
- package/dist/suggestions.d.ts.map +1 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +2 -1
- package/dist/tools.js.map +1 -1
- package/dist/{version-DoRPyhTL.js → version-BRTaTnUG.js} +2 -2
- package/dist/{version-DoRPyhTL.js.map → version-BRTaTnUG.js.map} +1 -1
- package/package.json +3 -3
- package/skills/SKILL.md +139 -16
- package/dist/handlers-DWowqxFA.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ProductiveApi, formatAttachment, formatBooking, formatComment, formatCompany, formatDeal, formatDiscussion, formatListResponse, formatPage, formatPerson, formatProject, formatService, formatTask, formatTimeEntry, formatTimer } from "@studiometa/productive-api";
|
|
2
|
-
import { RESOURCES, ResolveError, VALID_REPORT_TYPES, createBooking, createComment, createCompany, createDeal, createDiscussion, createPage, createTask, createTimeEntry, deleteAttachment, deleteDiscussion, deletePage, deleteTimeEntry, fromHandlerContext, getAttachment, getBooking, getComment, getCompany, getDeal, getDiscussion, getPage, getPerson, getProject, getReport, getTask, getTimeEntry, getTimer, listAttachments, listBookings, listComments, listCompanies, listDeals, listDiscussions, listPages, listPeople, listProjects, listServices, listTasks, listTimeEntries, listTimers, reopenDiscussion, resolveDiscussion, resolveResource, startTimer, stopTimer, updateBooking, updateComment, updateCompany, updateDeal, updateDiscussion, updatePage, updateTask, updateTimeEntry } from "@studiometa/productive-core";
|
|
1
|
+
import { ProductiveApi, formatActivity, formatAttachment, formatBooking, formatComment, formatCompany, formatDeal, formatDiscussion, formatListResponse, formatPage, formatPerson, formatProject, formatService, formatTask, formatTimeEntry, formatTimer } from "@studiometa/productive-api";
|
|
2
|
+
import { RESOURCES, ResolveError, VALID_REPORT_TYPES, completeTask, createBooking, createComment, createCompany, createDeal, createDiscussion, createPage, createTask, createTimeEntry, deleteAttachment, deleteDiscussion, deletePage, deleteTimeEntry, fromHandlerContext, getAttachment, getBooking, getComment, getCompany, getDeal, getDealContext, getDiscussion, getMyDaySummary, getPage, getPerson, getProject, getProjectContext, getProjectHealthSummary, getReport, getTask, getTaskContext, getTeamPulseSummary, getTimeEntry, getTimer, listActivities, listAttachments, listBookings, listComments, listCompanies, listDeals, listDiscussions, listPages, listPeople, listProjects, listServices, listTasks, listTimeEntries, listTimers, logDay, reopenDiscussion, resolveDiscussion, resolveResource, startTimer, stopTimer, updateBooking, updateComment, updateCompany, updateDeal, updateDiscussion, updatePage, updateTask, updateTimeEntry, weeklyStandup } from "@studiometa/productive-core";
|
|
3
3
|
/**
|
|
4
4
|
* Custom error classes for MCP server
|
|
5
5
|
*
|
|
@@ -233,6 +233,12 @@ function formatDiscussion$1(discussion, options) {
|
|
|
233
233
|
if (options?.compact) return compactify(result, ["body"]);
|
|
234
234
|
return result;
|
|
235
235
|
}
|
|
236
|
+
function formatActivity$1(activity, options) {
|
|
237
|
+
return formatActivity(activity, {
|
|
238
|
+
...MCP_FORMAT_OPTIONS,
|
|
239
|
+
included: options?.included
|
|
240
|
+
});
|
|
241
|
+
}
|
|
236
242
|
/**
|
|
237
243
|
* Format list response with pagination
|
|
238
244
|
*
|
|
@@ -251,6 +257,337 @@ function formatListResponse$1(data, formatter, meta, options) {
|
|
|
251
257
|
});
|
|
252
258
|
}
|
|
253
259
|
/**
|
|
260
|
+
* Get today's date string in YYYY-MM-DD format
|
|
261
|
+
*/
|
|
262
|
+
function getToday() {
|
|
263
|
+
return (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Get suggestions for tasks.list response.
|
|
267
|
+
*
|
|
268
|
+
* - Warns about overdue tasks (due_date < today and not closed)
|
|
269
|
+
* - Informs about unassigned tasks
|
|
270
|
+
*/
|
|
271
|
+
function getTaskListSuggestions(tasks) {
|
|
272
|
+
const suggestions = [];
|
|
273
|
+
if (!tasks || tasks.length === 0) return suggestions;
|
|
274
|
+
const today = getToday();
|
|
275
|
+
let overdueCount = 0;
|
|
276
|
+
let unassignedCount = 0;
|
|
277
|
+
for (const task of tasks) {
|
|
278
|
+
const attrs = task.attributes;
|
|
279
|
+
const relationships = task.relationships;
|
|
280
|
+
const dueDate = attrs.due_date;
|
|
281
|
+
const closed = attrs.closed;
|
|
282
|
+
const closedAt = attrs.closed_at;
|
|
283
|
+
if (dueDate && dueDate < today && !closed && !closedAt) overdueCount++;
|
|
284
|
+
if (!(relationships?.assignee)?.data) unassignedCount++;
|
|
285
|
+
}
|
|
286
|
+
if (overdueCount > 0) suggestions.push(`⚠️ ${overdueCount} task(s) are overdue`);
|
|
287
|
+
if (unassignedCount > 0) suggestions.push(`ℹ️ ${unassignedCount} task(s) have no assignee`);
|
|
288
|
+
return suggestions;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Get suggestions for tasks.get response.
|
|
292
|
+
*
|
|
293
|
+
* - Warns if the single task is overdue (and by how many days)
|
|
294
|
+
* - Informs if no time has been logged (only when time_entries are included)
|
|
295
|
+
*/
|
|
296
|
+
function getTaskGetSuggestions(task, included) {
|
|
297
|
+
const suggestions = [];
|
|
298
|
+
if (!task) return suggestions;
|
|
299
|
+
const attrs = task.attributes;
|
|
300
|
+
const today = getToday();
|
|
301
|
+
const dueDate = attrs.due_date;
|
|
302
|
+
const closed = attrs.closed;
|
|
303
|
+
const closedAt = attrs.closed_at;
|
|
304
|
+
if (dueDate && dueDate < today && !closed && !closedAt) {
|
|
305
|
+
const due = new Date(dueDate);
|
|
306
|
+
const now = new Date(today);
|
|
307
|
+
const diffDays = Math.round((now.getTime() - due.getTime()) / (1e3 * 60 * 60 * 24));
|
|
308
|
+
suggestions.push(`⚠️ Task is ${diffDays} day(s) overdue`);
|
|
309
|
+
}
|
|
310
|
+
if (included && included.length > 0) {
|
|
311
|
+
const taskId = task.id;
|
|
312
|
+
if (included.filter((r) => {
|
|
313
|
+
if (r.type !== "time_entries") return false;
|
|
314
|
+
return ((r.relationships?.task)?.data)?.id === taskId;
|
|
315
|
+
}).length === 0) suggestions.push("ℹ️ No time entries on this task");
|
|
316
|
+
}
|
|
317
|
+
return suggestions;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Get suggestions for time.list response.
|
|
321
|
+
*
|
|
322
|
+
* - Shows total hours logged across all entries in the response.
|
|
323
|
+
* - If filtered by today, shows hours vs 8h target.
|
|
324
|
+
*/
|
|
325
|
+
function getTimeListSuggestions(entries, filter) {
|
|
326
|
+
const suggestions = [];
|
|
327
|
+
if (!entries || entries.length === 0) return suggestions;
|
|
328
|
+
const totalMinutes = entries.reduce((sum, entry) => {
|
|
329
|
+
return sum + (entry.attributes.time || 0);
|
|
330
|
+
}, 0);
|
|
331
|
+
const totalHours = +(totalMinutes / 60).toFixed(1);
|
|
332
|
+
const today = getToday();
|
|
333
|
+
if (filter?.after === today && filter?.before === today) suggestions.push(`📊 ${totalHours}h/8h logged today`);
|
|
334
|
+
else if (totalMinutes > 0) suggestions.push(`📊 Total: ${totalHours}h logged`);
|
|
335
|
+
return suggestions;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Get suggestions for summaries.my_day response.
|
|
339
|
+
*
|
|
340
|
+
* - Warns if no time has been logged today.
|
|
341
|
+
* - Warns if a timer has been running for more than 2 hours.
|
|
342
|
+
*/
|
|
343
|
+
function getMyDaySuggestions(data) {
|
|
344
|
+
const suggestions = [];
|
|
345
|
+
if (!data) return suggestions;
|
|
346
|
+
if (data.time.logged_today_minutes === 0 && data.time.entries_today === 0) suggestions.push("⚠️ No time logged today");
|
|
347
|
+
if (data.timers && data.timers.length > 0) {
|
|
348
|
+
for (const timer of data.timers) if (timer.total_time > 120) {
|
|
349
|
+
const hours = +(timer.total_time / 60).toFixed(1);
|
|
350
|
+
suggestions.push(`⏱️ Timer running for ${hours}h — remember to stop it`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return suggestions;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Helper to create a successful JSON response
|
|
357
|
+
*/
|
|
358
|
+
function jsonResult(data) {
|
|
359
|
+
return { content: [{
|
|
360
|
+
type: "text",
|
|
361
|
+
text: JSON.stringify(data, null, 2)
|
|
362
|
+
}] };
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Helper to create an error response from a string message
|
|
366
|
+
*/
|
|
367
|
+
function errorResult(message) {
|
|
368
|
+
return {
|
|
369
|
+
content: [{
|
|
370
|
+
type: "text",
|
|
371
|
+
text: `**Error:** ${message}`
|
|
372
|
+
}],
|
|
373
|
+
isError: true
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Helper to create an error response from a UserInputError
|
|
378
|
+
* Includes formatted hints for LLM consumption
|
|
379
|
+
*/
|
|
380
|
+
function inputErrorResult(error) {
|
|
381
|
+
return {
|
|
382
|
+
content: [{
|
|
383
|
+
type: "text",
|
|
384
|
+
text: error.toFormattedMessage()
|
|
385
|
+
}],
|
|
386
|
+
isError: true
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Helper to create an error response from any error type
|
|
391
|
+
* Automatically formats UserInputError with hints
|
|
392
|
+
*/
|
|
393
|
+
function formatError(error) {
|
|
394
|
+
if (isUserInputError(error)) return inputErrorResult(error);
|
|
395
|
+
return errorResult(error instanceof Error ? error.message : String(error));
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Convert unknown filter to string filter for API
|
|
399
|
+
*/
|
|
400
|
+
function toStringFilter(filter) {
|
|
401
|
+
if (!filter) return void 0;
|
|
402
|
+
const result = {};
|
|
403
|
+
for (const [key, value] of Object.entries(filter)) if (value !== void 0 && value !== null) result[key] = String(value);
|
|
404
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Resolve handler for MCP.
|
|
408
|
+
*
|
|
409
|
+
* Thin wrapper around core's resource resolver.
|
|
410
|
+
* Provides handleResolve for the MCP 'resolve' action.
|
|
411
|
+
*/
|
|
412
|
+
/**
|
|
413
|
+
* Handle resolve action for a resource.
|
|
414
|
+
*
|
|
415
|
+
* Delegates to core's resolveResource function and wraps
|
|
416
|
+
* errors in MCP-friendly format.
|
|
417
|
+
*/
|
|
418
|
+
async function handleResolve(args, ctx) {
|
|
419
|
+
const { query, type, project_id } = args;
|
|
420
|
+
if (!query) return errorResult("query is required for resolve action");
|
|
421
|
+
try {
|
|
422
|
+
const results = await resolveResource(ctx.executor().api, query, {
|
|
423
|
+
type,
|
|
424
|
+
projectId: project_id
|
|
425
|
+
});
|
|
426
|
+
return jsonResult({
|
|
427
|
+
query,
|
|
428
|
+
matches: results,
|
|
429
|
+
exact: results.length === 1 && results[0].exact
|
|
430
|
+
});
|
|
431
|
+
} catch (error) {
|
|
432
|
+
if (error instanceof ResolveError) return inputErrorResult(new UserInputError(error.message, [`Query: "${error.query}"`, ...error.type ? [`Type: ${error.type}`] : []]));
|
|
433
|
+
throw error;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Merge user includes with defaults, ensuring no duplicates
|
|
438
|
+
*/
|
|
439
|
+
function mergeIncludes(userInclude, defaults) {
|
|
440
|
+
if (!userInclude?.length && !defaults?.length) return void 0;
|
|
441
|
+
if (!userInclude?.length) return defaults;
|
|
442
|
+
if (!defaults?.length) return userInclude;
|
|
443
|
+
return [...new Set([...defaults, ...userInclude])];
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Create a resource handler function from configuration.
|
|
447
|
+
*
|
|
448
|
+
* @example
|
|
449
|
+
* ```typescript
|
|
450
|
+
* export const handleProjects = createResourceHandler({
|
|
451
|
+
* resource: 'projects',
|
|
452
|
+
* actions: ['list', 'get', 'resolve'],
|
|
453
|
+
* formatter: formatProject,
|
|
454
|
+
* hints: (data, id) => getProjectHints(id),
|
|
455
|
+
* supportsResolve: true,
|
|
456
|
+
* executors: {
|
|
457
|
+
* list: listProjects,
|
|
458
|
+
* get: getProject,
|
|
459
|
+
* },
|
|
460
|
+
* });
|
|
461
|
+
* ```
|
|
462
|
+
*/
|
|
463
|
+
function createResourceHandler(config) {
|
|
464
|
+
const { resource, displayName = resource, actions, formatter, hints, defaultInclude, supportsResolve, listFilterFromArgs, resolveArgsFromArgs, customActions, create: createConfig, update: updateConfig, executors } = config;
|
|
465
|
+
return async (action, args, ctx) => {
|
|
466
|
+
const { formatOptions, filter, page, perPage, include: userInclude } = ctx;
|
|
467
|
+
const { id, query, type } = args;
|
|
468
|
+
const execCtx = ctx.executor();
|
|
469
|
+
if (customActions?.[action]) return customActions[action](args, ctx, execCtx);
|
|
470
|
+
if (action === "resolve") {
|
|
471
|
+
if (!supportsResolve) return inputErrorResult(ErrorMessages.invalidAction(action, resource, actions));
|
|
472
|
+
return handleResolve({
|
|
473
|
+
query,
|
|
474
|
+
type,
|
|
475
|
+
...resolveArgsFromArgs?.(args)
|
|
476
|
+
}, ctx);
|
|
477
|
+
}
|
|
478
|
+
if (action === "get") {
|
|
479
|
+
if (!executors.get) return inputErrorResult(ErrorMessages.invalidAction(action, resource, actions));
|
|
480
|
+
if (!id) return inputErrorResult(ErrorMessages.missingId("get"));
|
|
481
|
+
const include = mergeIncludes(userInclude, defaultInclude?.get);
|
|
482
|
+
const result = await executors.get({
|
|
483
|
+
id,
|
|
484
|
+
include
|
|
485
|
+
}, execCtx);
|
|
486
|
+
const getResponseData = { ...formatter(result.data, {
|
|
487
|
+
...formatOptions,
|
|
488
|
+
included: result.included
|
|
489
|
+
}) };
|
|
490
|
+
if (ctx.includeHints) {
|
|
491
|
+
if (hints) getResponseData._hints = hints(result.data, id);
|
|
492
|
+
}
|
|
493
|
+
if (ctx.includeSuggestions !== false) {
|
|
494
|
+
let getSuggestions = [];
|
|
495
|
+
if (resource === "tasks") getSuggestions = getTaskGetSuggestions(result.data, result.included);
|
|
496
|
+
if (getSuggestions.length > 0) getResponseData._suggestions = getSuggestions;
|
|
497
|
+
}
|
|
498
|
+
return jsonResult(getResponseData);
|
|
499
|
+
}
|
|
500
|
+
if (action === "create") {
|
|
501
|
+
if (!executors.create || !createConfig) return inputErrorResult(ErrorMessages.invalidAction(action, resource, actions));
|
|
502
|
+
const missingFields = createConfig.required.filter((field) => !args[field]);
|
|
503
|
+
if (missingFields.length > 0) return inputErrorResult(ErrorMessages.missingRequiredFields(displayName, missingFields));
|
|
504
|
+
if (createConfig.validateArgs) {
|
|
505
|
+
const errorResult = createConfig.validateArgs(args);
|
|
506
|
+
if (errorResult) return errorResult;
|
|
507
|
+
}
|
|
508
|
+
const options = createConfig.mapOptions(args);
|
|
509
|
+
return jsonResult({
|
|
510
|
+
success: true,
|
|
511
|
+
...formatter((await executors.create(options, execCtx)).data, formatOptions)
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
if (action === "update") {
|
|
515
|
+
if (!executors.update || !updateConfig) return inputErrorResult(ErrorMessages.invalidAction(action, resource, actions));
|
|
516
|
+
if (!id) return inputErrorResult(ErrorMessages.missingId("update"));
|
|
517
|
+
if (updateConfig.allowedFields && updateConfig.allowedFields.length > 0) {
|
|
518
|
+
if (!updateConfig.allowedFields.some((field) => args[field] !== void 0)) return inputErrorResult(ErrorMessages.noUpdateFieldsSpecified(updateConfig.allowedFields));
|
|
519
|
+
}
|
|
520
|
+
const options = {
|
|
521
|
+
id,
|
|
522
|
+
...updateConfig.mapOptions(args)
|
|
523
|
+
};
|
|
524
|
+
return jsonResult({
|
|
525
|
+
success: true,
|
|
526
|
+
...formatter((await executors.update(options, execCtx)).data, formatOptions)
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
if (action === "delete") {
|
|
530
|
+
if (!executors.delete) return inputErrorResult(ErrorMessages.invalidAction(action, resource, actions));
|
|
531
|
+
if (!id) return inputErrorResult(ErrorMessages.missingId("delete"));
|
|
532
|
+
await executors.delete({ id }, execCtx);
|
|
533
|
+
return jsonResult({
|
|
534
|
+
success: true,
|
|
535
|
+
deleted: id
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
if (action === "list") {
|
|
539
|
+
const include = mergeIncludes(userInclude, defaultInclude?.list);
|
|
540
|
+
const additionalFilters = {
|
|
541
|
+
...filter,
|
|
542
|
+
...listFilterFromArgs?.(args)
|
|
543
|
+
};
|
|
544
|
+
const result = await executors.list({
|
|
545
|
+
page,
|
|
546
|
+
perPage,
|
|
547
|
+
additionalFilters,
|
|
548
|
+
include
|
|
549
|
+
}, execCtx);
|
|
550
|
+
const listResponseData = { ...formatListResponse$1(result.data, formatter, result.meta, {
|
|
551
|
+
...formatOptions,
|
|
552
|
+
included: result.included
|
|
553
|
+
}) };
|
|
554
|
+
if (result.resolved && Object.keys(result.resolved).length > 0) listResponseData._resolved = result.resolved;
|
|
555
|
+
if (ctx.includeSuggestions !== false) {
|
|
556
|
+
let listSuggestions = [];
|
|
557
|
+
if (resource === "tasks") listSuggestions = getTaskListSuggestions(result.data);
|
|
558
|
+
else if (resource === "time") listSuggestions = getTimeListSuggestions(result.data, additionalFilters);
|
|
559
|
+
if (listSuggestions.length > 0) listResponseData._suggestions = listSuggestions;
|
|
560
|
+
}
|
|
561
|
+
return jsonResult(listResponseData);
|
|
562
|
+
}
|
|
563
|
+
return inputErrorResult(ErrorMessages.invalidAction(action, resource, actions));
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Activities MCP handler.
|
|
568
|
+
*
|
|
569
|
+
* Uses the createResourceHandler factory for the common list pattern.
|
|
570
|
+
* Activities are read-only — only `list` is supported.
|
|
571
|
+
*/
|
|
572
|
+
/**
|
|
573
|
+
* Handle activities resource.
|
|
574
|
+
*
|
|
575
|
+
* Supports: list
|
|
576
|
+
*/
|
|
577
|
+
const handleActivities = createResourceHandler({
|
|
578
|
+
resource: "activities",
|
|
579
|
+
actions: ["list"],
|
|
580
|
+
formatter: formatActivity$1,
|
|
581
|
+
executors: { list: listActivities },
|
|
582
|
+
defaultInclude: { list: ["creator"] },
|
|
583
|
+
listFilterFromArgs: (args) => {
|
|
584
|
+
const filter = {};
|
|
585
|
+
if (args.after) filter.after = args.after;
|
|
586
|
+
if (args.event) filter.event = args.event;
|
|
587
|
+
return filter;
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
/**
|
|
254
591
|
* Generate hints for a task
|
|
255
592
|
*/
|
|
256
593
|
function getTaskHints(taskId, serviceId) {
|
|
@@ -470,6 +807,25 @@ function getPersonHints(personId) {
|
|
|
470
807
|
] };
|
|
471
808
|
}
|
|
472
809
|
/**
|
|
810
|
+
* Generate hints for a services list response.
|
|
811
|
+
*
|
|
812
|
+
* When the query doesn't already filter by deal_id, suggests filtering
|
|
813
|
+
* by deal_id to scope services to a specific budget/deal.
|
|
814
|
+
*
|
|
815
|
+
* @param currentFilter - The filter object currently applied to the list query
|
|
816
|
+
*/
|
|
817
|
+
function getServiceListHints(currentFilter) {
|
|
818
|
+
if (currentFilter?.deal_id) return null;
|
|
819
|
+
return { common_actions: [{
|
|
820
|
+
action: "Filter services by deal to see budget line items for a specific deal",
|
|
821
|
+
example: {
|
|
822
|
+
resource: "services",
|
|
823
|
+
action: "list",
|
|
824
|
+
filter: { deal_id: "<deal_id>" }
|
|
825
|
+
}
|
|
826
|
+
}] };
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
473
829
|
* Generate hints for a company
|
|
474
830
|
*/
|
|
475
831
|
function getCompanyHints(companyId) {
|
|
@@ -724,240 +1080,36 @@ function getDiscussionHints(discussionId, pageId) {
|
|
|
724
1080
|
example: {
|
|
725
1081
|
resource: "pages",
|
|
726
1082
|
action: "get",
|
|
727
|
-
id: pageId
|
|
728
|
-
}
|
|
729
|
-
});
|
|
730
|
-
return hints;
|
|
731
|
-
}
|
|
732
|
-
/**
|
|
733
|
-
* Generate hints for a timer
|
|
734
|
-
*/
|
|
735
|
-
function getTimerHints(timerId, serviceId) {
|
|
736
|
-
const hints = {
|
|
737
|
-
common_actions: [{
|
|
738
|
-
action: "Stop this timer",
|
|
739
|
-
example: {
|
|
740
|
-
resource: "timers",
|
|
741
|
-
action: "stop",
|
|
742
|
-
id: timerId
|
|
743
|
-
}
|
|
744
|
-
}],
|
|
745
|
-
related_resources: []
|
|
746
|
-
};
|
|
747
|
-
if (serviceId) hints.related_resources.push({
|
|
748
|
-
resource: "services",
|
|
749
|
-
description: "Get the service this timer is running on",
|
|
750
|
-
example: {
|
|
751
|
-
resource: "services",
|
|
752
|
-
action: "get",
|
|
753
|
-
id: serviceId
|
|
754
|
-
}
|
|
755
|
-
});
|
|
756
|
-
return hints;
|
|
757
|
-
}
|
|
758
|
-
/**
|
|
759
|
-
* Helper to create a successful JSON response
|
|
760
|
-
*/
|
|
761
|
-
function jsonResult(data) {
|
|
762
|
-
return { content: [{
|
|
763
|
-
type: "text",
|
|
764
|
-
text: JSON.stringify(data, null, 2)
|
|
765
|
-
}] };
|
|
766
|
-
}
|
|
767
|
-
/**
|
|
768
|
-
* Helper to create an error response from a string message
|
|
769
|
-
*/
|
|
770
|
-
function errorResult(message) {
|
|
771
|
-
return {
|
|
772
|
-
content: [{
|
|
773
|
-
type: "text",
|
|
774
|
-
text: `**Error:** ${message}`
|
|
775
|
-
}],
|
|
776
|
-
isError: true
|
|
777
|
-
};
|
|
778
|
-
}
|
|
779
|
-
/**
|
|
780
|
-
* Helper to create an error response from a UserInputError
|
|
781
|
-
* Includes formatted hints for LLM consumption
|
|
782
|
-
*/
|
|
783
|
-
function inputErrorResult(error) {
|
|
784
|
-
return {
|
|
785
|
-
content: [{
|
|
786
|
-
type: "text",
|
|
787
|
-
text: error.toFormattedMessage()
|
|
788
|
-
}],
|
|
789
|
-
isError: true
|
|
790
|
-
};
|
|
791
|
-
}
|
|
792
|
-
/**
|
|
793
|
-
* Helper to create an error response from any error type
|
|
794
|
-
* Automatically formats UserInputError with hints
|
|
795
|
-
*/
|
|
796
|
-
function formatError(error) {
|
|
797
|
-
if (isUserInputError(error)) return inputErrorResult(error);
|
|
798
|
-
return errorResult(error instanceof Error ? error.message : String(error));
|
|
799
|
-
}
|
|
800
|
-
/**
|
|
801
|
-
* Convert unknown filter to string filter for API
|
|
802
|
-
*/
|
|
803
|
-
function toStringFilter(filter) {
|
|
804
|
-
if (!filter) return void 0;
|
|
805
|
-
const result = {};
|
|
806
|
-
for (const [key, value] of Object.entries(filter)) if (value !== void 0 && value !== null) result[key] = String(value);
|
|
807
|
-
return Object.keys(result).length > 0 ? result : void 0;
|
|
808
|
-
}
|
|
809
|
-
/**
|
|
810
|
-
* Resolve handler for MCP.
|
|
811
|
-
*
|
|
812
|
-
* Thin wrapper around core's resource resolver.
|
|
813
|
-
* Provides handleResolve for the MCP 'resolve' action.
|
|
814
|
-
*/
|
|
815
|
-
/**
|
|
816
|
-
* Handle resolve action for a resource.
|
|
817
|
-
*
|
|
818
|
-
* Delegates to core's resolveResource function and wraps
|
|
819
|
-
* errors in MCP-friendly format.
|
|
820
|
-
*/
|
|
821
|
-
async function handleResolve(args, ctx) {
|
|
822
|
-
const { query, type, project_id } = args;
|
|
823
|
-
if (!query) return errorResult("query is required for resolve action");
|
|
824
|
-
try {
|
|
825
|
-
const results = await resolveResource(ctx.executor().api, query, {
|
|
826
|
-
type,
|
|
827
|
-
projectId: project_id
|
|
828
|
-
});
|
|
829
|
-
return jsonResult({
|
|
830
|
-
query,
|
|
831
|
-
matches: results,
|
|
832
|
-
exact: results.length === 1 && results[0].exact
|
|
833
|
-
});
|
|
834
|
-
} catch (error) {
|
|
835
|
-
if (error instanceof ResolveError) return inputErrorResult(new UserInputError(error.message, [`Query: "${error.query}"`, ...error.type ? [`Type: ${error.type}`] : []]));
|
|
836
|
-
throw error;
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
/**
|
|
840
|
-
* Merge user includes with defaults, ensuring no duplicates
|
|
841
|
-
*/
|
|
842
|
-
function mergeIncludes(userInclude, defaults) {
|
|
843
|
-
if (!userInclude?.length && !defaults?.length) return void 0;
|
|
844
|
-
if (!userInclude?.length) return defaults;
|
|
845
|
-
if (!defaults?.length) return userInclude;
|
|
846
|
-
return [...new Set([...defaults, ...userInclude])];
|
|
847
|
-
}
|
|
848
|
-
/**
|
|
849
|
-
* Create a resource handler function from configuration.
|
|
850
|
-
*
|
|
851
|
-
* @example
|
|
852
|
-
* ```typescript
|
|
853
|
-
* export const handleProjects = createResourceHandler({
|
|
854
|
-
* resource: 'projects',
|
|
855
|
-
* actions: ['list', 'get', 'resolve'],
|
|
856
|
-
* formatter: formatProject,
|
|
857
|
-
* hints: (data, id) => getProjectHints(id),
|
|
858
|
-
* supportsResolve: true,
|
|
859
|
-
* executors: {
|
|
860
|
-
* list: listProjects,
|
|
861
|
-
* get: getProject,
|
|
862
|
-
* },
|
|
863
|
-
* });
|
|
864
|
-
* ```
|
|
865
|
-
*/
|
|
866
|
-
function createResourceHandler(config) {
|
|
867
|
-
const { resource, displayName = resource, actions, formatter, hints, defaultInclude, supportsResolve, listFilterFromArgs, resolveArgsFromArgs, customActions, create: createConfig, update: updateConfig, executors } = config;
|
|
868
|
-
return async (action, args, ctx) => {
|
|
869
|
-
const { formatOptions, filter, page, perPage, include: userInclude } = ctx;
|
|
870
|
-
const { id, query, type } = args;
|
|
871
|
-
const execCtx = ctx.executor();
|
|
872
|
-
if (customActions?.[action]) return customActions[action](args, ctx, execCtx);
|
|
873
|
-
if (action === "resolve") {
|
|
874
|
-
if (!supportsResolve) return inputErrorResult(ErrorMessages.invalidAction(action, resource, actions));
|
|
875
|
-
return handleResolve({
|
|
876
|
-
query,
|
|
877
|
-
type,
|
|
878
|
-
...resolveArgsFromArgs?.(args)
|
|
879
|
-
}, ctx);
|
|
880
|
-
}
|
|
881
|
-
if (action === "get") {
|
|
882
|
-
if (!executors.get) return inputErrorResult(ErrorMessages.invalidAction(action, resource, actions));
|
|
883
|
-
if (!id) return inputErrorResult(ErrorMessages.missingId("get"));
|
|
884
|
-
const include = mergeIncludes(userInclude, defaultInclude?.get);
|
|
885
|
-
const result = await executors.get({
|
|
886
|
-
id,
|
|
887
|
-
include
|
|
888
|
-
}, execCtx);
|
|
889
|
-
const formatted = formatter(result.data, {
|
|
890
|
-
...formatOptions,
|
|
891
|
-
included: result.included
|
|
892
|
-
});
|
|
893
|
-
if (ctx.includeHints !== false && hints) return jsonResult({
|
|
894
|
-
...formatted,
|
|
895
|
-
_hints: hints(result.data, id)
|
|
896
|
-
});
|
|
897
|
-
return jsonResult(formatted);
|
|
898
|
-
}
|
|
899
|
-
if (action === "create") {
|
|
900
|
-
if (!executors.create || !createConfig) return inputErrorResult(ErrorMessages.invalidAction(action, resource, actions));
|
|
901
|
-
const missingFields = createConfig.required.filter((field) => !args[field]);
|
|
902
|
-
if (missingFields.length > 0) return inputErrorResult(ErrorMessages.missingRequiredFields(displayName, missingFields));
|
|
903
|
-
if (createConfig.validateArgs) {
|
|
904
|
-
const errorResult = createConfig.validateArgs(args);
|
|
905
|
-
if (errorResult) return errorResult;
|
|
906
|
-
}
|
|
907
|
-
const options = createConfig.mapOptions(args);
|
|
908
|
-
return jsonResult({
|
|
909
|
-
success: true,
|
|
910
|
-
...formatter((await executors.create(options, execCtx)).data, formatOptions)
|
|
911
|
-
});
|
|
1083
|
+
id: pageId
|
|
912
1084
|
}
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1085
|
+
});
|
|
1086
|
+
return hints;
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Generate hints for a timer
|
|
1090
|
+
*/
|
|
1091
|
+
function getTimerHints(timerId, serviceId) {
|
|
1092
|
+
const hints = {
|
|
1093
|
+
common_actions: [{
|
|
1094
|
+
action: "Stop this timer",
|
|
1095
|
+
example: {
|
|
1096
|
+
resource: "timers",
|
|
1097
|
+
action: "stop",
|
|
1098
|
+
id: timerId
|
|
918
1099
|
}
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
...updateConfig.mapOptions(args)
|
|
922
|
-
};
|
|
923
|
-
return jsonResult({
|
|
924
|
-
success: true,
|
|
925
|
-
...formatter((await executors.update(options, execCtx)).data, formatOptions)
|
|
926
|
-
});
|
|
927
|
-
}
|
|
928
|
-
if (action === "delete") {
|
|
929
|
-
if (!executors.delete) return inputErrorResult(ErrorMessages.invalidAction(action, resource, actions));
|
|
930
|
-
if (!id) return inputErrorResult(ErrorMessages.missingId("delete"));
|
|
931
|
-
await executors.delete({ id }, execCtx);
|
|
932
|
-
return jsonResult({
|
|
933
|
-
success: true,
|
|
934
|
-
deleted: id
|
|
935
|
-
});
|
|
936
|
-
}
|
|
937
|
-
if (action === "list") {
|
|
938
|
-
const include = mergeIncludes(userInclude, defaultInclude?.list);
|
|
939
|
-
const additionalFilters = {
|
|
940
|
-
...filter,
|
|
941
|
-
...listFilterFromArgs?.(args)
|
|
942
|
-
};
|
|
943
|
-
const result = await executors.list({
|
|
944
|
-
page,
|
|
945
|
-
perPage,
|
|
946
|
-
additionalFilters,
|
|
947
|
-
include
|
|
948
|
-
}, execCtx);
|
|
949
|
-
const response = formatListResponse$1(result.data, formatter, result.meta, {
|
|
950
|
-
...formatOptions,
|
|
951
|
-
included: result.included
|
|
952
|
-
});
|
|
953
|
-
if (result.resolved && Object.keys(result.resolved).length > 0) return jsonResult({
|
|
954
|
-
...response,
|
|
955
|
-
_resolved: result.resolved
|
|
956
|
-
});
|
|
957
|
-
return jsonResult(response);
|
|
958
|
-
}
|
|
959
|
-
return inputErrorResult(ErrorMessages.invalidAction(action, resource, actions));
|
|
1100
|
+
}],
|
|
1101
|
+
related_resources: []
|
|
960
1102
|
};
|
|
1103
|
+
if (serviceId) hints.related_resources.push({
|
|
1104
|
+
resource: "services",
|
|
1105
|
+
description: "Get the service this timer is running on",
|
|
1106
|
+
example: {
|
|
1107
|
+
resource: "services",
|
|
1108
|
+
action: "get",
|
|
1109
|
+
id: serviceId
|
|
1110
|
+
}
|
|
1111
|
+
});
|
|
1112
|
+
return hints;
|
|
961
1113
|
}
|
|
962
1114
|
/**
|
|
963
1115
|
* Attachments MCP handler.
|
|
@@ -1238,7 +1390,8 @@ const handleDeals = createResourceHandler({
|
|
|
1238
1390
|
"get",
|
|
1239
1391
|
"create",
|
|
1240
1392
|
"update",
|
|
1241
|
-
"resolve"
|
|
1393
|
+
"resolve",
|
|
1394
|
+
"context"
|
|
1242
1395
|
],
|
|
1243
1396
|
formatter: formatDeal$1,
|
|
1244
1397
|
hints: (_data, id) => getDealHints(id),
|
|
@@ -1259,6 +1412,20 @@ const handleDeals = createResourceHandler({
|
|
|
1259
1412
|
})
|
|
1260
1413
|
},
|
|
1261
1414
|
update: { mapOptions: (args) => ({ name: args.name }) },
|
|
1415
|
+
customActions: { context: async (args, ctx, execCtx) => {
|
|
1416
|
+
if (!args.id) return inputErrorResult(ErrorMessages.missingId("context"));
|
|
1417
|
+
const result = await getDealContext({ id: args.id }, execCtx);
|
|
1418
|
+
const formatOptions = {
|
|
1419
|
+
...ctx.formatOptions,
|
|
1420
|
+
included: result.included
|
|
1421
|
+
};
|
|
1422
|
+
return jsonResult({
|
|
1423
|
+
...formatDeal$1(result.data.deal, formatOptions),
|
|
1424
|
+
services: result.data.services.map((s) => formatService$1(s, { compact: true })),
|
|
1425
|
+
comments: result.data.comments.map((c) => formatComment$1(c, { compact: true })),
|
|
1426
|
+
time_entries: result.data.time_entries.map((t) => formatTimeEntry$1(t, { compact: true }))
|
|
1427
|
+
});
|
|
1428
|
+
} },
|
|
1262
1429
|
executors: {
|
|
1263
1430
|
list: listDeals,
|
|
1264
1431
|
get: getDeal,
|
|
@@ -1389,7 +1556,8 @@ var RESOURCE_HELP = {
|
|
|
1389
1556
|
actions: {
|
|
1390
1557
|
list: "List all projects with optional filters",
|
|
1391
1558
|
get: "Get a single project by ID (supports PRJ-123, P-123 format)",
|
|
1392
|
-
resolve: "Resolve by project number (PRJ-123, P-123)"
|
|
1559
|
+
resolve: "Resolve by project number (PRJ-123, P-123)",
|
|
1560
|
+
context: "Get full project context in one call: project details + open tasks + services + recent time entries"
|
|
1393
1561
|
},
|
|
1394
1562
|
filters: {
|
|
1395
1563
|
query: "Text search on project name",
|
|
@@ -1430,6 +1598,14 @@ var RESOURCE_HELP = {
|
|
|
1430
1598
|
action: "get",
|
|
1431
1599
|
id: "12345"
|
|
1432
1600
|
}
|
|
1601
|
+
},
|
|
1602
|
+
{
|
|
1603
|
+
description: "Get full project context",
|
|
1604
|
+
params: {
|
|
1605
|
+
resource: "projects",
|
|
1606
|
+
action: "context",
|
|
1607
|
+
id: "12345"
|
|
1608
|
+
}
|
|
1433
1609
|
}
|
|
1434
1610
|
]
|
|
1435
1611
|
},
|
|
@@ -1440,7 +1616,8 @@ var RESOURCE_HELP = {
|
|
|
1440
1616
|
get: "Get a single task by ID with full details (description, comments, etc.)",
|
|
1441
1617
|
create: "Create a new task (requires title, project_id, task_list_id)",
|
|
1442
1618
|
update: "Update an existing task",
|
|
1443
|
-
resolve: "Resolve by text search"
|
|
1619
|
+
resolve: "Resolve by text search",
|
|
1620
|
+
context: "Get full task context in one call: task details + comments + time entries + subtasks"
|
|
1444
1621
|
},
|
|
1445
1622
|
filters: {
|
|
1446
1623
|
query: "Text search on task title",
|
|
@@ -1516,6 +1693,14 @@ var RESOURCE_HELP = {
|
|
|
1516
1693
|
project_id: "12345",
|
|
1517
1694
|
task_list_id: "111"
|
|
1518
1695
|
}
|
|
1696
|
+
},
|
|
1697
|
+
{
|
|
1698
|
+
description: "Get full task context",
|
|
1699
|
+
params: {
|
|
1700
|
+
resource: "tasks",
|
|
1701
|
+
action: "context",
|
|
1702
|
+
id: "67890"
|
|
1703
|
+
}
|
|
1519
1704
|
}
|
|
1520
1705
|
]
|
|
1521
1706
|
},
|
|
@@ -1596,6 +1781,13 @@ var RESOURCE_HELP = {
|
|
|
1596
1781
|
worked_time: "Logged time in minutes"
|
|
1597
1782
|
},
|
|
1598
1783
|
examples: [{
|
|
1784
|
+
description: "List services for a deal (budget line items)",
|
|
1785
|
+
params: {
|
|
1786
|
+
resource: "services",
|
|
1787
|
+
action: "list",
|
|
1788
|
+
filter: { deal_id: "12345" }
|
|
1789
|
+
}
|
|
1790
|
+
}, {
|
|
1599
1791
|
description: "List services for a project",
|
|
1600
1792
|
params: {
|
|
1601
1793
|
resource: "services",
|
|
@@ -1833,7 +2025,8 @@ var RESOURCE_HELP = {
|
|
|
1833
2025
|
get: "Get a single deal by ID (supports D-123, DEAL-123 format)",
|
|
1834
2026
|
create: "Create a new deal (requires name, company_id)",
|
|
1835
2027
|
update: "Update an existing deal",
|
|
1836
|
-
resolve: "Resolve by deal number (D-123, DEAL-123)"
|
|
2028
|
+
resolve: "Resolve by deal number (D-123, DEAL-123)",
|
|
2029
|
+
context: "Get full deal context in one call: deal details + services + comments + time entries"
|
|
1837
2030
|
},
|
|
1838
2031
|
filters: {
|
|
1839
2032
|
query: "Text search on deal name",
|
|
@@ -1883,6 +2076,14 @@ var RESOURCE_HELP = {
|
|
|
1883
2076
|
action: "list",
|
|
1884
2077
|
filter: { type: "2" }
|
|
1885
2078
|
}
|
|
2079
|
+
},
|
|
2080
|
+
{
|
|
2081
|
+
description: "Get full deal context",
|
|
2082
|
+
params: {
|
|
2083
|
+
resource: "deals",
|
|
2084
|
+
action: "context",
|
|
2085
|
+
id: "12345"
|
|
2086
|
+
}
|
|
1886
2087
|
}
|
|
1887
2088
|
]
|
|
1888
2089
|
},
|
|
@@ -2061,6 +2262,137 @@ var RESOURCE_HELP = {
|
|
|
2061
2262
|
}
|
|
2062
2263
|
]
|
|
2063
2264
|
},
|
|
2265
|
+
workflows: {
|
|
2266
|
+
description: "Compound workflows that chain multiple resource operations into a single tool call. Use these for common multi-step patterns.",
|
|
2267
|
+
actions: {
|
|
2268
|
+
complete_task: "Mark a task closed, optionally post a comment, and stop running timers",
|
|
2269
|
+
log_day: "Create multiple time entries in parallel from a structured list",
|
|
2270
|
+
weekly_standup: "Aggregate completed tasks, time logged, and upcoming deadlines for a week"
|
|
2271
|
+
},
|
|
2272
|
+
fields: {
|
|
2273
|
+
task_id: "(complete_task) Required. Task ID to mark as complete",
|
|
2274
|
+
comment: "(complete_task) Optional. Completion comment text to post",
|
|
2275
|
+
stop_timer: "(complete_task) Optional. Whether to stop running timers (default: true)",
|
|
2276
|
+
entries: "(log_day) Required. Array of { project_id, service_id, duration_minutes, note?, date? }",
|
|
2277
|
+
date: "(log_day) Optional. Default date for all entries (YYYY-MM-DD, defaults to today)",
|
|
2278
|
+
person_id: "(log_day / weekly_standup) Optional. Person to act on (defaults to current user)",
|
|
2279
|
+
week_start: "(weekly_standup) Optional. Monday date of the target week (defaults to this Monday)"
|
|
2280
|
+
},
|
|
2281
|
+
examples: [
|
|
2282
|
+
{
|
|
2283
|
+
description: "Complete a task with a comment",
|
|
2284
|
+
params: {
|
|
2285
|
+
resource: "workflows",
|
|
2286
|
+
action: "complete_task",
|
|
2287
|
+
task_id: "12345",
|
|
2288
|
+
comment: "Done! All tests passing."
|
|
2289
|
+
}
|
|
2290
|
+
},
|
|
2291
|
+
{
|
|
2292
|
+
description: "Complete a task without stopping timers",
|
|
2293
|
+
params: {
|
|
2294
|
+
resource: "workflows",
|
|
2295
|
+
action: "complete_task",
|
|
2296
|
+
task_id: "12345",
|
|
2297
|
+
stop_timer: false
|
|
2298
|
+
}
|
|
2299
|
+
},
|
|
2300
|
+
{
|
|
2301
|
+
description: "Log a full day across multiple projects",
|
|
2302
|
+
params: {
|
|
2303
|
+
resource: "workflows",
|
|
2304
|
+
action: "log_day",
|
|
2305
|
+
date: "2024-01-16",
|
|
2306
|
+
entries: [
|
|
2307
|
+
{
|
|
2308
|
+
project_id: "100",
|
|
2309
|
+
service_id: "111",
|
|
2310
|
+
duration_minutes: 240,
|
|
2311
|
+
note: "Frontend dev"
|
|
2312
|
+
},
|
|
2313
|
+
{
|
|
2314
|
+
project_id: "200",
|
|
2315
|
+
service_id: "222",
|
|
2316
|
+
duration_minutes: 120,
|
|
2317
|
+
note: "Code review"
|
|
2318
|
+
},
|
|
2319
|
+
{
|
|
2320
|
+
project_id: "100",
|
|
2321
|
+
service_id: "333",
|
|
2322
|
+
duration_minutes: 60,
|
|
2323
|
+
note: "Meetings"
|
|
2324
|
+
}
|
|
2325
|
+
]
|
|
2326
|
+
}
|
|
2327
|
+
},
|
|
2328
|
+
{
|
|
2329
|
+
description: "Get this week standup",
|
|
2330
|
+
params: {
|
|
2331
|
+
resource: "workflows",
|
|
2332
|
+
action: "weekly_standup"
|
|
2333
|
+
}
|
|
2334
|
+
},
|
|
2335
|
+
{
|
|
2336
|
+
description: "Get standup for a specific past week",
|
|
2337
|
+
params: {
|
|
2338
|
+
resource: "workflows",
|
|
2339
|
+
action: "weekly_standup",
|
|
2340
|
+
week_start: "2024-01-15"
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
]
|
|
2344
|
+
},
|
|
2345
|
+
activities: {
|
|
2346
|
+
description: "Read-only activity feed — audit log of create/update/delete events across the organization",
|
|
2347
|
+
actions: { list: "List recent activities with optional filters" },
|
|
2348
|
+
filters: {
|
|
2349
|
+
event: "Filter by event type: create, update, delete",
|
|
2350
|
+
after: "Filter to activities after this ISO 8601 timestamp (e.g. 2026-01-01T00:00:00Z)",
|
|
2351
|
+
person_id: "Filter by creator person ID",
|
|
2352
|
+
project_id: "Filter by project ID"
|
|
2353
|
+
},
|
|
2354
|
+
includes: ["creator"],
|
|
2355
|
+
fields: {
|
|
2356
|
+
id: "Unique activity identifier",
|
|
2357
|
+
event: "Event type: create, update, or delete",
|
|
2358
|
+
changeset: "Human-readable summary of field changes (e.g. \"name: null → My Project\")",
|
|
2359
|
+
created_at: "When the activity occurred (ISO 8601)",
|
|
2360
|
+
creator_name: "Full name of the person who triggered the activity (when creator included)"
|
|
2361
|
+
},
|
|
2362
|
+
examples: [
|
|
2363
|
+
{
|
|
2364
|
+
description: "List recent activities",
|
|
2365
|
+
params: {
|
|
2366
|
+
resource: "activities",
|
|
2367
|
+
action: "list"
|
|
2368
|
+
}
|
|
2369
|
+
},
|
|
2370
|
+
{
|
|
2371
|
+
description: "List only create events",
|
|
2372
|
+
params: {
|
|
2373
|
+
resource: "activities",
|
|
2374
|
+
action: "list",
|
|
2375
|
+
filter: { event: "create" }
|
|
2376
|
+
}
|
|
2377
|
+
},
|
|
2378
|
+
{
|
|
2379
|
+
description: "List activities after a date",
|
|
2380
|
+
params: {
|
|
2381
|
+
resource: "activities",
|
|
2382
|
+
action: "list",
|
|
2383
|
+
filter: { after: "2026-02-01T00:00:00Z" }
|
|
2384
|
+
}
|
|
2385
|
+
},
|
|
2386
|
+
{
|
|
2387
|
+
description: "List activities by a specific person",
|
|
2388
|
+
params: {
|
|
2389
|
+
resource: "activities",
|
|
2390
|
+
action: "list",
|
|
2391
|
+
filter: { person_id: "12345" }
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
]
|
|
2395
|
+
},
|
|
2064
2396
|
reports: {
|
|
2065
2397
|
description: "Generate various reports (time, budget, project, etc.)",
|
|
2066
2398
|
actions: { get: "Generate a report (requires report_type)" },
|
|
@@ -2173,7 +2505,7 @@ const handlePages = createResourceHandler({
|
|
|
2173
2505
|
/**
|
|
2174
2506
|
* People MCP handler.
|
|
2175
2507
|
*/
|
|
2176
|
-
var VALID_ACTIONS$
|
|
2508
|
+
var VALID_ACTIONS$3 = [
|
|
2177
2509
|
"list",
|
|
2178
2510
|
"get",
|
|
2179
2511
|
"me",
|
|
@@ -2224,7 +2556,7 @@ async function handlePeople(action, args, ctx, credentials) {
|
|
|
2224
2556
|
});
|
|
2225
2557
|
return jsonResult(response);
|
|
2226
2558
|
}
|
|
2227
|
-
return inputErrorResult(ErrorMessages.invalidAction(action, "people", VALID_ACTIONS$
|
|
2559
|
+
return inputErrorResult(ErrorMessages.invalidAction(action, "people", VALID_ACTIONS$3));
|
|
2228
2560
|
}
|
|
2229
2561
|
/**
|
|
2230
2562
|
* Projects MCP handler.
|
|
@@ -2234,18 +2566,36 @@ async function handlePeople(action, args, ctx, credentials) {
|
|
|
2234
2566
|
/**
|
|
2235
2567
|
* Handle projects resource.
|
|
2236
2568
|
*
|
|
2237
|
-
* Supports: list, get, resolve
|
|
2569
|
+
* Supports: list, get, resolve, context
|
|
2238
2570
|
*/
|
|
2239
2571
|
const handleProjects = createResourceHandler({
|
|
2240
2572
|
resource: "projects",
|
|
2241
2573
|
actions: [
|
|
2242
2574
|
"list",
|
|
2243
2575
|
"get",
|
|
2244
|
-
"resolve"
|
|
2576
|
+
"resolve",
|
|
2577
|
+
"context"
|
|
2245
2578
|
],
|
|
2246
2579
|
formatter: formatProject$1,
|
|
2247
2580
|
hints: (_data, id) => getProjectHints(id),
|
|
2248
2581
|
supportsResolve: true,
|
|
2582
|
+
customActions: { context: async (args, ctx, execCtx) => {
|
|
2583
|
+
if (!args.id) return inputErrorResult(ErrorMessages.missingId("context"));
|
|
2584
|
+
const result = await getProjectContext({ id: args.id }, execCtx);
|
|
2585
|
+
const formatOptions = {
|
|
2586
|
+
...ctx.formatOptions,
|
|
2587
|
+
included: result.included
|
|
2588
|
+
};
|
|
2589
|
+
return jsonResult({
|
|
2590
|
+
...formatProject$1(result.data.project, ctx.formatOptions),
|
|
2591
|
+
tasks: result.data.tasks.map((t) => formatTask$1(t, {
|
|
2592
|
+
...formatOptions,
|
|
2593
|
+
compact: true
|
|
2594
|
+
})),
|
|
2595
|
+
services: result.data.services.map((s) => formatService$1(s, { compact: true })),
|
|
2596
|
+
time_entries: result.data.time_entries.map((t) => formatTimeEntry$1(t, { compact: true }))
|
|
2597
|
+
});
|
|
2598
|
+
} },
|
|
2249
2599
|
executors: {
|
|
2250
2600
|
list: listProjects,
|
|
2251
2601
|
get: getProject
|
|
@@ -2264,11 +2614,11 @@ function formatReportData(data) {
|
|
|
2264
2614
|
};
|
|
2265
2615
|
});
|
|
2266
2616
|
}
|
|
2267
|
-
var VALID_ACTIONS = ["get"];
|
|
2617
|
+
var VALID_ACTIONS$2 = ["get"];
|
|
2268
2618
|
async function handleReports(action, args, ctx) {
|
|
2269
2619
|
const { filter, page, perPage } = ctx;
|
|
2270
2620
|
const { report_type, group, from, to, person_id, project_id, company_id, deal_id, status } = args;
|
|
2271
|
-
if (action !== "get") return inputErrorResult(ErrorMessages.invalidAction(action, "reports", VALID_ACTIONS));
|
|
2621
|
+
if (action !== "get") return inputErrorResult(ErrorMessages.invalidAction(action, "reports", VALID_ACTIONS$2));
|
|
2272
2622
|
if (!report_type) return inputErrorResult(ErrorMessages.missingReportType());
|
|
2273
2623
|
if (!VALID_REPORT_TYPES.includes(report_type)) return inputErrorResult(ErrorMessages.invalidReportType(report_type, [...VALID_REPORT_TYPES]));
|
|
2274
2624
|
const execCtx = ctx.executor();
|
|
@@ -2753,9 +3103,104 @@ const handleServices = createResourceHandler({
|
|
|
2753
3103
|
resource: "services",
|
|
2754
3104
|
actions: ["list"],
|
|
2755
3105
|
formatter: formatService$1,
|
|
2756
|
-
executors: { list: listServices }
|
|
3106
|
+
executors: { list: listServices },
|
|
3107
|
+
customActions: { list: async (args, ctx, execCtx) => {
|
|
3108
|
+
const { formatOptions, filter, page, perPage } = ctx;
|
|
3109
|
+
const additionalFilters = { ...filter };
|
|
3110
|
+
const result = await listServices({
|
|
3111
|
+
page,
|
|
3112
|
+
perPage,
|
|
3113
|
+
additionalFilters
|
|
3114
|
+
}, execCtx);
|
|
3115
|
+
const response = formatListResponse$1(result.data, formatService$1, result.meta, {
|
|
3116
|
+
...formatOptions,
|
|
3117
|
+
included: result.included
|
|
3118
|
+
});
|
|
3119
|
+
const hints = getServiceListHints(additionalFilters);
|
|
3120
|
+
if (hints) return jsonResult({
|
|
3121
|
+
...response,
|
|
3122
|
+
_hints: hints
|
|
3123
|
+
});
|
|
3124
|
+
return jsonResult(response);
|
|
3125
|
+
} }
|
|
2757
3126
|
});
|
|
2758
3127
|
/**
|
|
3128
|
+
* Summaries MCP handler.
|
|
3129
|
+
*
|
|
3130
|
+
* Custom handler for dashboard-style summaries (not using createResourceHandler).
|
|
3131
|
+
* Routes actions to the appropriate summary executor.
|
|
3132
|
+
*/
|
|
3133
|
+
var VALID_ACTIONS$1 = [
|
|
3134
|
+
"my_day",
|
|
3135
|
+
"project_health",
|
|
3136
|
+
"team_pulse",
|
|
3137
|
+
"help"
|
|
3138
|
+
];
|
|
3139
|
+
/**
|
|
3140
|
+
* Handle summaries resource.
|
|
3141
|
+
*
|
|
3142
|
+
* Supports: my_day, project_health, team_pulse
|
|
3143
|
+
*/
|
|
3144
|
+
async function handleSummaries(action, args, ctx) {
|
|
3145
|
+
if (!VALID_ACTIONS$1.includes(action)) return inputErrorResult(ErrorMessages.invalidAction(action, "summaries", VALID_ACTIONS$1));
|
|
3146
|
+
const execCtx = ctx.executor();
|
|
3147
|
+
switch (action) {
|
|
3148
|
+
case "my_day": {
|
|
3149
|
+
const result = await getMyDaySummary({}, execCtx);
|
|
3150
|
+
if (ctx.includeSuggestions !== false) {
|
|
3151
|
+
const suggestions = getMyDaySuggestions(result.data);
|
|
3152
|
+
if (suggestions.length > 0) return jsonResult({
|
|
3153
|
+
...result.data,
|
|
3154
|
+
_suggestions: suggestions
|
|
3155
|
+
});
|
|
3156
|
+
}
|
|
3157
|
+
return jsonResult(result.data);
|
|
3158
|
+
}
|
|
3159
|
+
case "project_health":
|
|
3160
|
+
if (!args.project_id) return inputErrorResult(new UserInputError("project_id is required for project_health summary", [
|
|
3161
|
+
"Provide the project_id parameter",
|
|
3162
|
+
"You can find project IDs using resource=\"projects\" action=\"list\"",
|
|
3163
|
+
"Or use a project number like \"PRJ-123\""
|
|
3164
|
+
]));
|
|
3165
|
+
return jsonResult((await getProjectHealthSummary({ projectId: args.project_id }, execCtx)).data);
|
|
3166
|
+
case "team_pulse": return jsonResult((await getTeamPulseSummary({}, execCtx)).data);
|
|
3167
|
+
case "help": return jsonResult({
|
|
3168
|
+
resource: "summaries",
|
|
3169
|
+
description: "Dashboard-style summaries that aggregate data from multiple resources",
|
|
3170
|
+
actions: {
|
|
3171
|
+
my_day: {
|
|
3172
|
+
description: "Personal dashboard for the current user",
|
|
3173
|
+
parameters: {},
|
|
3174
|
+
returns: {
|
|
3175
|
+
tasks: "Open and overdue tasks assigned to you",
|
|
3176
|
+
time: "Time entries logged today",
|
|
3177
|
+
timers: "Currently running timers"
|
|
3178
|
+
}
|
|
3179
|
+
},
|
|
3180
|
+
project_health: {
|
|
3181
|
+
description: "Project status with budget burn and task stats",
|
|
3182
|
+
parameters: { project_id: "Required. Project ID or project number (e.g., PRJ-123)" },
|
|
3183
|
+
returns: {
|
|
3184
|
+
project: "Project details",
|
|
3185
|
+
tasks: "Open and overdue task counts",
|
|
3186
|
+
budget: "Budget burn rate by service",
|
|
3187
|
+
recent_activity: "Time tracking activity in last 7 days"
|
|
3188
|
+
}
|
|
3189
|
+
},
|
|
3190
|
+
team_pulse: {
|
|
3191
|
+
description: "Team-wide time tracking activity for today",
|
|
3192
|
+
parameters: {},
|
|
3193
|
+
returns: {
|
|
3194
|
+
team: "Counts of active users, those tracking time, and with timers",
|
|
3195
|
+
people: "Per-person breakdown of time logged and active timers"
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
});
|
|
3200
|
+
default: return inputErrorResult(ErrorMessages.invalidAction(action, "summaries", VALID_ACTIONS$1));
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
/**
|
|
2759
3204
|
* Tasks MCP handler.
|
|
2760
3205
|
*/
|
|
2761
3206
|
const handleTasks = createResourceHandler({
|
|
@@ -2766,7 +3211,8 @@ const handleTasks = createResourceHandler({
|
|
|
2766
3211
|
"get",
|
|
2767
3212
|
"create",
|
|
2768
3213
|
"update",
|
|
2769
|
-
"resolve"
|
|
3214
|
+
"resolve",
|
|
3215
|
+
"context"
|
|
2770
3216
|
],
|
|
2771
3217
|
formatter: formatTask$1,
|
|
2772
3218
|
hints: (data, id) => {
|
|
@@ -2798,6 +3244,23 @@ const handleTasks = createResourceHandler({
|
|
|
2798
3244
|
description: args.description,
|
|
2799
3245
|
assigneeId: args.assignee_id
|
|
2800
3246
|
}) },
|
|
3247
|
+
customActions: { context: async (args, ctx, execCtx) => {
|
|
3248
|
+
if (!args.id) return inputErrorResult(ErrorMessages.missingId("context"));
|
|
3249
|
+
const result = await getTaskContext({ id: args.id }, execCtx);
|
|
3250
|
+
const formatOptions = {
|
|
3251
|
+
...ctx.formatOptions,
|
|
3252
|
+
included: result.included
|
|
3253
|
+
};
|
|
3254
|
+
return jsonResult({
|
|
3255
|
+
...formatTask$1(result.data.task, formatOptions),
|
|
3256
|
+
comments: result.data.comments.map((c) => formatComment$1(c, { compact: true })),
|
|
3257
|
+
time_entries: result.data.time_entries.map((t) => formatTimeEntry$1(t, { compact: true })),
|
|
3258
|
+
subtasks: result.data.subtasks.map((s) => formatTask$1(s, {
|
|
3259
|
+
...formatOptions,
|
|
3260
|
+
compact: true
|
|
3261
|
+
}))
|
|
3262
|
+
});
|
|
3263
|
+
} },
|
|
2801
3264
|
executors: {
|
|
2802
3265
|
list: listTasks,
|
|
2803
3266
|
get: getTask,
|
|
@@ -2829,23 +3292,28 @@ const handleTime = createResourceHandler({
|
|
|
2829
3292
|
},
|
|
2830
3293
|
supportsResolve: true,
|
|
2831
3294
|
resolveArgsFromArgs: (args) => ({ project_id: args.project_id }),
|
|
2832
|
-
create: {
|
|
2833
|
-
|
|
2834
|
-
"person_id",
|
|
3295
|
+
customActions: { create: async (args, ctx, execCtx) => {
|
|
3296
|
+
const missingFields = [
|
|
2835
3297
|
"service_id",
|
|
2836
3298
|
"time",
|
|
2837
3299
|
"date"
|
|
2838
|
-
]
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
3300
|
+
].filter((field) => !args[field]);
|
|
3301
|
+
if (missingFields.length > 0) return inputErrorResult(ErrorMessages.missingRequiredFields("time entry", missingFields));
|
|
3302
|
+
const personId = args.person_id ?? execCtx.config.userId;
|
|
3303
|
+
if (!personId) return inputErrorResult(new UserInputError("person_id is required (could not auto-resolve: userId not configured)", ["Provide person_id explicitly", "Or configure userId in your credentials"]));
|
|
3304
|
+
return jsonResult({
|
|
3305
|
+
success: true,
|
|
3306
|
+
...formatTimeEntry$1((await createTimeEntry({
|
|
3307
|
+
personId,
|
|
3308
|
+
serviceId: args.service_id,
|
|
3309
|
+
time: args.time,
|
|
3310
|
+
date: args.date,
|
|
3311
|
+
note: args.note ?? void 0,
|
|
3312
|
+
taskId: args.task_id,
|
|
3313
|
+
projectId: args.project_id
|
|
3314
|
+
}, execCtx)).data, ctx.formatOptions)
|
|
3315
|
+
});
|
|
3316
|
+
} },
|
|
2849
3317
|
update: { mapOptions: (args) => ({
|
|
2850
3318
|
time: args.time ?? void 0,
|
|
2851
3319
|
date: args.date ?? void 0,
|
|
@@ -2907,6 +3375,191 @@ const handleTimers = createResourceHandler({
|
|
|
2907
3375
|
}
|
|
2908
3376
|
});
|
|
2909
3377
|
/**
|
|
3378
|
+
* Valid include values per resource.
|
|
3379
|
+
*
|
|
3380
|
+
* Sourced from help.ts and schema.ts include lists.
|
|
3381
|
+
* Resources not listed here have no include validation (pass-through).
|
|
3382
|
+
*/
|
|
3383
|
+
const VALID_INCLUDES = {
|
|
3384
|
+
activities: ["creator"],
|
|
3385
|
+
tasks: [
|
|
3386
|
+
"project",
|
|
3387
|
+
"project.company",
|
|
3388
|
+
"assignee",
|
|
3389
|
+
"workflow_status",
|
|
3390
|
+
"comments",
|
|
3391
|
+
"attachments",
|
|
3392
|
+
"subtasks"
|
|
3393
|
+
],
|
|
3394
|
+
comments: [
|
|
3395
|
+
"creator",
|
|
3396
|
+
"task",
|
|
3397
|
+
"deal"
|
|
3398
|
+
],
|
|
3399
|
+
deals: [
|
|
3400
|
+
"company",
|
|
3401
|
+
"deal_status",
|
|
3402
|
+
"responsible",
|
|
3403
|
+
"project"
|
|
3404
|
+
],
|
|
3405
|
+
bookings: [
|
|
3406
|
+
"person",
|
|
3407
|
+
"service",
|
|
3408
|
+
"event"
|
|
3409
|
+
],
|
|
3410
|
+
time: [
|
|
3411
|
+
"person",
|
|
3412
|
+
"service",
|
|
3413
|
+
"task"
|
|
3414
|
+
]
|
|
3415
|
+
};
|
|
3416
|
+
/**
|
|
3417
|
+
* Known misleading include values and their suggestions.
|
|
3418
|
+
* These are values that agents commonly try that don't exist.
|
|
3419
|
+
*/
|
|
3420
|
+
var KNOWN_SUGGESTIONS = {
|
|
3421
|
+
notes: "Use resource=comments to fetch comments on a resource",
|
|
3422
|
+
services: "Use resource=services with filter.deal_id or filter.project_id to list services",
|
|
3423
|
+
time_entries: "Use resource=time with a filter (e.g. filter.task_id, filter.project_id) to list time entries",
|
|
3424
|
+
time: "Use resource=time with a filter (e.g. filter.task_id) to list time entries",
|
|
3425
|
+
user: "Use \"assignee\" or \"person\" instead",
|
|
3426
|
+
author: "Use \"creator\" instead",
|
|
3427
|
+
owner: "Use \"responsible\" or \"assignee\" instead",
|
|
3428
|
+
company: "Use \"project.company\" to include the project's company on tasks",
|
|
3429
|
+
status: "Use \"workflow_status\" to include the workflow/kanban status on tasks"
|
|
3430
|
+
};
|
|
3431
|
+
/**
|
|
3432
|
+
* Validate include values for a given resource.
|
|
3433
|
+
*
|
|
3434
|
+
* Returns the valid and invalid values, plus suggestions for invalid values.
|
|
3435
|
+
* If the resource is not in VALID_INCLUDES, skips validation (returns all as valid).
|
|
3436
|
+
*/
|
|
3437
|
+
function validateIncludes(resource, includes) {
|
|
3438
|
+
const validSet = VALID_INCLUDES[resource];
|
|
3439
|
+
if (!validSet) return null;
|
|
3440
|
+
const valid = [];
|
|
3441
|
+
const invalid = [];
|
|
3442
|
+
const suggestions = {};
|
|
3443
|
+
for (const inc of includes) if (validSet.includes(inc)) valid.push(inc);
|
|
3444
|
+
else {
|
|
3445
|
+
invalid.push(inc);
|
|
3446
|
+
if (KNOWN_SUGGESTIONS[inc]) suggestions[inc] = KNOWN_SUGGESTIONS[inc];
|
|
3447
|
+
else suggestions[inc] = `Valid includes for ${resource}: ${validSet.join(", ")}`;
|
|
3448
|
+
}
|
|
3449
|
+
return {
|
|
3450
|
+
valid,
|
|
3451
|
+
invalid,
|
|
3452
|
+
suggestions
|
|
3453
|
+
};
|
|
3454
|
+
}
|
|
3455
|
+
/**
|
|
3456
|
+
* Workflows MCP handler.
|
|
3457
|
+
*
|
|
3458
|
+
* Custom handler for compound workflows that chain multiple executors.
|
|
3459
|
+
* NOT using createResourceHandler — standalone routing like summaries.ts.
|
|
3460
|
+
*
|
|
3461
|
+
* Supported actions:
|
|
3462
|
+
* - complete_task: Mark task closed, optionally comment and stop timers
|
|
3463
|
+
* - log_day: Create multiple time entries in parallel from a structured list
|
|
3464
|
+
* - weekly_standup: Aggregate completed tasks, time logged, and upcoming deadlines
|
|
3465
|
+
*/
|
|
3466
|
+
var VALID_ACTIONS = [
|
|
3467
|
+
"complete_task",
|
|
3468
|
+
"log_day",
|
|
3469
|
+
"weekly_standup",
|
|
3470
|
+
"help"
|
|
3471
|
+
];
|
|
3472
|
+
/**
|
|
3473
|
+
* Handle workflows resource.
|
|
3474
|
+
*
|
|
3475
|
+
* Supports: complete_task, log_day, weekly_standup
|
|
3476
|
+
*/
|
|
3477
|
+
async function handleWorkflows(action, args, ctx) {
|
|
3478
|
+
if (!VALID_ACTIONS.includes(action)) return inputErrorResult(ErrorMessages.invalidAction(action, "workflows", VALID_ACTIONS));
|
|
3479
|
+
const execCtx = ctx.executor();
|
|
3480
|
+
switch (action) {
|
|
3481
|
+
case "complete_task":
|
|
3482
|
+
if (!args.task_id) return inputErrorResult(new UserInputError("task_id is required for complete_task workflow", ["Provide the task_id parameter (numeric task ID)", "You can find task IDs using resource=\"tasks\" action=\"list\""]));
|
|
3483
|
+
return jsonResult((await completeTask({
|
|
3484
|
+
taskId: args.task_id,
|
|
3485
|
+
comment: args.comment,
|
|
3486
|
+
stopTimer: args.stop_timer
|
|
3487
|
+
}, execCtx)).data);
|
|
3488
|
+
case "log_day":
|
|
3489
|
+
if (!args.entries || !Array.isArray(args.entries) || args.entries.length === 0) return inputErrorResult(new UserInputError("entries is required and must be a non-empty array for log_day workflow", [
|
|
3490
|
+
"Provide entries as an array of { project_id, service_id, duration_minutes, note?, date? }",
|
|
3491
|
+
"Example: { \"entries\": [{ \"project_id\": \"123\", \"service_id\": \"456\", \"duration_minutes\": 120, \"note\": \"Development\" }] }",
|
|
3492
|
+
"You can find service IDs using resource=\"services\" action=\"list\" with filter.project_id"
|
|
3493
|
+
]));
|
|
3494
|
+
return jsonResult((await logDay({
|
|
3495
|
+
entries: args.entries.map((e) => {
|
|
3496
|
+
const entry = e;
|
|
3497
|
+
return {
|
|
3498
|
+
project_id: String(entry.project_id),
|
|
3499
|
+
service_id: String(entry.service_id),
|
|
3500
|
+
duration_minutes: Number(entry.duration_minutes),
|
|
3501
|
+
note: entry.note != null ? String(entry.note) : void 0,
|
|
3502
|
+
date: entry.date != null ? String(entry.date) : void 0
|
|
3503
|
+
};
|
|
3504
|
+
}),
|
|
3505
|
+
date: args.date,
|
|
3506
|
+
personId: args.person_id
|
|
3507
|
+
}, execCtx)).data);
|
|
3508
|
+
case "weekly_standup": return jsonResult((await weeklyStandup({
|
|
3509
|
+
personId: args.person_id,
|
|
3510
|
+
weekStart: args.week_start
|
|
3511
|
+
}, execCtx)).data);
|
|
3512
|
+
case "help": return jsonResult({
|
|
3513
|
+
resource: "workflows",
|
|
3514
|
+
description: "Compound workflows that chain multiple resource operations into a single tool call",
|
|
3515
|
+
actions: {
|
|
3516
|
+
complete_task: {
|
|
3517
|
+
description: "Mark a task as complete, optionally post a comment and stop running timers",
|
|
3518
|
+
parameters: {
|
|
3519
|
+
task_id: "Required. The task ID to complete",
|
|
3520
|
+
comment: "Optional. A completion comment to post on the task",
|
|
3521
|
+
stop_timer: "Optional. Whether to stop running timers (default: true)"
|
|
3522
|
+
},
|
|
3523
|
+
returns: {
|
|
3524
|
+
task: "Updated task info (id, title, closed status)",
|
|
3525
|
+
comment_posted: "Whether the comment was posted",
|
|
3526
|
+
comment_id: "ID of the created comment (if posted)",
|
|
3527
|
+
timers_stopped: "Number of timers stopped",
|
|
3528
|
+
errors: "Any sub-step errors (partial results are still returned)"
|
|
3529
|
+
}
|
|
3530
|
+
},
|
|
3531
|
+
log_day: {
|
|
3532
|
+
description: "Create multiple time entries in parallel from a structured list",
|
|
3533
|
+
parameters: {
|
|
3534
|
+
entries: "Required. Array of { project_id, service_id, duration_minutes, note?, date? }",
|
|
3535
|
+
date: "Optional. Default date for all entries (YYYY-MM-DD, defaults to today)",
|
|
3536
|
+
person_id: "Optional. Person to log for (defaults to current user)"
|
|
3537
|
+
},
|
|
3538
|
+
returns: {
|
|
3539
|
+
entries: "Per-entry results with success/failure status",
|
|
3540
|
+
succeeded: "Number of entries successfully created",
|
|
3541
|
+
failed: "Number of entries that failed",
|
|
3542
|
+
total_minutes_logged: "Sum of minutes for successful entries"
|
|
3543
|
+
}
|
|
3544
|
+
},
|
|
3545
|
+
weekly_standup: {
|
|
3546
|
+
description: "Aggregate a weekly standup: completed tasks, time logged, and upcoming deadlines",
|
|
3547
|
+
parameters: {
|
|
3548
|
+
person_id: "Optional. Person to generate standup for (defaults to current user)",
|
|
3549
|
+
week_start: "Optional. ISO date for Monday of the week (defaults to this Monday)"
|
|
3550
|
+
},
|
|
3551
|
+
returns: {
|
|
3552
|
+
completed_tasks: "Tasks closed this week (count + list)",
|
|
3553
|
+
time_logged: "Total minutes and breakdown by project",
|
|
3554
|
+
upcoming_deadlines: "Open tasks due in the next 7 days"
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
});
|
|
3559
|
+
default: return inputErrorResult(ErrorMessages.invalidAction(action, "workflows", VALID_ACTIONS));
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3562
|
+
/**
|
|
2910
3563
|
* Tool execution handlers for Productive MCP server
|
|
2911
3564
|
* These are shared between stdio and HTTP transports
|
|
2912
3565
|
*
|
|
@@ -2918,12 +3571,67 @@ var VALID_RESOURCES = [...RESOURCES];
|
|
|
2918
3571
|
/** Default page size for MCP (smaller than CLI to reduce token usage) */
|
|
2919
3572
|
var DEFAULT_PER_PAGE = 20;
|
|
2920
3573
|
/**
|
|
3574
|
+
* Route to the appropriate resource handler.
|
|
3575
|
+
* Extracted from executeToolWithCredentials to keep cyclomatic complexity manageable.
|
|
3576
|
+
*/
|
|
3577
|
+
async function routeToHandler(resource, action, restArgs, resolveArgs, ctx, credentials) {
|
|
3578
|
+
switch (resource) {
|
|
3579
|
+
case "projects": return await handleProjects(action, {
|
|
3580
|
+
...restArgs,
|
|
3581
|
+
...resolveArgs
|
|
3582
|
+
}, ctx);
|
|
3583
|
+
case "time": return await handleTime(action, {
|
|
3584
|
+
...restArgs,
|
|
3585
|
+
...resolveArgs
|
|
3586
|
+
}, ctx);
|
|
3587
|
+
case "tasks": return await handleTasks(action, {
|
|
3588
|
+
...restArgs,
|
|
3589
|
+
...resolveArgs
|
|
3590
|
+
}, ctx);
|
|
3591
|
+
case "services": return await handleServices(action, restArgs, ctx);
|
|
3592
|
+
case "people": return await handlePeople(action, {
|
|
3593
|
+
...restArgs,
|
|
3594
|
+
...resolveArgs
|
|
3595
|
+
}, ctx, credentials);
|
|
3596
|
+
case "companies": return await handleCompanies(action, {
|
|
3597
|
+
...restArgs,
|
|
3598
|
+
...resolveArgs
|
|
3599
|
+
}, ctx);
|
|
3600
|
+
case "comments": return await handleComments(action, restArgs, ctx);
|
|
3601
|
+
case "attachments": return await handleAttachments(action, restArgs, ctx);
|
|
3602
|
+
case "timers": return await handleTimers(action, restArgs, ctx);
|
|
3603
|
+
case "deals": return await handleDeals(action, {
|
|
3604
|
+
...restArgs,
|
|
3605
|
+
...resolveArgs
|
|
3606
|
+
}, ctx);
|
|
3607
|
+
case "bookings": return await handleBookings(action, restArgs, ctx);
|
|
3608
|
+
case "pages": return await handlePages(action, restArgs, ctx);
|
|
3609
|
+
case "discussions": return await handleDiscussions(action, restArgs, ctx);
|
|
3610
|
+
case "activities": return await handleActivities(action, restArgs, ctx);
|
|
3611
|
+
case "reports": return await handleReports(action, restArgs, ctx);
|
|
3612
|
+
case "summaries": return await handleSummaries(action, restArgs, ctx);
|
|
3613
|
+
case "workflows": return await handleWorkflows(action, restArgs, ctx);
|
|
3614
|
+
case "budgets": return inputErrorResult(new UserInputError("The \"budgets\" resource has been removed. Budgets are deals with type=2.", [
|
|
3615
|
+
"Use resource=\"deals\" with filter[type]=\"2\" to list only budgets",
|
|
3616
|
+
"To create a budget: resource=\"deals\" action=\"create\" with budget=true",
|
|
3617
|
+
"Use action=\"help\" resource=\"deals\" for full documentation"
|
|
3618
|
+
]));
|
|
3619
|
+
case "docs": return inputErrorResult(new UserInputError("Unknown resource \"docs\". Did you mean \"pages\"?", [
|
|
3620
|
+
"Use resource=\"pages\" to access Productive pages/documents",
|
|
3621
|
+
"Use action=\"list\" to list all pages",
|
|
3622
|
+
"Use action=\"help\" resource=\"pages\" for full documentation"
|
|
3623
|
+
]));
|
|
3624
|
+
default: return inputErrorResult(ErrorMessages.unknownResource(resource, VALID_RESOURCES));
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
/**
|
|
2921
3628
|
* Execute a tool with the given credentials and arguments
|
|
2922
3629
|
*/
|
|
2923
3630
|
async function executeToolWithCredentials(name, args, credentials) {
|
|
2924
3631
|
if (name !== "productive") return errorResult(`Unknown tool: ${name}`);
|
|
2925
3632
|
const typedArgs = args;
|
|
2926
3633
|
if (typedArgs.resource === "batch") return handleBatch(typedArgs.operations, credentials, executeToolWithCredentials);
|
|
3634
|
+
if (args.params !== void 0) return inputErrorResult(new UserInputError("Unknown field \"params\". Use \"filter\" instead.", ["Example: { \"filter\": { \"assignee_id\": \"me\" } }", "The MCP tool uses \"filter\" for query parameters, not \"params\""]));
|
|
2927
3635
|
const { resource, action, filter, page, per_page, compact, include, query, resources, no_hints, type, ...restArgs } = typedArgs;
|
|
2928
3636
|
if (resource === "search") return await handleSearch(query, resources, credentials, executeToolWithCredentials);
|
|
2929
3637
|
const api = new ProductiveApi({ config: {
|
|
@@ -2941,7 +3649,18 @@ async function executeToolWithCredentials(name, args, credentials) {
|
|
|
2941
3649
|
query
|
|
2942
3650
|
};
|
|
2943
3651
|
const includeHints = no_hints !== true && action === "get" && !isCompact;
|
|
2944
|
-
const
|
|
3652
|
+
const includeSuggestions = no_hints !== true;
|
|
3653
|
+
if (include && include.length > 0) {
|
|
3654
|
+
const includeValidation = validateIncludes(resource, include);
|
|
3655
|
+
if (includeValidation && includeValidation.invalid.length > 0) {
|
|
3656
|
+
const { invalid, valid, suggestions } = includeValidation;
|
|
3657
|
+
const hintLines = [`Invalid include value${invalid.length > 1 ? "s" : ""}: ${invalid.join(", ")}`, `Valid includes for ${resource}: ${(VALID_INCLUDES[resource] ?? []).join(", ")}`];
|
|
3658
|
+
for (const [value, suggestion] of Object.entries(suggestions)) hintLines.push(`"${value}": ${suggestion}`);
|
|
3659
|
+
if (valid.length > 0) hintLines.push(`The following includes are valid and will be used if you remove the invalid ones: ${valid.join(", ")}`);
|
|
3660
|
+
return inputErrorResult(new UserInputError(`Invalid include value${invalid.length > 1 ? "s" : ""} for resource "${resource}": ${invalid.join(", ")}`, hintLines));
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3663
|
+
const execCtx = fromHandlerContext({ api }, { userId: credentials.userId });
|
|
2945
3664
|
const ctx = {
|
|
2946
3665
|
formatOptions,
|
|
2947
3666
|
filter: stringFilter,
|
|
@@ -2949,55 +3668,29 @@ async function executeToolWithCredentials(name, args, credentials) {
|
|
|
2949
3668
|
perPage,
|
|
2950
3669
|
include,
|
|
2951
3670
|
includeHints,
|
|
3671
|
+
includeSuggestions,
|
|
2952
3672
|
executor: () => execCtx
|
|
2953
3673
|
};
|
|
2954
3674
|
try {
|
|
2955
|
-
if (action === "
|
|
3675
|
+
if (action === "search" && resource !== "search") return inputErrorResult(new UserInputError(`action="search" is not supported on resource="${resource}". Use action="list" with a query parameter for text filtering, or use resource="search" for cross-resource search.`, [
|
|
3676
|
+
`Use resource="${resource}" action="list" with query="<your search terms>" to filter ${resource}`,
|
|
3677
|
+
"Use resource=\"search\" action=\"run\" with query=\"<your search terms>\" to search across all resources",
|
|
3678
|
+
`Use action="help" resource="${resource}" to see all supported actions and filters`
|
|
3679
|
+
]));
|
|
3680
|
+
if (action.startsWith("get_")) {
|
|
3681
|
+
const suggestedResource = action.replace(/^get_/, "").replace(/_/g, " ");
|
|
3682
|
+
return inputErrorResult(new UserInputError(`action="${action}" is not valid. Actions use simple verbs like "list", "get", "create", not function-style names.`, [
|
|
3683
|
+
`To retrieve a single item, use action="get" with an id parameter`,
|
|
3684
|
+
`To retrieve multiple items, use action="list" (e.g. resource="${resource || suggestedResource}" action="list")`,
|
|
3685
|
+
`Use action="help" resource="${resource || "tasks"}" to see all supported actions for a resource`
|
|
3686
|
+
]));
|
|
3687
|
+
}
|
|
3688
|
+
if (action === "help" && resource !== "summaries") return resource ? handleHelp(resource) : handleHelpOverview();
|
|
2956
3689
|
if (action === "schema") return resource ? handleSchema(resource) : handleSchemaOverview();
|
|
2957
|
-
|
|
3690
|
+
return await routeToHandler(resource, action, restArgs, {
|
|
2958
3691
|
query,
|
|
2959
3692
|
type
|
|
2960
|
-
};
|
|
2961
|
-
switch (resource) {
|
|
2962
|
-
case "projects": return await handleProjects(action, {
|
|
2963
|
-
...restArgs,
|
|
2964
|
-
...resolveArgs
|
|
2965
|
-
}, ctx);
|
|
2966
|
-
case "time": return await handleTime(action, {
|
|
2967
|
-
...restArgs,
|
|
2968
|
-
...resolveArgs
|
|
2969
|
-
}, ctx);
|
|
2970
|
-
case "tasks": return await handleTasks(action, {
|
|
2971
|
-
...restArgs,
|
|
2972
|
-
...resolveArgs
|
|
2973
|
-
}, ctx);
|
|
2974
|
-
case "services": return await handleServices(action, restArgs, ctx);
|
|
2975
|
-
case "people": return await handlePeople(action, {
|
|
2976
|
-
...restArgs,
|
|
2977
|
-
...resolveArgs
|
|
2978
|
-
}, ctx, credentials);
|
|
2979
|
-
case "companies": return await handleCompanies(action, {
|
|
2980
|
-
...restArgs,
|
|
2981
|
-
...resolveArgs
|
|
2982
|
-
}, ctx);
|
|
2983
|
-
case "comments": return await handleComments(action, restArgs, ctx);
|
|
2984
|
-
case "attachments": return await handleAttachments(action, restArgs, ctx);
|
|
2985
|
-
case "timers": return await handleTimers(action, restArgs, ctx);
|
|
2986
|
-
case "deals": return await handleDeals(action, {
|
|
2987
|
-
...restArgs,
|
|
2988
|
-
...resolveArgs
|
|
2989
|
-
}, ctx);
|
|
2990
|
-
case "bookings": return await handleBookings(action, restArgs, ctx);
|
|
2991
|
-
case "pages": return await handlePages(action, restArgs, ctx);
|
|
2992
|
-
case "discussions": return await handleDiscussions(action, restArgs, ctx);
|
|
2993
|
-
case "reports": return await handleReports(action, restArgs, ctx);
|
|
2994
|
-
case "budgets": return inputErrorResult(new UserInputError("The \"budgets\" resource has been removed. Budgets are deals with type=2.", [
|
|
2995
|
-
"Use resource=\"deals\" with filter[type]=\"2\" to list only budgets",
|
|
2996
|
-
"To create a budget: resource=\"deals\" action=\"create\" with budget=true",
|
|
2997
|
-
"Use action=\"help\" resource=\"deals\" for full documentation"
|
|
2998
|
-
]));
|
|
2999
|
-
default: return inputErrorResult(ErrorMessages.unknownResource(resource, VALID_RESOURCES));
|
|
3000
|
-
}
|
|
3693
|
+
}, ctx, credentials);
|
|
3001
3694
|
} catch (error) {
|
|
3002
3695
|
if (isUserInputError(error)) return formatError(error);
|
|
3003
3696
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -3011,4 +3704,4 @@ async function executeToolWithCredentials(name, args, credentials) {
|
|
|
3011
3704
|
}
|
|
3012
3705
|
export { executeToolWithCredentials as t };
|
|
3013
3706
|
|
|
3014
|
-
//# sourceMappingURL=handlers-
|
|
3707
|
+
//# sourceMappingURL=handlers-CaOBYauF.js.map
|