@mgsoftwarebv/mcp-server-bridge 3.3.0 → 3.3.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
@@ -23574,7 +23574,14 @@ var init_quotations = __esm({
23574
23574
  withTimezone: true,
23575
23575
  mode: "string"
23576
23576
  }),
23577
- compiledHash: text("compiled_hash")
23577
+ compiledHash: text("compiled_hash"),
23578
+ // Manual, terminal lock: once set, signatures can no longer be cleared,
23579
+ // edited, or added. Set when the provider clicks "Definitief maken".
23580
+ finalizedAt: timestamp("finalized_at", {
23581
+ withTimezone: true,
23582
+ mode: "string"
23583
+ }),
23584
+ finalizedBy: uuid3("finalized_by")
23578
23585
  },
23579
23586
  (table) => [
23580
23587
  index("idx_documents_team_id").on(table.teamId),
@@ -23595,6 +23602,11 @@ var init_quotations = __esm({
23595
23602
  foreignColumns: [users.id],
23596
23603
  name: "documents_user_id_fkey"
23597
23604
  }).onDelete("set null"),
23605
+ foreignKey({
23606
+ columns: [table.finalizedBy],
23607
+ foreignColumns: [users.id],
23608
+ name: "documents_finalized_by_fkey"
23609
+ }).onDelete("set null"),
23598
23610
  foreignKey({
23599
23611
  columns: [table.templateId],
23600
23612
  foreignColumns: [documentTemplates.id],
@@ -23707,7 +23719,15 @@ var init_quotations = __esm({
23707
23719
  recipientEmail: text("recipient_email"),
23708
23720
  message: text(),
23709
23721
  expiresAt: timestamp("expires_at", { withTimezone: true, mode: "string" }),
23710
- signedAt: timestamp("signed_at", { withTimezone: true, mode: "string" })
23722
+ signedAt: timestamp("signed_at", { withTimezone: true, mode: "string" }),
23723
+ // Which signer slot this signature belongs to (mirrors
23724
+ // document_signatures.signature_data.signerId for direct querying).
23725
+ signerSlotId: text("signer_slot_id"),
23726
+ // Hash of the private edit token returned to the signer's browser so an
23727
+ // anonymous signer can later replace their own signature.
23728
+ editTokenHash: text("edit_token_hash"),
23729
+ // Set when a logged-in (portal) user signs, enabling identity-based edits.
23730
+ signerUserId: uuid3("signer_user_id")
23711
23731
  },
23712
23732
  (table) => [
23713
23733
  index("idx_document_signing_requests_document_id").on(table.documentId),
@@ -23732,6 +23752,11 @@ var init_quotations = __esm({
23732
23752
  columns: [table.createdBy],
23733
23753
  foreignColumns: [users.id],
23734
23754
  name: "document_signing_requests_created_by_fkey"
23755
+ }).onDelete("set null"),
23756
+ foreignKey({
23757
+ columns: [table.signerUserId],
23758
+ foreignColumns: [users.id],
23759
+ name: "document_signing_requests_signer_user_id_fkey"
23735
23760
  }).onDelete("set null")
23736
23761
  ]
23737
23762
  );
@@ -106989,6 +107014,109 @@ var TOOLS = [
106989
107014
  required: ["name"]
106990
107015
  }
106991
107016
  },
107017
+ {
107018
+ name: "update-project",
107019
+ description: "Update an existing project's fields (name, description, customer, rate, currency, billable, estimate, internal). Only provided fields change. Renaming a project renumbers its tickets. There is no project 'status' field. Find the project id via get-projects.",
107020
+ inputSchema: {
107021
+ type: "object",
107022
+ properties: {
107023
+ teamId: teamIdProp,
107024
+ id: { type: "string", description: "Project ID" },
107025
+ name: { type: "string" },
107026
+ description: { type: ["string", "null"] },
107027
+ customerId: {
107028
+ type: ["string", "null"],
107029
+ description: "Customer ID to link, or null to unlink."
107030
+ },
107031
+ rate: { type: ["number", "null"], description: "Hourly rate" },
107032
+ currency: { type: ["string", "null"] },
107033
+ billable: { type: "boolean" },
107034
+ estimate: {
107035
+ type: ["number", "null"],
107036
+ description: "Estimated hours/units"
107037
+ },
107038
+ internal: {
107039
+ type: "boolean",
107040
+ description: "Whether this is an internal (no-customer) project"
107041
+ }
107042
+ },
107043
+ required: ["id"]
107044
+ }
107045
+ },
107046
+ {
107047
+ name: "get-project-members",
107048
+ description: "List the members explicitly assigned to a project (member_project_access) plus the full team roster with each member's effective access. Use the returned userIds with set/add/remove-project-member. Access model: owners and members with no restrictions see ALL projects; once a member is explicitly assigned to ANY project they can ONLY see their explicitly-assigned projects.",
107049
+ inputSchema: {
107050
+ type: "object",
107051
+ properties: {
107052
+ teamId: teamIdProp,
107053
+ projectId: { type: "string", description: "Project ID" }
107054
+ },
107055
+ required: ["projectId"]
107056
+ }
107057
+ },
107058
+ {
107059
+ name: "set-project-members",
107060
+ description: "Replace the COMPLETE set of members explicitly assigned to a project. Members not listed lose their explicit assignment; pass an empty list to clear all assignments (the project then reverts to being visible to every owner and unrestricted member). Identify members by userIds and/or emails (each must be a member of the team). Requires team OWNER privileges. Warning: assigning a member who previously had no restrictions limits them to ONLY their explicitly-assigned projects.",
107061
+ inputSchema: {
107062
+ type: "object",
107063
+ properties: {
107064
+ teamId: teamIdProp,
107065
+ projectId: { type: "string", description: "Project ID" },
107066
+ userIds: {
107067
+ type: "array",
107068
+ items: { type: "string" },
107069
+ description: "User IDs to assign to the project"
107070
+ },
107071
+ emails: {
107072
+ type: "array",
107073
+ items: { type: "string" },
107074
+ description: "Member emails to assign (resolved to user IDs)"
107075
+ }
107076
+ },
107077
+ required: ["projectId"]
107078
+ }
107079
+ },
107080
+ {
107081
+ name: "add-project-member",
107082
+ description: "Grant a single member explicit access to a project, preserving existing assignments. Identify the member by userId or email. Requires team OWNER privileges. Warning: if the member previously had no restrictions (saw all projects), granting this first assignment restricts them to ONLY their explicitly-assigned projects.",
107083
+ inputSchema: {
107084
+ type: "object",
107085
+ properties: {
107086
+ teamId: teamIdProp,
107087
+ projectId: { type: "string", description: "Project ID" },
107088
+ userId: {
107089
+ type: "string",
107090
+ description: "User ID of the member to add"
107091
+ },
107092
+ email: {
107093
+ type: "string",
107094
+ description: "Email of the member to add (alternative to userId)"
107095
+ }
107096
+ },
107097
+ required: ["projectId"]
107098
+ }
107099
+ },
107100
+ {
107101
+ name: "remove-project-member",
107102
+ description: "Remove a single member's explicit access to a project, preserving their other assignments. Identify the member by userId or email. Requires team OWNER privileges. If this was the member's only assignment, their restrictions are cleared and they can see all projects again.",
107103
+ inputSchema: {
107104
+ type: "object",
107105
+ properties: {
107106
+ teamId: teamIdProp,
107107
+ projectId: { type: "string", description: "Project ID" },
107108
+ userId: {
107109
+ type: "string",
107110
+ description: "User ID of the member to remove"
107111
+ },
107112
+ email: {
107113
+ type: "string",
107114
+ description: "Email of the member to remove (alternative to userId)"
107115
+ }
107116
+ },
107117
+ required: ["projectId"]
107118
+ }
107119
+ },
106992
107120
  {
106993
107121
  name: "list-documents",
106994
107122
  description: "List quote/proposal/deliverables documents with optional filtering by type, status, customer, linked invoice, or a title search. Returns document ids for use with get-document / update-document.",
@@ -108262,6 +108390,7 @@ var signatureBlockSchema = external_exports5.object({
108262
108390
  name: external_exports5.string().optional(),
108263
108391
  company: external_exports5.string().optional(),
108264
108392
  role: external_exports5.string().optional(),
108393
+ optional: external_exports5.boolean().optional(),
108265
108394
  signedBy: external_exports5.string().optional(),
108266
108395
  signedAt: external_exports5.string().optional(),
108267
108396
  signatureType: external_exports5.enum(["drawn", "typed"]).optional(),
@@ -113123,6 +113252,479 @@ ${description ? `Description: ${description}
113123
113252
  ]
113124
113253
  };
113125
113254
  }
113255
+ function textResponse(text3) {
113256
+ return { content: [{ type: "text", text: text3 }] };
113257
+ }
113258
+ function memberLabel(m4) {
113259
+ return m4.fullName || m4.email || m4.userId;
113260
+ }
113261
+ var OWNER_REQUIRED = "Only team owners can manage project members. Ask a team owner to run this action (or use an owner's API key).";
113262
+ async function requireTeamOwner(teamId, userId) {
113263
+ const [membership] = await db.select({ role: schema_exports.usersOnTeam.role }).from(schema_exports.usersOnTeam).where(
113264
+ and(
113265
+ eq(schema_exports.usersOnTeam.userId, userId),
113266
+ eq(schema_exports.usersOnTeam.teamId, teamId)
113267
+ )
113268
+ ).limit(1);
113269
+ return membership?.role === "owner" ? null : textResponse(OWNER_REQUIRED);
113270
+ }
113271
+ async function setProjectMemberAccess(params) {
113272
+ const { projectId, teamId, memberIds, createdBy } = params;
113273
+ const baseFilter = and(
113274
+ eq(schema_exports.memberProjectAccess.projectId, projectId),
113275
+ eq(schema_exports.memberProjectAccess.teamId, teamId)
113276
+ );
113277
+ if (memberIds.length > 0) {
113278
+ await db.delete(schema_exports.memberProjectAccess).where(
113279
+ and(baseFilter, notInArray(schema_exports.memberProjectAccess.userId, memberIds))
113280
+ );
113281
+ await db.insert(schema_exports.memberProjectAccess).values(
113282
+ memberIds.map((userId) => ({
113283
+ userId,
113284
+ teamId,
113285
+ projectId,
113286
+ hasAccess: true,
113287
+ createdBy,
113288
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
113289
+ }))
113290
+ ).onConflictDoUpdate({
113291
+ target: [
113292
+ schema_exports.memberProjectAccess.userId,
113293
+ schema_exports.memberProjectAccess.teamId,
113294
+ schema_exports.memberProjectAccess.projectId
113295
+ ],
113296
+ set: {
113297
+ hasAccess: true,
113298
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
113299
+ createdBy
113300
+ }
113301
+ });
113302
+ } else {
113303
+ await db.delete(schema_exports.memberProjectAccess).where(baseFilter);
113304
+ }
113305
+ }
113306
+ function generateProjectAbbreviation(projectName) {
113307
+ if (!projectName || projectName.trim() === "") {
113308
+ return "PROJ";
113309
+ }
113310
+ const cleanName = projectName.toUpperCase().replace(/[^A-Z0-9\s]/g, "");
113311
+ const words = cleanName.split(" ").filter((word) => word.length > 0);
113312
+ if (words.length === 0) {
113313
+ const cleaned = projectName.toUpperCase().replace(/[^A-Z0-9]/g, "");
113314
+ const abbr = cleaned.substring(0, 5);
113315
+ return abbr.length < 2 ? "PROJ" : abbr;
113316
+ }
113317
+ let abbreviation = "";
113318
+ if (words.length === 1) {
113319
+ const word = words[0];
113320
+ if (word && word.length <= 5) {
113321
+ abbreviation = word;
113322
+ } else if (word) {
113323
+ abbreviation = word.substring(
113324
+ 0,
113325
+ word.substring(3, 4).match(/[AEIOU]/) ? 3 : 5
113326
+ );
113327
+ }
113328
+ } else if (words.length === 2) {
113329
+ const firstWord = words[0];
113330
+ const secondWord = words[1];
113331
+ if (firstWord && secondWord) {
113332
+ abbreviation = firstWord.substring(0, 3) + secondWord.substring(0, 2);
113333
+ }
113334
+ } else {
113335
+ for (let i6 = 0; i6 < Math.min(words.length, 6); i6++) {
113336
+ const word = words[i6];
113337
+ if (word && (word.length > 2 || i6 === 0)) {
113338
+ abbreviation += word.charAt(0);
113339
+ }
113340
+ }
113341
+ if (abbreviation.length < 3 && words.length > 0) {
113342
+ const firstWord = words[0];
113343
+ if (firstWord) {
113344
+ abbreviation = firstWord.substring(0, 3) + abbreviation.substring(1);
113345
+ }
113346
+ }
113347
+ }
113348
+ if (abbreviation.length === 0) {
113349
+ abbreviation = "PROJ";
113350
+ } else if (abbreviation.length > 8) {
113351
+ abbreviation = abbreviation.substring(0, 8);
113352
+ }
113353
+ if (abbreviation.length < 2) {
113354
+ abbreviation += "X";
113355
+ }
113356
+ return abbreviation;
113357
+ }
113358
+ async function getTeamRoster(teamId) {
113359
+ return db.select({
113360
+ userId: schema_exports.usersOnTeam.userId,
113361
+ role: schema_exports.usersOnTeam.role,
113362
+ fullName: schema_exports.users.fullName,
113363
+ email: schema_exports.users.email
113364
+ }).from(schema_exports.usersOnTeam).innerJoin(schema_exports.users, eq(schema_exports.usersOnTeam.userId, schema_exports.users.id)).where(eq(schema_exports.usersOnTeam.teamId, teamId));
113365
+ }
113366
+ async function resolveTeamMember(teamId, opts) {
113367
+ const roster = await getTeamRoster(teamId);
113368
+ if (opts.userId) {
113369
+ const match = roster.find((m4) => m4.userId === opts.userId);
113370
+ if (!match) {
113371
+ return {
113372
+ ok: false,
113373
+ response: textResponse(
113374
+ `User ${opts.userId} is not a member of this team. Call get-project-members to see the team roster.`
113375
+ )
113376
+ };
113377
+ }
113378
+ return { ok: true, member: match };
113379
+ }
113380
+ if (opts.email) {
113381
+ const needle = opts.email.trim().toLowerCase();
113382
+ const matches = roster.filter((m4) => (m4.email ?? "").toLowerCase() === needle);
113383
+ if (matches.length === 0) {
113384
+ return {
113385
+ ok: false,
113386
+ response: textResponse(
113387
+ `No team member found with email "${opts.email}". Call get-project-members to see the team roster.`
113388
+ )
113389
+ };
113390
+ }
113391
+ if (matches.length > 1) {
113392
+ return {
113393
+ ok: false,
113394
+ response: textResponse(
113395
+ `Multiple team members match email "${opts.email}". Pass an explicit userId instead.`
113396
+ )
113397
+ };
113398
+ }
113399
+ return { ok: true, member: matches[0] };
113400
+ }
113401
+ return {
113402
+ ok: false,
113403
+ response: textResponse(
113404
+ "Provide either a userId or an email to identify the member."
113405
+ )
113406
+ };
113407
+ }
113408
+ async function loadProjectInTeam(projectId, teamId) {
113409
+ const accessibleTeamIds = await getAccessibleTeamIds(teamId);
113410
+ const [row] = await db.select({
113411
+ id: schema_exports.projects.id,
113412
+ name: schema_exports.projects.name,
113413
+ description: schema_exports.projects.description,
113414
+ customerId: schema_exports.projects.customerId,
113415
+ rate: schema_exports.projects.rate,
113416
+ currency: schema_exports.projects.currency,
113417
+ billable: schema_exports.projects.billable,
113418
+ estimate: schema_exports.projects.estimate,
113419
+ internal: schema_exports.projects.internal,
113420
+ ownedByCustomer: schema_exports.projects.ownedByCustomer,
113421
+ teamId: schema_exports.projects.teamId
113422
+ }).from(schema_exports.projects).where(eq(schema_exports.projects.id, projectId)).limit(1);
113423
+ if (!row || !row.teamId || !accessibleTeamIds.includes(row.teamId)) {
113424
+ return null;
113425
+ }
113426
+ return row;
113427
+ }
113428
+ async function getProjectAccessState(teamId, projectId) {
113429
+ const rows = await db.select({
113430
+ userId: schema_exports.memberProjectAccess.userId,
113431
+ projectId: schema_exports.memberProjectAccess.projectId,
113432
+ hasAccess: schema_exports.memberProjectAccess.hasAccess
113433
+ }).from(schema_exports.memberProjectAccess).where(eq(schema_exports.memberProjectAccess.teamId, teamId));
113434
+ const restrictedUserIds = /* @__PURE__ */ new Set();
113435
+ const projectMemberIds = /* @__PURE__ */ new Set();
113436
+ const rowCountByUser = /* @__PURE__ */ new Map();
113437
+ for (const r6 of rows) {
113438
+ restrictedUserIds.add(r6.userId);
113439
+ rowCountByUser.set(r6.userId, (rowCountByUser.get(r6.userId) ?? 0) + 1);
113440
+ if (r6.projectId === projectId && r6.hasAccess) {
113441
+ projectMemberIds.add(r6.userId);
113442
+ }
113443
+ }
113444
+ return { restrictedUserIds, projectMemberIds, rowCountByUser };
113445
+ }
113446
+ async function handleUpdateProject(input) {
113447
+ const { id } = input;
113448
+ const resolved = await resolveTeamId(input.teamId);
113449
+ if (!resolved.ok) return resolved.response;
113450
+ const existing = await loadProjectInTeam(id, resolved.teamId);
113451
+ if (!existing) {
113452
+ return textResponse(
113453
+ `Project ${id} not found, or it is not owned by this team.`
113454
+ );
113455
+ }
113456
+ const owningTeamId = existing.teamId;
113457
+ const willRename = input.name !== void 0 && input.name !== existing.name;
113458
+ if (willRename) {
113459
+ const [dupe] = await db.select({ id: schema_exports.projects.id }).from(schema_exports.projects).where(
113460
+ and(
113461
+ eq(schema_exports.projects.teamId, owningTeamId),
113462
+ eq(schema_exports.projects.name, input.name),
113463
+ sql`${schema_exports.projects.id} != ${id}`
113464
+ )
113465
+ ).limit(1);
113466
+ if (dupe) {
113467
+ return textResponse(
113468
+ `A project named "${input.name}" already exists in this team. Choose a different name.`
113469
+ );
113470
+ }
113471
+ }
113472
+ const oldRate = existing.rate;
113473
+ const oldInternal = existing.internal;
113474
+ const newRate = input.rate !== void 0 ? input.rate : existing.rate;
113475
+ const newInternal = input.internal !== void 0 ? input.internal : existing.internal;
113476
+ await db.update(schema_exports.projects).set({
113477
+ ...input.name !== void 0 ? { name: input.name } : {},
113478
+ ...input.description !== void 0 ? { description: input.description } : {},
113479
+ ...input.customerId !== void 0 ? { customerId: input.customerId } : {},
113480
+ ...input.rate !== void 0 ? { rate: input.rate } : {},
113481
+ ...input.currency !== void 0 ? { currency: input.currency } : {},
113482
+ ...input.billable !== void 0 ? { billable: input.billable } : {},
113483
+ ...input.estimate !== void 0 ? { estimate: input.estimate } : {},
113484
+ ...input.internal !== void 0 ? { internal: input.internal } : {},
113485
+ updatedAt: sql`now()`
113486
+ }).where(eq(schema_exports.projects.id, id));
113487
+ if (willRename) {
113488
+ const abbreviation = generateProjectAbbreviation(input.name);
113489
+ const currentYear = (/* @__PURE__ */ new Date()).getFullYear();
113490
+ await db.execute(sql`
113491
+ UPDATE tickets
113492
+ SET
113493
+ ticket_number = ${currentYear} || '-' || ${abbreviation} || '-' || LPAD(numbered.rn::text, 3, '0'),
113494
+ updated_at = NOW()
113495
+ FROM (
113496
+ SELECT id, ROW_NUMBER() OVER (ORDER BY created_at ASC) AS rn
113497
+ FROM tickets
113498
+ WHERE project_id = ${id}
113499
+ ) AS numbered
113500
+ WHERE tickets.id = numbered.id
113501
+ `);
113502
+ }
113503
+ const wasBillable = (oldRate ?? 0) > 0 && !(oldInternal ?? false);
113504
+ const isBillable = (newRate ?? 0) > 0 && !(newInternal ?? false);
113505
+ if (!wasBillable && isBillable) {
113506
+ await db.update(schema_exports.timesheetEvents).set({ billingStatus: "to_bill", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
113507
+ and(
113508
+ eq(schema_exports.timesheetEvents.projectId, id),
113509
+ eq(schema_exports.timesheetEvents.billingStatus, "unbillable"),
113510
+ eq(schema_exports.timesheetEvents.isDeleted, false)
113511
+ )
113512
+ );
113513
+ } else if (wasBillable && !isBillable) {
113514
+ await db.update(schema_exports.timesheetEvents).set({ billingStatus: "unbillable", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
113515
+ and(
113516
+ eq(schema_exports.timesheetEvents.projectId, id),
113517
+ eq(schema_exports.timesheetEvents.billingStatus, "to_bill"),
113518
+ eq(schema_exports.timesheetEvents.isDeleted, false)
113519
+ )
113520
+ );
113521
+ }
113522
+ const [updated] = await db.select({
113523
+ id: schema_exports.projects.id,
113524
+ name: schema_exports.projects.name,
113525
+ description: schema_exports.projects.description,
113526
+ rate: schema_exports.projects.rate,
113527
+ currency: schema_exports.projects.currency,
113528
+ internal: schema_exports.projects.internal,
113529
+ customerName: schema_exports.customers.name
113530
+ }).from(schema_exports.projects).leftJoin(schema_exports.customers, eq(schema_exports.projects.customerId, schema_exports.customers.id)).where(eq(schema_exports.projects.id, id)).limit(1);
113531
+ if (!updated) {
113532
+ return textResponse(`Failed to update project ${id}.`);
113533
+ }
113534
+ const lines = [
113535
+ "\u2705 **Project Updated**",
113536
+ "",
113537
+ `Name: ${updated.name} (ID: ${updated.id})`
113538
+ ];
113539
+ if (updated.description) lines.push(`Description: ${updated.description}`);
113540
+ if (updated.customerName) lines.push(`Customer: ${updated.customerName}`);
113541
+ if (updated.rate != null) {
113542
+ lines.push(
113543
+ `Rate: ${updated.rate}${updated.currency ? ` ${updated.currency}` : ""}`
113544
+ );
113545
+ }
113546
+ lines.push(`Internal: ${updated.internal ? "yes" : "no"}`);
113547
+ if (willRename) {
113548
+ lines.push("", "Note: tickets for this project were renumbered.");
113549
+ }
113550
+ return textResponse(lines.join("\n"));
113551
+ }
113552
+ async function handleGetProjectMembers(input) {
113553
+ const { projectId } = input;
113554
+ const resolved = await resolveTeamId(input.teamId);
113555
+ if (!resolved.ok) return resolved.response;
113556
+ const project = await loadProjectInTeam(projectId, resolved.teamId);
113557
+ if (!project) {
113558
+ return textResponse(
113559
+ `Project ${projectId} not found, or it is not owned by this team.`
113560
+ );
113561
+ }
113562
+ const [roster, state2] = await Promise.all([
113563
+ getTeamRoster(resolved.teamId),
113564
+ getProjectAccessState(resolved.teamId, projectId)
113565
+ ]);
113566
+ const rosterById = new Map(roster.map((m4) => [m4.userId, m4]));
113567
+ const explicitList = [...state2.projectMemberIds].map((uid2) => {
113568
+ const m4 = rosterById.get(uid2);
113569
+ return `- ${m4 ? memberLabel(m4) : uid2} (userId: ${uid2})`;
113570
+ }).join("\n") || " (none)";
113571
+ const rosterList = roster.map((m4) => {
113572
+ const isOwner = m4.role === "owner";
113573
+ const restricted = state2.restrictedUserIds.has(m4.userId);
113574
+ let access;
113575
+ if (isOwner) {
113576
+ access = "all projects (owner)";
113577
+ } else if (!restricted) {
113578
+ access = "all projects (no restrictions)";
113579
+ } else if (state2.projectMemberIds.has(m4.userId)) {
113580
+ access = "HAS access to this project";
113581
+ } else {
113582
+ access = "NO access to this project";
113583
+ }
113584
+ return `- ${memberLabel(m4)} (userId: ${m4.userId}, role: ${m4.role ?? "member"}) \u2014 ${access}`;
113585
+ }).join("\n");
113586
+ const note = state2.projectMemberIds.size === 0 ? "No members are explicitly assigned to this project, so every owner and every unrestricted member can see it." : `${state2.projectMemberIds.size} member(s) are explicitly assigned to this project.`;
113587
+ return textResponse(
113588
+ `**Project members for "${project.name}"** (ID: ${project.id})
113589
+
113590
+ ${note}
113591
+
113592
+ Explicitly assigned members:
113593
+ ${explicitList}
113594
+
113595
+ Team roster (use these userIds with set/add/remove-project-member):
113596
+ ${rosterList}`
113597
+ );
113598
+ }
113599
+ async function handleSetProjectMembers(input) {
113600
+ const ctx = authContext;
113601
+ const { projectId } = input;
113602
+ const resolved = await resolveTeamId(input.teamId);
113603
+ if (!resolved.ok) return resolved.response;
113604
+ const ownerError = await requireTeamOwner(resolved.teamId, ctx.userId);
113605
+ if (ownerError) return ownerError;
113606
+ const project = await loadProjectInTeam(projectId, resolved.teamId);
113607
+ if (!project) {
113608
+ return textResponse(
113609
+ `Project ${projectId} not found, or it is not owned by this team.`
113610
+ );
113611
+ }
113612
+ const before = await getProjectAccessState(resolved.teamId, projectId);
113613
+ const memberIds = /* @__PURE__ */ new Set();
113614
+ for (const userId of input.userIds ?? []) {
113615
+ const r6 = await resolveTeamMember(resolved.teamId, { userId });
113616
+ if (!r6.ok) return r6.response;
113617
+ memberIds.add(r6.member.userId);
113618
+ }
113619
+ for (const email5 of input.emails ?? []) {
113620
+ const r6 = await resolveTeamMember(resolved.teamId, { email: email5 });
113621
+ if (!r6.ok) return r6.response;
113622
+ memberIds.add(r6.member.userId);
113623
+ }
113624
+ await setProjectMemberAccess({
113625
+ projectId,
113626
+ teamId: resolved.teamId,
113627
+ memberIds: [...memberIds],
113628
+ createdBy: ctx.userId
113629
+ });
113630
+ const roster = await getTeamRoster(resolved.teamId);
113631
+ const rosterById = new Map(roster.map((m4) => [m4.userId, m4]));
113632
+ const list = [...memberIds].map((uid2) => {
113633
+ const m4 = rosterById.get(uid2);
113634
+ return `- ${m4 ? memberLabel(m4) : uid2} (userId: ${uid2})`;
113635
+ }).join("\n") || " (none \u2014 all explicit assignments cleared; the project is visible to every owner and unrestricted member)";
113636
+ const newlyRestricted = [...memberIds].filter(
113637
+ (uid2) => !before.restrictedUserIds.has(uid2)
113638
+ );
113639
+ let warning2 = "";
113640
+ if (newlyRestricted.length > 0) {
113641
+ const names = newlyRestricted.map((uid2) => memberLabel(rosterById.get(uid2) ?? { fullName: null, email: null, userId: uid2 })).join(", ");
113642
+ warning2 = `
113643
+
113644
+ \u26A0\uFE0F ${names} previously had no restrictions (could see all projects). They are now restricted to only the projects explicitly assigned to them.`;
113645
+ }
113646
+ return textResponse(
113647
+ `\u2705 **Project members updated**
113648
+
113649
+ Members with explicit access to this project:
113650
+ ${list}${warning2}`
113651
+ );
113652
+ }
113653
+ async function handleAddProjectMember(input) {
113654
+ const ctx = authContext;
113655
+ const { projectId } = input;
113656
+ const resolved = await resolveTeamId(input.teamId);
113657
+ if (!resolved.ok) return resolved.response;
113658
+ const ownerError = await requireTeamOwner(resolved.teamId, ctx.userId);
113659
+ if (ownerError) return ownerError;
113660
+ const project = await loadProjectInTeam(projectId, resolved.teamId);
113661
+ if (!project) {
113662
+ return textResponse(
113663
+ `Project ${projectId} not found, or it is not owned by this team.`
113664
+ );
113665
+ }
113666
+ const member2 = await resolveTeamMember(resolved.teamId, {
113667
+ userId: input.userId,
113668
+ email: input.email
113669
+ });
113670
+ if (!member2.ok) return member2.response;
113671
+ const state2 = await getProjectAccessState(resolved.teamId, projectId);
113672
+ if (state2.projectMemberIds.has(member2.member.userId)) {
113673
+ return textResponse(
113674
+ `${memberLabel(member2.member)} already has explicit access to this project.`
113675
+ );
113676
+ }
113677
+ const wasUnrestricted = !state2.restrictedUserIds.has(member2.member.userId);
113678
+ await setProjectMemberAccess({
113679
+ projectId,
113680
+ teamId: resolved.teamId,
113681
+ memberIds: [...state2.projectMemberIds, member2.member.userId],
113682
+ createdBy: ctx.userId
113683
+ });
113684
+ let text3 = `\u2705 Added ${memberLabel(member2.member)} (userId: ${member2.member.userId}) to the project.`;
113685
+ if (wasUnrestricted) {
113686
+ text3 += "\n\n\u26A0\uFE0F This member previously had no access restrictions (they could see all projects). They are now restricted to ONLY the projects explicitly assigned to them. Grant any other projects they still need with add-project-member, or remove all their assignments to restore full visibility.";
113687
+ }
113688
+ return textResponse(text3);
113689
+ }
113690
+ async function handleRemoveProjectMember(input) {
113691
+ const ctx = authContext;
113692
+ const { projectId } = input;
113693
+ const resolved = await resolveTeamId(input.teamId);
113694
+ if (!resolved.ok) return resolved.response;
113695
+ const ownerError = await requireTeamOwner(resolved.teamId, ctx.userId);
113696
+ if (ownerError) return ownerError;
113697
+ const project = await loadProjectInTeam(projectId, resolved.teamId);
113698
+ if (!project) {
113699
+ return textResponse(
113700
+ `Project ${projectId} not found, or it is not owned by this team.`
113701
+ );
113702
+ }
113703
+ const member2 = await resolveTeamMember(resolved.teamId, {
113704
+ userId: input.userId,
113705
+ email: input.email
113706
+ });
113707
+ if (!member2.ok) return member2.response;
113708
+ const state2 = await getProjectAccessState(resolved.teamId, projectId);
113709
+ if (!state2.projectMemberIds.has(member2.member.userId)) {
113710
+ return textResponse(
113711
+ `${memberLabel(member2.member)} has no explicit assignment to this project; nothing to remove.`
113712
+ );
113713
+ }
113714
+ await setProjectMemberAccess({
113715
+ projectId,
113716
+ teamId: resolved.teamId,
113717
+ memberIds: [...state2.projectMemberIds].filter(
113718
+ (uid2) => uid2 !== member2.member.userId
113719
+ ),
113720
+ createdBy: ctx.userId
113721
+ });
113722
+ let text3 = `\u2705 Removed ${memberLabel(member2.member)} (userId: ${member2.member.userId}) from the project.`;
113723
+ if ((state2.rowCountByUser.get(member2.member.userId) ?? 0) <= 1) {
113724
+ text3 += "\n\nThis was the member's last project assignment, so their access restrictions were cleared \u2014 they can see all projects in the team again (default behavior).";
113725
+ }
113726
+ return textResponse(text3);
113727
+ }
113126
113728
 
113127
113729
  // src/tools/session-completion.ts
113128
113730
  init_drizzle_orm();
@@ -120260,6 +120862,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request3) => {
120260
120862
  return await handleGetProjects(asToolArgs(toolArgs));
120261
120863
  case "create-project":
120262
120864
  return await handleCreateProject(asToolArgs(toolArgs));
120865
+ case "update-project":
120866
+ return await handleUpdateProject(asToolArgs(toolArgs));
120867
+ case "get-project-members":
120868
+ return await handleGetProjectMembers(
120869
+ asToolArgs(toolArgs)
120870
+ );
120871
+ case "set-project-members":
120872
+ return await handleSetProjectMembers(
120873
+ asToolArgs(toolArgs)
120874
+ );
120875
+ case "add-project-member":
120876
+ return await handleAddProjectMember(
120877
+ asToolArgs(toolArgs)
120878
+ );
120879
+ case "remove-project-member":
120880
+ return await handleRemoveProjectMember(
120881
+ asToolArgs(toolArgs)
120882
+ );
120263
120883
  case "list-documents":
120264
120884
  return await handleListDocuments(asToolArgs(toolArgs));
120265
120885
  case "get-document":