@studiometa/productive-mcp 0.10.0 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
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";
2
+ import { RESOURCES, ResolveError, VALID_REPORT_TYPES, createBooking, createComment, createCompany, createDeal, createDiscussion, createPage, createTask, createTimeEntry, deleteAttachment, deleteDiscussion, deletePage, deleteTimeEntry, fromHandlerContext, getAttachment, getBooking, getComment, getCompany, getDeal, getDealContext, getDiscussion, getMyDaySummary, getPage, getPerson, getProject, getProjectContext, getProjectHealthSummary, getReport, getTask, getTaskContext, getTeamPulseSummary, getTimeEntry, getTimer, listAttachments, listBookings, listComments, listCompanies, listDeals, listDiscussions, listPages, listPeople, listProjects, listServices, listTasks, listTimeEntries, listTimers, reopenDiscussion, resolveDiscussion, resolveResource, startTimer, stopTimer, updateBooking, updateComment, updateCompany, updateDeal, updateDiscussion, updatePage, updateTask, updateTimeEntry } from "@studiometa/productive-core";
3
3
  /**
4
4
  * Custom error classes for MCP server
5
5
  *
@@ -988,6 +988,112 @@ const handleAttachments = createResourceHandler({
988
988
  }
989
989
  });
990
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);
1009
+ }
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
+ };
1066
+ }
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;
1083
+ }
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
+ /**
991
1097
  * Bookings MCP handler.
992
1098
  */
993
1099
  const handleBookings = createResourceHandler({
@@ -1132,7 +1238,8 @@ const handleDeals = createResourceHandler({
1132
1238
  "get",
1133
1239
  "create",
1134
1240
  "update",
1135
- "resolve"
1241
+ "resolve",
1242
+ "context"
1136
1243
  ],
1137
1244
  formatter: formatDeal$1,
1138
1245
  hints: (_data, id) => getDealHints(id),
@@ -1153,6 +1260,20 @@ const handleDeals = createResourceHandler({
1153
1260
  })
1154
1261
  },
1155
1262
  update: { mapOptions: (args) => ({ name: args.name }) },
1263
+ customActions: { context: async (args, ctx, execCtx) => {
1264
+ if (!args.id) return inputErrorResult(ErrorMessages.missingId("context"));
1265
+ const result = await getDealContext({ id: args.id }, execCtx);
1266
+ const formatOptions = {
1267
+ ...ctx.formatOptions,
1268
+ included: result.included
1269
+ };
1270
+ return jsonResult({
1271
+ ...formatDeal$1(result.data.deal, formatOptions),
1272
+ services: result.data.services.map((s) => formatService$1(s, { compact: true })),
1273
+ comments: result.data.comments.map((c) => formatComment$1(c, { compact: true })),
1274
+ time_entries: result.data.time_entries.map((t) => formatTimeEntry$1(t, { compact: true }))
1275
+ });
1276
+ } },
1156
1277
  executors: {
1157
1278
  list: listDeals,
1158
1279
  get: getDeal,
@@ -1228,12 +1349,63 @@ const handleDiscussions = createResourceHandler({
1228
1349
  }
1229
1350
  });
1230
1351
  var RESOURCE_HELP = {
1352
+ batch: {
1353
+ description: "Execute multiple operations in a single call. Operations run in parallel via Promise.all, reducing round-trips for AI agents.",
1354
+ actions: { run: "Execute a batch of operations (max 10)" },
1355
+ fields: { operations: "Array of operation objects. Each must have \"resource\" and \"action\", plus any additional params for that resource." },
1356
+ examples: [{
1357
+ description: "Batch multiple queries",
1358
+ params: {
1359
+ resource: "batch",
1360
+ action: "run",
1361
+ operations: [
1362
+ {
1363
+ resource: "projects",
1364
+ action: "get",
1365
+ id: "123"
1366
+ },
1367
+ {
1368
+ resource: "time",
1369
+ action: "list",
1370
+ filter: { project_id: "123" }
1371
+ },
1372
+ {
1373
+ resource: "services",
1374
+ action: "list",
1375
+ filter: { project_id: "123" }
1376
+ }
1377
+ ]
1378
+ }
1379
+ }, {
1380
+ description: "Batch create time entries",
1381
+ params: {
1382
+ resource: "batch",
1383
+ action: "run",
1384
+ operations: [{
1385
+ resource: "time",
1386
+ action: "create",
1387
+ service_id: "111",
1388
+ date: "2024-01-15",
1389
+ time: 60,
1390
+ note: "Morning work"
1391
+ }, {
1392
+ resource: "time",
1393
+ action: "create",
1394
+ service_id: "111",
1395
+ date: "2024-01-15",
1396
+ time: 120,
1397
+ note: "Afternoon work"
1398
+ }]
1399
+ }
1400
+ }]
1401
+ },
1231
1402
  projects: {
1232
1403
  description: "Manage projects in Productive.io",
1233
1404
  actions: {
1234
1405
  list: "List all projects with optional filters",
1235
1406
  get: "Get a single project by ID (supports PRJ-123, P-123 format)",
1236
- resolve: "Resolve by project number (PRJ-123, P-123)"
1407
+ resolve: "Resolve by project number (PRJ-123, P-123)",
1408
+ context: "Get full project context in one call: project details + open tasks + services + recent time entries"
1237
1409
  },
1238
1410
  filters: {
1239
1411
  query: "Text search on project name",
@@ -1274,6 +1446,14 @@ var RESOURCE_HELP = {
1274
1446
  action: "get",
1275
1447
  id: "12345"
1276
1448
  }
1449
+ },
1450
+ {
1451
+ description: "Get full project context",
1452
+ params: {
1453
+ resource: "projects",
1454
+ action: "context",
1455
+ id: "12345"
1456
+ }
1277
1457
  }
1278
1458
  ]
1279
1459
  },
@@ -1284,7 +1464,8 @@ var RESOURCE_HELP = {
1284
1464
  get: "Get a single task by ID with full details (description, comments, etc.)",
1285
1465
  create: "Create a new task (requires title, project_id, task_list_id)",
1286
1466
  update: "Update an existing task",
1287
- resolve: "Resolve by text search"
1467
+ resolve: "Resolve by text search",
1468
+ context: "Get full task context in one call: task details + comments + time entries + subtasks"
1288
1469
  },
1289
1470
  filters: {
1290
1471
  query: "Text search on task title",
@@ -1360,6 +1541,14 @@ var RESOURCE_HELP = {
1360
1541
  project_id: "12345",
1361
1542
  task_list_id: "111"
1362
1543
  }
1544
+ },
1545
+ {
1546
+ description: "Get full task context",
1547
+ params: {
1548
+ resource: "tasks",
1549
+ action: "context",
1550
+ id: "67890"
1551
+ }
1363
1552
  }
1364
1553
  ]
1365
1554
  },
@@ -1677,7 +1866,8 @@ var RESOURCE_HELP = {
1677
1866
  get: "Get a single deal by ID (supports D-123, DEAL-123 format)",
1678
1867
  create: "Create a new deal (requires name, company_id)",
1679
1868
  update: "Update an existing deal",
1680
- resolve: "Resolve by deal number (D-123, DEAL-123)"
1869
+ resolve: "Resolve by deal number (D-123, DEAL-123)",
1870
+ context: "Get full deal context in one call: deal details + services + comments + time entries"
1681
1871
  },
1682
1872
  filters: {
1683
1873
  query: "Text search on deal name",
@@ -1727,6 +1917,14 @@ var RESOURCE_HELP = {
1727
1917
  action: "list",
1728
1918
  filter: { type: "2" }
1729
1919
  }
1920
+ },
1921
+ {
1922
+ description: "Get full deal context",
1923
+ params: {
1924
+ resource: "deals",
1925
+ action: "context",
1926
+ id: "12345"
1927
+ }
1730
1928
  }
1731
1929
  ]
1732
1930
  },
@@ -1949,7 +2147,8 @@ function handleHelp(resource) {
1949
2147
  const help = RESOURCE_HELP[resource];
1950
2148
  if (!help) return jsonResult({
1951
2149
  error: `Unknown resource: ${resource}`,
1952
- available_resources: Object.keys(RESOURCE_HELP)
2150
+ available_resources: Object.keys(RESOURCE_HELP),
2151
+ _tip: "Call { action: 'help' } without a resource to see all available resources."
1953
2152
  });
1954
2153
  return jsonResult({
1955
2154
  resource,
@@ -1966,7 +2165,8 @@ function handleHelpOverview() {
1966
2165
  resource,
1967
2166
  description: help.description,
1968
2167
  actions: Object.keys(help.actions)
1969
- }))
2168
+ })),
2169
+ _tip: "Always call { action: 'help', resource: '<name>' } before your first interaction with any resource to learn valid filters, required fields, and examples."
1970
2170
  });
1971
2171
  }
1972
2172
  /**
@@ -2015,7 +2215,7 @@ const handlePages = createResourceHandler({
2015
2215
  /**
2016
2216
  * People MCP handler.
2017
2217
  */
2018
- var VALID_ACTIONS$1 = [
2218
+ var VALID_ACTIONS$2 = [
2019
2219
  "list",
2020
2220
  "get",
2021
2221
  "me",
@@ -2066,7 +2266,7 @@ async function handlePeople(action, args, ctx, credentials) {
2066
2266
  });
2067
2267
  return jsonResult(response);
2068
2268
  }
2069
- return inputErrorResult(ErrorMessages.invalidAction(action, "people", VALID_ACTIONS$1));
2269
+ return inputErrorResult(ErrorMessages.invalidAction(action, "people", VALID_ACTIONS$2));
2070
2270
  }
2071
2271
  /**
2072
2272
  * Projects MCP handler.
@@ -2076,18 +2276,36 @@ async function handlePeople(action, args, ctx, credentials) {
2076
2276
  /**
2077
2277
  * Handle projects resource.
2078
2278
  *
2079
- * Supports: list, get, resolve
2279
+ * Supports: list, get, resolve, context
2080
2280
  */
2081
2281
  const handleProjects = createResourceHandler({
2082
2282
  resource: "projects",
2083
2283
  actions: [
2084
2284
  "list",
2085
2285
  "get",
2086
- "resolve"
2286
+ "resolve",
2287
+ "context"
2087
2288
  ],
2088
2289
  formatter: formatProject$1,
2089
2290
  hints: (_data, id) => getProjectHints(id),
2090
2291
  supportsResolve: true,
2292
+ customActions: { context: async (args, ctx, execCtx) => {
2293
+ if (!args.id) return inputErrorResult(ErrorMessages.missingId("context"));
2294
+ const result = await getProjectContext({ id: args.id }, execCtx);
2295
+ const formatOptions = {
2296
+ ...ctx.formatOptions,
2297
+ included: result.included
2298
+ };
2299
+ return jsonResult({
2300
+ ...formatProject$1(result.data.project, ctx.formatOptions),
2301
+ tasks: result.data.tasks.map((t) => formatTask$1(t, {
2302
+ ...formatOptions,
2303
+ compact: true
2304
+ })),
2305
+ services: result.data.services.map((s) => formatService$1(s, { compact: true })),
2306
+ time_entries: result.data.time_entries.map((t) => formatTimeEntry$1(t, { compact: true }))
2307
+ });
2308
+ } },
2091
2309
  executors: {
2092
2310
  list: listProjects,
2093
2311
  get: getProject
@@ -2106,11 +2324,11 @@ function formatReportData(data) {
2106
2324
  };
2107
2325
  });
2108
2326
  }
2109
- var VALID_ACTIONS = ["get"];
2327
+ var VALID_ACTIONS$1 = ["get"];
2110
2328
  async function handleReports(action, args, ctx) {
2111
2329
  const { filter, page, perPage } = ctx;
2112
2330
  const { report_type, group, from, to, person_id, project_id, company_id, deal_id, status } = args;
2113
- if (action !== "get") return inputErrorResult(ErrorMessages.invalidAction(action, "reports", VALID_ACTIONS));
2331
+ if (action !== "get") return inputErrorResult(ErrorMessages.invalidAction(action, "reports", VALID_ACTIONS$1));
2114
2332
  if (!report_type) return inputErrorResult(ErrorMessages.missingReportType());
2115
2333
  if (!VALID_REPORT_TYPES.includes(report_type)) return inputErrorResult(ErrorMessages.invalidReportType(report_type, [...VALID_REPORT_TYPES]));
2116
2334
  const execCtx = ctx.executor();
@@ -2134,6 +2352,454 @@ async function handleReports(action, args, ctx) {
2134
2352
  });
2135
2353
  }
2136
2354
  /**
2355
+ * Schema definitions for all resources.
2356
+ *
2357
+ * This provides a compact, machine-readable specification of each resource's
2358
+ * capabilities. For detailed documentation with examples, use action=help.
2359
+ */
2360
+ var RESOURCE_SCHEMAS = {
2361
+ projects: {
2362
+ actions: [
2363
+ "list",
2364
+ "get",
2365
+ "resolve"
2366
+ ],
2367
+ filters: {
2368
+ query: "string — text search on project name",
2369
+ project_type: "1=internal|2=client",
2370
+ company_id: "string",
2371
+ responsible_id: "string",
2372
+ person_id: "string",
2373
+ status: "1=active|2=archived"
2374
+ }
2375
+ },
2376
+ time: {
2377
+ actions: [
2378
+ "list",
2379
+ "get",
2380
+ "create",
2381
+ "update",
2382
+ "delete"
2383
+ ],
2384
+ filters: {
2385
+ person_id: "string — use 'me' for current user",
2386
+ after: "date YYYY-MM-DD",
2387
+ before: "date YYYY-MM-DD",
2388
+ project_id: "string",
2389
+ service_id: "string",
2390
+ task_id: "string",
2391
+ status: "1=approved|2=unapproved|3=rejected"
2392
+ },
2393
+ create: {
2394
+ person_id: {
2395
+ required: true,
2396
+ type: "string"
2397
+ },
2398
+ service_id: {
2399
+ required: true,
2400
+ type: "string"
2401
+ },
2402
+ date: {
2403
+ required: true,
2404
+ type: "date YYYY-MM-DD"
2405
+ },
2406
+ time: {
2407
+ required: true,
2408
+ type: "minutes integer"
2409
+ },
2410
+ note: {
2411
+ required: false,
2412
+ type: "string"
2413
+ },
2414
+ task_id: {
2415
+ required: false,
2416
+ type: "string"
2417
+ }
2418
+ },
2419
+ includes: [
2420
+ "person",
2421
+ "service",
2422
+ "task"
2423
+ ]
2424
+ },
2425
+ tasks: {
2426
+ actions: [
2427
+ "list",
2428
+ "get",
2429
+ "create",
2430
+ "update",
2431
+ "resolve"
2432
+ ],
2433
+ filters: {
2434
+ query: "string — text search on task title",
2435
+ project_id: "string",
2436
+ assignee_id: "string",
2437
+ status: "1=open|2=closed (or \"open\", \"closed\", \"all\")",
2438
+ task_list_id: "string",
2439
+ workflow_status_id: "string — kanban column"
2440
+ },
2441
+ create: {
2442
+ title: {
2443
+ required: true,
2444
+ type: "string"
2445
+ },
2446
+ project_id: {
2447
+ required: true,
2448
+ type: "string"
2449
+ },
2450
+ task_list_id: {
2451
+ required: true,
2452
+ type: "string"
2453
+ },
2454
+ description: {
2455
+ required: false,
2456
+ type: "string"
2457
+ },
2458
+ assignee_id: {
2459
+ required: false,
2460
+ type: "string"
2461
+ }
2462
+ },
2463
+ includes: [
2464
+ "project",
2465
+ "assignee",
2466
+ "comments",
2467
+ "subtasks",
2468
+ "workflow_status"
2469
+ ]
2470
+ },
2471
+ services: {
2472
+ actions: ["list", "get"],
2473
+ filters: {
2474
+ project_id: "string",
2475
+ deal_id: "string",
2476
+ task_id: "string",
2477
+ budget_status: "1=open|2=delivered"
2478
+ }
2479
+ },
2480
+ people: {
2481
+ actions: [
2482
+ "list",
2483
+ "get",
2484
+ "me",
2485
+ "resolve"
2486
+ ],
2487
+ filters: {
2488
+ query: "string — text search on name or email",
2489
+ status: "1=active|2=deactivated",
2490
+ company_id: "string"
2491
+ }
2492
+ },
2493
+ companies: {
2494
+ actions: [
2495
+ "list",
2496
+ "get",
2497
+ "create",
2498
+ "update",
2499
+ "resolve"
2500
+ ],
2501
+ filters: {
2502
+ query: "string — text search on company name",
2503
+ archived: "boolean"
2504
+ },
2505
+ create: { name: {
2506
+ required: true,
2507
+ type: "string"
2508
+ } }
2509
+ },
2510
+ comments: {
2511
+ actions: [
2512
+ "list",
2513
+ "get",
2514
+ "create",
2515
+ "update"
2516
+ ],
2517
+ filters: {
2518
+ task_id: "string",
2519
+ deal_id: "string",
2520
+ page_id: "string",
2521
+ discussion_id: "string"
2522
+ },
2523
+ create: {
2524
+ body: {
2525
+ required: true,
2526
+ type: "string"
2527
+ },
2528
+ task_id: {
2529
+ required: false,
2530
+ type: "string — one of task_id, deal_id required"
2531
+ },
2532
+ deal_id: {
2533
+ required: false,
2534
+ type: "string — one of task_id, deal_id required"
2535
+ }
2536
+ },
2537
+ includes: ["creator"]
2538
+ },
2539
+ attachments: {
2540
+ actions: [
2541
+ "list",
2542
+ "get",
2543
+ "delete"
2544
+ ],
2545
+ filters: {
2546
+ task_id: "string",
2547
+ comment_id: "string",
2548
+ deal_id: "string",
2549
+ page_id: "string"
2550
+ }
2551
+ },
2552
+ timers: {
2553
+ actions: [
2554
+ "list",
2555
+ "get",
2556
+ "start",
2557
+ "stop"
2558
+ ],
2559
+ filters: {
2560
+ person_id: "string",
2561
+ time_entry_id: "string"
2562
+ }
2563
+ },
2564
+ deals: {
2565
+ actions: [
2566
+ "list",
2567
+ "get",
2568
+ "create",
2569
+ "update",
2570
+ "resolve"
2571
+ ],
2572
+ filters: {
2573
+ query: "string — text search on deal name",
2574
+ company_id: "string",
2575
+ type: "1=deal|2=budget",
2576
+ stage_status_id: "1=open|2=won|3=lost"
2577
+ },
2578
+ create: {
2579
+ name: {
2580
+ required: true,
2581
+ type: "string"
2582
+ },
2583
+ company_id: {
2584
+ required: true,
2585
+ type: "string"
2586
+ }
2587
+ },
2588
+ includes: ["company", "deal_status"]
2589
+ },
2590
+ bookings: {
2591
+ actions: [
2592
+ "list",
2593
+ "get",
2594
+ "create",
2595
+ "update"
2596
+ ],
2597
+ filters: {
2598
+ person_id: "string",
2599
+ after: "date YYYY-MM-DD",
2600
+ before: "date YYYY-MM-DD",
2601
+ service_id: "string"
2602
+ },
2603
+ create: {
2604
+ person_id: {
2605
+ required: true,
2606
+ type: "string"
2607
+ },
2608
+ started_on: {
2609
+ required: true,
2610
+ type: "date YYYY-MM-DD"
2611
+ },
2612
+ ended_on: {
2613
+ required: true,
2614
+ type: "date YYYY-MM-DD"
2615
+ },
2616
+ service_id: {
2617
+ required: false,
2618
+ type: "string — one of service_id, event_id required"
2619
+ },
2620
+ event_id: {
2621
+ required: false,
2622
+ type: "string — one of service_id, event_id required"
2623
+ }
2624
+ }
2625
+ },
2626
+ pages: {
2627
+ actions: [
2628
+ "list",
2629
+ "get",
2630
+ "create",
2631
+ "update",
2632
+ "delete"
2633
+ ],
2634
+ filters: { project_id: "string" },
2635
+ create: {
2636
+ title: {
2637
+ required: true,
2638
+ type: "string"
2639
+ },
2640
+ project_id: {
2641
+ required: true,
2642
+ type: "string"
2643
+ },
2644
+ body: {
2645
+ required: false,
2646
+ type: "string"
2647
+ },
2648
+ parent_page_id: {
2649
+ required: false,
2650
+ type: "string"
2651
+ }
2652
+ }
2653
+ },
2654
+ discussions: {
2655
+ actions: [
2656
+ "list",
2657
+ "get",
2658
+ "create",
2659
+ "update",
2660
+ "delete",
2661
+ "resolve",
2662
+ "reopen"
2663
+ ],
2664
+ filters: {
2665
+ page_id: "string",
2666
+ status: "1=active|2=resolved"
2667
+ },
2668
+ create: {
2669
+ body: {
2670
+ required: true,
2671
+ type: "string"
2672
+ },
2673
+ page_id: {
2674
+ required: true,
2675
+ type: "string"
2676
+ }
2677
+ }
2678
+ },
2679
+ reports: {
2680
+ actions: ["get"],
2681
+ filters: {
2682
+ person_id: "string",
2683
+ project_id: "string",
2684
+ company_id: "string",
2685
+ after: "date YYYY-MM-DD",
2686
+ before: "date YYYY-MM-DD"
2687
+ },
2688
+ create: {
2689
+ report_type: {
2690
+ required: true,
2691
+ type: "time_reports|project_reports|budget_reports|..."
2692
+ },
2693
+ from: {
2694
+ required: false,
2695
+ type: "date YYYY-MM-DD"
2696
+ },
2697
+ to: {
2698
+ required: false,
2699
+ type: "date YYYY-MM-DD"
2700
+ },
2701
+ group: {
2702
+ required: false,
2703
+ type: "string — grouping dimension"
2704
+ }
2705
+ }
2706
+ }
2707
+ };
2708
+ /**
2709
+ * Handle schema action - returns compact specification for a specific resource
2710
+ */
2711
+ function handleSchema(resource) {
2712
+ const schema = RESOURCE_SCHEMAS[resource];
2713
+ if (!schema) return errorResult(`Unknown resource: ${resource}. Valid resources: ${Object.keys(RESOURCE_SCHEMAS).join(", ")}`);
2714
+ return jsonResult({
2715
+ resource,
2716
+ ...schema
2717
+ });
2718
+ }
2719
+ /**
2720
+ * Get schema overview for all resources
2721
+ */
2722
+ function handleSchemaOverview() {
2723
+ return jsonResult({
2724
+ _tip: "Use action=\"schema\" with a specific resource for full filter/create/includes spec",
2725
+ resources: Object.entries(RESOURCE_SCHEMAS).map(([resource, schema]) => ({
2726
+ resource,
2727
+ actions: schema.actions
2728
+ }))
2729
+ });
2730
+ }
2731
+ /**
2732
+ * Resources that support the query filter for text search
2733
+ */
2734
+ const SEARCHABLE_RESOURCES = [
2735
+ "projects",
2736
+ "companies",
2737
+ "people",
2738
+ "tasks",
2739
+ "deals"
2740
+ ];
2741
+ /**
2742
+ * Default resources to search when not specified
2743
+ */
2744
+ var DEFAULT_SEARCH_RESOURCES = [
2745
+ "projects",
2746
+ "companies",
2747
+ "people",
2748
+ "tasks"
2749
+ ];
2750
+ /**
2751
+ * Handle cross-resource search.
2752
+ *
2753
+ * @param query - Search query text (required)
2754
+ * @param resources - Resource types to search (optional, defaults to DEFAULT_SEARCH_RESOURCES)
2755
+ * @param credentials - Productive API credentials
2756
+ * @param execute - Function to execute tool calls (injected for delegation and testing)
2757
+ * @returns Grouped search results across all requested resources
2758
+ */
2759
+ async function handleSearch(query, resources, credentials, execute) {
2760
+ if (!query || query.trim() === "") return errorResult("Missing required parameter: query. Provide a non-empty search string.");
2761
+ const trimmedQuery = query.trim();
2762
+ const resourcesToSearch = resources && resources.length > 0 ? resources : DEFAULT_SEARCH_RESOURCES;
2763
+ const invalidResources = resourcesToSearch.filter((r) => !SEARCHABLE_RESOURCES.includes(r));
2764
+ if (invalidResources.length > 0) return errorResult(`Invalid searchable resources: ${invalidResources.join(", ")}. Valid searchable resources: ${SEARCHABLE_RESOURCES.join(", ")}.`);
2765
+ const searchPromises = resourcesToSearch.map(async (resource) => {
2766
+ try {
2767
+ const textContent = (await execute("productive", {
2768
+ resource,
2769
+ action: "list",
2770
+ query: trimmedQuery,
2771
+ compact: true,
2772
+ per_page: 10
2773
+ }, credentials)).content.find((c) => c.type === "text");
2774
+ if (!textContent || textContent.type !== "text") return [resource, { error: "No content in response" }];
2775
+ try {
2776
+ const parsed = JSON.parse(textContent.text);
2777
+ const items = parsed.items ?? parsed.data ?? [];
2778
+ return [resource, { items: Array.isArray(items) ? items : [] }];
2779
+ } catch {
2780
+ return [resource, { error: "Failed to parse response JSON" }];
2781
+ }
2782
+ } catch (err) {
2783
+ return [resource, { error: err instanceof Error ? err.message : String(err) }];
2784
+ }
2785
+ });
2786
+ const searchResults = await Promise.all(searchPromises);
2787
+ const results = {};
2788
+ let totalResults = 0;
2789
+ for (const [resource, result] of searchResults) if (result.error) results[resource] = { error: result.error };
2790
+ else {
2791
+ const items = result.items ?? [];
2792
+ results[resource] = items;
2793
+ totalResults += items.length;
2794
+ }
2795
+ return jsonResult({
2796
+ query: trimmedQuery,
2797
+ resources_searched: resourcesToSearch,
2798
+ results,
2799
+ total_results: totalResults
2800
+ });
2801
+ }
2802
+ /**
2137
2803
  * Services MCP handler.
2138
2804
  *
2139
2805
  * Uses the createResourceHandler factory for the common list pattern.
@@ -2150,6 +2816,72 @@ const handleServices = createResourceHandler({
2150
2816
  executors: { list: listServices }
2151
2817
  });
2152
2818
  /**
2819
+ * Summaries MCP handler.
2820
+ *
2821
+ * Custom handler for dashboard-style summaries (not using createResourceHandler).
2822
+ * Routes actions to the appropriate summary executor.
2823
+ */
2824
+ var VALID_ACTIONS = [
2825
+ "my_day",
2826
+ "project_health",
2827
+ "team_pulse",
2828
+ "help"
2829
+ ];
2830
+ /**
2831
+ * Handle summaries resource.
2832
+ *
2833
+ * Supports: my_day, project_health, team_pulse
2834
+ */
2835
+ async function handleSummaries(action, args, ctx) {
2836
+ if (!VALID_ACTIONS.includes(action)) return inputErrorResult(ErrorMessages.invalidAction(action, "summaries", VALID_ACTIONS));
2837
+ const execCtx = ctx.executor();
2838
+ switch (action) {
2839
+ case "my_day": return jsonResult((await getMyDaySummary({}, execCtx)).data);
2840
+ case "project_health":
2841
+ if (!args.project_id) return inputErrorResult(new UserInputError("project_id is required for project_health summary", [
2842
+ "Provide the project_id parameter",
2843
+ "You can find project IDs using resource=\"projects\" action=\"list\"",
2844
+ "Or use a project number like \"PRJ-123\""
2845
+ ]));
2846
+ return jsonResult((await getProjectHealthSummary({ projectId: args.project_id }, execCtx)).data);
2847
+ case "team_pulse": return jsonResult((await getTeamPulseSummary({}, execCtx)).data);
2848
+ case "help": return jsonResult({
2849
+ resource: "summaries",
2850
+ description: "Dashboard-style summaries that aggregate data from multiple resources",
2851
+ actions: {
2852
+ my_day: {
2853
+ description: "Personal dashboard for the current user",
2854
+ parameters: {},
2855
+ returns: {
2856
+ tasks: "Open and overdue tasks assigned to you",
2857
+ time: "Time entries logged today",
2858
+ timers: "Currently running timers"
2859
+ }
2860
+ },
2861
+ project_health: {
2862
+ description: "Project status with budget burn and task stats",
2863
+ parameters: { project_id: "Required. Project ID or project number (e.g., PRJ-123)" },
2864
+ returns: {
2865
+ project: "Project details",
2866
+ tasks: "Open and overdue task counts",
2867
+ budget: "Budget burn rate by service",
2868
+ recent_activity: "Time tracking activity in last 7 days"
2869
+ }
2870
+ },
2871
+ team_pulse: {
2872
+ description: "Team-wide time tracking activity for today",
2873
+ parameters: {},
2874
+ returns: {
2875
+ team: "Counts of active users, those tracking time, and with timers",
2876
+ people: "Per-person breakdown of time logged and active timers"
2877
+ }
2878
+ }
2879
+ }
2880
+ });
2881
+ default: return inputErrorResult(ErrorMessages.invalidAction(action, "summaries", VALID_ACTIONS));
2882
+ }
2883
+ }
2884
+ /**
2153
2885
  * Tasks MCP handler.
2154
2886
  */
2155
2887
  const handleTasks = createResourceHandler({
@@ -2160,7 +2892,8 @@ const handleTasks = createResourceHandler({
2160
2892
  "get",
2161
2893
  "create",
2162
2894
  "update",
2163
- "resolve"
2895
+ "resolve",
2896
+ "context"
2164
2897
  ],
2165
2898
  formatter: formatTask$1,
2166
2899
  hints: (data, id) => {
@@ -2192,6 +2925,23 @@ const handleTasks = createResourceHandler({
2192
2925
  description: args.description,
2193
2926
  assigneeId: args.assignee_id
2194
2927
  }) },
2928
+ customActions: { context: async (args, ctx, execCtx) => {
2929
+ if (!args.id) return inputErrorResult(ErrorMessages.missingId("context"));
2930
+ const result = await getTaskContext({ id: args.id }, execCtx);
2931
+ const formatOptions = {
2932
+ ...ctx.formatOptions,
2933
+ included: result.included
2934
+ };
2935
+ return jsonResult({
2936
+ ...formatTask$1(result.data.task, formatOptions),
2937
+ comments: result.data.comments.map((c) => formatComment$1(c, { compact: true })),
2938
+ time_entries: result.data.time_entries.map((t) => formatTimeEntry$1(t, { compact: true })),
2939
+ subtasks: result.data.subtasks.map((s) => formatTask$1(s, {
2940
+ ...formatOptions,
2941
+ compact: true
2942
+ }))
2943
+ });
2944
+ } },
2195
2945
  executors: {
2196
2946
  list: listTasks,
2197
2947
  get: getTask,
@@ -2315,14 +3065,17 @@ var DEFAULT_PER_PAGE = 20;
2315
3065
  * Execute a tool with the given credentials and arguments
2316
3066
  */
2317
3067
  async function executeToolWithCredentials(name, args, credentials) {
3068
+ if (name !== "productive") return errorResult(`Unknown tool: ${name}`);
3069
+ const typedArgs = args;
3070
+ if (typedArgs.resource === "batch") return handleBatch(typedArgs.operations, credentials, executeToolWithCredentials);
3071
+ const { resource, action, filter, page, per_page, compact, include, query, resources, no_hints, type, ...restArgs } = typedArgs;
3072
+ if (resource === "search") return await handleSearch(query, resources, credentials, executeToolWithCredentials);
2318
3073
  const api = new ProductiveApi({ config: {
2319
3074
  apiToken: credentials.apiToken,
2320
3075
  organizationId: credentials.organizationId,
2321
3076
  userId: credentials.userId,
2322
3077
  baseUrl: process.env.PRODUCTIVE_BASE_URL
2323
3078
  } });
2324
- if (name !== "productive") return errorResult(`Unknown tool: ${name}`);
2325
- const { resource, action, filter, page, per_page, compact, include, query, no_hints, type, ...restArgs } = args;
2326
3079
  const isCompact = compact ?? action !== "get";
2327
3080
  const formatOptions = { compact: isCompact };
2328
3081
  let stringFilter = toStringFilter(filter);
@@ -2332,7 +3085,7 @@ async function executeToolWithCredentials(name, args, credentials) {
2332
3085
  query
2333
3086
  };
2334
3087
  const includeHints = no_hints !== true && action === "get" && !isCompact;
2335
- const execCtx = fromHandlerContext({ api });
3088
+ const execCtx = fromHandlerContext({ api }, { userId: credentials.userId });
2336
3089
  const ctx = {
2337
3090
  formatOptions,
2338
3091
  filter: stringFilter,
@@ -2343,7 +3096,8 @@ async function executeToolWithCredentials(name, args, credentials) {
2343
3096
  executor: () => execCtx
2344
3097
  };
2345
3098
  try {
2346
- if (action === "help") return resource ? handleHelp(resource) : handleHelpOverview();
3099
+ if (action === "help" && resource !== "summaries") return resource ? handleHelp(resource) : handleHelpOverview();
3100
+ if (action === "schema") return resource ? handleSchema(resource) : handleSchemaOverview();
2347
3101
  const resolveArgs = {
2348
3102
  query,
2349
3103
  type
@@ -2381,6 +3135,7 @@ async function executeToolWithCredentials(name, args, credentials) {
2381
3135
  case "pages": return await handlePages(action, restArgs, ctx);
2382
3136
  case "discussions": return await handleDiscussions(action, restArgs, ctx);
2383
3137
  case "reports": return await handleReports(action, restArgs, ctx);
3138
+ case "summaries": return await handleSummaries(action, restArgs, ctx);
2384
3139
  case "budgets": return inputErrorResult(new UserInputError("The \"budgets\" resource has been removed. Budgets are deals with type=2.", [
2385
3140
  "Use resource=\"deals\" with filter[type]=\"2\" to list only budgets",
2386
3141
  "To create a budget: resource=\"deals\" action=\"create\" with budget=true",
@@ -2401,4 +3156,4 @@ async function executeToolWithCredentials(name, args, credentials) {
2401
3156
  }
2402
3157
  export { executeToolWithCredentials as t };
2403
3158
 
2404
- //# sourceMappingURL=handlers-BE3CYyVX.js.map
3159
+ //# sourceMappingURL=handlers-B8GRTaDu.js.map