@mgsoftwarebv/mcp-server-bridge 3.3.3 → 3.3.4

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
@@ -95053,6 +95053,15 @@ var quotations = pgTable(
95053
95053
  topBlock: jsonb("top_block"),
95054
95054
  bottomBlock: jsonb("bottom_block"),
95055
95055
  template: jsonb(),
95056
+ // Typst block-document content (mirrors `documents.blocks`). The pricing
95057
+ // block is the financial source of truth; line_items/amount/subtotal/vat
95058
+ // are derived from it on save for invoicing/KPIs/AI.
95059
+ blocks: jsonb().default([]).notNull(),
95060
+ branding: jsonb().default({}).notNull(),
95061
+ pageSize: text("page_size").default("a4").notNull(),
95062
+ // PDF staleness marker: hash of the compiled block content + file size.
95063
+ compiledHash: text("compiled_hash"),
95064
+ fileSize: integer2("file_size"),
95056
95065
  vat: numericCasted({ precision: 10, scale: 2 }),
95057
95066
  tax: numericCasted({ precision: 10, scale: 2 }),
95058
95067
  discount: numericCasted({ precision: 10, scale: 2 }),
@@ -105507,7 +105516,7 @@ var TOOLS = [
105507
105516
  },
105508
105517
  {
105509
105518
  name: "get-tickets",
105510
- description: "Get tickets with optional filtering by status, priority, project, customer, or search query",
105519
+ description: "Get tickets with optional filtering by status, priority, project, customer, tags, or search query. Each ticket includes its tags.",
105511
105520
  inputSchema: {
105512
105521
  type: "object",
105513
105522
  properties: {
@@ -105533,6 +105542,20 @@ var TOOLS = [
105533
105542
  type: "string",
105534
105543
  description: "Search query for ticket number, title, or description"
105535
105544
  },
105545
+ tag: {
105546
+ type: "string",
105547
+ description: "Filter by a single tag name (case-insensitive, team-specific)"
105548
+ },
105549
+ tags: {
105550
+ type: "array",
105551
+ items: { type: "string" },
105552
+ description: "Filter by tag names (case-insensitive). Tickets matching any listed tag are returned."
105553
+ },
105554
+ tagIds: {
105555
+ type: "array",
105556
+ items: { type: "string" },
105557
+ description: "Filter by tag IDs"
105558
+ },
105536
105559
  pageSize: { type: "number", default: 20, maximum: 100 }
105537
105560
  },
105538
105561
  required: []
@@ -105540,7 +105563,7 @@ var TOOLS = [
105540
105563
  },
105541
105564
  {
105542
105565
  name: "get-ticket-by-id",
105543
- description: "Get a specific ticket by its ID, including comment text and a full attachment listing (with ids). Images from ticket and comment attachments are downloaded and returned inline as base64. For non-image attachments, call get-ticket-attachment with the listed id to get a download URL.",
105566
+ description: "Get a specific ticket by its ID, including tags, comment text and a full attachment listing (with ids). Images from ticket and comment attachments are downloaded and returned inline as base64. For non-image attachments, call get-ticket-attachment with the listed id to get a download URL.",
105544
105567
  inputSchema: {
105545
105568
  type: "object",
105546
105569
  properties: {
@@ -105552,7 +105575,7 @@ var TOOLS = [
105552
105575
  },
105553
105576
  {
105554
105577
  name: "create-ticket",
105555
- description: "Create a new ticket",
105578
+ description: "Create a new ticket. Tags can be passed by name (auto-created if missing) or by existing tag IDs. Tag names are matched case-insensitively within the team.",
105556
105579
  inputSchema: {
105557
105580
  type: "object",
105558
105581
  properties: {
@@ -105589,14 +105612,24 @@ var TOOLS = [
105589
105612
  default: "task"
105590
105613
  },
105591
105614
  projectId: { type: "string" },
105592
- customerId: { type: "string" }
105615
+ customerId: { type: "string" },
105616
+ tags: {
105617
+ type: "array",
105618
+ items: { type: "string" },
105619
+ description: "Tag names to attach. Missing tags are created as general team tags (case-insensitive deduplication)."
105620
+ },
105621
+ tagIds: {
105622
+ type: "array",
105623
+ items: { type: "string" },
105624
+ description: "Existing tag IDs to attach"
105625
+ }
105593
105626
  },
105594
105627
  required: ["title"]
105595
105628
  }
105596
105629
  },
105597
105630
  {
105598
105631
  name: "update-ticket",
105599
- description: "Update an existing ticket's fields (title, description, status, priority, type, project, customer, assignee, estimated hours). Only provided fields are changed. Changes are written to the ticket activity feed but do NOT send notifications. Set assigneeId to null to unassign; a provided assigneeId must be a member of the ticket's team. Common workflow: set status=in_progress when starting work; after merge/push set status=review and assigneeId to the requester (creator) id from get-ticket-by-id.",
105632
+ description: "Update an existing ticket's fields (title, description, status, priority, type, project, customer, assignee, estimated hours, tags). Only provided fields are changed. Changes are written to the ticket activity feed but do NOT send notifications. Set assigneeId to null to unassign; a provided assigneeId must be a member of the ticket's team. Tags: use tagMode replace (default) to set the full tag list, merge to add tags, or remove to drop listed tags. Common workflow: set status=in_progress when starting work; after merge/push set status=review and assigneeId to the requester (creator) id from get-ticket-by-id.",
105600
105633
  inputSchema: {
105601
105634
  type: "object",
105602
105635
  properties: {
@@ -105639,7 +105672,159 @@ var TOOLS = [
105639
105672
  type: ["string", "null"],
105640
105673
  description: "User ID to assign, or null to unassign."
105641
105674
  },
105642
- estimatedHours: { type: "number" }
105675
+ estimatedHours: { type: "number" },
105676
+ dueDate: {
105677
+ type: ["string", "null"],
105678
+ description: "Ticket deadline as YYYY-MM-DD. Creates or updates a linked agenda item. Pass null to remove the deadline."
105679
+ },
105680
+ tags: {
105681
+ type: "array",
105682
+ items: { type: "string" },
105683
+ description: "Tag names. With tagMode replace (default), sets the ticket's tags. With merge, adds tags. With remove, removes these tags. Missing names are auto-created when adding."
105684
+ },
105685
+ tagIds: {
105686
+ type: "array",
105687
+ items: { type: "string" },
105688
+ description: "Tag IDs (same tagMode semantics as tags)"
105689
+ },
105690
+ tagMode: {
105691
+ type: "string",
105692
+ enum: ["replace", "merge", "remove"],
105693
+ default: "replace",
105694
+ description: "How to apply tags/tagIds: replace = set full list, merge = add, remove = remove listed tags"
105695
+ }
105696
+ },
105697
+ required: ["id"]
105698
+ }
105699
+ },
105700
+ {
105701
+ name: "get-tags",
105702
+ description: "List tags for the provider team. Tags are team-specific; optional projectId includes general tags plus project-specific tags.",
105703
+ inputSchema: {
105704
+ type: "object",
105705
+ properties: {
105706
+ teamId: teamIdProp,
105707
+ projectId: {
105708
+ type: "string",
105709
+ description: "Optional project ID to include project-specific tags"
105710
+ },
105711
+ pageSize: { type: "number", default: 100, maximum: 200 }
105712
+ },
105713
+ required: []
105714
+ }
105715
+ },
105716
+ {
105717
+ name: "create-tag",
105718
+ description: "Create a team tag. Tag names are deduplicated case-insensitively within the same team/project scope.",
105719
+ inputSchema: {
105720
+ type: "object",
105721
+ properties: {
105722
+ teamId: teamIdProp,
105723
+ name: { type: "string", description: "Tag name" },
105724
+ projectId: {
105725
+ type: "string",
105726
+ description: "Optional project ID for a project-specific tag; omit for a general team tag"
105727
+ }
105728
+ },
105729
+ required: ["name"]
105730
+ }
105731
+ },
105732
+ {
105733
+ name: "get-calendar-items",
105734
+ description: "List agenda/calendar items (deadlines, meetings, reminders, deliveries) with optional filters by date range, project, ticket, customer, assignee, type, or status. Returns items with id, title, startsAt, endsAt, dueDate, linked ticket/project/customer ids, and status.",
105735
+ inputSchema: {
105736
+ type: "object",
105737
+ properties: {
105738
+ teamId: teamIdProp,
105739
+ projectId: { type: "string" },
105740
+ ticketId: { type: "string" },
105741
+ customerId: { type: "string" },
105742
+ assigneeId: { type: "string" },
105743
+ dateFrom: {
105744
+ type: "string",
105745
+ description: "ISO datetime or YYYY-MM-DD (inclusive start)"
105746
+ },
105747
+ dateTo: {
105748
+ type: "string",
105749
+ description: "ISO datetime or YYYY-MM-DD (inclusive end)"
105750
+ },
105751
+ status: {
105752
+ type: "string",
105753
+ enum: ["draft", "scheduled", "completed", "cancelled"]
105754
+ },
105755
+ type: {
105756
+ type: "string",
105757
+ enum: ["deadline", "meeting", "reminder", "delivery"]
105758
+ },
105759
+ pageSize: { type: "number", default: 50, maximum: 100 }
105760
+ },
105761
+ required: []
105762
+ }
105763
+ },
105764
+ {
105765
+ name: "create-calendar-item",
105766
+ description: "Create an agenda/calendar item linked to tickets, projects, or customers. Use type 'deadline' with dueDate (YYYY-MM-DD) for delivery deadlines. Prevents duplicate deadline items for the same ticket \u2014 updates the existing one instead. Types: deadline, meeting, reminder, delivery.",
105767
+ inputSchema: {
105768
+ type: "object",
105769
+ properties: {
105770
+ teamId: teamIdProp,
105771
+ title: { type: "string" },
105772
+ description: { type: "string" },
105773
+ dueDate: {
105774
+ type: "string",
105775
+ description: "Deadline date YYYY-MM-DD (all-day). Preferred for deadlines."
105776
+ },
105777
+ startsAt: {
105778
+ type: "string",
105779
+ description: "Start datetime (ISO). Use when a specific time is needed."
105780
+ },
105781
+ endsAt: { type: "string", description: "End datetime (ISO)" },
105782
+ projectId: { type: "string" },
105783
+ ticketId: { type: "string" },
105784
+ customerId: { type: "string" },
105785
+ assigneeId: {
105786
+ type: "string",
105787
+ description: "User ID of the assignee (defaults to the API key user)"
105788
+ },
105789
+ type: {
105790
+ type: "string",
105791
+ enum: ["deadline", "meeting", "reminder", "delivery"],
105792
+ default: "deadline"
105793
+ },
105794
+ status: {
105795
+ type: "string",
105796
+ enum: ["draft", "scheduled", "completed"],
105797
+ default: "scheduled"
105798
+ }
105799
+ },
105800
+ required: ["title"]
105801
+ }
105802
+ },
105803
+ {
105804
+ name: "update-calendar-item",
105805
+ description: "Update an existing agenda/calendar item by id. Supports changing title, dates, status, type, and linked ticket/project/customer. Set status to 'completed' or 'cancelled' to finish or remove the item.",
105806
+ inputSchema: {
105807
+ type: "object",
105808
+ properties: {
105809
+ teamId: teamIdProp,
105810
+ id: { type: "string", description: "Calendar item ID" },
105811
+ title: { type: "string" },
105812
+ description: { type: ["string", "null"] },
105813
+ dueDate: { type: "string", description: "YYYY-MM-DD" },
105814
+ startsAt: { type: "string" },
105815
+ endsAt: { type: "string" },
105816
+ projectId: { type: ["string", "null"] },
105817
+ ticketId: { type: ["string", "null"] },
105818
+ customerId: { type: ["string", "null"] },
105819
+ assigneeId: { type: "string" },
105820
+ type: {
105821
+ type: "string",
105822
+ enum: ["deadline", "meeting", "reminder", "delivery"]
105823
+ },
105824
+ status: {
105825
+ type: "string",
105826
+ enum: ["draft", "scheduled", "completed", "cancelled"]
105827
+ }
105643
105828
  },
105644
105829
  required: ["id"]
105645
105830
  }
@@ -106434,64 +106619,605 @@ function teamSelectionResponse(teams2) {
106434
106619
  content: [
106435
106620
  {
106436
106621
  type: "text",
106437
- text: `You belong to multiple providers, so this action is ambiguous. Re-call this tool with a \`teamId\` set to the intended provider.
106438
-
106439
- Available providers:
106440
- ${list}
106622
+ text: `You belong to multiple providers, so this action is ambiguous. Re-call this tool with a \`teamId\` set to the intended provider.
106623
+
106624
+ Available providers:
106625
+ ${list}
106626
+
106627
+ Ask the user which provider to use (or infer it from the conversation), then call the tool again with the chosen \`teamId\`.`
106628
+ }
106629
+ ]
106630
+ };
106631
+ }
106632
+ function notAMemberResponse(teamId) {
106633
+ return {
106634
+ content: [
106635
+ {
106636
+ type: "text",
106637
+ text: `Access denied: you are not a member of team ${teamId}. Call \`get-teams\` to list the providers you can act on.`
106638
+ }
106639
+ ]
106640
+ };
106641
+ }
106642
+ async function resolveTeamId(requestedTeamId) {
106643
+ const ctx = getAuthContext();
106644
+ if (requestedTeamId) {
106645
+ const member2 = await isUserTeamMember(ctx.userId, requestedTeamId);
106646
+ if (!member2) {
106647
+ return { ok: false, response: notAMemberResponse(requestedTeamId) };
106648
+ }
106649
+ return { ok: true, teamId: requestedTeamId };
106650
+ }
106651
+ const teams2 = await getUserProviderTeams(ctx.userId);
106652
+ if (teams2.length === 0) {
106653
+ return { ok: true, teamId: ctx.teamId };
106654
+ }
106655
+ if (teams2.length === 1) {
106656
+ return { ok: true, teamId: teams2[0].id };
106657
+ }
106658
+ return { ok: false, response: teamSelectionResponse(teams2) };
106659
+ }
106660
+ async function resolveTeamScope(requestedTeamId) {
106661
+ const ctx = getAuthContext();
106662
+ if (requestedTeamId) {
106663
+ const member2 = await isUserTeamMember(ctx.userId, requestedTeamId);
106664
+ if (!member2) {
106665
+ return { ok: false, response: notAMemberResponse(requestedTeamId) };
106666
+ }
106667
+ const [teamIds2, projectIds2, customerIds2] = await Promise.all([
106668
+ getAccessibleTeamIds(requestedTeamId),
106669
+ getAccessibleProjectIds(ctx.userId, requestedTeamId),
106670
+ getAccessibleCustomerIds(requestedTeamId)
106671
+ ]);
106672
+ return { ok: true, teamIds: teamIds2, projectIds: projectIds2, customerIds: customerIds2 };
106673
+ }
106674
+ const [teamIds, projectIds, customerIds] = await Promise.all([
106675
+ getUserAccessibleTeamIds(ctx.userId),
106676
+ getUserAccessibleProjectIds(ctx.userId),
106677
+ getUserAccessibleCustomerIds(ctx.userId)
106678
+ ]);
106679
+ return { ok: true, teamIds, projectIds, customerIds };
106680
+ }
106681
+
106682
+ // src/tools/ticket-access.ts
106683
+ function notFoundResponse(ticketId) {
106684
+ return {
106685
+ content: [
106686
+ {
106687
+ type: "text",
106688
+ text: `Ticket not found or no access: ${ticketId}. Call get-tickets to find the correct ticket.`
106689
+ }
106690
+ ]
106691
+ };
106692
+ }
106693
+ async function loadAccessibleTicket(requestedTeamId, ticketId) {
106694
+ const scope = await resolveTeamScope(requestedTeamId);
106695
+ if (!scope.ok) return scope;
106696
+ const [ticket] = await db.select({
106697
+ id: schema_exports.tickets.id,
106698
+ teamId: schema_exports.tickets.teamId,
106699
+ projectId: schema_exports.tickets.projectId,
106700
+ customerId: schema_exports.tickets.customerId,
106701
+ ticketNumber: schema_exports.tickets.ticketNumber,
106702
+ title: schema_exports.tickets.title,
106703
+ status: schema_exports.tickets.status,
106704
+ priority: schema_exports.tickets.priority,
106705
+ type: schema_exports.tickets.type,
106706
+ assigneeId: schema_exports.tickets.assigneeId
106707
+ }).from(schema_exports.tickets).where(eq(schema_exports.tickets.id, ticketId)).limit(1);
106708
+ if (!ticket) return { ok: false, response: notFoundResponse(ticketId) };
106709
+ const hasAccess = scope.teamIds.includes(ticket.teamId) || !!ticket.projectId && scope.projectIds.includes(ticket.projectId) || !!ticket.customerId && scope.customerIds.includes(ticket.customerId);
106710
+ if (!hasAccess) return { ok: false, response: notFoundResponse(ticketId) };
106711
+ return { ok: true, ticket };
106712
+ }
106713
+
106714
+ // src/tools/calendar-items.ts
106715
+ function mapInputType(type) {
106716
+ switch (type) {
106717
+ case "reminder":
106718
+ return "task";
106719
+ case "delivery":
106720
+ return "work";
106721
+ default:
106722
+ return type;
106723
+ }
106724
+ }
106725
+ function mapOutputType(type, metadata) {
106726
+ const meta5 = metadata && typeof metadata === "object" && !Array.isArray(metadata) ? metadata : null;
106727
+ const stored = meta5?.calendarType;
106728
+ if (typeof stored === "string") return stored;
106729
+ if (type === "task") return "reminder";
106730
+ if (type === "work") return "delivery";
106731
+ return type;
106732
+ }
106733
+ function mapOutputStatus(status, metadata, isDeleted) {
106734
+ if (isDeleted) return "cancelled";
106735
+ const meta5 = metadata && typeof metadata === "object" && !Array.isArray(metadata) ? metadata : null;
106736
+ if (meta5?.completed === true) return "completed";
106737
+ return status === "draft" ? "draft" : "scheduled";
106738
+ }
106739
+ function parseDueDate(dueDate) {
106740
+ const dateOnly = dueDate.split("T")[0];
106741
+ return {
106742
+ startTime: `${dateOnly}T00:00:00.000Z`,
106743
+ endTime: `${dateOnly}T23:59:59.999Z`
106744
+ };
106745
+ }
106746
+ function resolveEventTimes(input) {
106747
+ if (input.dueDate) {
106748
+ const { startTime, endTime } = parseDueDate(input.dueDate);
106749
+ return { startTime, endTime, allDay: true };
106750
+ }
106751
+ if (input.startsAt) {
106752
+ const startTime = input.startsAt.includes("T") ? input.startsAt : `${input.startsAt}T09:00:00.000Z`;
106753
+ const endTime = input.endsAt ? input.endsAt.includes("T") ? input.endsAt : `${input.endsAt}T10:00:00.000Z` : null;
106754
+ const allDay = input.type === "deadline" || !input.endsAt && !input.startsAt.includes(":");
106755
+ return { startTime, endTime, allDay };
106756
+ }
106757
+ throw new Error(
106758
+ "Provide dueDate (YYYY-MM-DD) or startsAt (ISO datetime) for the calendar item."
106759
+ );
106760
+ }
106761
+ function formatCalendarItem(row) {
106762
+ const dueDate = row.allDay || row.type === "deadline" ? row.startTime.split("T")[0] : void 0;
106763
+ return {
106764
+ id: row.id,
106765
+ title: row.title,
106766
+ description: row.description,
106767
+ type: mapOutputType(row.type, row.metadata),
106768
+ startsAt: row.startTime,
106769
+ endsAt: row.endTime,
106770
+ dueDate,
106771
+ projectId: row.projectId,
106772
+ customerId: row.customerId,
106773
+ assigneeId: row.userId,
106774
+ ticketIds: row.ticketIds,
106775
+ status: mapOutputStatus(row.status, row.metadata, row.isDeleted)
106776
+ };
106777
+ }
106778
+ async function loadTicketLinks(eventIds) {
106779
+ const map3 = /* @__PURE__ */ new Map();
106780
+ if (eventIds.length === 0) return map3;
106781
+ const links = await db.select({
106782
+ eventId: schema_exports.timesheetEventTickets.timesheetEventId,
106783
+ ticketId: schema_exports.timesheetEventTickets.ticketId
106784
+ }).from(schema_exports.timesheetEventTickets).where(
106785
+ inArray(schema_exports.timesheetEventTickets.timesheetEventId, eventIds)
106786
+ );
106787
+ for (const link of links) {
106788
+ const existing = map3.get(link.eventId) ?? [];
106789
+ existing.push(link.ticketId);
106790
+ map3.set(link.eventId, existing);
106791
+ }
106792
+ return map3;
106793
+ }
106794
+ async function findExistingTicketDeadline(teamId, ticketId) {
106795
+ const links = await db.select({ eventId: schema_exports.timesheetEventTickets.timesheetEventId }).from(schema_exports.timesheetEventTickets).innerJoin(
106796
+ schema_exports.timesheetEvents,
106797
+ eq(
106798
+ schema_exports.timesheetEvents.id,
106799
+ schema_exports.timesheetEventTickets.timesheetEventId
106800
+ )
106801
+ ).where(
106802
+ and(
106803
+ eq(schema_exports.timesheetEventTickets.ticketId, ticketId),
106804
+ eq(schema_exports.timesheetEvents.teamId, teamId),
106805
+ eq(schema_exports.timesheetEvents.type, "deadline"),
106806
+ eq(schema_exports.timesheetEvents.isDeleted, false)
106807
+ )
106808
+ ).limit(1);
106809
+ return links[0]?.eventId ?? null;
106810
+ }
106811
+ async function validateProjectAccess(teamId, projectId, projectIds) {
106812
+ if (!projectIds.includes(projectId)) {
106813
+ throw new Error(
106814
+ `Project not found or no access: ${projectId}. Call get-projects first.`
106815
+ );
106816
+ }
106817
+ }
106818
+ async function validateAssignee(teamId, assigneeId) {
106819
+ const member2 = await isUserTeamMember(assigneeId, teamId);
106820
+ if (!member2) {
106821
+ throw new Error(
106822
+ `Assignee ${assigneeId} is not a member of team ${teamId}.`
106823
+ );
106824
+ }
106825
+ }
106826
+ async function loadCalendarItemById(teamId, id) {
106827
+ const [event] = await db.select({
106828
+ id: schema_exports.timesheetEvents.id,
106829
+ title: schema_exports.timesheetEvents.title,
106830
+ description: schema_exports.timesheetEvents.description,
106831
+ type: schema_exports.timesheetEvents.type,
106832
+ status: schema_exports.timesheetEvents.status,
106833
+ startTime: schema_exports.timesheetEvents.startTime,
106834
+ endTime: schema_exports.timesheetEvents.endTime,
106835
+ allDay: schema_exports.timesheetEvents.allDay,
106836
+ projectId: schema_exports.timesheetEvents.projectId,
106837
+ customerId: schema_exports.timesheetEvents.customerId,
106838
+ userId: schema_exports.timesheetEvents.userId,
106839
+ metadata: schema_exports.timesheetEvents.metadata,
106840
+ isDeleted: schema_exports.timesheetEvents.isDeleted
106841
+ }).from(schema_exports.timesheetEvents).where(
106842
+ and(
106843
+ eq(schema_exports.timesheetEvents.id, id),
106844
+ eq(schema_exports.timesheetEvents.teamId, teamId),
106845
+ eq(schema_exports.timesheetEvents.isDeleted, false)
106846
+ )
106847
+ ).limit(1);
106848
+ if (!event) return null;
106849
+ const ticketLinks = await loadTicketLinks([event.id]);
106850
+ return {
106851
+ ...event,
106852
+ ticketIds: ticketLinks.get(event.id) ?? []
106853
+ };
106854
+ }
106855
+ async function handleGetCalendarItems(input) {
106856
+ getAuthContext();
106857
+ const resolved = await resolveTeamId(input.teamId);
106858
+ if (!resolved.ok) return resolved.response;
106859
+ const teamId = resolved.teamId;
106860
+ const teamIds = await getAccessibleTeamIds(teamId);
106861
+ const pageSize = Math.min(input.pageSize ?? 50, 100);
106862
+ const dateFrom = input.dateFrom ?? (/* @__PURE__ */ new Date()).toISOString().split("T")[0] + "T00:00:00.000Z";
106863
+ const dateTo = input.dateTo ?? new Date(Date.now() + 90 * 24 * 60 * 60 * 1e3).toISOString();
106864
+ const filters = [
106865
+ inArray(schema_exports.timesheetEvents.teamId, teamIds),
106866
+ gte(schema_exports.timesheetEvents.startTime, dateFrom),
106867
+ lte(schema_exports.timesheetEvents.startTime, dateTo),
106868
+ eq(schema_exports.timesheetEvents.isDeleted, false)
106869
+ ];
106870
+ if (input.projectId) {
106871
+ filters.push(eq(schema_exports.timesheetEvents.projectId, input.projectId));
106872
+ }
106873
+ if (input.customerId) {
106874
+ filters.push(eq(schema_exports.timesheetEvents.customerId, input.customerId));
106875
+ }
106876
+ if (input.assigneeId) {
106877
+ filters.push(eq(schema_exports.timesheetEvents.userId, input.assigneeId));
106878
+ }
106879
+ if (input.status === "draft") {
106880
+ filters.push(eq(schema_exports.timesheetEvents.status, "draft"));
106881
+ } else if (input.status === "scheduled") {
106882
+ filters.push(eq(schema_exports.timesheetEvents.status, "confirmed"));
106883
+ }
106884
+ let eventIdsForTicket;
106885
+ if (input.ticketId) {
106886
+ const links = await db.select({ eventId: schema_exports.timesheetEventTickets.timesheetEventId }).from(schema_exports.timesheetEventTickets).where(eq(schema_exports.timesheetEventTickets.ticketId, input.ticketId));
106887
+ eventIdsForTicket = links.map((l4) => l4.eventId);
106888
+ if (eventIdsForTicket.length === 0) {
106889
+ return {
106890
+ content: [
106891
+ {
106892
+ type: "text",
106893
+ text: JSON.stringify({ count: 0, items: [] }, null, 2)
106894
+ }
106895
+ ]
106896
+ };
106897
+ }
106898
+ filters.push(inArray(schema_exports.timesheetEvents.id, eventIdsForTicket));
106899
+ }
106900
+ const rows = await db.select({
106901
+ id: schema_exports.timesheetEvents.id,
106902
+ title: schema_exports.timesheetEvents.title,
106903
+ description: schema_exports.timesheetEvents.description,
106904
+ type: schema_exports.timesheetEvents.type,
106905
+ status: schema_exports.timesheetEvents.status,
106906
+ startTime: schema_exports.timesheetEvents.startTime,
106907
+ endTime: schema_exports.timesheetEvents.endTime,
106908
+ allDay: schema_exports.timesheetEvents.allDay,
106909
+ projectId: schema_exports.timesheetEvents.projectId,
106910
+ customerId: schema_exports.timesheetEvents.customerId,
106911
+ userId: schema_exports.timesheetEvents.userId,
106912
+ metadata: schema_exports.timesheetEvents.metadata,
106913
+ isDeleted: schema_exports.timesheetEvents.isDeleted
106914
+ }).from(schema_exports.timesheetEvents).where(and(...filters)).orderBy(asc(schema_exports.timesheetEvents.startTime)).limit(pageSize);
106915
+ const ticketLinks = await loadTicketLinks(rows.map((r6) => r6.id));
106916
+ let items = rows.map(
106917
+ (row) => formatCalendarItem({
106918
+ ...row,
106919
+ ticketIds: ticketLinks.get(row.id) ?? []
106920
+ })
106921
+ );
106922
+ if (input.status === "completed") {
106923
+ items = items.filter((item) => item.status === "completed");
106924
+ } else if (input.status === "scheduled") {
106925
+ items = items.filter((item) => item.status === "scheduled");
106926
+ }
106927
+ if (input.type) {
106928
+ items = items.filter((item) => item.type === input.type);
106929
+ }
106930
+ return {
106931
+ content: [
106932
+ {
106933
+ type: "text",
106934
+ text: JSON.stringify({ count: items.length, items }, null, 2)
106935
+ }
106936
+ ]
106937
+ };
106938
+ }
106939
+ async function handleCreateCalendarItem(input) {
106940
+ const ctx = getAuthContext();
106941
+ const resolved = await resolveTeamId(input.teamId);
106942
+ if (!resolved.ok) return resolved.response;
106943
+ const teamId = resolved.teamId;
106944
+ const projectIds = await getAccessibleProjectIds(ctx.userId, teamId);
106945
+ const customerIds = await getAccessibleCustomerIds(teamId);
106946
+ if (input.projectId) {
106947
+ await validateProjectAccess(teamId, input.projectId, projectIds);
106948
+ }
106949
+ if (input.customerId && !customerIds.includes(input.customerId)) {
106950
+ throw new Error(
106951
+ `Customer not found or no access: ${input.customerId}. Call get-customers first.`
106952
+ );
106953
+ }
106954
+ let ticketId = input.ticketId;
106955
+ let projectId = input.projectId ?? null;
106956
+ let customerId = input.customerId ?? null;
106957
+ if (ticketId) {
106958
+ const access = await loadAccessibleTicket(input.teamId, ticketId);
106959
+ if (!access.ok) return access.response;
106960
+ if (!projectId) projectId = access.ticket.projectId;
106961
+ if (!customerId) customerId = access.ticket.customerId;
106962
+ }
106963
+ const itemType = input.type ?? "deadline";
106964
+ const timesheetType = mapInputType(itemType);
106965
+ if (ticketId && timesheetType === "deadline") {
106966
+ const existingId = await findExistingTicketDeadline(teamId, ticketId);
106967
+ if (existingId) {
106968
+ return handleUpdateCalendarItem({
106969
+ teamId: input.teamId,
106970
+ id: existingId,
106971
+ title: input.title,
106972
+ description: input.description,
106973
+ dueDate: input.dueDate,
106974
+ startsAt: input.startsAt,
106975
+ endsAt: input.endsAt,
106976
+ projectId: projectId ?? void 0,
106977
+ customerId: customerId ?? void 0,
106978
+ assigneeId: input.assigneeId,
106979
+ status: input.status,
106980
+ type: itemType
106981
+ });
106982
+ }
106983
+ }
106984
+ const assigneeId = input.assigneeId ?? ctx.userId;
106985
+ await validateAssignee(teamId, assigneeId);
106986
+ const { startTime, endTime, allDay } = resolveEventTimes({
106987
+ dueDate: input.dueDate,
106988
+ startsAt: input.startsAt,
106989
+ endsAt: input.endsAt,
106990
+ type: itemType
106991
+ });
106992
+ const metadata = {
106993
+ calendarType: itemType,
106994
+ source: "mcp_calendar_item"
106995
+ };
106996
+ if (input.status === "completed") metadata.completed = true;
106997
+ const [created] = await db.insert(schema_exports.timesheetEvents).values({
106998
+ teamId,
106999
+ userId: assigneeId,
107000
+ title: input.title,
107001
+ description: input.description ?? null,
107002
+ type: timesheetType,
107003
+ status: input.status === "draft" ? "draft" : "confirmed",
107004
+ startTime,
107005
+ endTime,
107006
+ allDay,
107007
+ projectId,
107008
+ customerId,
107009
+ metadata,
107010
+ billingStatus: "unbillable"
107011
+ }).returning({ id: schema_exports.timesheetEvents.id });
107012
+ if (!created) throw new Error("Failed to create calendar item.");
107013
+ if (ticketId) {
107014
+ await db.insert(schema_exports.timesheetEventTickets).values({
107015
+ timesheetEventId: created.id,
107016
+ ticketId
107017
+ });
107018
+ }
107019
+ const item = await loadCalendarItemById(teamId, created.id);
107020
+ return {
107021
+ content: [
107022
+ {
107023
+ type: "text",
107024
+ text: `\u2705 **Calendar item created**
107025
+
107026
+ ` + JSON.stringify(formatCalendarItem(item), null, 2)
107027
+ }
107028
+ ]
107029
+ };
107030
+ }
107031
+ async function handleUpdateCalendarItem(input) {
107032
+ const ctx = getAuthContext();
107033
+ const resolved = await resolveTeamId(input.teamId);
107034
+ if (!resolved.ok) return resolved.response;
107035
+ const teamId = resolved.teamId;
107036
+ const existing = await loadCalendarItemById(teamId, input.id);
107037
+ if (!existing) {
107038
+ return {
107039
+ content: [
107040
+ {
107041
+ type: "text",
107042
+ text: `Calendar item not found or no access: ${input.id}. Call get-calendar-items to find the correct id.`
107043
+ }
107044
+ ]
107045
+ };
107046
+ }
107047
+ const projectIds = await getAccessibleProjectIds(ctx.userId, teamId);
107048
+ const customerIds = await getAccessibleCustomerIds(teamId);
107049
+ if (input.projectId) {
107050
+ await validateProjectAccess(teamId, input.projectId, projectIds);
107051
+ }
107052
+ if (input.customerId && !customerIds.includes(input.customerId)) {
107053
+ throw new Error(`Customer not found or no access: ${input.customerId}.`);
107054
+ }
107055
+ if (input.assigneeId) {
107056
+ await validateAssignee(teamId, input.assigneeId);
107057
+ }
107058
+ const updateValues = {
107059
+ updatedAt: sql`NOW()`
107060
+ };
107061
+ if (input.title !== void 0) updateValues.title = input.title;
107062
+ if (input.description !== void 0) {
107063
+ updateValues.description = input.description;
107064
+ }
107065
+ if (input.projectId !== void 0) updateValues.projectId = input.projectId;
107066
+ if (input.customerId !== void 0) {
107067
+ updateValues.customerId = input.customerId;
107068
+ }
107069
+ if (input.assigneeId !== void 0) updateValues.userId = input.assigneeId;
107070
+ if (input.type !== void 0) {
107071
+ updateValues.type = mapInputType(input.type);
107072
+ }
107073
+ if (input.dueDate !== void 0 || input.startsAt !== void 0 || input.endsAt !== void 0) {
107074
+ const currentType = input.type ?? mapOutputType(existing.type, existing.metadata);
107075
+ const { startTime, endTime, allDay } = resolveEventTimes({
107076
+ dueDate: input.dueDate,
107077
+ startsAt: input.startsAt ?? existing.startTime,
107078
+ endsAt: input.endsAt ?? existing.endTime ?? void 0,
107079
+ type: currentType
107080
+ });
107081
+ updateValues.startTime = startTime;
107082
+ updateValues.endTime = endTime;
107083
+ updateValues.allDay = allDay;
107084
+ }
107085
+ const existingMeta = existing.metadata && typeof existing.metadata === "object" && !Array.isArray(existing.metadata) ? existing.metadata : {};
107086
+ const nextMeta = { ...existingMeta };
107087
+ if (input.type !== void 0) nextMeta.calendarType = input.type;
107088
+ if (input.status === "completed") {
107089
+ nextMeta.completed = true;
107090
+ updateValues.status = "confirmed";
107091
+ } else if (input.status === "cancelled") {
107092
+ await db.update(schema_exports.timesheetEvents).set({
107093
+ isDeleted: true,
107094
+ deletedAt: sql`NOW()`,
107095
+ updatedAt: sql`NOW()`
107096
+ }).where(
107097
+ and(
107098
+ eq(schema_exports.timesheetEvents.id, input.id),
107099
+ eq(schema_exports.timesheetEvents.teamId, teamId)
107100
+ )
107101
+ );
107102
+ const cancelled = await db.select({ id: schema_exports.timesheetEvents.id }).from(schema_exports.timesheetEvents).where(eq(schema_exports.timesheetEvents.id, input.id)).limit(1);
107103
+ if (cancelled[0]) {
107104
+ return {
107105
+ content: [
107106
+ {
107107
+ type: "text",
107108
+ text: `\u2705 Calendar item ${input.id} cancelled (soft-deleted).`
107109
+ }
107110
+ ]
107111
+ };
107112
+ }
107113
+ } else if (input.status === "draft") {
107114
+ updateValues.status = "draft";
107115
+ delete nextMeta.completed;
107116
+ } else if (input.status === "scheduled") {
107117
+ updateValues.status = "confirmed";
107118
+ delete nextMeta.completed;
107119
+ }
107120
+ updateValues.metadata = nextMeta;
107121
+ await db.update(schema_exports.timesheetEvents).set(updateValues).where(
107122
+ and(
107123
+ eq(schema_exports.timesheetEvents.id, input.id),
107124
+ eq(schema_exports.timesheetEvents.teamId, teamId)
107125
+ )
107126
+ );
107127
+ if (input.ticketId !== void 0) {
107128
+ await db.delete(schema_exports.timesheetEventTickets).where(eq(schema_exports.timesheetEventTickets.timesheetEventId, input.id));
107129
+ if (input.ticketId) {
107130
+ const access = await loadAccessibleTicket(input.teamId, input.ticketId);
107131
+ if (!access.ok) return access.response;
107132
+ await db.insert(schema_exports.timesheetEventTickets).values({
107133
+ timesheetEventId: input.id,
107134
+ ticketId: input.ticketId
107135
+ });
107136
+ }
107137
+ }
107138
+ const item = await loadCalendarItemById(teamId, input.id);
107139
+ return {
107140
+ content: [
107141
+ {
107142
+ type: "text",
107143
+ text: `\u2705 **Calendar item updated**
106441
107144
 
106442
- Ask the user which provider to use (or infer it from the conversation), then call the tool again with the chosen \`teamId\`.`
106443
- }
106444
- ]
106445
- };
106446
- }
106447
- function notAMemberResponse(teamId) {
106448
- return {
106449
- content: [
106450
- {
106451
- type: "text",
106452
- text: `Access denied: you are not a member of team ${teamId}. Call \`get-teams\` to list the providers you can act on.`
107145
+ ` + JSON.stringify(formatCalendarItem(item), null, 2)
106453
107146
  }
106454
107147
  ]
106455
107148
  };
106456
107149
  }
106457
- async function resolveTeamId(requestedTeamId) {
107150
+ async function syncTicketDeadline(teamId, ticket, dueDate) {
106458
107151
  const ctx = getAuthContext();
106459
- if (requestedTeamId) {
106460
- const member2 = await isUserTeamMember(ctx.userId, requestedTeamId);
106461
- if (!member2) {
106462
- return { ok: false, response: notAMemberResponse(requestedTeamId) };
107152
+ if (dueDate === null) {
107153
+ const existingId = await findExistingTicketDeadline(teamId, ticket.id);
107154
+ if (existingId) {
107155
+ await db.update(schema_exports.timesheetEvents).set({ isDeleted: true, deletedAt: sql`NOW()`, updatedAt: sql`NOW()` }).where(eq(schema_exports.timesheetEvents.id, existingId));
107156
+ }
107157
+ return "deadline removed";
107158
+ }
107159
+ if (dueDate) {
107160
+ const title = `Deadline: ${ticket.title}`;
107161
+ const { startTime, endTime, allDay } = parseDueDate(dueDate);
107162
+ const assigneeId = ticket.assigneeId ?? ctx.userId;
107163
+ const existingId = await findExistingTicketDeadline(teamId, ticket.id);
107164
+ if (existingId) {
107165
+ await db.update(schema_exports.timesheetEvents).set({
107166
+ title,
107167
+ startTime,
107168
+ endTime,
107169
+ allDay,
107170
+ projectId: ticket.projectId,
107171
+ customerId: ticket.customerId,
107172
+ userId: assigneeId,
107173
+ type: "deadline",
107174
+ status: "confirmed",
107175
+ metadata: {
107176
+ calendarType: "deadline",
107177
+ source: "mcp_ticket_due_date"
107178
+ },
107179
+ updatedAt: sql`NOW()`
107180
+ }).where(eq(schema_exports.timesheetEvents.id, existingId));
107181
+ return "deadline updated";
106463
107182
  }
106464
- return { ok: true, teamId: requestedTeamId };
106465
- }
106466
- const teams2 = await getUserProviderTeams(ctx.userId);
106467
- if (teams2.length === 0) {
106468
- return { ok: true, teamId: ctx.teamId };
106469
- }
106470
- if (teams2.length === 1) {
106471
- return { ok: true, teamId: teams2[0].id };
107183
+ const [created] = await db.insert(schema_exports.timesheetEvents).values({
107184
+ teamId,
107185
+ userId: assigneeId,
107186
+ title,
107187
+ type: "deadline",
107188
+ status: "confirmed",
107189
+ startTime,
107190
+ endTime,
107191
+ allDay,
107192
+ projectId: ticket.projectId,
107193
+ customerId: ticket.customerId,
107194
+ metadata: {
107195
+ calendarType: "deadline",
107196
+ source: "mcp_ticket_due_date"
107197
+ },
107198
+ billingStatus: "unbillable"
107199
+ }).returning({ id: schema_exports.timesheetEvents.id });
107200
+ if (created) {
107201
+ await db.insert(schema_exports.timesheetEventTickets).values({
107202
+ timesheetEventId: created.id,
107203
+ ticketId: ticket.id
107204
+ });
107205
+ }
107206
+ return "deadline created";
106472
107207
  }
106473
- return { ok: false, response: teamSelectionResponse(teams2) };
106474
- }
106475
- async function resolveTeamScope(requestedTeamId) {
106476
- const ctx = getAuthContext();
106477
- if (requestedTeamId) {
106478
- const member2 = await isUserTeamMember(ctx.userId, requestedTeamId);
106479
- if (!member2) {
106480
- return { ok: false, response: notAMemberResponse(requestedTeamId) };
107208
+ if (ticket.status === "resolved" || ticket.status === "closed") {
107209
+ const existingId = await findExistingTicketDeadline(teamId, ticket.id);
107210
+ if (existingId) {
107211
+ const [existing] = await db.select({ metadata: schema_exports.timesheetEvents.metadata }).from(schema_exports.timesheetEvents).where(eq(schema_exports.timesheetEvents.id, existingId)).limit(1);
107212
+ const meta5 = existing?.metadata && typeof existing.metadata === "object" && !Array.isArray(existing.metadata) ? existing.metadata : {};
107213
+ await db.update(schema_exports.timesheetEvents).set({
107214
+ metadata: { ...meta5, completed: true },
107215
+ updatedAt: sql`NOW()`
107216
+ }).where(eq(schema_exports.timesheetEvents.id, existingId));
107217
+ return "deadline marked completed";
106481
107218
  }
106482
- const [teamIds2, projectIds2, customerIds2] = await Promise.all([
106483
- getAccessibleTeamIds(requestedTeamId),
106484
- getAccessibleProjectIds(ctx.userId, requestedTeamId),
106485
- getAccessibleCustomerIds(requestedTeamId)
106486
- ]);
106487
- return { ok: true, teamIds: teamIds2, projectIds: projectIds2, customerIds: customerIds2 };
106488
107219
  }
106489
- const [teamIds, projectIds, customerIds] = await Promise.all([
106490
- getUserAccessibleTeamIds(ctx.userId),
106491
- getUserAccessibleProjectIds(ctx.userId),
106492
- getUserAccessibleCustomerIds(ctx.userId)
106493
- ]);
106494
- return { ok: true, teamIds, projectIds, customerIds };
107220
+ return null;
106495
107221
  }
106496
107222
 
106497
107223
  // src/tools/customers.ts
@@ -118800,38 +119526,6 @@ var storage = new Proxy({}, {
118800
119526
  }
118801
119527
  });
118802
119528
 
118803
- // src/tools/ticket-access.ts
118804
- function notFoundResponse(ticketId) {
118805
- return {
118806
- content: [
118807
- {
118808
- type: "text",
118809
- text: `Ticket not found or no access: ${ticketId}. Call get-tickets to find the correct ticket.`
118810
- }
118811
- ]
118812
- };
118813
- }
118814
- async function loadAccessibleTicket(requestedTeamId, ticketId) {
118815
- const scope = await resolveTeamScope(requestedTeamId);
118816
- if (!scope.ok) return scope;
118817
- const [ticket] = await db.select({
118818
- id: schema_exports.tickets.id,
118819
- teamId: schema_exports.tickets.teamId,
118820
- projectId: schema_exports.tickets.projectId,
118821
- customerId: schema_exports.tickets.customerId,
118822
- ticketNumber: schema_exports.tickets.ticketNumber,
118823
- title: schema_exports.tickets.title,
118824
- status: schema_exports.tickets.status,
118825
- priority: schema_exports.tickets.priority,
118826
- type: schema_exports.tickets.type,
118827
- assigneeId: schema_exports.tickets.assigneeId
118828
- }).from(schema_exports.tickets).where(eq(schema_exports.tickets.id, ticketId)).limit(1);
118829
- if (!ticket) return { ok: false, response: notFoundResponse(ticketId) };
118830
- const hasAccess = scope.teamIds.includes(ticket.teamId) || !!ticket.projectId && scope.projectIds.includes(ticket.projectId) || !!ticket.customerId && scope.customerIds.includes(ticket.customerId);
118831
- if (!hasAccess) return { ok: false, response: notFoundResponse(ticketId) };
118832
- return { ok: true, ticket };
118833
- }
118834
-
118835
119529
  // src/tools/ticket-attachments.ts
118836
119530
  var ALLOWED_IMAGE_TYPES = [
118837
119531
  "image/jpeg",
@@ -119182,6 +119876,262 @@ ${rendered}`
119182
119876
  };
119183
119877
  }
119184
119878
 
119879
+ // src/tools/ticket-tags.ts
119880
+ function normalizeTagName(name21) {
119881
+ return name21.toLowerCase().trim();
119882
+ }
119883
+ function formatTagList(tags2) {
119884
+ if (tags2.length === 0) return "";
119885
+ return tags2.map((t8) => t8.name).join(", ");
119886
+ }
119887
+ async function getTagsForTickets(ticketIds) {
119888
+ const result = /* @__PURE__ */ new Map();
119889
+ if (ticketIds.length === 0) return result;
119890
+ const rows = await db.select({
119891
+ ticketId: schema_exports.ticketTags.ticketId,
119892
+ id: schema_exports.tags.id,
119893
+ name: schema_exports.tags.name
119894
+ }).from(schema_exports.ticketTags).innerJoin(schema_exports.tags, eq(schema_exports.tags.id, schema_exports.ticketTags.tagId)).where(inArray(schema_exports.ticketTags.ticketId, ticketIds)).orderBy(schema_exports.tags.name);
119895
+ for (const row of rows) {
119896
+ const existing = result.get(row.ticketId) ?? [];
119897
+ existing.push({ id: row.id, name: row.name });
119898
+ result.set(row.ticketId, existing);
119899
+ }
119900
+ return result;
119901
+ }
119902
+ async function resolveTagFilterIds(teamId, input) {
119903
+ const ids = new Set(input.tagIds ?? []);
119904
+ const names = [
119905
+ ...input.tag ? [input.tag] : [],
119906
+ ...input.tags ?? []
119907
+ ].map(normalizeTagName).filter(Boolean);
119908
+ if (names.length > 0) {
119909
+ const nameConditions = names.map(
119910
+ (name21) => sql`lower(${schema_exports.tags.name}) = ${name21}`
119911
+ );
119912
+ const rows = await db.select({ id: schema_exports.tags.id }).from(schema_exports.tags).where(and(eq(schema_exports.tags.teamId, teamId), or(...nameConditions)));
119913
+ for (const row of rows) ids.add(row.id);
119914
+ }
119915
+ return [...ids];
119916
+ }
119917
+ async function resolveTags(teamId, input) {
119918
+ const tags2 = [];
119919
+ const errors = [];
119920
+ const seenIds = /* @__PURE__ */ new Set();
119921
+ if (input.tagIds?.length) {
119922
+ const rows = await db.select({ id: schema_exports.tags.id, name: schema_exports.tags.name }).from(schema_exports.tags).where(
119923
+ and(
119924
+ eq(schema_exports.tags.teamId, teamId),
119925
+ inArray(schema_exports.tags.id, input.tagIds)
119926
+ )
119927
+ );
119928
+ for (const row of rows) {
119929
+ if (seenIds.has(row.id)) continue;
119930
+ seenIds.add(row.id);
119931
+ tags2.push(row);
119932
+ }
119933
+ for (const id of input.tagIds) {
119934
+ if (!seenIds.has(id)) errors.push(`Unknown tag ID: ${id}`);
119935
+ }
119936
+ }
119937
+ const rawNames = input.tagNames ?? [];
119938
+ if (rawNames.length === 0) return { tags: tags2, errors };
119939
+ const normalizedNames = [
119940
+ ...new Set(rawNames.map(normalizeTagName).filter(Boolean))
119941
+ ];
119942
+ if (normalizedNames.length === 0) return { tags: tags2, errors };
119943
+ const nameConditions = normalizedNames.map(
119944
+ (name21) => sql`lower(${schema_exports.tags.name}) = ${name21}`
119945
+ );
119946
+ const existing = await db.select({ id: schema_exports.tags.id, name: schema_exports.tags.name }).from(schema_exports.tags).where(and(eq(schema_exports.tags.teamId, teamId), or(...nameConditions)));
119947
+ const existingByNorm = /* @__PURE__ */ new Map();
119948
+ for (const tag of existing) {
119949
+ existingByNorm.set(normalizeTagName(tag.name), tag);
119950
+ }
119951
+ for (const rawName of rawNames) {
119952
+ const norm = normalizeTagName(rawName);
119953
+ if (!norm) continue;
119954
+ const found = existingByNorm.get(norm);
119955
+ if (found) {
119956
+ if (!seenIds.has(found.id)) {
119957
+ seenIds.add(found.id);
119958
+ tags2.push(found);
119959
+ }
119960
+ continue;
119961
+ }
119962
+ if (!input.createMissing) {
119963
+ errors.push(`Tag not found: ${rawName}`);
119964
+ continue;
119965
+ }
119966
+ try {
119967
+ const [created] = await db.insert(schema_exports.tags).values({
119968
+ teamId,
119969
+ name: norm,
119970
+ projectId: input.projectId ?? null
119971
+ }).returning({ id: schema_exports.tags.id, name: schema_exports.tags.name });
119972
+ if (created) {
119973
+ seenIds.add(created.id);
119974
+ tags2.push(created);
119975
+ existingByNorm.set(norm, created);
119976
+ }
119977
+ } catch {
119978
+ const [retry2] = await db.select({ id: schema_exports.tags.id, name: schema_exports.tags.name }).from(schema_exports.tags).where(
119979
+ and(
119980
+ eq(schema_exports.tags.teamId, teamId),
119981
+ sql`lower(${schema_exports.tags.name}) = ${norm}`,
119982
+ input.projectId ? or(
119983
+ eq(schema_exports.tags.projectId, input.projectId),
119984
+ isNull(schema_exports.tags.projectId)
119985
+ ) : isNull(schema_exports.tags.projectId)
119986
+ )
119987
+ ).limit(1);
119988
+ if (retry2 && !seenIds.has(retry2.id)) {
119989
+ seenIds.add(retry2.id);
119990
+ tags2.push(retry2);
119991
+ existingByNorm.set(norm, retry2);
119992
+ } else {
119993
+ errors.push(`Failed to create tag: ${rawName}`);
119994
+ }
119995
+ }
119996
+ }
119997
+ return { tags: tags2, errors };
119998
+ }
119999
+ async function syncTicketTags(ticketId, teamId, tagIds, mode = "replace") {
120000
+ const current = await db.select({ tagId: schema_exports.ticketTags.tagId }).from(schema_exports.ticketTags).where(eq(schema_exports.ticketTags.ticketId, ticketId));
120001
+ const currentIds = new Set(current.map((row) => row.tagId));
120002
+ let targetIds;
120003
+ if (mode === "remove") {
120004
+ targetIds = new Set(
120005
+ [...currentIds].filter((id) => !tagIds.includes(id))
120006
+ );
120007
+ } else if (mode === "merge") {
120008
+ targetIds = /* @__PURE__ */ new Set([...currentIds, ...tagIds]);
120009
+ } else {
120010
+ targetIds = new Set(tagIds);
120011
+ }
120012
+ const toInsert = [...targetIds].filter((id) => !currentIds.has(id));
120013
+ const toDelete = [...currentIds].filter((id) => !targetIds.has(id));
120014
+ if (toDelete.length > 0) {
120015
+ await db.delete(schema_exports.ticketTags).where(
120016
+ and(
120017
+ eq(schema_exports.ticketTags.ticketId, ticketId),
120018
+ inArray(schema_exports.ticketTags.tagId, toDelete)
120019
+ )
120020
+ );
120021
+ }
120022
+ if (toInsert.length > 0) {
120023
+ await db.insert(schema_exports.ticketTags).values(
120024
+ toInsert.map((tagId) => ({
120025
+ ticketId,
120026
+ tagId,
120027
+ teamId
120028
+ }))
120029
+ );
120030
+ }
120031
+ const tagIdsToDescribe = [.../* @__PURE__ */ new Set([...toInsert, ...toDelete])];
120032
+ const tagNamesById = /* @__PURE__ */ new Map();
120033
+ if (tagIdsToDescribe.length > 0) {
120034
+ const rows = await db.select({ id: schema_exports.tags.id, name: schema_exports.tags.name }).from(schema_exports.tags).where(inArray(schema_exports.tags.id, tagIdsToDescribe));
120035
+ for (const row of rows) tagNamesById.set(row.id, row.name);
120036
+ }
120037
+ return {
120038
+ added: toInsert.map((id) => tagNamesById.get(id) ?? id),
120039
+ removed: toDelete.map((id) => tagNamesById.get(id) ?? id)
120040
+ };
120041
+ }
120042
+
120043
+ // src/tools/tags.ts
120044
+ async function handleGetTags(input) {
120045
+ const resolved = await resolveTeamId(input.teamId);
120046
+ if (!resolved.ok) return resolved.response;
120047
+ const filters = [eq(schema_exports.tags.teamId, resolved.teamId)];
120048
+ if (input.projectId !== void 0) {
120049
+ filters.push(
120050
+ or(
120051
+ eq(schema_exports.tags.projectId, input.projectId),
120052
+ isNull(schema_exports.tags.projectId)
120053
+ )
120054
+ );
120055
+ }
120056
+ const rows = await db.select({
120057
+ id: schema_exports.tags.id,
120058
+ name: schema_exports.tags.name,
120059
+ projectId: schema_exports.tags.projectId,
120060
+ createdAt: schema_exports.tags.createdAt
120061
+ }).from(schema_exports.tags).where(and(...filters)).orderBy(asc(schema_exports.tags.name)).limit(Math.min(input.pageSize ?? 100, 200));
120062
+ return {
120063
+ content: [
120064
+ {
120065
+ type: "text",
120066
+ text: `Found ${rows.length} tags (team-specific${input.projectId ? ", including general tags for this project" : ""}):
120067
+
120068
+ ` + (rows.map(
120069
+ (tag) => `**${tag.name}** (id: ${tag.id})${tag.projectId ? " [project-specific]" : " [general]"}`
120070
+ ).join("\n") || "No tags found.")
120071
+ }
120072
+ ]
120073
+ };
120074
+ }
120075
+ async function handleCreateTag(input) {
120076
+ const name21 = input.name.trim();
120077
+ if (!name21) {
120078
+ return {
120079
+ content: [{ type: "text", text: "Tag name is required." }]
120080
+ };
120081
+ }
120082
+ const resolved = await resolveTeamId(input.teamId);
120083
+ if (!resolved.ok) return resolved.response;
120084
+ const normalized = normalizeTagName(name21);
120085
+ const scopeFilter = input.projectId ? eq(schema_exports.tags.projectId, input.projectId) : isNull(schema_exports.tags.projectId);
120086
+ const [existing] = await db.select({ id: schema_exports.tags.id, name: schema_exports.tags.name }).from(schema_exports.tags).where(
120087
+ and(
120088
+ eq(schema_exports.tags.teamId, resolved.teamId),
120089
+ scopeFilter,
120090
+ sql`lower(${schema_exports.tags.name}) = ${normalized}`
120091
+ )
120092
+ ).limit(1);
120093
+ if (existing) {
120094
+ return {
120095
+ content: [
120096
+ {
120097
+ type: "text",
120098
+ text: `\u2139\uFE0F Tag already exists (case-insensitive match):
120099
+
120100
+ Name: **${existing.name}**
120101
+ ID: ${existing.id}`
120102
+ }
120103
+ ]
120104
+ };
120105
+ }
120106
+ const [created] = await db.insert(schema_exports.tags).values({
120107
+ teamId: resolved.teamId,
120108
+ name: name21,
120109
+ projectId: input.projectId ?? null
120110
+ }).returning({
120111
+ id: schema_exports.tags.id,
120112
+ name: schema_exports.tags.name,
120113
+ projectId: schema_exports.tags.projectId
120114
+ });
120115
+ if (!created) {
120116
+ return {
120117
+ content: [{ type: "text", text: "Failed to create tag." }]
120118
+ };
120119
+ }
120120
+ return {
120121
+ content: [
120122
+ {
120123
+ type: "text",
120124
+ text: `\u2705 **Tag Created**
120125
+
120126
+ Name: **${created.name}**
120127
+ ID: ${created.id}
120128
+ ${created.projectId ? `Project ID: ${created.projectId}
120129
+ ` : "Scope: general (team-wide)\n"}`
120130
+ }
120131
+ ]
120132
+ };
120133
+ }
120134
+
119185
120135
  // src/tools/ticket-update.ts
119186
120136
  async function handleUpdateTicket(input) {
119187
120137
  const ctx = getAuthContext();
@@ -119227,6 +120177,34 @@ async function handleUpdateTicket(input) {
119227
120177
  if (input.estimatedHours !== void 0) {
119228
120178
  updateValues.estimatedHours = input.estimatedHours;
119229
120179
  }
120180
+ let deadlineChange = null;
120181
+ if (input.dueDate !== void 0) {
120182
+ deadlineChange = await syncTicketDeadline(
120183
+ ticket.teamId,
120184
+ {
120185
+ id: ticket.id,
120186
+ title: input.title ?? ticket.title,
120187
+ status: input.status ?? ticket.status,
120188
+ projectId: input.projectId ?? ticket.projectId,
120189
+ customerId: input.customerId ?? ticket.customerId,
120190
+ assigneeId: input.assigneeId !== void 0 ? input.assigneeId : ticket.assigneeId
120191
+ },
120192
+ input.dueDate
120193
+ );
120194
+ } else if (input.status !== void 0 && (input.status === "resolved" || input.status === "closed") && input.status !== ticket.status) {
120195
+ deadlineChange = await syncTicketDeadline(
120196
+ ticket.teamId,
120197
+ {
120198
+ id: ticket.id,
120199
+ title: input.title ?? ticket.title,
120200
+ status: input.status,
120201
+ projectId: input.projectId ?? ticket.projectId,
120202
+ customerId: input.customerId ?? ticket.customerId,
120203
+ assigneeId: input.assigneeId !== void 0 ? input.assigneeId : ticket.assigneeId
120204
+ },
120205
+ void 0
120206
+ );
120207
+ }
119230
120208
  await db.update(schema_exports.tickets).set(updateValues).where(eq(schema_exports.tickets.id, ticket.id));
119231
120209
  const changes = [];
119232
120210
  const activities2 = [];
@@ -119271,6 +120249,61 @@ async function handleUpdateTicket(input) {
119271
120249
  if (input.projectId !== void 0) changes.push("project updated");
119272
120250
  if (input.customerId !== void 0) changes.push("customer updated");
119273
120251
  if (input.estimatedHours !== void 0) changes.push("estimated hours updated");
120252
+ if (deadlineChange) changes.push(deadlineChange);
120253
+ if (input.tags !== void 0 || input.tagIds !== void 0) {
120254
+ const tagMode = input.tagMode ?? "replace";
120255
+ const createMissing = tagMode === "replace" || tagMode === "merge";
120256
+ const resolvedTags = await resolveTags(ticket.teamId, {
120257
+ tagNames: input.tags,
120258
+ tagIds: input.tagIds,
120259
+ projectId: input.projectId ?? ticket.projectId,
120260
+ createMissing
120261
+ });
120262
+ if (resolvedTags.errors.length > 0) {
120263
+ return {
120264
+ content: [
120265
+ {
120266
+ type: "text",
120267
+ text: `Cannot update tags on ${ticket.ticketNumber}:
120268
+ ` + resolvedTags.errors.map((e6) => ` \u2022 ${e6}`).join("\n")
120269
+ }
120270
+ ]
120271
+ };
120272
+ }
120273
+ const { added, removed } = await syncTicketTags(
120274
+ ticket.id,
120275
+ ticket.teamId,
120276
+ resolvedTags.tags.map((t8) => t8.id),
120277
+ tagMode
120278
+ );
120279
+ if (added.length > 0) {
120280
+ changes.push(`tags added: ${added.join(", ")}`);
120281
+ for (const tagName of added) {
120282
+ await db.insert(schema_exports.ticketActivity).values({
120283
+ ticketId: ticket.id,
120284
+ teamId: ticket.teamId,
120285
+ userId: ctx.userId,
120286
+ activityType: "tag_added",
120287
+ newValue: tagName
120288
+ });
120289
+ }
120290
+ }
120291
+ if (removed.length > 0) {
120292
+ changes.push(`tags removed: ${removed.join(", ")}`);
120293
+ for (const tagName of removed) {
120294
+ await db.insert(schema_exports.ticketActivity).values({
120295
+ ticketId: ticket.id,
120296
+ teamId: ticket.teamId,
120297
+ userId: ctx.userId,
120298
+ activityType: "tag_removed",
120299
+ newValue: tagName
120300
+ });
120301
+ }
120302
+ }
120303
+ if (tagMode === "replace" && added.length === 0 && removed.length === 0) {
120304
+ changes.push("tags cleared");
120305
+ }
120306
+ }
119274
120307
  for (const activity of activities2) {
119275
120308
  await db.insert(schema_exports.ticketActivity).values({
119276
120309
  ticketId: ticket.id,
@@ -119330,7 +120363,17 @@ async function downloadImageAsBase64(storageKey) {
119330
120363
  }
119331
120364
  async function handleGetTickets(input) {
119332
120365
  const ctx = getAuthContext();
119333
- const { status, priority, projectId, customerId, q: q3, pageSize = 20 } = input;
120366
+ const {
120367
+ status,
120368
+ priority,
120369
+ projectId,
120370
+ customerId,
120371
+ q: q3,
120372
+ tag,
120373
+ tags: tags2,
120374
+ tagIds,
120375
+ pageSize = 20
120376
+ } = input;
119334
120377
  const resolved = await resolveTeamId(input.teamId);
119335
120378
  if (!resolved.ok) return resolved.response;
119336
120379
  const teamId = resolved.teamId;
@@ -119357,6 +120400,29 @@ async function handleGetTickets(input) {
119357
120400
  )
119358
120401
  );
119359
120402
  }
120403
+ const filterTagIds = await resolveTagFilterIds(teamId, { tag, tags: tags2, tagIds });
120404
+ if (tag || tags2?.length || tagIds?.length) {
120405
+ if (filterTagIds.length === 0) {
120406
+ return {
120407
+ content: [
120408
+ {
120409
+ type: "text",
120410
+ text: "No tickets found (no matching tags for the given filter)."
120411
+ }
120412
+ ]
120413
+ };
120414
+ }
120415
+ filters.push(
120416
+ sql`EXISTS (
120417
+ SELECT 1 FROM ${schema_exports.ticketTags}
120418
+ WHERE ${schema_exports.ticketTags.ticketId} = ${schema_exports.tickets.id}
120419
+ AND ${schema_exports.ticketTags.tagId} IN (${sql.join(
120420
+ filterTagIds.map((id) => sql`${id}`),
120421
+ sql`, `
120422
+ )})
120423
+ )`
120424
+ );
120425
+ }
119360
120426
  const rows = await db.select({
119361
120427
  id: schema_exports.tickets.id,
119362
120428
  ticketNumber: schema_exports.tickets.ticketNumber,
@@ -119374,20 +120440,23 @@ async function handleGetTickets(input) {
119374
120440
  schema_exports.customers,
119375
120441
  eq(schema_exports.customers.id, schema_exports.tickets.customerId)
119376
120442
  ).where(and(...filters)).orderBy(desc(schema_exports.tickets.createdAt)).limit(Math.min(pageSize, 100));
120443
+ const tagsByTicket = await getTagsForTickets(rows.map((t8) => t8.id));
119377
120444
  return {
119378
120445
  content: [
119379
120446
  {
119380
120447
  type: "text",
119381
120448
  text: `Found ${rows.length} tickets:
119382
120449
 
119383
- ${rows.map(
119384
- (t8) => `**${t8.ticketNumber}**: ${t8.title}
120450
+ ${rows.map((t8) => {
120451
+ const ticketTags2 = tagsByTicket.get(t8.id) ?? [];
120452
+ return `**${t8.ticketNumber}**: ${t8.title}
119385
120453
  Status: ${t8.status} | Priority: ${t8.priority}
119386
- ${t8.projectName ? `Project: ${t8.projectName}
120454
+ ${ticketTags2.length > 0 ? `Tags: ${formatTagList(ticketTags2)}
120455
+ ` : ""}${t8.projectName ? `Project: ${t8.projectName}
119387
120456
  ` : ""}${t8.customerName ? `Customer: ${t8.customerName}
119388
120457
  ` : ""}Created: ${new Date(t8.createdAt).toLocaleDateString()}
119389
- `
119390
- ).join("\n") || "No tickets found."}`
120458
+ `;
120459
+ }).join("\n") || "No tickets found."}`
119391
120460
  }
119392
120461
  ]
119393
120462
  };
@@ -119500,6 +120569,11 @@ ${text3.split("\n").map((l4) => ` ${l4}`).join("\n")}`;
119500
120569
  const assigneeLine = ticketRow.assignee ? `Assignee: ${ticketRow.assignee.fullName || "Unknown"} [id: ${ticketRow.assignee.id}]
119501
120570
  ` : ticketRow.requester ? `Assignee: (unassigned) \u2014 use requester id ${ticketRow.requester.id} for review handoff
119502
120571
  ` : `Assignee: (unassigned)
120572
+ `;
120573
+ const ticketTagRows = await getTagsForTickets([id]);
120574
+ const ticketTags2 = ticketTagRows.get(id) ?? [];
120575
+ const tagsLine = ticketTags2.length > 0 ? `Tags: ${formatTagList(ticketTags2)}
120576
+ ` : `Tags: (none)
119503
120577
  `;
119504
120578
  const content = [
119505
120579
  {
@@ -119515,7 +120589,7 @@ Type: ${ticketRow.type}
119515
120589
  ${ticketRow.description ? `Description: ${tiptapToPlainText(ticketRow.description)}
119516
120590
  ` : ""}${ticketRow.project?.name ? `Project: ${ticketRow.project.name}
119517
120591
  ` : ""}${ticketRow.customer?.name ? `Customer: ${ticketRow.customer.name}
119518
- ` : ""}` + assigneeLine + requesterLine + `Created: ${new Date(ticketRow.createdAt).toLocaleDateString()}
120592
+ ` : ""}` + tagsLine + assigneeLine + requesterLine + `Created: ${new Date(ticketRow.createdAt).toLocaleDateString()}
119519
120593
  ` + attachmentList + commentList
119520
120594
  }
119521
120595
  ];
@@ -119592,7 +120666,9 @@ async function handleCreateTicket(input) {
119592
120666
  priority = "medium",
119593
120667
  type = "task",
119594
120668
  projectId,
119595
- customerId
120669
+ customerId,
120670
+ tags: tags2,
120671
+ tagIds
119596
120672
  } = input;
119597
120673
  const resolved = await resolveTeamId(input.teamId);
119598
120674
  if (!resolved.ok) return resolved.response;
@@ -119649,7 +120725,7 @@ async function handleCreateTicket(input) {
119649
120725
  const count = Number(countRow?.n ?? 0);
119650
120726
  ticketNumber = `${year2}-${String(count + 1).padStart(3, "0")}`;
119651
120727
  }
119652
- await db.insert(schema_exports.tickets).values({
120728
+ const [created] = await db.insert(schema_exports.tickets).values({
119653
120729
  teamId: resolvedTeamId,
119654
120730
  ticketNumber,
119655
120731
  title,
@@ -119660,19 +120736,50 @@ async function handleCreateTicket(input) {
119660
120736
  projectId: projectId ?? null,
119661
120737
  customerId: resolvedCustomerId ?? null,
119662
120738
  requesterId: ctx.userId
120739
+ }).returning({
120740
+ id: schema_exports.tickets.id,
120741
+ ticketNumber: schema_exports.tickets.ticketNumber
119663
120742
  });
120743
+ if (!created) {
120744
+ throw new Error("Failed to create ticket");
120745
+ }
120746
+ let appliedTags = [];
120747
+ const tagErrors = [];
120748
+ if ((tags2?.length ?? 0) > 0 || (tagIds?.length ?? 0) > 0) {
120749
+ const resolvedTags = await resolveTags(resolvedTeamId, {
120750
+ tagNames: tags2,
120751
+ tagIds,
120752
+ projectId: projectId ?? null,
120753
+ createMissing: true
120754
+ });
120755
+ tagErrors.push(...resolvedTags.errors);
120756
+ if (resolvedTags.tags.length > 0) {
120757
+ await syncTicketTags(
120758
+ created.id,
120759
+ resolvedTeamId,
120760
+ resolvedTags.tags.map((t8) => t8.id),
120761
+ "replace"
120762
+ );
120763
+ appliedTags = resolvedTags.tags;
120764
+ }
120765
+ }
119664
120766
  return {
119665
120767
  content: [
119666
120768
  {
119667
120769
  type: "text",
119668
120770
  text: `\u2705 **Ticket Created Successfully!**
119669
120771
 
119670
- Ticket Number: **${ticketNumber}**
120772
+ Ticket Number: **${created.ticketNumber}**
120773
+ ID: ${created.id}
119671
120774
  Title: ${title}
119672
120775
  Status: ${status}
119673
120776
  Priority: ${priority}
119674
120777
  Type: ${type}
119675
- `
120778
+ ${appliedTags.length > 0 ? `Tags: ${formatTagList(appliedTags)}
120779
+ ` : ""}${tagErrors.length > 0 ? `
120780
+ \u26A0\uFE0F Tag warnings:
120781
+ ${tagErrors.map((e6) => ` \u2022 ${e6}`).join("\n")}
120782
+ ` : ""}`
119676
120783
  }
119677
120784
  ]
119678
120785
  };
@@ -119723,6 +120830,22 @@ function createMcpServer() {
119723
120830
  return await handleCreateTicket(asToolArgs(toolArgs));
119724
120831
  case "update-ticket":
119725
120832
  return await handleUpdateTicket(asToolArgs(toolArgs));
120833
+ case "get-tags":
120834
+ return await handleGetTags(asToolArgs(toolArgs));
120835
+ case "create-tag":
120836
+ return await handleCreateTag(asToolArgs(toolArgs));
120837
+ case "get-calendar-items":
120838
+ return await handleGetCalendarItems(
120839
+ asToolArgs(toolArgs)
120840
+ );
120841
+ case "create-calendar-item":
120842
+ return await handleCreateCalendarItem(
120843
+ asToolArgs(toolArgs)
120844
+ );
120845
+ case "update-calendar-item":
120846
+ return await handleUpdateCalendarItem(
120847
+ asToolArgs(toolArgs)
120848
+ );
119726
120849
  case "add-ticket-comment":
119727
120850
  return await handleAddTicketComment(
119728
120851
  asToolArgs(toolArgs)