@mgsoftwarebv/mcp-server-bridge 3.5.17 → 3.5.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -107843,6 +107843,59 @@ var TOOLS = [
107843
107843
  },
107844
107844
  required: ["id"]
107845
107845
  }
107846
+ },
107847
+ {
107848
+ name: "get-user-activity-report",
107849
+ description: "Daily execution/activity report for a single user (or the whole team) over a date range \u2014 built for standups and end-of-day summaries like 'what did Prince do today?'. Aggregates, per requested timezone day boundaries, the user's ticket changes (created/status/assignee/priority/type/tag/GitHub events from the audit log), comments, attachments, time entries (worked hours) and organized calendar items into one chronological timeline plus summary counts. The user is matched as the ACTOR who performed each action (distinct from a ticket's assignee/requester). Scoped to your provider team(s); pass a user UUID for a colleague, 'me' for yourself (default), or 'all' for the whole team. Each activity carries the ticket UUID + ticket number + title, project, a plain-English summary and the actor. Returns explicit `limitations` (e.g. invalid timezone fallback, unknown user, project filter not applicable to calendar items). Also accepts the alias name get_user_activity_report. Use get-projects to resolve a projectId and get-teams to resolve a teamId first.",
107850
+ inputSchema: {
107851
+ type: "object",
107852
+ properties: {
107853
+ teamId: teamIdProp,
107854
+ userId: {
107855
+ type: "string",
107856
+ description: "User to report on (the actor). Defaults to the API key user ('me'). Pass a user UUID for a colleague, or 'all' for every accessible team member."
107857
+ },
107858
+ dateFrom: {
107859
+ type: "string",
107860
+ description: "Inclusive start day (YYYY-MM-DD) in `timezone`. Defaults to today. If only dateTo is given, dateFrom mirrors it."
107861
+ },
107862
+ dateTo: {
107863
+ type: "string",
107864
+ description: "Inclusive end day (YYYY-MM-DD) in `timezone`. Defaults to dateFrom (or today). For a single-day report pass the same value as dateFrom or omit it."
107865
+ },
107866
+ timezone: {
107867
+ type: "string",
107868
+ default: "Europe/Amsterdam",
107869
+ description: "IANA timezone (e.g. Europe/Amsterdam) used to compute day boundaries and local timestamps. Invalid values fall back to Europe/Amsterdam with a note in `limitations`."
107870
+ },
107871
+ include: {
107872
+ type: "array",
107873
+ items: {
107874
+ type: "string",
107875
+ enum: [
107876
+ "tickets",
107877
+ "comments",
107878
+ "status_changes",
107879
+ "attachments",
107880
+ "time_entries",
107881
+ "calendar_items"
107882
+ ]
107883
+ },
107884
+ description: "Activity categories to include. Defaults to all six. 'tickets' = ticket lifecycle/audit events (created, tag, GitHub, generic updates); 'status_changes' = status/assignee/priority/type/project transitions; 'comments', 'attachments', 'time_entries' (worked hours), 'calendar_items' (agenda events organized by the user)."
107885
+ },
107886
+ projectId: {
107887
+ type: "string",
107888
+ description: "Restrict ticket-linked activity (and time entries) to this project UUID. Does not apply to calendar items, which are then omitted (noted in `limitations`)."
107889
+ },
107890
+ pageSize: {
107891
+ type: "number",
107892
+ default: 200,
107893
+ maximum: 500,
107894
+ description: "Max activities returned in the timeline. Summary counts always cover ALL matching activity regardless of this limit."
107895
+ }
107896
+ },
107897
+ required: []
107898
+ }
107846
107899
  }
107847
107900
  ];
107848
107901
  var RESOURCES = [
@@ -127169,6 +127222,562 @@ ${tagErrors.map((e6) => ` \u2022 ${e6}`).join("\n")}
127169
127222
  };
127170
127223
  }
127171
127224
 
127225
+ // src/tools/user-activity-report.ts
127226
+ var DEFAULT_TIMEZONE = "Europe/Amsterdam";
127227
+ var DEFAULT_PAGE_SIZE = 200;
127228
+ var MAX_PAGE_SIZE = 500;
127229
+ var MAX_ROWS_PER_SOURCE = 2e3;
127230
+ var COMMENT_PREVIEW_LENGTH = 140;
127231
+ var ACTIVITY_INCLUDE_VALUES = [
127232
+ "tickets",
127233
+ "comments",
127234
+ "status_changes",
127235
+ "attachments",
127236
+ "time_entries",
127237
+ "calendar_items"
127238
+ ];
127239
+ var STATUS_CHANGE_ACTIVITY_TYPES = /* @__PURE__ */ new Set([
127240
+ "status_change",
127241
+ "status_changed",
127242
+ "assignment",
127243
+ "assignee_changed",
127244
+ "priority_change",
127245
+ "priority_changed",
127246
+ "type_change",
127247
+ "type_changed",
127248
+ "project_changed"
127249
+ ]);
127250
+ var COMMENT_ACTIVITY_TYPES = /* @__PURE__ */ new Set([
127251
+ "comment_added",
127252
+ "comment_internal_added",
127253
+ "comment_updated",
127254
+ "comment_deleted"
127255
+ ]);
127256
+ function resolveIncludes(input) {
127257
+ if (input == null || input.length === 0) {
127258
+ return { includes: new Set(ACTIVITY_INCLUDE_VALUES), invalid: [] };
127259
+ }
127260
+ const includes = /* @__PURE__ */ new Set();
127261
+ const invalid = [];
127262
+ for (const raw of input) {
127263
+ const value = String(raw).trim();
127264
+ if (ACTIVITY_INCLUDE_VALUES.includes(value)) {
127265
+ includes.add(value);
127266
+ } else if (value.length > 0) {
127267
+ invalid.push(value);
127268
+ }
127269
+ }
127270
+ return { includes, invalid };
127271
+ }
127272
+ function isValidIsoDate2(value) {
127273
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
127274
+ const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00Z`);
127275
+ if (Number.isNaN(parsed.getTime())) return false;
127276
+ return parsed.toISOString().slice(0, 10) === value;
127277
+ }
127278
+ function isValidTimeZone(timeZone) {
127279
+ if (!timeZone) return false;
127280
+ try {
127281
+ new Intl.DateTimeFormat("en-US", { timeZone });
127282
+ return true;
127283
+ } catch {
127284
+ return false;
127285
+ }
127286
+ }
127287
+ function todayInTimeZone(timeZone, now2 = /* @__PURE__ */ new Date()) {
127288
+ return new Intl.DateTimeFormat("en-CA", {
127289
+ timeZone,
127290
+ year: "numeric",
127291
+ month: "2-digit",
127292
+ day: "2-digit"
127293
+ }).format(now2);
127294
+ }
127295
+ function resolveDayWindow(args2) {
127296
+ const today = todayInTimeZone(args2.timezone, args2.now);
127297
+ const from = args2.dateFrom ?? args2.dateTo ?? today;
127298
+ const to = args2.dateTo ?? args2.dateFrom ?? today;
127299
+ return { from, to };
127300
+ }
127301
+ function classifyTicketActivity(activityType) {
127302
+ if (COMMENT_ACTIVITY_TYPES.has(activityType)) return null;
127303
+ if (STATUS_CHANGE_ACTIVITY_TYPES.has(activityType)) return "status_changes";
127304
+ return "tickets";
127305
+ }
127306
+ function humanizeActivityType(activityType) {
127307
+ const text3 = activityType.replace(/_/g, " ").trim();
127308
+ if (text3.length === 0) return "Activity";
127309
+ return text3.charAt(0).toUpperCase() + text3.slice(1);
127310
+ }
127311
+ function summarizeTicketActivity(activityType, oldValue, newValue) {
127312
+ const oldV = oldValue && oldValue.trim() !== "" ? oldValue.trim() : null;
127313
+ const newV = newValue && newValue.trim() !== "" ? newValue.trim() : null;
127314
+ switch (activityType) {
127315
+ case "created":
127316
+ return "Created the ticket";
127317
+ case "updated":
127318
+ return "Updated the ticket";
127319
+ case "status_change":
127320
+ case "status_changed":
127321
+ if (oldV && newV) return `Changed status from ${oldV} to ${newV}`;
127322
+ if (newV) return `Changed status to ${newV}`;
127323
+ return "Changed status";
127324
+ case "assignment":
127325
+ case "assignee_changed":
127326
+ if (!oldV && newV) return `Assigned to ${newV}`;
127327
+ if (oldV && !newV) return `Unassigned from ${oldV}`;
127328
+ if (newV) return `Reassigned to ${newV}`;
127329
+ return "Changed assignment";
127330
+ case "priority_change":
127331
+ case "priority_changed":
127332
+ if (oldV && newV) return `Changed priority from ${oldV} to ${newV}`;
127333
+ if (newV) return `Changed priority to ${newV}`;
127334
+ return "Changed priority";
127335
+ case "type_change":
127336
+ case "type_changed":
127337
+ if (oldV && newV) return `Changed type from ${oldV} to ${newV}`;
127338
+ if (newV) return `Changed type to ${newV}`;
127339
+ return "Changed type";
127340
+ case "project_changed":
127341
+ if (oldV && newV) return `Moved project from ${oldV} to ${newV}`;
127342
+ return "Changed project";
127343
+ case "tag_added":
127344
+ return newV ? `Added tag ${newV}` : "Added a tag";
127345
+ case "tag_removed":
127346
+ return oldV ? `Removed tag ${oldV}` : "Removed a tag";
127347
+ case "sequence_changed":
127348
+ return "Reordered the ticket";
127349
+ case "github_commit_linked":
127350
+ return "Linked a GitHub commit";
127351
+ case "github_pr_opened":
127352
+ return "Opened a GitHub pull request";
127353
+ case "github_pr_merged":
127354
+ return "Merged a GitHub pull request";
127355
+ case "github_pr_closed":
127356
+ return "Closed a GitHub pull request";
127357
+ default:
127358
+ return humanizeActivityType(activityType);
127359
+ }
127360
+ }
127361
+ function truncate(text3, max) {
127362
+ const clean = text3.replace(/\s+/g, " ").trim();
127363
+ if (clean.length <= max) return clean;
127364
+ return `${clean.slice(0, Math.max(0, max - 1)).trimEnd()}\u2026`;
127365
+ }
127366
+ function timeZoneOffset(date10, timeZone) {
127367
+ try {
127368
+ const name21 = new Intl.DateTimeFormat("en-US", {
127369
+ timeZone,
127370
+ timeZoneName: "longOffset"
127371
+ }).formatToParts(date10).find((p3) => p3.type === "timeZoneName")?.value;
127372
+ const match = name21?.match(/GMT([+-]\d{2}:\d{2})/);
127373
+ return match?.[1] ?? "+00:00";
127374
+ } catch {
127375
+ return "+00:00";
127376
+ }
127377
+ }
127378
+ function formatIsoWithOffset(date10, timeZone) {
127379
+ const parts = Object.fromEntries(
127380
+ new Intl.DateTimeFormat("en-US", {
127381
+ timeZone,
127382
+ year: "numeric",
127383
+ month: "2-digit",
127384
+ day: "2-digit",
127385
+ hour: "2-digit",
127386
+ minute: "2-digit",
127387
+ second: "2-digit",
127388
+ hour12: false
127389
+ }).formatToParts(date10).map((p3) => [p3.type, p3.value])
127390
+ );
127391
+ const hour2 = parts.hour === "24" ? "00" : parts.hour;
127392
+ const local = `${parts.year}-${parts.month}-${parts.day}T${hour2}:${parts.minute}:${parts.second}`;
127393
+ return `${local}${timeZoneOffset(date10, timeZone)}`;
127394
+ }
127395
+ function textResponse13(text3) {
127396
+ return { content: [{ type: "text", text: text3 }] };
127397
+ }
127398
+ function jsonResponse3(payload) {
127399
+ return {
127400
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
127401
+ };
127402
+ }
127403
+ function toNumber4(value) {
127404
+ if (value == null) return 0;
127405
+ if (typeof value === "number") return value;
127406
+ const parsed = Number.parseFloat(String(value));
127407
+ return Number.isFinite(parsed) ? parsed : 0;
127408
+ }
127409
+ function hoursFrom2(seconds) {
127410
+ return Math.round(toNumber4(seconds) / 3600 * 100) / 100;
127411
+ }
127412
+ function utcIsoExpr(column) {
127413
+ return sql`to_char(${column} AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`;
127414
+ }
127415
+ async function handleGetUserActivityReport(input) {
127416
+ const ctx = getAuthContext();
127417
+ const limitations = [];
127418
+ const { includes, invalid } = resolveIncludes(input.include);
127419
+ for (const value of invalid) {
127420
+ limitations.push(`Ignored unknown include value "${value}".`);
127421
+ }
127422
+ if (includes.size === 0) {
127423
+ return textResponse13(
127424
+ `Error: \`include\` contained no valid categories. Allowed: ${ACTIVITY_INCLUDE_VALUES.join(", ")}.`
127425
+ );
127426
+ }
127427
+ let timezone = input.timezone?.trim() || DEFAULT_TIMEZONE;
127428
+ if (!isValidTimeZone(timezone)) {
127429
+ limitations.push(
127430
+ `Invalid timezone "${input.timezone}"; fell back to ${DEFAULT_TIMEZONE}.`
127431
+ );
127432
+ timezone = DEFAULT_TIMEZONE;
127433
+ }
127434
+ if (input.dateFrom && !isValidIsoDate2(input.dateFrom)) {
127435
+ return textResponse13(
127436
+ `Error: invalid dateFrom "${input.dateFrom}". Use YYYY-MM-DD.`
127437
+ );
127438
+ }
127439
+ if (input.dateTo && !isValidIsoDate2(input.dateTo)) {
127440
+ return textResponse13(
127441
+ `Error: invalid dateTo "${input.dateTo}". Use YYYY-MM-DD.`
127442
+ );
127443
+ }
127444
+ const window2 = resolveDayWindow({
127445
+ dateFrom: input.dateFrom,
127446
+ dateTo: input.dateTo,
127447
+ timezone
127448
+ });
127449
+ if (window2.from > window2.to) {
127450
+ return textResponse13(
127451
+ `Error: dateFrom (${window2.from}) is after dateTo (${window2.to}).`
127452
+ );
127453
+ }
127454
+ const scope = await resolveTeamScope(input.teamId);
127455
+ if (!scope.ok) return scope.response;
127456
+ if (scope.teamIds.length === 0) {
127457
+ return textResponse13("No accessible teams found.");
127458
+ }
127459
+ if (input.projectId && !scope.projectIds.includes(input.projectId)) {
127460
+ return textResponse13(
127461
+ `Project not found or no access: ${input.projectId}. Call get-projects first.`
127462
+ );
127463
+ }
127464
+ const rawUserId = input.userId?.trim();
127465
+ const userFilter = !rawUserId || rawUserId === "me" ? ctx.userId : rawUserId;
127466
+ const isAll = userFilter === "all";
127467
+ let user;
127468
+ if (isAll) {
127469
+ user = { id: "all", name: "All team members" };
127470
+ } else {
127471
+ const [row] = await db.select({ id: schema_exports.users.id, name: schema_exports.users.fullName }).from(schema_exports.users).where(eq(schema_exports.users.id, userFilter)).limit(1);
127472
+ if (!row) {
127473
+ limitations.push(
127474
+ `User ${userFilter} was not found; reporting any team-scoped activity recorded for that id.`
127475
+ );
127476
+ user = { id: userFilter, name: null };
127477
+ } else {
127478
+ user = { id: row.id, name: row.name };
127479
+ }
127480
+ }
127481
+ const dayWindow = (column) => sql`(${column} AT TIME ZONE ${timezone})::date BETWEEN ${window2.from}::date AND ${window2.to}::date`;
127482
+ const ticketAccess = and(
127483
+ buildTicketAccessPredicate(
127484
+ scope.teamIds,
127485
+ scope.projectIds,
127486
+ scope.customerIds
127487
+ ),
127488
+ eq(schema_exports.tickets.isDeleted, false)
127489
+ );
127490
+ const activities2 = [];
127491
+ const ticketsTouched = /* @__PURE__ */ new Set();
127492
+ let commentsAdded = 0;
127493
+ let statusChanges = 0;
127494
+ let attachmentsAdded = 0;
127495
+ let timeEntrySeconds = 0;
127496
+ const pushTicketTouched = (ticketId) => {
127497
+ if (ticketId) ticketsTouched.add(ticketId);
127498
+ };
127499
+ if (includes.has("tickets") || includes.has("status_changes")) {
127500
+ const ta = schema_exports.ticketActivity;
127501
+ const conditions = [ticketAccess, dayWindow(ta.createdAt)];
127502
+ if (!isAll) conditions.push(eq(ta.userId, userFilter));
127503
+ if (input.projectId)
127504
+ conditions.push(eq(schema_exports.tickets.projectId, input.projectId));
127505
+ const rows = await db.select({
127506
+ ts: utcIsoExpr(ta.createdAt),
127507
+ activityType: ta.activityType,
127508
+ oldValue: ta.oldValue,
127509
+ newValue: ta.newValue,
127510
+ actorId: ta.userId,
127511
+ actorName: schema_exports.users.fullName,
127512
+ ticketId: schema_exports.tickets.id,
127513
+ ticketNumber: schema_exports.tickets.ticketNumber,
127514
+ ticketTitle: schema_exports.tickets.title,
127515
+ projectId: schema_exports.projects.id,
127516
+ projectName: schema_exports.projects.name
127517
+ }).from(ta).innerJoin(schema_exports.tickets, eq(schema_exports.tickets.id, ta.ticketId)).leftJoin(schema_exports.users, eq(schema_exports.users.id, ta.userId)).leftJoin(schema_exports.projects, eq(schema_exports.projects.id, schema_exports.tickets.projectId)).where(and(...conditions)).limit(MAX_ROWS_PER_SOURCE);
127518
+ if (rows.length >= MAX_ROWS_PER_SOURCE) {
127519
+ limitations.push(
127520
+ `Ticket activity capped at ${MAX_ROWS_PER_SOURCE} rows; counts may be partial. Narrow the date range.`
127521
+ );
127522
+ }
127523
+ for (const r6 of rows) {
127524
+ const bucket = classifyTicketActivity(r6.activityType);
127525
+ if (!bucket || !includes.has(bucket)) continue;
127526
+ pushTicketTouched(r6.ticketId);
127527
+ if (bucket === "status_changes") statusChanges += 1;
127528
+ activities2.push({
127529
+ timestamp: formatIsoWithOffset(new Date(r6.ts), timezone),
127530
+ timestampUtc: r6.ts,
127531
+ type: `ticket_${r6.activityType}`,
127532
+ ticketId: r6.ticketId,
127533
+ ticketNumber: r6.ticketNumber,
127534
+ ticketTitle: r6.ticketTitle,
127535
+ project: r6.projectId ? { id: r6.projectId, name: r6.projectName } : null,
127536
+ summary: summarizeTicketActivity(
127537
+ r6.activityType,
127538
+ r6.oldValue,
127539
+ r6.newValue
127540
+ ),
127541
+ actor: { id: r6.actorId, name: r6.actorName },
127542
+ meta: {
127543
+ activityType: r6.activityType,
127544
+ from: r6.oldValue ?? null,
127545
+ to: r6.newValue ?? null
127546
+ }
127547
+ });
127548
+ }
127549
+ }
127550
+ if (includes.has("comments")) {
127551
+ const tc = schema_exports.ticketComments;
127552
+ const conditions = [ticketAccess, dayWindow(tc.createdAt)];
127553
+ if (!isAll) conditions.push(eq(tc.userId, userFilter));
127554
+ if (input.projectId)
127555
+ conditions.push(eq(schema_exports.tickets.projectId, input.projectId));
127556
+ const rows = await db.select({
127557
+ ts: utcIsoExpr(tc.createdAt),
127558
+ content: tc.content,
127559
+ isInternal: tc.isInternal,
127560
+ actorId: tc.userId,
127561
+ actorUserName: tc.userName,
127562
+ actorName: schema_exports.users.fullName,
127563
+ ticketId: schema_exports.tickets.id,
127564
+ ticketNumber: schema_exports.tickets.ticketNumber,
127565
+ ticketTitle: schema_exports.tickets.title,
127566
+ projectId: schema_exports.projects.id,
127567
+ projectName: schema_exports.projects.name
127568
+ }).from(tc).innerJoin(schema_exports.tickets, eq(schema_exports.tickets.id, tc.ticketId)).leftJoin(schema_exports.users, eq(schema_exports.users.id, tc.userId)).leftJoin(schema_exports.projects, eq(schema_exports.projects.id, schema_exports.tickets.projectId)).where(and(...conditions)).limit(MAX_ROWS_PER_SOURCE);
127569
+ if (rows.length >= MAX_ROWS_PER_SOURCE) {
127570
+ limitations.push(
127571
+ `Comments capped at ${MAX_ROWS_PER_SOURCE} rows; counts may be partial. Narrow the date range.`
127572
+ );
127573
+ }
127574
+ for (const r6 of rows) {
127575
+ commentsAdded += 1;
127576
+ pushTicketTouched(r6.ticketId);
127577
+ activities2.push({
127578
+ timestamp: formatIsoWithOffset(new Date(r6.ts), timezone),
127579
+ timestampUtc: r6.ts,
127580
+ type: r6.isInternal ? "ticket_internal_comment" : "ticket_comment",
127581
+ ticketId: r6.ticketId,
127582
+ ticketNumber: r6.ticketNumber,
127583
+ ticketTitle: r6.ticketTitle,
127584
+ project: r6.projectId ? { id: r6.projectId, name: r6.projectName } : null,
127585
+ summary: `Added ${r6.isInternal ? "internal " : ""}comment: ${truncate(
127586
+ r6.content,
127587
+ COMMENT_PREVIEW_LENGTH
127588
+ )}`,
127589
+ actor: { id: r6.actorId, name: r6.actorName ?? r6.actorUserName },
127590
+ meta: { isInternal: r6.isInternal }
127591
+ });
127592
+ }
127593
+ }
127594
+ if (includes.has("attachments")) {
127595
+ const collectAttachments = async (table, type) => {
127596
+ const conditions = [ticketAccess, dayWindow(table.createdAt)];
127597
+ if (!isAll) conditions.push(eq(table.userId, userFilter));
127598
+ if (input.projectId)
127599
+ conditions.push(eq(schema_exports.tickets.projectId, input.projectId));
127600
+ const rows = await db.select({
127601
+ ts: utcIsoExpr(table.createdAt),
127602
+ fileName: table.fileName,
127603
+ actorId: table.userId,
127604
+ actorName: schema_exports.users.fullName,
127605
+ ticketId: schema_exports.tickets.id,
127606
+ ticketNumber: schema_exports.tickets.ticketNumber,
127607
+ ticketTitle: schema_exports.tickets.title,
127608
+ projectId: schema_exports.projects.id,
127609
+ projectName: schema_exports.projects.name
127610
+ }).from(table).innerJoin(schema_exports.tickets, eq(schema_exports.tickets.id, table.ticketId)).leftJoin(schema_exports.users, eq(schema_exports.users.id, table.userId)).leftJoin(
127611
+ schema_exports.projects,
127612
+ eq(schema_exports.projects.id, schema_exports.tickets.projectId)
127613
+ ).where(and(...conditions)).limit(MAX_ROWS_PER_SOURCE);
127614
+ for (const r6 of rows) {
127615
+ attachmentsAdded += 1;
127616
+ pushTicketTouched(r6.ticketId);
127617
+ activities2.push({
127618
+ timestamp: formatIsoWithOffset(new Date(r6.ts), timezone),
127619
+ timestampUtc: r6.ts,
127620
+ type,
127621
+ ticketId: r6.ticketId,
127622
+ ticketNumber: r6.ticketNumber,
127623
+ ticketTitle: r6.ticketTitle,
127624
+ project: r6.projectId ? { id: r6.projectId, name: r6.projectName } : null,
127625
+ summary: `Uploaded attachment ${r6.fileName}`,
127626
+ actor: { id: r6.actorId, name: r6.actorName },
127627
+ meta: { fileName: r6.fileName }
127628
+ });
127629
+ }
127630
+ };
127631
+ await collectAttachments(schema_exports.ticketAttachments, "ticket_attachment");
127632
+ await collectAttachments(
127633
+ schema_exports.ticketCommentAttachments,
127634
+ "ticket_comment_attachment"
127635
+ );
127636
+ }
127637
+ if (includes.has("time_entries")) {
127638
+ const te = schema_exports.timesheetEvents;
127639
+ const durationSeconds = sql`CASE WHEN ${te.endTime} IS NOT NULL THEN GREATEST(0, EXTRACT(EPOCH FROM (${te.endTime} - ${te.startTime}))) ELSE COALESCE(${te.trackedDuration}, 0) END`;
127640
+ const conditions = [
127641
+ inArray(te.teamId, scope.teamIds),
127642
+ eq(te.isDeleted, false),
127643
+ eq(te.allDay, false),
127644
+ sql`${te.type}::text <> 'deadline'`,
127645
+ dayWindow(te.startTime)
127646
+ ];
127647
+ if (!isAll) conditions.push(eq(te.userId, userFilter));
127648
+ if (input.projectId) conditions.push(eq(te.projectId, input.projectId));
127649
+ const rows = await db.select({
127650
+ id: te.id,
127651
+ ts: utcIsoExpr(te.startTime),
127652
+ title: te.title,
127653
+ type: te.type,
127654
+ billingStatus: te.billingStatus,
127655
+ durationSeconds,
127656
+ actorId: te.userId,
127657
+ actorName: schema_exports.users.fullName,
127658
+ projectId: te.projectId,
127659
+ projectName: schema_exports.projects.name
127660
+ }).from(te).leftJoin(schema_exports.projects, eq(schema_exports.projects.id, te.projectId)).leftJoin(schema_exports.users, eq(schema_exports.users.id, te.userId)).where(and(...conditions)).limit(MAX_ROWS_PER_SOURCE);
127661
+ const entryIds = rows.map((r6) => r6.id);
127662
+ const ticketsByEvent = /* @__PURE__ */ new Map();
127663
+ if (entryIds.length > 0) {
127664
+ const links = await db.select({
127665
+ eventId: schema_exports.timesheetEventTickets.timesheetEventId,
127666
+ ticketId: schema_exports.tickets.id,
127667
+ ticketNumber: schema_exports.tickets.ticketNumber,
127668
+ ticketTitle: schema_exports.tickets.title
127669
+ }).from(schema_exports.timesheetEventTickets).innerJoin(
127670
+ schema_exports.tickets,
127671
+ eq(schema_exports.timesheetEventTickets.ticketId, schema_exports.tickets.id)
127672
+ ).where(
127673
+ inArray(schema_exports.timesheetEventTickets.timesheetEventId, entryIds)
127674
+ );
127675
+ for (const link of links) {
127676
+ const list = ticketsByEvent.get(link.eventId) ?? [];
127677
+ list.push({
127678
+ id: link.ticketId,
127679
+ ticketNumber: link.ticketNumber,
127680
+ title: link.ticketTitle
127681
+ });
127682
+ ticketsByEvent.set(link.eventId, list);
127683
+ }
127684
+ }
127685
+ for (const r6 of rows) {
127686
+ const seconds = toNumber4(r6.durationSeconds);
127687
+ timeEntrySeconds += seconds;
127688
+ const hours = hoursFrom2(seconds);
127689
+ const linked = ticketsByEvent.get(r6.id) ?? [];
127690
+ for (const t8 of linked) ticketsTouched.add(t8.id);
127691
+ const primary = linked[0] ?? null;
127692
+ const projectLabel = r6.projectName ? ` on ${r6.projectName}` : "";
127693
+ const titleLabel = r6.title ? ` \u2014 ${r6.title}` : "";
127694
+ activities2.push({
127695
+ timestamp: formatIsoWithOffset(new Date(r6.ts), timezone),
127696
+ timestampUtc: r6.ts,
127697
+ type: "time_entry",
127698
+ ticketId: primary?.id ?? null,
127699
+ ticketNumber: primary?.ticketNumber ?? null,
127700
+ ticketTitle: primary?.title ?? null,
127701
+ project: r6.projectId ? { id: r6.projectId, name: r6.projectName } : null,
127702
+ summary: `Logged ${hours}h${projectLabel}${titleLabel}`,
127703
+ actor: { id: r6.actorId, name: r6.actorName },
127704
+ meta: {
127705
+ hours,
127706
+ eventType: r6.type,
127707
+ billingStatus: r6.billingStatus,
127708
+ linkedTickets: linked.map((t8) => ({
127709
+ id: t8.id,
127710
+ ticketNumber: t8.ticketNumber
127711
+ }))
127712
+ }
127713
+ });
127714
+ }
127715
+ }
127716
+ if (includes.has("calendar_items")) {
127717
+ if (input.projectId) {
127718
+ limitations.push(
127719
+ "calendar_items were omitted because a projectId filter was set (agenda events have no project link)."
127720
+ );
127721
+ } else {
127722
+ const ae2 = schema_exports.agendaEvents;
127723
+ const conditions = [
127724
+ inArray(ae2.teamId, scope.teamIds),
127725
+ eq(ae2.isDeleted, false),
127726
+ dayWindow(ae2.startTime)
127727
+ ];
127728
+ if (!isAll) conditions.push(eq(ae2.organizerId, userFilter));
127729
+ const rows = await db.select({
127730
+ ts: utcIsoExpr(ae2.startTime),
127731
+ title: ae2.title,
127732
+ status: ae2.status,
127733
+ actorId: ae2.organizerId,
127734
+ actorName: schema_exports.users.fullName
127735
+ }).from(ae2).leftJoin(schema_exports.users, eq(schema_exports.users.id, ae2.organizerId)).where(and(...conditions)).limit(MAX_ROWS_PER_SOURCE);
127736
+ for (const r6 of rows) {
127737
+ activities2.push({
127738
+ timestamp: formatIsoWithOffset(new Date(r6.ts), timezone),
127739
+ timestampUtc: r6.ts,
127740
+ type: "calendar_item",
127741
+ ticketId: null,
127742
+ ticketNumber: null,
127743
+ ticketTitle: null,
127744
+ project: null,
127745
+ summary: `Calendar: ${r6.title}`,
127746
+ actor: { id: r6.actorId, name: r6.actorName },
127747
+ meta: { status: r6.status }
127748
+ });
127749
+ }
127750
+ }
127751
+ }
127752
+ activities2.sort((a6, b7) => a6.timestampUtc.localeCompare(b7.timestampUtc));
127753
+ const pageSize = Math.min(
127754
+ Math.max(1, input.pageSize ?? DEFAULT_PAGE_SIZE),
127755
+ MAX_PAGE_SIZE
127756
+ );
127757
+ const pagedActivities = activities2.slice(0, pageSize);
127758
+ return jsonResponse3({
127759
+ user,
127760
+ period: { from: window2.from, to: window2.to, timezone },
127761
+ filters: {
127762
+ teamIds: scope.teamIds,
127763
+ userId: isAll ? "all" : userFilter,
127764
+ projectId: input.projectId ?? null,
127765
+ include: [...includes]
127766
+ },
127767
+ summary: {
127768
+ ticketsTouched: ticketsTouched.size,
127769
+ commentsAdded,
127770
+ statusChanges,
127771
+ attachmentsAdded,
127772
+ timeEntriesHours: hoursFrom2(timeEntrySeconds),
127773
+ activitiesTotal: activities2.length
127774
+ },
127775
+ activities: pagedActivities,
127776
+ activitiesTruncated: activities2.length > pagedActivities.length,
127777
+ limitations
127778
+ });
127779
+ }
127780
+
127172
127781
  // src/server.ts
127173
127782
  var SERVER_VERSION = "3.5.12";
127174
127783
  function createMcpServer() {
@@ -127414,6 +128023,11 @@ function createMcpServer() {
127414
128023
  return await handleGetTimeEntries(
127415
128024
  asToolArgs(toolArgs)
127416
128025
  );
128026
+ case "get-user-activity-report":
128027
+ case "get_user_activity_report":
128028
+ return await handleGetUserActivityReport(
128029
+ asToolArgs(toolArgs)
128030
+ );
127417
128031
  case "create-time-entries":
127418
128032
  return await handleCreateTimeEntries(
127419
128033
  asToolArgs(toolArgs)