@mgsoftwarebv/mcp-server-bridge 3.4.0 → 3.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -106277,6 +106277,106 @@ var TOOLS = [
106277
106277
  required: ["documentId", "invoiceId"]
106278
106278
  }
106279
106279
  },
106280
+ {
106281
+ name: "get-products",
106282
+ description: "List catalog products used on invoices AND quotes (the shared `invoice_products` catalog). Each entry includes its ID (UUID), name, unit price, currency, unit, active/archived flag, configurable flag, and usage stats. Editing or archiving a catalog product never changes existing invoices/quotes \u2014 those keep an immutable line-item snapshot; catalog changes only affect documents created afterwards.",
106283
+ inputSchema: {
106284
+ type: "object",
106285
+ properties: {
106286
+ teamId: teamIdProp,
106287
+ q: {
106288
+ type: "string",
106289
+ description: "Search query for product name/description"
106290
+ },
106291
+ status: {
106292
+ type: "string",
106293
+ enum: ["active", "archived", "all"],
106294
+ default: "active",
106295
+ description: "Filter by catalog status (archived = isActive false)"
106296
+ },
106297
+ currency: {
106298
+ type: "string",
106299
+ description: "Filter by currency code (e.g. EUR)"
106300
+ },
106301
+ pageSize: { type: "number", default: 20, maximum: 100 }
106302
+ },
106303
+ required: []
106304
+ }
106305
+ },
106306
+ {
106307
+ name: "get-product-by-id",
106308
+ description: "Get a single catalog product by its ID (UUID), including name, unit price, currency, unit, active/archived flag, configurable flag, and usage stats.",
106309
+ inputSchema: {
106310
+ type: "object",
106311
+ properties: {
106312
+ teamId: teamIdProp,
106313
+ productId: { type: "string", description: "Product ID (UUID)" }
106314
+ },
106315
+ required: ["productId"]
106316
+ }
106317
+ },
106318
+ {
106319
+ name: "create-product",
106320
+ description: "Create a catalog product for use on invoices and quotes. Stored in the shared `invoice_products` catalog. This only adds a reusable catalog entry; it does not place the product on any document. Returns the created product with its ID and normalized fields.",
106321
+ inputSchema: {
106322
+ type: "object",
106323
+ properties: {
106324
+ teamId: teamIdProp,
106325
+ name: { type: "string", description: "Product name" },
106326
+ description: { type: "string" },
106327
+ price: {
106328
+ type: "number",
106329
+ description: "Unit price (catalog default, excl. quantity)"
106330
+ },
106331
+ currency: { type: "string", description: "Currency code (e.g. EUR)" },
106332
+ unit: {
106333
+ type: "string",
106334
+ description: "Unit label (e.g. hour, piece, month)"
106335
+ }
106336
+ },
106337
+ required: ["name"]
106338
+ }
106339
+ },
106340
+ {
106341
+ name: "update-product",
106342
+ description: "Update a catalog product's editable fields (name, description, price, currency, unit) or reactivate it (isActive: true). Only provided fields change. IMPORTANT: updates apply only to FUTURE invoices/quotes. Existing/sent/accepted/paid documents keep their immutable line-item snapshot and are never mutated. Find the product id via get-products.",
106343
+ inputSchema: {
106344
+ type: "object",
106345
+ properties: {
106346
+ teamId: teamIdProp,
106347
+ productId: { type: "string", description: "Product ID (UUID)" },
106348
+ name: { type: "string" },
106349
+ description: { type: ["string", "null"] },
106350
+ price: {
106351
+ type: ["number", "null"],
106352
+ description: "Unit price (catalog default)"
106353
+ },
106354
+ currency: { type: ["string", "null"] },
106355
+ unit: { type: ["string", "null"] },
106356
+ isActive: {
106357
+ type: "boolean",
106358
+ description: "Set true to reactivate an archived product"
106359
+ }
106360
+ },
106361
+ required: ["productId"]
106362
+ }
106363
+ },
106364
+ {
106365
+ name: "archive-product",
106366
+ description: "Archive (soft-disable) a catalog product so it no longer appears in invoice/quote product pickers. Preferred over deletion: products referenced historically stay safe because invoices/quotes hold their own line-item snapshots. Reactivate later via update-product (isActive: true).",
106367
+ inputSchema: {
106368
+ type: "object",
106369
+ properties: {
106370
+ teamId: teamIdProp,
106371
+ productId: { type: "string", description: "Product ID (UUID)" },
106372
+ reason: {
106373
+ type: "string",
106374
+ description: "Optional note about why it was archived (not persisted)"
106375
+ }
106376
+ },
106377
+ required: ["productId"]
106378
+ }
106379
+ },
106280
106380
  {
106281
106381
  name: "log-hours",
106282
106382
  description: "Analyze current chat conversation and log hours as draft tracker entry. AI analyzes chat context to estimate hours as a senior developer would (without AI assistance). Cursor AI matches workspace name to correct project from list (optional).",
@@ -113005,6 +113105,203 @@ async function handleRemoveProjectMember(input) {
113005
113105
  return textResponse(text3);
113006
113106
  }
113007
113107
 
113108
+ // src/tools/products.ts
113109
+ var PRODUCT_STATUSES = ["active", "archived", "all"];
113110
+ var PRODUCT_COLUMNS = {
113111
+ id: schema_exports.invoiceProducts.id,
113112
+ teamId: schema_exports.invoiceProducts.teamId,
113113
+ name: schema_exports.invoiceProducts.name,
113114
+ description: schema_exports.invoiceProducts.description,
113115
+ price: schema_exports.invoiceProducts.price,
113116
+ currency: schema_exports.invoiceProducts.currency,
113117
+ unit: schema_exports.invoiceProducts.unit,
113118
+ isConfigurable: schema_exports.invoiceProducts.isConfigurable,
113119
+ isActive: schema_exports.invoiceProducts.isActive,
113120
+ usageCount: schema_exports.invoiceProducts.usageCount,
113121
+ lastUsedAt: schema_exports.invoiceProducts.lastUsedAt,
113122
+ createdAt: schema_exports.invoiceProducts.createdAt,
113123
+ updatedAt: schema_exports.invoiceProducts.updatedAt
113124
+ };
113125
+ function textResponse2(text3) {
113126
+ return { content: [{ type: "text", text: text3 }] };
113127
+ }
113128
+ function formatPrice(p3) {
113129
+ if (p3.price == null) return "(no price)";
113130
+ return `${p3.price}${p3.currency ? ` ${p3.currency}` : ""}${p3.unit ? ` / ${p3.unit}` : ""}`;
113131
+ }
113132
+ function formatProduct(p3) {
113133
+ const flags = [p3.isActive ? "active" : "archived"];
113134
+ if (p3.isConfigurable) flags.push("configurable");
113135
+ return `**${p3.name}** (${flags.join(", ")})
113136
+ ID: ${p3.id}
113137
+ Price: ${formatPrice(p3)}
113138
+ ${p3.description ? `Description: ${p3.description}
113139
+ ` : ""}Used: ${p3.usageCount}x${p3.lastUsedAt ? ` (last ${new Date(p3.lastUsedAt).toLocaleDateString()})` : ""}
113140
+ `;
113141
+ }
113142
+ async function handleGetProducts(input) {
113143
+ const { q: q3, currency, pageSize = 20 } = input;
113144
+ const status = input.status ?? "active";
113145
+ if (!PRODUCT_STATUSES.includes(status)) {
113146
+ return textResponse2(
113147
+ `Error: invalid status "${status}". Allowed: ${PRODUCT_STATUSES.join(", ")}.`
113148
+ );
113149
+ }
113150
+ const scope = await resolveTeamScope(input.teamId);
113151
+ if (!scope.ok) return scope.response;
113152
+ if (scope.teamIds.length === 0) {
113153
+ return textResponse2("No accessible teams found.");
113154
+ }
113155
+ const filters = [inArray(schema_exports.invoiceProducts.teamId, scope.teamIds)];
113156
+ if (status === "active") {
113157
+ filters.push(eq(schema_exports.invoiceProducts.isActive, true));
113158
+ } else if (status === "archived") {
113159
+ filters.push(eq(schema_exports.invoiceProducts.isActive, false));
113160
+ }
113161
+ if (currency) filters.push(eq(schema_exports.invoiceProducts.currency, currency));
113162
+ if (q3) {
113163
+ filters.push(
113164
+ or(
113165
+ sql`${schema_exports.invoiceProducts.fts} @@ plainto_tsquery('english', ${q3})`,
113166
+ ilike(schema_exports.invoiceProducts.name, `%${q3}%`)
113167
+ )
113168
+ );
113169
+ }
113170
+ const rows = await db.select(PRODUCT_COLUMNS).from(schema_exports.invoiceProducts).where(and(...filters)).orderBy(
113171
+ desc(schema_exports.invoiceProducts.usageCount),
113172
+ desc(schema_exports.invoiceProducts.lastUsedAt),
113173
+ asc(schema_exports.invoiceProducts.name)
113174
+ ).limit(Math.min(pageSize, 100));
113175
+ if (rows.length === 0) {
113176
+ return textResponse2(
113177
+ `No products found${status !== "all" ? ` (status: ${status})` : ""}.`
113178
+ );
113179
+ }
113180
+ return textResponse2(
113181
+ `Found ${rows.length} product(s):
113182
+
113183
+ ${rows.map(formatProduct).join("\n")}`
113184
+ );
113185
+ }
113186
+ async function handleGetProductById(input) {
113187
+ const { productId } = input;
113188
+ if (!productId) return textResponse2("Error: `productId` is required.");
113189
+ const scope = await resolveTeamScope(input.teamId);
113190
+ if (!scope.ok) return scope.response;
113191
+ if (scope.teamIds.length === 0) {
113192
+ return textResponse2("No accessible teams found.");
113193
+ }
113194
+ const [row] = await db.select(PRODUCT_COLUMNS).from(schema_exports.invoiceProducts).where(
113195
+ and(
113196
+ eq(schema_exports.invoiceProducts.id, productId),
113197
+ inArray(schema_exports.invoiceProducts.teamId, scope.teamIds)
113198
+ )
113199
+ ).limit(1);
113200
+ if (!row) {
113201
+ return textResponse2(
113202
+ `Product ${productId} not found or you don't have access to it.`
113203
+ );
113204
+ }
113205
+ return textResponse2(formatProduct(row));
113206
+ }
113207
+ async function loadProductInTeam(productId, teamId) {
113208
+ const accessibleTeamIds = await getAccessibleTeamIds(teamId);
113209
+ const [row] = await db.select(PRODUCT_COLUMNS).from(schema_exports.invoiceProducts).where(
113210
+ and(
113211
+ eq(schema_exports.invoiceProducts.id, productId),
113212
+ inArray(schema_exports.invoiceProducts.teamId, accessibleTeamIds)
113213
+ )
113214
+ ).limit(1);
113215
+ return row ?? null;
113216
+ }
113217
+ async function handleCreateProduct(input) {
113218
+ const { name: name21, description, price, currency, unit } = input;
113219
+ if (!name21 || name21.trim().length === 0) {
113220
+ return textResponse2("Error: `name` is required.");
113221
+ }
113222
+ const resolved = await resolveTeamId(input.teamId);
113223
+ if (!resolved.ok) return resolved.response;
113224
+ const [created] = await db.insert(schema_exports.invoiceProducts).values({
113225
+ teamId: resolved.teamId,
113226
+ name: name21.trim(),
113227
+ description: description ?? null,
113228
+ price: price ?? null,
113229
+ currency: currency ?? null,
113230
+ unit: unit ?? null,
113231
+ isActive: true,
113232
+ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString()
113233
+ }).returning(PRODUCT_COLUMNS);
113234
+ if (!created) return textResponse2("Failed to create product.");
113235
+ return textResponse2(
113236
+ `\u2705 **Product created**
113237
+
113238
+ ${formatProduct(created)}`
113239
+ );
113240
+ }
113241
+ async function handleUpdateProduct(input) {
113242
+ const { productId } = input;
113243
+ if (!productId) return textResponse2("Error: `productId` is required.");
113244
+ const resolved = await resolveTeamId(input.teamId);
113245
+ if (!resolved.ok) return resolved.response;
113246
+ const existing = await loadProductInTeam(productId, resolved.teamId);
113247
+ if (!existing) {
113248
+ return textResponse2(
113249
+ `Product ${productId} not found, or it is not owned by this team.`
113250
+ );
113251
+ }
113252
+ const updates = {};
113253
+ if (input.name !== void 0) {
113254
+ if (!input.name || input.name.trim().length === 0) {
113255
+ return textResponse2("Error: `name` cannot be empty.");
113256
+ }
113257
+ updates.name = input.name.trim();
113258
+ }
113259
+ if (input.description !== void 0) updates.description = input.description;
113260
+ if (input.price !== void 0) updates.price = input.price;
113261
+ if (input.currency !== void 0) updates.currency = input.currency;
113262
+ if (input.unit !== void 0) updates.unit = input.unit;
113263
+ if (input.isActive !== void 0) updates.isActive = input.isActive;
113264
+ if (Object.keys(updates).length === 0) {
113265
+ return textResponse2(
113266
+ "No fields to update. Provide at least one of: name, description, price, currency, unit, isActive."
113267
+ );
113268
+ }
113269
+ updates.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
113270
+ const [updated] = await db.update(schema_exports.invoiceProducts).set(updates).where(eq(schema_exports.invoiceProducts.id, existing.id)).returning(PRODUCT_COLUMNS);
113271
+ if (!updated) return textResponse2(`Failed to update product ${productId}.`);
113272
+ return textResponse2(
113273
+ `\u2705 **Product updated**
113274
+
113275
+ ${formatProduct(updated)}
113276
+ Note: this only affects future invoices/quotes. Existing documents keep their line-item snapshots.`
113277
+ );
113278
+ }
113279
+ async function handleArchiveProduct(input) {
113280
+ const { productId, reason } = input;
113281
+ if (!productId) return textResponse2("Error: `productId` is required.");
113282
+ const resolved = await resolveTeamId(input.teamId);
113283
+ if (!resolved.ok) return resolved.response;
113284
+ const existing = await loadProductInTeam(productId, resolved.teamId);
113285
+ if (!existing) {
113286
+ return textResponse2(
113287
+ `Product ${productId} not found, or it is not owned by this team.`
113288
+ );
113289
+ }
113290
+ if (!existing.isActive) {
113291
+ return textResponse2(
113292
+ `Product "${existing.name}" (${existing.id}) is already archived.`
113293
+ );
113294
+ }
113295
+ const [archived] = await db.update(schema_exports.invoiceProducts).set({ isActive: false, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq(schema_exports.invoiceProducts.id, existing.id)).returning(PRODUCT_COLUMNS);
113296
+ if (!archived) return textResponse2(`Failed to archive product ${productId}.`);
113297
+ return textResponse2(
113298
+ `\u2705 **Product archived** (hidden from new invoices/quotes; existing documents are untouched).
113299
+
113300
+ ${formatProduct(archived)}${reason ? `Reason: ${reason}
113301
+ ` : ""}Reactivate it with update-product (isActive: true).`
113302
+ );
113303
+ }
113304
+
113008
113305
  // src/tools/teams.ts
113009
113306
  async function handleGetTeams() {
113010
113307
  const ctx = getAuthContext();
@@ -118496,7 +118793,7 @@ var EXT_MIME = {
118496
118793
  ppt: "application/vnd.ms-powerpoint",
118497
118794
  pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation"
118498
118795
  };
118499
- function textResponse2(text3) {
118796
+ function textResponse3(text3) {
118500
118797
  return { content: [{ type: "text", text: text3 }] };
118501
118798
  }
118502
118799
  function mimeFromName(name21) {
@@ -118577,12 +118874,12 @@ async function handleUploadTicketAttachment(input) {
118577
118874
  (v2) => typeof v2 === "string" && v2.trim().length > 0
118578
118875
  );
118579
118876
  if (sources.length === 0) {
118580
- return textResponse2(
118877
+ return textResponse3(
118581
118878
  "Provide exactly one source: filePath (absolute local path), imageUrl, or base64Data."
118582
118879
  );
118583
118880
  }
118584
118881
  if (sources.length > 1) {
118585
- return textResponse2(
118882
+ return textResponse3(
118586
118883
  "Provide only one source (filePath, imageUrl, or base64Data), not several."
118587
118884
  );
118588
118885
  }
@@ -118602,7 +118899,7 @@ async function handleUploadTicketAttachment(input) {
118602
118899
  } else if (input.imageUrl) {
118603
118900
  const res = await fetch(input.imageUrl);
118604
118901
  if (!res.ok) {
118605
- return textResponse2(
118902
+ return textResponse3(
118606
118903
  `Could not download from URL: HTTP ${res.status}.`
118607
118904
  );
118608
118905
  }
@@ -118630,22 +118927,22 @@ async function handleUploadTicketAttachment(input) {
118630
118927
  }
118631
118928
  }
118632
118929
  } catch (error49) {
118633
- return textResponse2(
118930
+ return textResponse3(
118634
118931
  `Failed to read the file: ${error49 instanceof Error ? error49.message : String(error49)}`
118635
118932
  );
118636
118933
  }
118637
118934
  if (buffer2.byteLength === 0) {
118638
- return textResponse2("The file is empty (0 bytes); nothing to upload.");
118935
+ return textResponse3("The file is empty (0 bytes); nothing to upload.");
118639
118936
  }
118640
118937
  if (buffer2.byteLength > MAX_FILE_SIZE) {
118641
- return textResponse2(
118938
+ return textResponse3(
118642
118939
  `File too large (${(buffer2.byteLength / 1024 / 1024).toFixed(
118643
118940
  1
118644
118941
  )} MB). Max: 25 MB.`
118645
118942
  );
118646
118943
  }
118647
118944
  if (!ALLOWED_MIME_TYPES.has(mimeType)) {
118648
- return textResponse2(
118945
+ return textResponse3(
118649
118946
  `Unsupported file type: ${mimeType}. Allowed: JPEG, PNG, GIF, WebP, PDF, DOC(X), XLS(X), PPT(X), TXT, CSV.`
118650
118947
  );
118651
118948
  }
@@ -118658,7 +118955,7 @@ async function handleUploadTicketAttachment(input) {
118658
118955
  options: { contentType: mimeType, upsert: true }
118659
118956
  });
118660
118957
  } catch (error49) {
118661
- return textResponse2(
118958
+ return textResponse3(
118662
118959
  `Upload failed: ${error49 instanceof Error ? error49.message : String(error49)}`
118663
118960
  );
118664
118961
  }
@@ -118681,7 +118978,7 @@ async function handleUploadTicketAttachment(input) {
118681
118978
  url3 = signed.url;
118682
118979
  } catch {
118683
118980
  }
118684
- return textResponse2(
118981
+ return textResponse3(
118685
118982
  `\u{1F4CE} **Attached to ${ticket.ticketNumber}**
118686
118983
  File: ${fileName}
118687
118984
  Type: ${mimeType}
@@ -119825,7 +120122,7 @@ ${tagErrors.map((e6) => ` \u2022 ${e6}`).join("\n")}
119825
120122
  }
119826
120123
 
119827
120124
  // src/server.ts
119828
- var SERVER_VERSION = "3.4.0";
120125
+ var SERVER_VERSION = "3.5.0";
119829
120126
  function createMcpServer() {
119830
120127
  const server = new Server(
119831
120128
  {
@@ -119949,6 +120246,24 @@ function createMcpServer() {
119949
120246
  return await handleLinkDocumentToInvoice(
119950
120247
  asToolArgs(toolArgs)
119951
120248
  );
120249
+ case "get-products":
120250
+ return await handleGetProducts(asToolArgs(toolArgs));
120251
+ case "get-product-by-id":
120252
+ return await handleGetProductById(
120253
+ asToolArgs(toolArgs)
120254
+ );
120255
+ case "create-product":
120256
+ return await handleCreateProduct(
120257
+ asToolArgs(toolArgs)
120258
+ );
120259
+ case "update-product":
120260
+ return await handleUpdateProduct(
120261
+ asToolArgs(toolArgs)
120262
+ );
120263
+ case "archive-product":
120264
+ return await handleArchiveProduct(
120265
+ asToolArgs(toolArgs)
120266
+ );
119952
120267
  case "log-hours":
119953
120268
  return await handleLogHours(asToolArgs(toolArgs));
119954
120269
  case "get-github-file":