@codazen/harmonica-mcp 4.0.1 → 4.1.0

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.
Files changed (2) hide show
  1. package/dist/index.js +101 -42
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -25533,17 +25533,14 @@ function registerMembershipTools(server, ctx, client) {
25533
25533
  );
25534
25534
  server.tool(
25535
25535
  "add_member",
25536
- "Add a user to the configured organization. Creates the user record if they do not exist yet.",
25536
+ "Grant a user access to the configured organization. Their name and avatar are filled in when they first sign in.",
25537
25537
  {
25538
- name: external_exports.string().describe("Full name of the user"),
25539
25538
  email: external_exports.string().email().describe("Email address (used as the user ID)"),
25540
- title: external_exports.string().optional().describe("Job title"),
25541
- responsibility: external_exports.string().optional().describe("Primary responsibility or role description"),
25542
25539
  role: external_exports.enum(["member", "admin", "client"]).optional().describe('Organization role (defaults to "member")')
25543
25540
  },
25544
- async ({ name, email: email2, title, responsibility, role }) => {
25541
+ async ({ email: email2, role }) => {
25545
25542
  try {
25546
- const member = await client.addOrgMember(ctx.orgId, { name, email: email2, title, responsibility, role });
25543
+ const member = await client.addOrgMember(ctx.orgId, { email: email2, role });
25547
25544
  const lines = [
25548
25545
  "Member added successfully.",
25549
25546
  "",
@@ -25562,17 +25559,14 @@ function registerMembershipTools(server, ctx, client) {
25562
25559
  );
25563
25560
  server.tool(
25564
25561
  "update_member_role",
25565
- "Update a member's role or profile within the configured organization.",
25562
+ "Change a member's role within the configured organization.",
25566
25563
  {
25567
25564
  userId: external_exports.string().describe("User ID (email address) of the member to update"),
25568
- role: external_exports.enum(["member", "admin", "client"]).optional().describe("New organization role"),
25569
- name: external_exports.string().optional().describe("Updated display name"),
25570
- title: external_exports.string().optional().describe("Updated job title"),
25571
- responsibility: external_exports.string().optional().describe("Updated responsibility description")
25565
+ role: external_exports.enum(["member", "admin", "client"]).describe("New organization role")
25572
25566
  },
25573
- async ({ userId, role, name, title, responsibility }) => {
25567
+ async ({ userId, role }) => {
25574
25568
  try {
25575
- const member = await client.updateOrgMember(ctx.orgId, userId, { role, name, title, responsibility });
25569
+ const member = await client.updateOrgMember(ctx.orgId, userId, { role });
25576
25570
  if (!member) {
25577
25571
  return { content: [{ type: "text", text: `Member not found: "${userId}"` }], isError: true };
25578
25572
  }
@@ -26335,6 +26329,15 @@ var normalizeSystemId = (raw) => {
26335
26329
  if (!id) return raw;
26336
26330
  return { ...b, systemId: id };
26337
26331
  };
26332
+ var normalizeSystemIdAndName = (raw) => {
26333
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return raw;
26334
+ const b = raw;
26335
+ const result = { ...b };
26336
+ const id = b["systemId"] || b["projectId"];
26337
+ if (id) result["systemId"] = id;
26338
+ if ((b["systemName"] == null || b["systemName"] === "") && b["projectName"]) result["systemName"] = b["projectName"];
26339
+ return result;
26340
+ };
26338
26341
  var normalizeSystemIdAndTitle = (raw) => {
26339
26342
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return raw;
26340
26343
  const b = raw;
@@ -26350,15 +26353,17 @@ var BeatApiSchema = external_exports.preprocess(
26350
26353
  if (typeof raw !== "object" || raw === null) return raw;
26351
26354
  const b = raw;
26352
26355
  const id = b["systemId"] || b["projectId"];
26353
- if (!id) return raw;
26354
- return { ...b, systemId: id };
26356
+ const result = { ...b };
26357
+ if (id) result["systemId"] = id;
26358
+ if ((b["systemName"] == null || b["systemName"] === "") && b["projectName"]) result["systemName"] = b["projectName"];
26359
+ return result;
26355
26360
  },
26356
26361
  external_exports.object({
26357
26362
  beatId: external_exports.string(),
26358
26363
  systemId: external_exports.string(),
26359
- /** Source project's title; attached by the flat /api/beats endpoint for
26360
- * multi-project (teamspace/org) responses. */
26361
- projectName: external_exports.string().nullish(),
26364
+ /** Source system's title; attached by the flat /api/beats endpoint for
26365
+ * multi-system (teamspace/org) responses. */
26366
+ systemName: external_exports.string().nullish(),
26362
26367
  title: external_exports.string(),
26363
26368
  version: external_exports.number().nullish(),
26364
26369
  description: external_exports.string().nullish(),
@@ -26824,13 +26829,13 @@ var NextActionSchema = external_exports.object({
26824
26829
  var NextActionsResponseSchema = external_exports.object({
26825
26830
  actions: external_exports.array(NextActionSchema)
26826
26831
  }).passthrough();
26827
- var SearchResultSchema = external_exports.preprocess(normalizeSystemId, external_exports.object({
26832
+ var SearchResultSchema = external_exports.preprocess(normalizeSystemIdAndName, external_exports.object({
26828
26833
  entityId: external_exports.string(),
26829
26834
  entityType: external_exports.enum(["beat", "note", "revision", "beatVersion", "drop", "project", "system"]),
26830
26835
  score: external_exports.number(),
26831
26836
  snippet: external_exports.string(),
26832
26837
  systemId: external_exports.string().optional(),
26833
- projectName: external_exports.string().optional(),
26838
+ systemName: external_exports.string().optional(),
26834
26839
  teamspaceId: external_exports.string().optional()
26835
26840
  }).passthrough());
26836
26841
  var SearchScopeSchema = external_exports.object({
@@ -27822,7 +27827,7 @@ function registerNoteTools(server, ctx, client) {
27822
27827
  const noteId = await client.getNextNoteId(projectCode, systemId);
27823
27828
  note = await client.createNote({
27824
27829
  noteId,
27825
- projectId: systemId,
27830
+ systemId,
27826
27831
  beatId,
27827
27832
  revisionId,
27828
27833
  beatVersionId,
@@ -28921,7 +28926,7 @@ Systems (${createdProjects.length}):`);
28921
28926
  const n = await client.createNote({
28922
28927
  noteId: `N-TEMP-${(0, import_node_crypto.randomUUID)()}`,
28923
28928
  // overridden server-side
28924
- projectId: project.systemId,
28929
+ systemId: project.systemId,
28925
28930
  noteType: note.noteType,
28926
28931
  content: note.content,
28927
28932
  rationale: note.rationale,
@@ -29581,6 +29586,7 @@ function registerPulseReportTools(server, _ctx, client) {
29581
29586
  }
29582
29587
 
29583
29588
  // ../../libs/harmonica-services/src/delivery-policy.decide.ts
29589
+ var DELIVERY_STEPS = ["openPullRequest", "requestAgentReview", "watchPullRequest"];
29584
29590
  function decideDeliveryStep(resolution, step) {
29585
29591
  if (!resolution.resolved) return "ask";
29586
29592
  const value = resolution.policy[step];
@@ -29588,6 +29594,19 @@ function decideDeliveryStep(resolution, step) {
29588
29594
  if (value === false) return "prohibited";
29589
29595
  return "ask";
29590
29596
  }
29597
+ function mergeAgentDeliveryPolicy(current, patch) {
29598
+ const merged = { ...current };
29599
+ for (const step of DELIVERY_STEPS) {
29600
+ if (!(step in patch)) continue;
29601
+ const value = patch[step];
29602
+ if (value === null) {
29603
+ delete merged[step];
29604
+ } else if (value === true || value === false) {
29605
+ merged[step] = value;
29606
+ }
29607
+ }
29608
+ return merged;
29609
+ }
29591
29610
 
29592
29611
  // ../../libs/harmonica-services/src/mcp/tools/revision-lifecycle-tools.ts
29593
29612
  async function checkDeliveryStep(client, revisionId, step) {
@@ -30691,7 +30710,7 @@ function formatImportSummary(result, targetTeamspaceId) {
30691
30710
  lines.push("", `Errors (${result.errors.length}):`);
30692
30711
  result.errors.forEach((e) => lines.push(` - ${e}`));
30693
30712
  }
30694
- const hasTeamspaceData = result.projectTeamspaceId !== void 0 || (result.counts.drops ?? 0) + (result.counts.deliverables ?? 0) > 0;
30713
+ const hasTeamspaceData = result.systemTeamspaceId !== void 0 || (result.counts.drops ?? 0) + (result.counts.deliverables ?? 0) > 0;
30695
30714
  if (!targetTeamspaceId && hasTeamspaceData) {
30696
30715
  lines.push(
30697
30716
  "",
@@ -30965,8 +30984,14 @@ function registerSystemTools(server, ctx, client) {
30965
30984
  if (Object.keys(nonEmpty).length === 0) {
30966
30985
  return { content: [{ type: "text", text: "No updates provided." }], isError: true };
30967
30986
  }
30968
- await assertSystemInOrg(client, systemId, ctx.orgId);
30987
+ const currentSystem = await fetchSystemInOrg(client, systemId, ctx.orgId);
30969
30988
  if (nonEmpty["accountId"] === "") delete nonEmpty["accountId"];
30989
+ if (nonEmpty["agentDeliveryPolicy"] && typeof nonEmpty["agentDeliveryPolicy"] === "object") {
30990
+ nonEmpty["agentDeliveryPolicy"] = mergeAgentDeliveryPolicy(
30991
+ currentSystem.agentDeliveryPolicy,
30992
+ nonEmpty["agentDeliveryPolicy"]
30993
+ );
30994
+ }
30970
30995
  if (typeof updates.teamspaceId === "string") {
30971
30996
  const teamspace = await client.getTeamspace(updates.teamspaceId);
30972
30997
  if (!teamspace) {
@@ -32688,6 +32713,16 @@ function safeParseErrorBody(body) {
32688
32713
  return void 0;
32689
32714
  }
32690
32715
  }
32716
+ var ORG_ROLE_PRECEDENCE = ["admin", "member", "client"];
32717
+ function asLegacyMember(m) {
32718
+ const roles = Array.isArray(m["roles"]) ? m["roles"] : [];
32719
+ const rank = (role2) => {
32720
+ const i = ORG_ROLE_PRECEDENCE.indexOf(role2);
32721
+ return i === -1 ? ORG_ROLE_PRECEDENCE.length : i;
32722
+ };
32723
+ const role = [...roles].sort((a, b) => rank(a) - rank(b) || a.localeCompare(b))[0];
32724
+ return { ...m, role: role ?? "" };
32725
+ }
32691
32726
  function createHttpClient(config2) {
32692
32727
  const url = new URL(config2.apiBaseUrl);
32693
32728
  const isLocalhost = url.hostname === "localhost" || url.hostname === "127.0.0.1";
@@ -32802,21 +32837,37 @@ function createHttpClient(config2) {
32802
32837
  getMembership: async (userId, orgId) => {
32803
32838
  return { userId, orgId, role: "member" };
32804
32839
  },
32805
- // Org membership management
32840
+ // Org membership management — the generic member API, shared with the
32841
+ // teamspace, account and system tiers (rev-4551c900). The org-only routes
32842
+ // this used to call were a second implementation over the same Grant rows
32843
+ // that skipped the lockout rule.
32844
+ //
32845
+ // Those routes also set a name, title and responsibility on the User
32846
+ // record. Nothing does that any more: a name is written on first sign-in
32847
+ // from the identity provider, and the fields are gone from the tool
32848
+ // schemas rather than accepted and silently dropped.
32806
32849
  listOrgMembers: async (orgId) => {
32807
- const result = await request("GET", `/api/organizations/${encodeURIComponent(orgId)}/members`);
32808
- return result ?? [];
32850
+ const result = await request("GET", `/api/orgs/${encodeURIComponent(orgId)}/members`);
32851
+ return (result?.members ?? []).map(asLegacyMember);
32809
32852
  },
32810
32853
  addOrgMember: async (orgId, input) => {
32811
- const result = await request("POST", `/api/organizations/${encodeURIComponent(orgId)}/members`, input);
32812
- return result;
32854
+ const result = await request("POST", `/api/orgs/${encodeURIComponent(orgId)}/members`, {
32855
+ userId: input.email.toLowerCase(),
32856
+ roles: [input.role ?? "member"]
32857
+ });
32858
+ return asLegacyMember(result?.member ?? {});
32813
32859
  },
32814
32860
  updateOrgMember: async (orgId, userId, input) => {
32815
- const result = await request("PUT", `/api/organizations/${encodeURIComponent(orgId)}/members/${encodeURIComponent(userId)}`, input);
32816
- return result;
32861
+ if (!input.role) {
32862
+ const current = await request("GET", `/api/orgs/${encodeURIComponent(orgId)}/members`);
32863
+ const found = (current?.members ?? []).find((m) => String(m["userId"]).toLowerCase() === userId.toLowerCase());
32864
+ return found ? asLegacyMember(found) : void 0;
32865
+ }
32866
+ const result = await request("PATCH", `/api/orgs/${encodeURIComponent(orgId)}/members/${encodeURIComponent(userId)}`, { roles: [input.role] });
32867
+ return asLegacyMember(result?.member ?? {});
32817
32868
  },
32818
32869
  removeOrgMember: async (orgId, userId) => {
32819
- await request("DELETE", `/api/organizations/${encodeURIComponent(orgId)}/members/${encodeURIComponent(userId)}`);
32870
+ await request("DELETE", `/api/orgs/${encodeURIComponent(orgId)}/members/${encodeURIComponent(userId)}`);
32820
32871
  },
32821
32872
  // Both renameOrganization and updateOrganization PATCH the same endpoint.
32822
32873
  // Server-side routing determines which service runs based on which fields are present.
@@ -32846,7 +32897,7 @@ function createHttpClient(config2) {
32846
32897
  try {
32847
32898
  const result = await request(
32848
32899
  "POST",
32849
- `/api/organizations/${encodeURIComponent(sourceOrgId)}/transfer-project`,
32900
+ `/api/organizations/${encodeURIComponent(sourceOrgId)}/transfer-system`,
32850
32901
  { systemId, targetOrgId }
32851
32902
  );
32852
32903
  const system = result?.system ? {
@@ -32903,7 +32954,11 @@ function createHttpClient(config2) {
32903
32954
  const result = await request("GET", `/api/systems/${encodeURIComponent(systemId)}/facts`);
32904
32955
  return result?.data;
32905
32956
  },
32906
- saveSystemFact: (input) => request("PUT", `/api/systems/${encodeURIComponent(String(input.projectId))}/facts`, input),
32957
+ saveSystemFact: (input) => {
32958
+ const effectiveId = input.systemId ?? input.projectId;
32959
+ if (!effectiveId) throw new Error("saveSystemFact: systemId (or legacy projectId) is required");
32960
+ return request("PUT", `/api/systems/${encodeURIComponent(String(effectiveId))}/facts`, input);
32961
+ },
32907
32962
  transitionSystemLifecycleState: async (systemId, targetState, options) => {
32908
32963
  return request("POST", `/api/systems/${encodeURIComponent(systemId)}/lifecycle/transitions`, {
32909
32964
  targetState,
@@ -33136,12 +33191,14 @@ function createHttpClient(config2) {
33136
33191
  return result?.note ?? result;
33137
33192
  },
33138
33193
  createNote: async (input) => {
33139
- const result = await request("POST", `/api/systems/${encodeURIComponent(input.projectId ?? "")}/notes`, input);
33194
+ const effectiveSystemId = input.systemId;
33195
+ if (!effectiveSystemId) throw new Error("createNote: systemId is required");
33196
+ const result = await request("POST", `/api/systems/${encodeURIComponent(effectiveSystemId)}/notes`, input);
33140
33197
  return result?.note ?? result;
33141
33198
  },
33142
33199
  updateNote: async (noteId, input) => {
33143
33200
  const existing = await request("GET", `/api/notes/${encodeURIComponent(noteId)}`);
33144
- const systemId = existing?.projectId ?? existing?.note?.projectId;
33201
+ const systemId = existing?.systemId ?? existing?.note?.systemId;
33145
33202
  if (systemId) {
33146
33203
  const result2 = await request("PATCH", `/api/systems/${encodeURIComponent(systemId)}/notes/${encodeURIComponent(noteId)}`, input);
33147
33204
  return result2?.note ?? result2;
@@ -33182,20 +33239,20 @@ function createHttpClient(config2) {
33182
33239
  },
33183
33240
  reassignNote: async (noteId, targetBeatId, reason, _actorId, targetRevisionId) => {
33184
33241
  const note = await request("GET", `/api/notes/${encodeURIComponent(noteId)}`);
33185
- if (!note?.note) return void 0;
33242
+ if (!note?.note?.systemId) return void 0;
33186
33243
  const result = await request(
33187
33244
  "POST",
33188
- `/api/systems/${encodeURIComponent(note.note.projectId)}/notes/${encodeURIComponent(noteId)}/reassign`,
33245
+ `/api/systems/${encodeURIComponent(note.note.systemId)}/notes/${encodeURIComponent(noteId)}/reassign`,
33189
33246
  { targetBeatId, targetRevisionId, reason }
33190
33247
  );
33191
33248
  return result?.note ?? result;
33192
33249
  },
33193
33250
  removeNote: async (noteId, reason, _actorId) => {
33194
33251
  const note = await request("GET", `/api/notes/${encodeURIComponent(noteId)}`);
33195
- if (!note?.note) return void 0;
33252
+ if (!note?.note?.systemId) return void 0;
33196
33253
  const result = await request(
33197
33254
  "POST",
33198
- `/api/systems/${encodeURIComponent(note.note.projectId)}/notes/${encodeURIComponent(noteId)}/remove`,
33255
+ `/api/systems/${encodeURIComponent(note.note.systemId)}/notes/${encodeURIComponent(noteId)}/remove`,
33199
33256
  { reason }
33200
33257
  );
33201
33258
  return result?.note ?? result;
@@ -34145,7 +34202,9 @@ function createHttpClient(config2) {
34145
34202
  ...parentGroupId !== void 0 && { parentGroupId },
34146
34203
  ...createdBy !== void 0 && { createdBy }
34147
34204
  };
34148
- const url2 = input.projectId ? `/api/systems/${encodeURIComponent(input.projectId)}/deliverable-groups` : `/api/teamspaces/${encodeURIComponent(input.teamspaceId)}/deliverable-groups`;
34205
+ const inputRec = input;
34206
+ const effectiveSystemId = inputRec.systemId ?? input.projectId;
34207
+ const url2 = effectiveSystemId ? `/api/systems/${encodeURIComponent(effectiveSystemId)}/deliverable-groups` : `/api/teamspaces/${encodeURIComponent(input.teamspaceId)}/deliverable-groups`;
34149
34208
  const result = await request("POST", url2, body);
34150
34209
  if (result === void 0) throw new Error(`Resource not found when creating DeliverableGroup`);
34151
34210
  return result;
@@ -34965,7 +35024,7 @@ function loadConfig() {
34965
35024
  };
34966
35025
  }
34967
35026
  async function main() {
34968
- console.error(`[harmonica-mcp] v${"4.0.1"} starting\u2026`);
35027
+ console.error(`[harmonica-mcp] v${"4.1.0"} starting\u2026`);
34969
35028
  const config2 = loadConfig();
34970
35029
  const client = createHttpClient({
34971
35030
  apiBaseUrl: config2.apiBaseUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codazen/harmonica-mcp",
3
- "version": "4.0.1",
3
+ "version": "4.1.0",
4
4
  "description": "MCP server for Harmonica — connect any MCP-compatible AI assistant to Harmonica",
5
5
  "license": "MIT",
6
6
  "bin": {