@mgsoftwarebv/mcp-server-bridge 3.5.12 → 3.5.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -130,6 +130,8 @@ Optional PDF compilation: pass `compilePdf: true` to trigger the `compile-docume
130
130
  - **log-hours** — log work hours as a draft agenda event (see the `/hours` Cursor command)
131
131
  - **create-time-entries** — create one or more individual dated hour lines (one timesheet event per date), single or bulk via `entries[]`. Top-level `projectId`/`ticketId`/`userId`/`description`/`billable`/`status` are shared defaults each entry can override. Skips duplicates (same user/team/date/project/description) unless `allowDuplicate`, and reports created/skipped/errored lines plus `totalHours`. Use this instead of `log-hours` when you have separate dated lines (e.g. "8h on 19/6, 8h on 16/6").
132
132
  - **get-time-entries** — read and total tracked time entries (urenregistratie), filterable by period, user, project, customer, ticket, status and type; returns accurate aggregate totals (total/billable/non-billable/invoiced hours) plus an optional grouped breakdown (day/project/customer/user/ticket). Answers questions like "how many hours did I work this month for customer X?"
133
+ - **delete-time-entries** — soft-delete one or more existing hour lines by `id`/`ids[]` (e.g. to drop a wrongly-logged lump entry before re-creating dated lines). Only draft, non-invoiced entries are removed unless `allowInvoicedOverride`; confirmed/invoiced ones are skipped. Reports deleted/skipped/errored ids plus `deletedHours`.
134
+ - **update-time-entry** — correct a single hour line by `id`: change `date`/`hours` (re-times the line), `description`, `projectId`, `ticketId`, `status` or `billable`. On confirmed/invoiced entries the financial/time fields are locked unless `allowInvoicedOverride`; description and project/ticket links stay editable.
133
135
 
134
136
  ### GitHub Code Exploration
135
137
  - **get-github-file** — read one file from the GitHub repo linked to a project
package/dist/index.js CHANGED
@@ -106586,6 +106586,24 @@ var TOOLS = [
106586
106586
  required: ["id"]
106587
106587
  }
106588
106588
  },
106589
+ {
106590
+ name: "get-document-link",
106591
+ description: "Get a safe, usable link for a document (use after create-document to share it). Never leaks an auth/session URL. Choose `linkType`:\n- dashboard (default): internal editor deep link for logged-in Refront users, plus the current PDF status and a public link when the document is already shared publicly.\n- pdf_signed (alias: pdf): a temporary signed PDF download URL when a compiled PDF exists; if it is still compiling it returns pdfStatus 'pending' with pdfUrl null and triggers a (re)compile instead of failing.\n- public_share: enables a public customer share (token + visibility=public, recompiling only when no fresh PDF exists) and returns the /p/<token> link. Only use when the document may be shared publicly. Revoke later from the dashboard.\nReturns a JSON object: { documentId, title, status, dashboardUrl, pdfStatus (ready|stale|pending|unavailable), pdfUrl, publicShareUrl, expiresAt }. Respects team/provider access; draft/private documents never expose a public share URL unless they are actually shared.",
106592
+ inputSchema: {
106593
+ type: "object",
106594
+ properties: {
106595
+ teamId: teamIdProp,
106596
+ documentId: { type: "string", description: "Document ID (UUID)" },
106597
+ linkType: {
106598
+ type: "string",
106599
+ enum: ["dashboard", "pdf_signed", "pdf", "public_share"],
106600
+ default: "dashboard",
106601
+ description: "dashboard (default) = editor URL + PDF status; pdf_signed (alias pdf) = temporary signed PDF download URL; public_share = enable + return the public /p/<token> link."
106602
+ }
106603
+ },
106604
+ required: ["documentId"]
106605
+ }
106606
+ },
106589
106607
  {
106590
106608
  name: "create-document",
106591
106609
  description: 'Create a document (proposal, report, deliverables/opleverdocument, ...) from an array of content blocks. Use type \'deliverables\' for a document describing what was delivered for a customer; optionally pass `invoiceId` to link it to an invoice so it can be sent along as a PDF attachment. Each block is `{ id?, type, data }` (ids are auto-generated when omitted). Supported block types and their `data` shapes:\n- cover: { title, subtitle?, showLogo: bool, showDate: bool, clientName?, clientCompany?, date? }\n- toc: { title, maxDepth: 1-6 }\n- heading: { text, level: 1|2|3, numbered: bool }\n- text: { content: TipTapDoc | plain string (auto-converted; supports paragraphs and "- " bullet lists) }\n- callout: { variant: "info"|"warning"|"tip"|"important", title?, content: TipTapDoc | string }\n- table: { title?, headers: string[], rows: string[][] }\n- chart: { title?, chartType: "bar"|"line"|"pie", points: [{label, value}], seriesName?, series?: [{name, color?, points: [{label, value}]}], unit? ("\u20AC"|"%"|...), showValues: bool, color? }. Multi-series (bar/line only): extra series values are matched to the primary points by index; a legend is rendered automatically.\n- pricing: { title?, items: [{description, quantity?, unit?, price}], currency: "EUR", showTotal: bool, vatRate?, includeVat: bool }\n- timeline: { title?, phases: [{name, description?, duration, deliverables?: string[]}] }\n- two_column: { left: TipTapDoc|string, right: TipTapDoc|string, ratio?: "50_50"|"60_40"|"40_60"|"70_30"|"30_70" }\n- divider: { style: "page_break"|"line"|"space" }\n- signature: { title?, description?, signers: [{id, label, name?, company?, role?}] }\nAll text content runs through a humanizer that strips AI-sounding patterns (em-dashes, clich\xE9s, filler phrases); control it with `humanize`. The result includes ready-to-use links (internal dashboard editor URL + PDF status). Use get-document-link afterwards for a PDF download link or to publish a public customer share link.',
@@ -107382,6 +107400,80 @@ var TOOLS = [
107382
107400
  required: ["entries"]
107383
107401
  }
107384
107402
  },
107403
+ {
107404
+ name: "delete-time-entries",
107405
+ description: "Delete one or more existing tracked time entries (urenregels / timesheet events) by id \u2014 e.g. to remove a wrongly-logged lump entry before re-creating it as separate dated lines with create-time-entries. Soft-deletes so the lines disappear from the agenda / urenoverzicht. Pass a single `id` or a list of `ids` (max 100). Safety: only draft, non-invoiced entries are deleted by default; confirmed or invoiced entries are skipped unless `allowInvoicedOverride` is true. Processing is per-id: the result reports deleted ids, skipped ids (reason: confirmed / invoiced / already_deleted) and per-id errors (not found / no access), plus deletedHours. Use get-time-entries to find ids first.",
107406
+ inputSchema: {
107407
+ type: "object",
107408
+ properties: {
107409
+ teamId: teamIdProp,
107410
+ id: {
107411
+ type: "string",
107412
+ description: "A single timesheet event id (UUID) to delete."
107413
+ },
107414
+ ids: {
107415
+ type: "array",
107416
+ items: { type: "string" },
107417
+ description: "Multiple timesheet event ids (UUIDs) to delete in one call (max 100). Combined with `id` if both are given."
107418
+ },
107419
+ allowInvoicedOverride: {
107420
+ type: "boolean",
107421
+ default: false,
107422
+ description: "When true, also delete confirmed and/or invoiced entries. Default false skips them (safer)."
107423
+ }
107424
+ },
107425
+ required: []
107426
+ }
107427
+ },
107428
+ {
107429
+ name: "update-time-entry",
107430
+ description: "Update a single existing tracked time entry (urenregel / timesheet event) by id: correct its date, hours, description, project link, ticket link, status or billable flag. Changing date and/or hours re-times the line (date moves it to 09:00 Europe/Amsterdam, hours sets its duration). Safety: on a confirmed or invoiced entry the financial/time fields (date, hours, status, billable) are locked unless `allowInvoicedOverride` is true; description and project/ticket links stay editable. Use get-time-entries to find the id and get-projects / get-tickets to resolve links. To remove a line use delete-time-entries; to create new lines use create-time-entries.",
107431
+ inputSchema: {
107432
+ type: "object",
107433
+ properties: {
107434
+ teamId: teamIdProp,
107435
+ id: {
107436
+ type: "string",
107437
+ description: "Timesheet event id (UUID) to update."
107438
+ },
107439
+ date: {
107440
+ type: "string",
107441
+ description: "New calendar date (YYYY-MM-DD, Europe/Amsterdam). Moves the line to 09:00 on this day."
107442
+ },
107443
+ hours: {
107444
+ type: "number",
107445
+ description: "New worked hours for the line (> 0, <= 24)."
107446
+ },
107447
+ description: {
107448
+ type: "string",
107449
+ description: "New line description (updates both the line title and description)."
107450
+ },
107451
+ projectId: {
107452
+ type: ["string", "null"],
107453
+ description: "New project UUID, or null to clear the project link."
107454
+ },
107455
+ ticketId: {
107456
+ type: ["string", "null"],
107457
+ description: "Replace the linked ticket (UUID), or null to unlink all tickets."
107458
+ },
107459
+ status: {
107460
+ type: "string",
107461
+ enum: ["draft", "confirmed", "submitted"],
107462
+ description: "New status. 'submitted' is treated as 'confirmed'."
107463
+ },
107464
+ billable: {
107465
+ type: "boolean",
107466
+ description: "New billable flag. true -> to_bill, false -> unbillable."
107467
+ },
107468
+ allowInvoicedOverride: {
107469
+ type: "boolean",
107470
+ default: false,
107471
+ description: "Allow editing locked fields (date/hours/status/billable) on a confirmed/invoiced entry."
107472
+ }
107473
+ },
107474
+ required: ["id"]
107475
+ }
107476
+ },
107385
107477
  {
107386
107478
  name: "get-trips",
107387
107479
  description: "List trips / kilometer registration entries (rides) scoped to your provider team(s), with optional filters by period (dateFrom/dateTo), user, project, customer, trip type (business/private), billing type, and invoiced status. Returns each trip's id, date, start/end location, distance (km), odometer readings, trip type, billing type, rate/amount, linked user/project/customer/invoice/vehicle, plus aggregate business/private/total km and total amount.",
@@ -109730,6 +109822,59 @@ function ensureTipTapFormat(text3) {
109730
109822
  if (isTipTapJson(text3)) return text3;
109731
109823
  return JSON.stringify(plainTextToTipTap(text3));
109732
109824
  }
109825
+
109826
+ // src/tools/document-links.ts
109827
+ function derivePdfStatus(doc) {
109828
+ const currentHash = computeDocumentContentHash({
109829
+ blocks: doc.blocks,
109830
+ branding: doc.branding,
109831
+ pageSize: doc.pageSize,
109832
+ locale: doc.locale,
109833
+ title: doc.title
109834
+ });
109835
+ if (doc.filePath) {
109836
+ if (doc.compiledHash && doc.compiledHash === currentHash) {
109837
+ return { status: "ready", currentHash };
109838
+ }
109839
+ return { status: "stale", currentHash };
109840
+ }
109841
+ if (doc.compiledHash) return { status: "pending", currentHash };
109842
+ return { status: "unavailable", currentHash };
109843
+ }
109844
+ function getDashboardBaseUrl() {
109845
+ return (process.env.DASHBOARD_URL || "https://app.refront.nl").replace(
109846
+ /\/+$/,
109847
+ ""
109848
+ );
109849
+ }
109850
+ function buildDashboardUrl(documentId) {
109851
+ return `${getDashboardBaseUrl()}/quotations/documents?editDocument=true&documentId=${encodeURIComponent(
109852
+ documentId
109853
+ )}`;
109854
+ }
109855
+ function buildPublicShareUrl(token) {
109856
+ return `${getDashboardBaseUrl()}/p/${encodeURIComponent(token)}`;
109857
+ }
109858
+ function isPubliclyShared(doc) {
109859
+ return !!doc.token && doc.shareVisibility === "public";
109860
+ }
109861
+ function formatDocumentLinksSummary(doc) {
109862
+ const { status: pdfStatus } = derivePdfStatus(doc);
109863
+ const lines = [
109864
+ "\u{1F517} Links:",
109865
+ `- Dashboard: ${buildDashboardUrl(doc.id)}`,
109866
+ `- PDF status: ${pdfStatus}`
109867
+ ];
109868
+ if (isPubliclyShared(doc) && doc.token) {
109869
+ lines.push(`- Public share: ${buildPublicShareUrl(doc.token)}`);
109870
+ }
109871
+ lines.push(
109872
+ "- Use get-document-link (linkType pdf_signed for a temporary PDF download URL, public_share to publish a public link)."
109873
+ );
109874
+ return lines.join("\n");
109875
+ }
109876
+
109877
+ // src/tools/documents.ts
109733
109878
  var DOCUMENT_TYPES = [
109734
109879
  "proposal",
109735
109880
  "plan_of_action",
@@ -109771,7 +109916,6 @@ var PDF_STATUS_LABEL = {
109771
109916
  };
109772
109917
  function buildLinksSection(doc, opts) {
109773
109918
  let status = computePdfStatus(doc);
109774
- if (opts?.compileTriggered && status !== "ready") status = "pending";
109775
109919
  const lines = [
109776
109920
  "Links:",
109777
109921
  `- Dashboard (internal, login required): ${documentDashboardUrl(doc.id)}`,
@@ -109872,7 +110016,10 @@ var TRIGGER_API_URL = process.env.TRIGGER_API_URL || "https://trigger-dev.mgsoft
109872
110016
  async function maybeTriggerPdfCompile(documentId, teamId) {
109873
110017
  const secret = process.env.TRIGGER_SECRET_KEY;
109874
110018
  if (!secret) {
109875
- return "PDF: niet gecompileerd (TRIGGER_SECRET_KEY ontbreekt); de dashboard-editor compileert bij openen/delen.";
110019
+ return {
110020
+ triggered: false,
110021
+ message: "PDF: niet gecompileerd (TRIGGER_SECRET_KEY ontbreekt); de dashboard-editor compileert bij openen/delen."
110022
+ };
109876
110023
  }
109877
110024
  try {
109878
110025
  const response = await fetch(
@@ -109888,12 +110035,21 @@ async function maybeTriggerPdfCompile(documentId, teamId) {
109888
110035
  );
109889
110036
  if (!response.ok) {
109890
110037
  const body = await response.text();
109891
- return `PDF: compile-trigger mislukt (HTTP ${response.status}): ${body.slice(0, 200)}`;
110038
+ return {
110039
+ triggered: false,
110040
+ message: `PDF: compile-trigger mislukt (HTTP ${response.status}): ${body.slice(0, 200)}`
110041
+ };
109892
110042
  }
109893
110043
  const json5 = await response.json();
109894
- return `PDF: compile-document-pdf getriggerd (run ${json5.id ?? "?"}).`;
110044
+ return {
110045
+ triggered: true,
110046
+ message: `PDF: compile-document-pdf getriggerd (run ${json5.id ?? "?"}).`
110047
+ };
109895
110048
  } catch (error49) {
109896
- return `PDF: compile-trigger mislukt: ${error49 instanceof Error ? error49.message : String(error49)}`;
110049
+ return {
110050
+ triggered: false,
110051
+ message: `PDF: compile-trigger mislukt: ${error49 instanceof Error ? error49.message : String(error49)}`
110052
+ };
109897
110053
  }
109898
110054
  }
109899
110055
  async function applyBlockOps(existing, ops, humanize) {
@@ -110078,7 +110234,7 @@ Type: ${doc.type} | Status: ${doc.status} | Locale: ${doc.locale}
110078
110234
  Team: ${doc.teamId}${doc.customerId ? ` | Customer: ${doc.customerId}` : ""}${doc.invoiceId ? ` | Invoice: ${doc.invoiceId}` : ""}
110079
110235
  Blocks (${blocks.length}): ${blockTypeSummary(blocks) || "none"}
110080
110236
 
110081
- ${buildLinksSection(doc)}
110237
+ ${formatDocumentLinksSummary(doc)}
110082
110238
 
110083
110239
  Blocks JSON:
110084
110240
  \`\`\`json
@@ -110265,22 +110421,19 @@ async function handleCreateDocument(input) {
110265
110421
  content: [{ type: "text", text: "Error: document insert failed." }]
110266
110422
  };
110267
110423
  }
110268
- const pdfLine = compilePdf ? await maybeTriggerPdfCompile(created.id, resolved.teamId) : null;
110269
- const linksSection = buildLinksSection(
110270
- {
110271
- id: created.id,
110272
- title: title.trim(),
110273
- blocks: humanized.blocks,
110274
- branding: {},
110275
- pageSize: "a4",
110276
- locale,
110277
- filePath: null,
110278
- compiledHash: null,
110279
- token: null,
110280
- shareVisibility: "private"
110281
- },
110282
- { compileTriggered: compilePdf && Boolean(process.env.TRIGGER_SECRET_KEY) }
110283
- );
110424
+ const pdfResult = compilePdf ? await maybeTriggerPdfCompile(created.id, resolved.teamId) : null;
110425
+ const linksSummary = formatDocumentLinksSummary({
110426
+ id: created.id,
110427
+ title: title.trim(),
110428
+ blocks: humanized.blocks,
110429
+ branding: {},
110430
+ pageSize: "a4",
110431
+ locale,
110432
+ filePath: null,
110433
+ compiledHash: null,
110434
+ token: null,
110435
+ shareVisibility: "private"
110436
+ });
110284
110437
  return {
110285
110438
  content: [
110286
110439
  {
@@ -110294,10 +110447,10 @@ Team: ${resolved.teamId}${customerId ? ` | Customer: ${customerId}` : ""}
110294
110447
  ${invoiceLine ? `${invoiceLine}
110295
110448
  ` : ""}Blocks (${humanized.blocks.length}): ${blockTypeSummary(humanized.blocks)}
110296
110449
 
110297
- ${humanized.summary}${pdfLine ? `
110298
- ${pdfLine}` : ""}
110450
+ ${humanized.summary}${pdfResult ? `
110451
+ ${pdfResult.message}` : ""}
110299
110452
 
110300
- ` + linksSection
110453
+ ${linksSummary}`
110301
110454
  }
110302
110455
  ]
110303
110456
  };
@@ -110459,23 +110612,20 @@ async function handleUpdateDocument(input) {
110459
110612
  };
110460
110613
  }
110461
110614
  await db.update(schema_exports.documents).set(patch).where(eq(schema_exports.documents.id, doc.id));
110462
- const pdfLine = compilePdf ? await maybeTriggerPdfCompile(doc.id, doc.teamId) : null;
110463
- const linksSection = buildLinksSection(
110464
- {
110465
- id: doc.id,
110466
- title: patch.title ?? doc.title,
110467
- status: patch.status ?? doc.status,
110468
- blocks: patch.blocks ?? doc.blocks,
110469
- branding: doc.branding,
110470
- pageSize: doc.pageSize,
110471
- locale: doc.locale,
110472
- filePath: doc.filePath,
110473
- compiledHash: doc.compiledHash,
110474
- token: doc.token,
110475
- shareVisibility: doc.shareVisibility
110476
- },
110477
- { compileTriggered: compilePdf && Boolean(process.env.TRIGGER_SECRET_KEY) }
110478
- );
110615
+ const pdfResult = compilePdf ? await maybeTriggerPdfCompile(doc.id, doc.teamId) : null;
110616
+ const linksSummary = formatDocumentLinksSummary({
110617
+ id: doc.id,
110618
+ title: patch.title ?? doc.title,
110619
+ status: patch.status ?? doc.status,
110620
+ blocks: "blocks" in patch ? patch.blocks : doc.blocks,
110621
+ branding: doc.branding,
110622
+ pageSize: doc.pageSize,
110623
+ locale: doc.locale,
110624
+ filePath: doc.filePath,
110625
+ compiledHash: doc.compiledHash,
110626
+ token: doc.token,
110627
+ shareVisibility: doc.shareVisibility
110628
+ });
110479
110629
  return {
110480
110630
  content: [
110481
110631
  {
@@ -110485,10 +110635,10 @@ async function handleUpdateDocument(input) {
110485
110635
  ID: ${doc.id}
110486
110636
  ${changeLines.map((l4) => `- ${l4}`).join("\n")}${humanizeSummary ? `
110487
110637
 
110488
- ${humanizeSummary}` : ""}${pdfLine ? `
110489
- ${pdfLine}` : ""}
110638
+ ${humanizeSummary}` : ""}${pdfResult ? `
110639
+ ${pdfResult.message}` : ""}
110490
110640
 
110491
- ` + linksSection
110641
+ ${linksSummary}`
110492
110642
  }
110493
110643
  ]
110494
110644
  };
@@ -115011,6 +115161,271 @@ async function handleCreateTimeEntries(input) {
115011
115161
  }
115012
115162
  });
115013
115163
  }
115164
+ var MAX_DELETE_ENTRIES = 100;
115165
+ function rowDurationSeconds(row) {
115166
+ if (row.startTime && row.endTime) {
115167
+ const seconds = (new Date(row.endTime).getTime() - new Date(row.startTime).getTime()) / 1e3;
115168
+ return Math.max(0, Math.round(seconds));
115169
+ }
115170
+ return Math.max(0, Math.round(toNumber(row.trackedDuration)));
115171
+ }
115172
+ function rowDurationHours(row) {
115173
+ return Math.round(rowDurationSeconds(row) / 3600 * 100) / 100;
115174
+ }
115175
+ function isInvoicedRow(row) {
115176
+ return row.invoiceId != null || row.billingStatus === "billed" || row.billingStatus === "draft_billed";
115177
+ }
115178
+ async function handleDeleteTimeEntries(input) {
115179
+ const te = schema_exports.timesheetEvents;
115180
+ const ids = [
115181
+ ...new Set(
115182
+ [input.id, ...Array.isArray(input.ids) ? input.ids : []].filter(
115183
+ (value) => typeof value === "string" && value.trim().length > 0
115184
+ ).map((value) => value.trim())
115185
+ )
115186
+ ];
115187
+ if (ids.length === 0) {
115188
+ return textResponse3(
115189
+ "Error: provide `id` or a non-empty `ids` array of timesheet event ids. Use get-time-entries to find ids."
115190
+ );
115191
+ }
115192
+ if (ids.length > MAX_DELETE_ENTRIES) {
115193
+ return textResponse3(
115194
+ `Error: too many ids (${ids.length}). Max ${MAX_DELETE_ENTRIES} per call.`
115195
+ );
115196
+ }
115197
+ const scope = await resolveTeamScope(input.teamId);
115198
+ if (!scope.ok) return scope.response;
115199
+ if (scope.teamIds.length === 0) {
115200
+ return textResponse3("No accessible teams found.");
115201
+ }
115202
+ const deleted = [];
115203
+ const skipped = [];
115204
+ const errors = [];
115205
+ for (const id of ids) {
115206
+ try {
115207
+ const [row] = await db.select({
115208
+ id: te.id,
115209
+ title: te.title,
115210
+ status: te.status,
115211
+ invoiceId: te.invoiceId,
115212
+ billingStatus: te.billingStatus,
115213
+ isDeleted: te.isDeleted,
115214
+ startTime: te.startTime,
115215
+ endTime: te.endTime,
115216
+ trackedDuration: te.trackedDuration,
115217
+ date: sql`${localDateTextExpr()}`
115218
+ }).from(te).where(and(eq(te.id, id), inArray(te.teamId, scope.teamIds))).limit(1);
115219
+ if (!row) {
115220
+ errors.push({
115221
+ id,
115222
+ message: "Not found or no access. Call get-time-entries to find a valid id."
115223
+ });
115224
+ continue;
115225
+ }
115226
+ const status = row.status === "confirmed" ? "confirmed" : "draft";
115227
+ const invoiced = isInvoicedRow(row);
115228
+ if (row.isDeleted) {
115229
+ skipped.push({ id, reason: "already_deleted", status, invoiced });
115230
+ continue;
115231
+ }
115232
+ const confirmed = status === "confirmed";
115233
+ if ((invoiced || confirmed) && !input.allowInvoicedOverride) {
115234
+ skipped.push({
115235
+ id,
115236
+ reason: invoiced ? "invoiced" : "confirmed",
115237
+ status,
115238
+ invoiced
115239
+ });
115240
+ continue;
115241
+ }
115242
+ await db.update(te).set({ isDeleted: true, deletedAt: sql`NOW()`, updatedAt: sql`NOW()` }).where(and(eq(te.id, id), inArray(te.teamId, scope.teamIds)));
115243
+ deleted.push({
115244
+ id,
115245
+ date: row.date ?? null,
115246
+ hours: rowDurationHours(row),
115247
+ title: row.title
115248
+ });
115249
+ } catch (error49) {
115250
+ errors.push({
115251
+ id,
115252
+ message: error49 instanceof Error ? error49.message : String(error49)
115253
+ });
115254
+ }
115255
+ }
115256
+ return jsonResponse({
115257
+ teamScope: scope.teamIds,
115258
+ deleted,
115259
+ skipped,
115260
+ errors,
115261
+ totals: {
115262
+ deletedCount: deleted.length,
115263
+ skippedCount: skipped.length,
115264
+ errorCount: errors.length,
115265
+ deletedHours: Math.round(deleted.reduce((sum, d6) => sum + d6.hours, 0) * 100) / 100
115266
+ }
115267
+ });
115268
+ }
115269
+ async function handleUpdateTimeEntry(input) {
115270
+ const te = schema_exports.timesheetEvents;
115271
+ const id = typeof input.id === "string" ? input.id.trim() : "";
115272
+ if (!id) return textResponse3("Error: `id` is required.");
115273
+ let mappedStatus;
115274
+ if (input.status !== void 0) {
115275
+ const resolvedStatus = mapCreateStatus(input.status);
115276
+ if (resolvedStatus === null) {
115277
+ return textResponse3(
115278
+ `Error: invalid status "${input.status}". Allowed: draft, confirmed (submitted is treated as confirmed).`
115279
+ );
115280
+ }
115281
+ mappedStatus = resolvedStatus;
115282
+ }
115283
+ const newDate = input.date !== void 0 ? String(input.date).trim() : void 0;
115284
+ if (newDate !== void 0 && !isValidIsoDate(newDate)) {
115285
+ return textResponse3("Error: invalid `date`; expected YYYY-MM-DD.");
115286
+ }
115287
+ if (input.hours !== void 0) {
115288
+ const hours = toNumber(input.hours);
115289
+ if (!(hours > 0)) {
115290
+ return textResponse3("Error: `hours` must be a number greater than 0.");
115291
+ }
115292
+ if (hours > 24) {
115293
+ return textResponse3("Error: `hours` must not exceed 24 for a single day.");
115294
+ }
115295
+ }
115296
+ const scope = await resolveTeamScope(input.teamId);
115297
+ if (!scope.ok) return scope.response;
115298
+ if (scope.teamIds.length === 0) {
115299
+ return textResponse3("No accessible teams found.");
115300
+ }
115301
+ const [existing] = await db.select({
115302
+ id: te.id,
115303
+ status: te.status,
115304
+ invoiceId: te.invoiceId,
115305
+ billingStatus: te.billingStatus,
115306
+ isDeleted: te.isDeleted,
115307
+ startTime: te.startTime,
115308
+ endTime: te.endTime,
115309
+ trackedDuration: te.trackedDuration
115310
+ }).from(te).where(and(eq(te.id, id), inArray(te.teamId, scope.teamIds))).limit(1);
115311
+ if (!existing || existing.isDeleted) {
115312
+ return textResponse3(
115313
+ `Time entry ${id} not found or no access. Call get-time-entries to find a valid id.`
115314
+ );
115315
+ }
115316
+ const invoiced = isInvoicedRow(existing);
115317
+ const confirmed = existing.status === "confirmed";
115318
+ if ((invoiced || confirmed) && !input.allowInvoicedOverride) {
115319
+ const attempted = [];
115320
+ if (input.date !== void 0) attempted.push("date");
115321
+ if (input.hours !== void 0) attempted.push("hours");
115322
+ if (input.billable !== void 0) attempted.push("billable");
115323
+ if (input.status !== void 0) attempted.push("status");
115324
+ if (attempted.length > 0) {
115325
+ return textResponse3(
115326
+ `Error: time entry ${id} is ${invoiced ? "invoiced" : "confirmed"}. Locked fields: ${attempted.join(", ")}. Re-call with allowInvoicedOverride: true to change them anyway, or only update description / project / ticket links.`
115327
+ );
115328
+ }
115329
+ }
115330
+ if (input.projectId) {
115331
+ if (!scope.projectIds.includes(input.projectId)) {
115332
+ return textResponse3(
115333
+ `Project not found or no access: ${input.projectId}. Call get-projects first.`
115334
+ );
115335
+ }
115336
+ }
115337
+ if (input.ticketId) {
115338
+ const [ticket] = await db.select({
115339
+ id: schema_exports.tickets.id,
115340
+ teamId: schema_exports.tickets.teamId,
115341
+ projectId: schema_exports.tickets.projectId,
115342
+ customerId: schema_exports.tickets.customerId
115343
+ }).from(schema_exports.tickets).where(eq(schema_exports.tickets.id, input.ticketId)).limit(1);
115344
+ if (!ticket) {
115345
+ return textResponse3(
115346
+ `Ticket not found: ${input.ticketId}. Call get-tickets first.`
115347
+ );
115348
+ }
115349
+ let hasAccess = scope.teamIds.includes(ticket.teamId);
115350
+ if (!hasAccess && ticket.projectId) {
115351
+ hasAccess = scope.projectIds.includes(ticket.projectId);
115352
+ }
115353
+ if (!hasAccess && ticket.customerId) {
115354
+ hasAccess = scope.customerIds.includes(ticket.customerId);
115355
+ }
115356
+ if (!hasAccess) {
115357
+ return textResponse3(
115358
+ `No access to ticket: ${input.ticketId}. Call get-tickets first.`
115359
+ );
115360
+ }
115361
+ }
115362
+ const updates = { updatedAt: sql`NOW()` };
115363
+ if (input.description !== void 0) {
115364
+ const description = String(input.description).trim();
115365
+ if (!description) {
115366
+ return textResponse3("Error: `description` cannot be empty.");
115367
+ }
115368
+ updates.title = description;
115369
+ updates.description = description;
115370
+ }
115371
+ if (input.projectId !== void 0) updates.projectId = input.projectId;
115372
+ if (input.billable !== void 0) {
115373
+ updates.billingStatus = input.billable ? "to_bill" : "unbillable";
115374
+ }
115375
+ if (mappedStatus !== void 0) updates.status = mappedStatus;
115376
+ if (newDate !== void 0 || input.hours !== void 0) {
115377
+ const durationSeconds = input.hours !== void 0 ? Math.round(toNumber(input.hours) * 3600) : rowDurationSeconds(existing);
115378
+ const startExpr = newDate !== void 0 ? sql`(${`${newDate} ${DEFAULT_START_HOUR}`}::timestamp AT TIME ZONE ${sql.raw(`'${TIMEZONE}'`)})` : sql`${existing.startTime}::timestamptz`;
115379
+ const endExpr = sql`(${startExpr} + ${`${durationSeconds} seconds`}::interval)`;
115380
+ updates.startTime = startExpr;
115381
+ updates.endTime = endExpr;
115382
+ updates.trackedDuration = durationSeconds;
115383
+ updates.isTracked = true;
115384
+ }
115385
+ const hasFieldUpdate = Object.keys(updates).length > 1;
115386
+ if (!hasFieldUpdate && input.ticketId === void 0) {
115387
+ return textResponse3(
115388
+ "No fields to update. Provide at least one of date, hours, description, projectId, ticketId, status, billable."
115389
+ );
115390
+ }
115391
+ if (hasFieldUpdate) {
115392
+ await db.update(te).set(updates).where(and(eq(te.id, id), inArray(te.teamId, scope.teamIds)));
115393
+ }
115394
+ if (input.ticketId !== void 0) {
115395
+ await db.delete(schema_exports.timesheetEventTickets).where(eq(schema_exports.timesheetEventTickets.timesheetEventId, id));
115396
+ if (input.ticketId) {
115397
+ await db.insert(schema_exports.timesheetEventTickets).values({ timesheetEventId: id, ticketId: input.ticketId }).onConflictDoNothing();
115398
+ }
115399
+ }
115400
+ const [row] = await db.select({
115401
+ id: te.id,
115402
+ title: te.title,
115403
+ description: te.description,
115404
+ status: te.status,
115405
+ invoiceId: te.invoiceId,
115406
+ billingStatus: te.billingStatus,
115407
+ startTime: te.startTime,
115408
+ endTime: te.endTime,
115409
+ trackedDuration: te.trackedDuration,
115410
+ projectId: te.projectId,
115411
+ date: sql`${localDateTextExpr()}`
115412
+ }).from(te).where(eq(te.id, id)).limit(1);
115413
+ const links = await db.select({ ticketId: schema_exports.timesheetEventTickets.ticketId }).from(schema_exports.timesheetEventTickets).where(eq(schema_exports.timesheetEventTickets.timesheetEventId, id));
115414
+ return jsonResponse({
115415
+ updated: {
115416
+ id,
115417
+ date: row?.date ?? null,
115418
+ hours: row ? rowDurationHours(row) : 0,
115419
+ title: row?.title ?? null,
115420
+ description: row?.description ?? null,
115421
+ status: row?.status === "confirmed" ? "confirmed" : "draft",
115422
+ invoiced: row ? isInvoicedRow(row) : false,
115423
+ billingStatus: row?.billingStatus ?? null,
115424
+ projectId: row?.projectId ?? null,
115425
+ ticketIds: links.map((link) => link.ticketId)
115426
+ }
115427
+ });
115428
+ }
115014
115429
 
115015
115430
  // ../invoice/src/utils/included-items.ts
115016
115431
  function parseIncludedItems(value) {
@@ -126489,6 +126904,14 @@ function createMcpServer() {
126489
126904
  return await handleCreateTimeEntries(
126490
126905
  asToolArgs(toolArgs)
126491
126906
  );
126907
+ case "delete-time-entries":
126908
+ return await handleDeleteTimeEntries(
126909
+ asToolArgs(toolArgs)
126910
+ );
126911
+ case "update-time-entry":
126912
+ return await handleUpdateTimeEntry(
126913
+ asToolArgs(toolArgs)
126914
+ );
126492
126915
  case "get-github-file":
126493
126916
  return await handleGetGithubFile(asToolArgs(toolArgs));
126494
126917
  case "list-github-directory":