@studiometa/productive-mcp 0.10.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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({
@@ -1228,6 +1334,56 @@ const handleDiscussions = createResourceHandler({
1228
1334
  }
1229
1335
  });
1230
1336
  var RESOURCE_HELP = {
1337
+ batch: {
1338
+ description: "Execute multiple operations in a single call. Operations run in parallel via Promise.all, reducing round-trips for AI agents.",
1339
+ actions: { run: "Execute a batch of operations (max 10)" },
1340
+ fields: { operations: "Array of operation objects. Each must have \"resource\" and \"action\", plus any additional params for that resource." },
1341
+ examples: [{
1342
+ description: "Batch multiple queries",
1343
+ params: {
1344
+ resource: "batch",
1345
+ action: "run",
1346
+ operations: [
1347
+ {
1348
+ resource: "projects",
1349
+ action: "get",
1350
+ id: "123"
1351
+ },
1352
+ {
1353
+ resource: "time",
1354
+ action: "list",
1355
+ filter: { project_id: "123" }
1356
+ },
1357
+ {
1358
+ resource: "services",
1359
+ action: "list",
1360
+ filter: { project_id: "123" }
1361
+ }
1362
+ ]
1363
+ }
1364
+ }, {
1365
+ description: "Batch create time entries",
1366
+ params: {
1367
+ resource: "batch",
1368
+ action: "run",
1369
+ operations: [{
1370
+ resource: "time",
1371
+ action: "create",
1372
+ service_id: "111",
1373
+ date: "2024-01-15",
1374
+ time: 60,
1375
+ note: "Morning work"
1376
+ }, {
1377
+ resource: "time",
1378
+ action: "create",
1379
+ service_id: "111",
1380
+ date: "2024-01-15",
1381
+ time: 120,
1382
+ note: "Afternoon work"
1383
+ }]
1384
+ }
1385
+ }]
1386
+ },
1231
1387
  projects: {
1232
1388
  description: "Manage projects in Productive.io",
1233
1389
  actions: {
@@ -1949,7 +2105,8 @@ function handleHelp(resource) {
1949
2105
  const help = RESOURCE_HELP[resource];
1950
2106
  if (!help) return jsonResult({
1951
2107
  error: `Unknown resource: ${resource}`,
1952
- available_resources: Object.keys(RESOURCE_HELP)
2108
+ available_resources: Object.keys(RESOURCE_HELP),
2109
+ _tip: "Call { action: 'help' } without a resource to see all available resources."
1953
2110
  });
1954
2111
  return jsonResult({
1955
2112
  resource,
@@ -1966,7 +2123,8 @@ function handleHelpOverview() {
1966
2123
  resource,
1967
2124
  description: help.description,
1968
2125
  actions: Object.keys(help.actions)
1969
- }))
2126
+ })),
2127
+ _tip: "Always call { action: 'help', resource: '<name>' } before your first interaction with any resource to learn valid filters, required fields, and examples."
1970
2128
  });
1971
2129
  }
1972
2130
  /**
@@ -2134,6 +2292,454 @@ async function handleReports(action, args, ctx) {
2134
2292
  });
2135
2293
  }
2136
2294
  /**
2295
+ * Schema definitions for all resources.
2296
+ *
2297
+ * This provides a compact, machine-readable specification of each resource's
2298
+ * capabilities. For detailed documentation with examples, use action=help.
2299
+ */
2300
+ var RESOURCE_SCHEMAS = {
2301
+ projects: {
2302
+ actions: [
2303
+ "list",
2304
+ "get",
2305
+ "resolve"
2306
+ ],
2307
+ filters: {
2308
+ query: "string — text search on project name",
2309
+ project_type: "1=internal|2=client",
2310
+ company_id: "string",
2311
+ responsible_id: "string",
2312
+ person_id: "string",
2313
+ status: "1=active|2=archived"
2314
+ }
2315
+ },
2316
+ time: {
2317
+ actions: [
2318
+ "list",
2319
+ "get",
2320
+ "create",
2321
+ "update",
2322
+ "delete"
2323
+ ],
2324
+ filters: {
2325
+ person_id: "string — use 'me' for current user",
2326
+ after: "date YYYY-MM-DD",
2327
+ before: "date YYYY-MM-DD",
2328
+ project_id: "string",
2329
+ service_id: "string",
2330
+ task_id: "string",
2331
+ status: "1=approved|2=unapproved|3=rejected"
2332
+ },
2333
+ create: {
2334
+ person_id: {
2335
+ required: true,
2336
+ type: "string"
2337
+ },
2338
+ service_id: {
2339
+ required: true,
2340
+ type: "string"
2341
+ },
2342
+ date: {
2343
+ required: true,
2344
+ type: "date YYYY-MM-DD"
2345
+ },
2346
+ time: {
2347
+ required: true,
2348
+ type: "minutes integer"
2349
+ },
2350
+ note: {
2351
+ required: false,
2352
+ type: "string"
2353
+ },
2354
+ task_id: {
2355
+ required: false,
2356
+ type: "string"
2357
+ }
2358
+ },
2359
+ includes: [
2360
+ "person",
2361
+ "service",
2362
+ "task"
2363
+ ]
2364
+ },
2365
+ tasks: {
2366
+ actions: [
2367
+ "list",
2368
+ "get",
2369
+ "create",
2370
+ "update",
2371
+ "resolve"
2372
+ ],
2373
+ filters: {
2374
+ query: "string — text search on task title",
2375
+ project_id: "string",
2376
+ assignee_id: "string",
2377
+ status: "1=open|2=closed (or \"open\", \"closed\", \"all\")",
2378
+ task_list_id: "string",
2379
+ workflow_status_id: "string — kanban column"
2380
+ },
2381
+ create: {
2382
+ title: {
2383
+ required: true,
2384
+ type: "string"
2385
+ },
2386
+ project_id: {
2387
+ required: true,
2388
+ type: "string"
2389
+ },
2390
+ task_list_id: {
2391
+ required: true,
2392
+ type: "string"
2393
+ },
2394
+ description: {
2395
+ required: false,
2396
+ type: "string"
2397
+ },
2398
+ assignee_id: {
2399
+ required: false,
2400
+ type: "string"
2401
+ }
2402
+ },
2403
+ includes: [
2404
+ "project",
2405
+ "assignee",
2406
+ "comments",
2407
+ "subtasks",
2408
+ "workflow_status"
2409
+ ]
2410
+ },
2411
+ services: {
2412
+ actions: ["list", "get"],
2413
+ filters: {
2414
+ project_id: "string",
2415
+ deal_id: "string",
2416
+ task_id: "string",
2417
+ budget_status: "1=open|2=delivered"
2418
+ }
2419
+ },
2420
+ people: {
2421
+ actions: [
2422
+ "list",
2423
+ "get",
2424
+ "me",
2425
+ "resolve"
2426
+ ],
2427
+ filters: {
2428
+ query: "string — text search on name or email",
2429
+ status: "1=active|2=deactivated",
2430
+ company_id: "string"
2431
+ }
2432
+ },
2433
+ companies: {
2434
+ actions: [
2435
+ "list",
2436
+ "get",
2437
+ "create",
2438
+ "update",
2439
+ "resolve"
2440
+ ],
2441
+ filters: {
2442
+ query: "string — text search on company name",
2443
+ archived: "boolean"
2444
+ },
2445
+ create: { name: {
2446
+ required: true,
2447
+ type: "string"
2448
+ } }
2449
+ },
2450
+ comments: {
2451
+ actions: [
2452
+ "list",
2453
+ "get",
2454
+ "create",
2455
+ "update"
2456
+ ],
2457
+ filters: {
2458
+ task_id: "string",
2459
+ deal_id: "string",
2460
+ page_id: "string",
2461
+ discussion_id: "string"
2462
+ },
2463
+ create: {
2464
+ body: {
2465
+ required: true,
2466
+ type: "string"
2467
+ },
2468
+ task_id: {
2469
+ required: false,
2470
+ type: "string — one of task_id, deal_id required"
2471
+ },
2472
+ deal_id: {
2473
+ required: false,
2474
+ type: "string — one of task_id, deal_id required"
2475
+ }
2476
+ },
2477
+ includes: ["creator"]
2478
+ },
2479
+ attachments: {
2480
+ actions: [
2481
+ "list",
2482
+ "get",
2483
+ "delete"
2484
+ ],
2485
+ filters: {
2486
+ task_id: "string",
2487
+ comment_id: "string",
2488
+ deal_id: "string",
2489
+ page_id: "string"
2490
+ }
2491
+ },
2492
+ timers: {
2493
+ actions: [
2494
+ "list",
2495
+ "get",
2496
+ "start",
2497
+ "stop"
2498
+ ],
2499
+ filters: {
2500
+ person_id: "string",
2501
+ time_entry_id: "string"
2502
+ }
2503
+ },
2504
+ deals: {
2505
+ actions: [
2506
+ "list",
2507
+ "get",
2508
+ "create",
2509
+ "update",
2510
+ "resolve"
2511
+ ],
2512
+ filters: {
2513
+ query: "string — text search on deal name",
2514
+ company_id: "string",
2515
+ type: "1=deal|2=budget",
2516
+ stage_status_id: "1=open|2=won|3=lost"
2517
+ },
2518
+ create: {
2519
+ name: {
2520
+ required: true,
2521
+ type: "string"
2522
+ },
2523
+ company_id: {
2524
+ required: true,
2525
+ type: "string"
2526
+ }
2527
+ },
2528
+ includes: ["company", "deal_status"]
2529
+ },
2530
+ bookings: {
2531
+ actions: [
2532
+ "list",
2533
+ "get",
2534
+ "create",
2535
+ "update"
2536
+ ],
2537
+ filters: {
2538
+ person_id: "string",
2539
+ after: "date YYYY-MM-DD",
2540
+ before: "date YYYY-MM-DD",
2541
+ service_id: "string"
2542
+ },
2543
+ create: {
2544
+ person_id: {
2545
+ required: true,
2546
+ type: "string"
2547
+ },
2548
+ started_on: {
2549
+ required: true,
2550
+ type: "date YYYY-MM-DD"
2551
+ },
2552
+ ended_on: {
2553
+ required: true,
2554
+ type: "date YYYY-MM-DD"
2555
+ },
2556
+ service_id: {
2557
+ required: false,
2558
+ type: "string — one of service_id, event_id required"
2559
+ },
2560
+ event_id: {
2561
+ required: false,
2562
+ type: "string — one of service_id, event_id required"
2563
+ }
2564
+ }
2565
+ },
2566
+ pages: {
2567
+ actions: [
2568
+ "list",
2569
+ "get",
2570
+ "create",
2571
+ "update",
2572
+ "delete"
2573
+ ],
2574
+ filters: { project_id: "string" },
2575
+ create: {
2576
+ title: {
2577
+ required: true,
2578
+ type: "string"
2579
+ },
2580
+ project_id: {
2581
+ required: true,
2582
+ type: "string"
2583
+ },
2584
+ body: {
2585
+ required: false,
2586
+ type: "string"
2587
+ },
2588
+ parent_page_id: {
2589
+ required: false,
2590
+ type: "string"
2591
+ }
2592
+ }
2593
+ },
2594
+ discussions: {
2595
+ actions: [
2596
+ "list",
2597
+ "get",
2598
+ "create",
2599
+ "update",
2600
+ "delete",
2601
+ "resolve",
2602
+ "reopen"
2603
+ ],
2604
+ filters: {
2605
+ page_id: "string",
2606
+ status: "1=active|2=resolved"
2607
+ },
2608
+ create: {
2609
+ body: {
2610
+ required: true,
2611
+ type: "string"
2612
+ },
2613
+ page_id: {
2614
+ required: true,
2615
+ type: "string"
2616
+ }
2617
+ }
2618
+ },
2619
+ reports: {
2620
+ actions: ["get"],
2621
+ filters: {
2622
+ person_id: "string",
2623
+ project_id: "string",
2624
+ company_id: "string",
2625
+ after: "date YYYY-MM-DD",
2626
+ before: "date YYYY-MM-DD"
2627
+ },
2628
+ create: {
2629
+ report_type: {
2630
+ required: true,
2631
+ type: "time_reports|project_reports|budget_reports|..."
2632
+ },
2633
+ from: {
2634
+ required: false,
2635
+ type: "date YYYY-MM-DD"
2636
+ },
2637
+ to: {
2638
+ required: false,
2639
+ type: "date YYYY-MM-DD"
2640
+ },
2641
+ group: {
2642
+ required: false,
2643
+ type: "string — grouping dimension"
2644
+ }
2645
+ }
2646
+ }
2647
+ };
2648
+ /**
2649
+ * Handle schema action - returns compact specification for a specific resource
2650
+ */
2651
+ function handleSchema(resource) {
2652
+ const schema = RESOURCE_SCHEMAS[resource];
2653
+ if (!schema) return errorResult(`Unknown resource: ${resource}. Valid resources: ${Object.keys(RESOURCE_SCHEMAS).join(", ")}`);
2654
+ return jsonResult({
2655
+ resource,
2656
+ ...schema
2657
+ });
2658
+ }
2659
+ /**
2660
+ * Get schema overview for all resources
2661
+ */
2662
+ function handleSchemaOverview() {
2663
+ return jsonResult({
2664
+ _tip: "Use action=\"schema\" with a specific resource for full filter/create/includes spec",
2665
+ resources: Object.entries(RESOURCE_SCHEMAS).map(([resource, schema]) => ({
2666
+ resource,
2667
+ actions: schema.actions
2668
+ }))
2669
+ });
2670
+ }
2671
+ /**
2672
+ * Resources that support the query filter for text search
2673
+ */
2674
+ const SEARCHABLE_RESOURCES = [
2675
+ "projects",
2676
+ "companies",
2677
+ "people",
2678
+ "tasks",
2679
+ "deals"
2680
+ ];
2681
+ /**
2682
+ * Default resources to search when not specified
2683
+ */
2684
+ var DEFAULT_SEARCH_RESOURCES = [
2685
+ "projects",
2686
+ "companies",
2687
+ "people",
2688
+ "tasks"
2689
+ ];
2690
+ /**
2691
+ * Handle cross-resource search.
2692
+ *
2693
+ * @param query - Search query text (required)
2694
+ * @param resources - Resource types to search (optional, defaults to DEFAULT_SEARCH_RESOURCES)
2695
+ * @param credentials - Productive API credentials
2696
+ * @param execute - Function to execute tool calls (injected for delegation and testing)
2697
+ * @returns Grouped search results across all requested resources
2698
+ */
2699
+ async function handleSearch(query, resources, credentials, execute) {
2700
+ if (!query || query.trim() === "") return errorResult("Missing required parameter: query. Provide a non-empty search string.");
2701
+ const trimmedQuery = query.trim();
2702
+ const resourcesToSearch = resources && resources.length > 0 ? resources : DEFAULT_SEARCH_RESOURCES;
2703
+ const invalidResources = resourcesToSearch.filter((r) => !SEARCHABLE_RESOURCES.includes(r));
2704
+ if (invalidResources.length > 0) return errorResult(`Invalid searchable resources: ${invalidResources.join(", ")}. Valid searchable resources: ${SEARCHABLE_RESOURCES.join(", ")}.`);
2705
+ const searchPromises = resourcesToSearch.map(async (resource) => {
2706
+ try {
2707
+ const textContent = (await execute("productive", {
2708
+ resource,
2709
+ action: "list",
2710
+ query: trimmedQuery,
2711
+ compact: true,
2712
+ per_page: 10
2713
+ }, credentials)).content.find((c) => c.type === "text");
2714
+ if (!textContent || textContent.type !== "text") return [resource, { error: "No content in response" }];
2715
+ try {
2716
+ const parsed = JSON.parse(textContent.text);
2717
+ const items = parsed.items ?? parsed.data ?? [];
2718
+ return [resource, { items: Array.isArray(items) ? items : [] }];
2719
+ } catch {
2720
+ return [resource, { error: "Failed to parse response JSON" }];
2721
+ }
2722
+ } catch (err) {
2723
+ return [resource, { error: err instanceof Error ? err.message : String(err) }];
2724
+ }
2725
+ });
2726
+ const searchResults = await Promise.all(searchPromises);
2727
+ const results = {};
2728
+ let totalResults = 0;
2729
+ for (const [resource, result] of searchResults) if (result.error) results[resource] = { error: result.error };
2730
+ else {
2731
+ const items = result.items ?? [];
2732
+ results[resource] = items;
2733
+ totalResults += items.length;
2734
+ }
2735
+ return jsonResult({
2736
+ query: trimmedQuery,
2737
+ resources_searched: resourcesToSearch,
2738
+ results,
2739
+ total_results: totalResults
2740
+ });
2741
+ }
2742
+ /**
2137
2743
  * Services MCP handler.
2138
2744
  *
2139
2745
  * Uses the createResourceHandler factory for the common list pattern.
@@ -2315,14 +2921,17 @@ var DEFAULT_PER_PAGE = 20;
2315
2921
  * Execute a tool with the given credentials and arguments
2316
2922
  */
2317
2923
  async function executeToolWithCredentials(name, args, credentials) {
2924
+ if (name !== "productive") return errorResult(`Unknown tool: ${name}`);
2925
+ const typedArgs = args;
2926
+ if (typedArgs.resource === "batch") return handleBatch(typedArgs.operations, credentials, executeToolWithCredentials);
2927
+ const { resource, action, filter, page, per_page, compact, include, query, resources, no_hints, type, ...restArgs } = typedArgs;
2928
+ if (resource === "search") return await handleSearch(query, resources, credentials, executeToolWithCredentials);
2318
2929
  const api = new ProductiveApi({ config: {
2319
2930
  apiToken: credentials.apiToken,
2320
2931
  organizationId: credentials.organizationId,
2321
2932
  userId: credentials.userId,
2322
2933
  baseUrl: process.env.PRODUCTIVE_BASE_URL
2323
2934
  } });
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
2935
  const isCompact = compact ?? action !== "get";
2327
2936
  const formatOptions = { compact: isCompact };
2328
2937
  let stringFilter = toStringFilter(filter);
@@ -2344,6 +2953,7 @@ async function executeToolWithCredentials(name, args, credentials) {
2344
2953
  };
2345
2954
  try {
2346
2955
  if (action === "help") return resource ? handleHelp(resource) : handleHelpOverview();
2956
+ if (action === "schema") return resource ? handleSchema(resource) : handleSchemaOverview();
2347
2957
  const resolveArgs = {
2348
2958
  query,
2349
2959
  type
@@ -2401,4 +3011,4 @@ async function executeToolWithCredentials(name, args, credentials) {
2401
3011
  }
2402
3012
  export { executeToolWithCredentials as t };
2403
3013
 
2404
- //# sourceMappingURL=handlers-BE3CYyVX.js.map
3014
+ //# sourceMappingURL=handlers-DWowqxFA.js.map