@codazen/harmonica-mcp 3.3.0 → 3.4.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 +296 -17
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -21261,13 +21261,40 @@ var EMPTY_COMPLETION_RESULT = {
21261
21261
  };
21262
21262
 
21263
21263
  // ../../libs/harmonica-services/src/mcp/server-factory.ts
21264
+ var DELIVERY_LATITUDE_INSTRUCTIONS = `AGENT DELIVERY LATITUDE (CHECK BEFORE YOU ACT)
21265
+
21266
+ Before you open a pull request, request the agent code review, or start watching
21267
+ a PR, resolve the delivery policy of the System the work belongs to and act on
21268
+ it. The policy hangs off the System and is reached through the Revision you are
21269
+ working \u2014 never through the repository, which may host many Systems.
21270
+
21271
+ - \`get_revision\` returns the resolved policy as an "Agent Delivery Policy"
21272
+ block, so the answer arrives in a call you were already making.
21273
+ - \`get_system_context\` carries the same block for a System you name directly.
21274
+ - For a doc-only or governance change that links a decision Note instead of a
21275
+ Revision, \`get_note\` on that Note carries the block.
21276
+
21277
+ Three independent toggles \u2014 openPullRequest, requestAgentReview,
21278
+ watchPullRequest:
21279
+
21280
+ - true: perform the step, do not ask, and say that you did it.
21281
+ - false: do NOT perform it and do not ask either \u2014 say the System's policy has
21282
+ it off, then move on. The tools also refuse a false step, including when you
21283
+ pass confirm: true.
21284
+ - unset, or unresolvable for any reason: ASK a human. Nothing except an explicit
21285
+ true is a grant, and every combination of the three is legitimate \u2014 never
21286
+ infer one toggle from another.
21287
+
21288
+ Re-read the policy before each step rather than caching it, so a human revoking
21289
+ a toggle mid-flight takes effect on your next action.`;
21264
21290
  async function createMcpServer(ctx, client, registerTools, registerResources) {
21265
- let instructions;
21291
+ let guidelines;
21266
21292
  try {
21267
- instructions = await client.getBeatGuidelines();
21293
+ guidelines = await client.getBeatGuidelines();
21268
21294
  } catch (err) {
21269
21295
  console.warn("[MCP] Failed to load beat guidelines for server instructions:", err);
21270
21296
  }
21297
+ const instructions = [guidelines, DELIVERY_LATITUDE_INSTRUCTIONS].filter(Boolean).join("\n\n");
21271
21298
  const server = new McpServer({
21272
21299
  name: "harmonica",
21273
21300
  version: "0.1.0"
@@ -25204,6 +25231,124 @@ ${members}${parent}`;
25204
25231
  );
25205
25232
  }
25206
25233
 
25234
+ // ../../libs/harmonica-services/src/mcp/tools/feature-flag-tools.ts
25235
+ var STAGE_ENUM = external_exports.enum([
25236
+ "local",
25237
+ "staging",
25238
+ "staging-alpha",
25239
+ "staging-beta",
25240
+ "alpha",
25241
+ "beta",
25242
+ "prod"
25243
+ ]);
25244
+ function registerFeatureFlagTools(server, _ctx, client) {
25245
+ server.tool(
25246
+ "list_flags",
25247
+ "List feature flags and their enabled stages. Use check_flag to test a single flag against a specific stage, or get_flag for full detail on one flag.",
25248
+ {
25249
+ includeArchived: external_exports.boolean().optional().describe("Include archived flags. Default false"),
25250
+ search: external_exports.string().optional().describe("Case-insensitive search against flag name and description"),
25251
+ cursor: external_exports.string().optional().describe("Pagination cursor from a prior list_flags result"),
25252
+ limit: external_exports.number().int().min(1).max(100).optional().describe("Flags per page. Default 20")
25253
+ },
25254
+ async ({ includeArchived, search, cursor, limit }) => {
25255
+ try {
25256
+ const page = await client.listFlags({ includeArchived, search, cursor, limit });
25257
+ if (page.flags.length === 0) {
25258
+ return { content: [{ type: "text", text: "No flags found." }] };
25259
+ }
25260
+ return { content: [{ type: "text", text: formatFlagList(page.flags, page.total, page.nextCursor) }] };
25261
+ } catch (err) {
25262
+ const message = err instanceof Error ? err.message : String(err);
25263
+ return { content: [{ type: "text", text: `Failed to list flags: ${message}` }], isError: true };
25264
+ }
25265
+ }
25266
+ );
25267
+ server.tool(
25268
+ "get_flag",
25269
+ "Get full detail for a single feature flag by name, including its enabled stages and optional change history.",
25270
+ {
25271
+ name: external_exports.string().describe("Flag name"),
25272
+ includeHistory: external_exports.boolean().optional().describe("Include change history entries. Default false")
25273
+ },
25274
+ async ({ name, includeHistory }) => {
25275
+ try {
25276
+ const flag = await client.getFlag(name, { includeHistory });
25277
+ if (!flag) {
25278
+ return { content: [{ type: "text", text: `Flag "${name}" not found.` }] };
25279
+ }
25280
+ return { content: [{ type: "text", text: formatFlag(flag, flag.history) }] };
25281
+ } catch (err) {
25282
+ const message = err instanceof Error ? err.message : String(err);
25283
+ return { content: [{ type: "text", text: `Failed to get flag: ${message}` }], isError: true };
25284
+ }
25285
+ }
25286
+ );
25287
+ server.tool(
25288
+ "check_flag",
25289
+ "Check whether a feature flag is enabled in a specific stage. Returns a simple enabled/disabled answer.",
25290
+ {
25291
+ name: external_exports.string().describe("Flag name"),
25292
+ stage: STAGE_ENUM.describe("Deployment stage to check against")
25293
+ },
25294
+ async ({ name, stage }) => {
25295
+ try {
25296
+ const enabled = await client.checkFlag(name, stage);
25297
+ const status = enabled ? "enabled" : "disabled";
25298
+ return {
25299
+ content: [{
25300
+ type: "text",
25301
+ text: `Flag "${name}" is **${status}** in stage "${stage}".`
25302
+ }]
25303
+ };
25304
+ } catch (err) {
25305
+ const message = err instanceof Error ? err.message : String(err);
25306
+ return { content: [{ type: "text", text: `Failed to check flag: ${message}` }], isError: true };
25307
+ }
25308
+ }
25309
+ );
25310
+ }
25311
+ function formatFlag(flag, history) {
25312
+ const archived = flag.archivedAt ? " **(archived)**" : "";
25313
+ const enabled = flag.enabledIn.length > 0 ? flag.enabledIn.join(", ") : "(none)";
25314
+ const parts = [
25315
+ `# Flag: ${flag.name}${archived}`,
25316
+ `**Scope:** ${flag.scope}`,
25317
+ `**Enabled in:** ${enabled}`,
25318
+ flag.description ? `**Description:** ${flag.description}` : null,
25319
+ `**Last updated:** ${flag.updatedAt.slice(0, 10)} by ${flag.updatedBy}`
25320
+ ].filter(Boolean).join("\n");
25321
+ if (!history?.length) return parts;
25322
+ const historyLines = history.map((h) => {
25323
+ const prev = h.previousEnabledIn.length > 0 ? h.previousEnabledIn.join(", ") : "(none)";
25324
+ const next = h.newEnabledIn.length > 0 ? h.newEnabledIn.join(", ") : "(none)";
25325
+ const note = h.note ? ` \u2014 ${h.note}` : "";
25326
+ return `- ${h.updatedAt.slice(0, 10)} ${h.updatedBy}: ${prev} \u2192 ${next}${note}`;
25327
+ }).join("\n");
25328
+ return `${parts}
25329
+
25330
+ ## Change History
25331
+ ${historyLines}`;
25332
+ }
25333
+ function formatFlagList(flags, total, nextCursor) {
25334
+ const rows = flags.map((f) => {
25335
+ const enabled = f.enabledIn.length > 0 ? f.enabledIn.join(", ") : "(none)";
25336
+ const archived = f.archivedAt ? " *(archived)*" : "";
25337
+ return `| ${f.name}${archived} | ${enabled} | ${f.updatedAt.slice(0, 10)} |`;
25338
+ });
25339
+ const table = [
25340
+ `# Feature Flags (${total} total, showing ${flags.length})`,
25341
+ "",
25342
+ "| Name | Enabled In | Updated |",
25343
+ "|---|---|---|",
25344
+ ...rows
25345
+ ].join("\n");
25346
+ if (!nextCursor) return table;
25347
+ return `${table}
25348
+
25349
+ More flags available. Pass \`cursor: "${nextCursor}"\` to list_flags to continue.`;
25350
+ }
25351
+
25207
25352
  // ../../libs/harmonica-services/src/mcp/tools/layer-tools.ts
25208
25353
  function registerLayerTools(server, ctx, client) {
25209
25354
  server.tool(
@@ -27109,6 +27254,20 @@ var DISMISSED_NOTE_STATUS = "dismissed";
27109
27254
  var NOTE_TYPE_VALUES = ["context", "assumption", "constraint", "guidance", "decision", "document"];
27110
27255
  var NOTE_STATUS_VALUES = ["active", "unvalidated", "validated", "invalidated", "inProgress", "resolved", "superseded", "dismissed"];
27111
27256
  var DECISION_SIGNIFICANCE_VALUES2 = ["strategic", "structural", "implementation"];
27257
+ async function deliveryPolicyBlock(client, note) {
27258
+ if (note.noteType !== "decision") return "";
27259
+ try {
27260
+ const resolution = await client.resolveDeliveryPolicyForNote(note.noteId);
27261
+ return `
27262
+
27263
+ ${formatDeliveryPolicy(
27264
+ resolution.resolved ? resolution.policy : void 0,
27265
+ resolution.resolved ? void 0 : resolution.reason
27266
+ )}`;
27267
+ } catch {
27268
+ return "";
27269
+ }
27270
+ }
27112
27271
  function registerNoteTools(server, ctx, client) {
27113
27272
  server.tool(
27114
27273
  "list_notes",
@@ -27179,7 +27338,7 @@ function registerNoteTools(server, ctx, client) {
27179
27338
  const SOURCE_DOC_FETCH_LIMIT = 5e3;
27180
27339
  let allNotesResult;
27181
27340
  try {
27182
- allNotesResult = await client.listAllProjectNotes(projectId, filters, SOURCE_DOC_FETCH_LIMIT);
27341
+ allNotesResult = await client.listAllSystemNotes(projectId, filters, SOURCE_DOC_FETCH_LIMIT);
27183
27342
  } catch (err) {
27184
27343
  const message = err instanceof Error ? err.message : String(err);
27185
27344
  return { content: [{ type: "text", text: `Failed to list project notes: ${message}` }], isError: true };
@@ -27200,7 +27359,7 @@ function registerNoteTools(server, ctx, client) {
27200
27359
  } else {
27201
27360
  let projectNotesResult;
27202
27361
  try {
27203
- projectNotesResult = await client.listAllProjectNotes(projectId, filters, limit, cursor);
27362
+ projectNotesResult = await client.listAllSystemNotes(projectId, filters, limit, cursor);
27204
27363
  } catch (err) {
27205
27364
  const message = err instanceof Error ? err.message : String(err);
27206
27365
  return { content: [{ type: "text", text: `Failed to list project notes: ${message}` }], isError: true };
@@ -27287,7 +27446,7 @@ function registerNoteTools(server, ctx, client) {
27287
27446
  async ({ projectId }) => {
27288
27447
  await assertProjectInOrg(client, projectId, ctx.orgId);
27289
27448
  const DOCUMENT_FETCH_LIMIT = 5e3;
27290
- const { notes: docs, hasMore: truncated } = await client.listAllProjectNotes(projectId, { noteType: "document" }, DOCUMENT_FETCH_LIMIT);
27449
+ const { notes: docs, hasMore: truncated } = await client.listAllSystemNotes(projectId, { noteType: "document" }, DOCUMENT_FETCH_LIMIT);
27291
27450
  const visibleDocs = docs.filter((d) => d.status !== DISMISSED_NOTE_STATUS);
27292
27451
  const truncationNotice = truncated && visibleDocs.length > 0 ? "\n\n> \u26A0\uFE0F This project has more than 5000 documents. Some documents may not be shown.\n" : "";
27293
27452
  const text = formatDocumentList(visibleDocs) + truncationNotice;
@@ -27325,7 +27484,7 @@ function registerNoteTools(server, ctx, client) {
27325
27484
  if (!note) {
27326
27485
  return { content: [{ type: "text", text: `Note not found: "${noteId}"` }], isError: true };
27327
27486
  }
27328
- const text = formatNoteDetail(note);
27487
+ const text = formatNoteDetail(note) + await deliveryPolicyBlock(client, note);
27329
27488
  return { content: [{ type: "text", text }] };
27330
27489
  } catch (err) {
27331
27490
  const message = err instanceof Error ? err.message : String(err);
@@ -29147,7 +29306,41 @@ function registerPulseReportTools(server, _ctx, client) {
29147
29306
  );
29148
29307
  }
29149
29308
 
29309
+ // ../../libs/harmonica-services/src/delivery-policy.decide.ts
29310
+ function decideDeliveryStep(resolution, step) {
29311
+ if (!resolution.resolved) return "ask";
29312
+ const value = resolution.policy[step];
29313
+ if (value === true) return "proceed";
29314
+ if (value === false) return "prohibited";
29315
+ return "ask";
29316
+ }
29317
+
29150
29318
  // ../../libs/harmonica-services/src/mcp/tools/revision-lifecycle-tools.ts
29319
+ async function checkDeliveryStep(client, revisionId, step) {
29320
+ const resolution = await client.resolveDeliveryPolicy(revisionId).catch(() => ({ resolved: false, reason: "unreadable" }));
29321
+ switch (decideDeliveryStep(resolution, step)) {
29322
+ case "prohibited":
29323
+ return {
29324
+ refusal: `Refused: the System's delivery policy prohibits this step.
29325
+
29326
+ **System:** ${resolution.resolved ? resolution.systemId : "unknown"}
29327
+ **Setting:** \`${step}\` is false
29328
+
29329
+ This is a deliberate choice by whoever configured the System, and \`confirm: true\` does not override it. If it is wrong, a human changes it with \`update_system\` (\`agentDeliveryPolicy\`) \u2014 do not work around it.`
29330
+ };
29331
+ case "proceed":
29332
+ return { notice: `Delivery policy for \`${step}\`: granted \u2014 no need to ask.` };
29333
+ // 'ask' is named rather than left to `default:` so a fourth DeliveryDecision
29334
+ // cannot silently inherit this wording. Both notices state the policy rather
29335
+ // than what happened — they are also appended to responses where the action
29336
+ // itself was a no-op (a dry run, or a gating feature flag being off).
29337
+ case "ask":
29338
+ default:
29339
+ return {
29340
+ notice: `Delivery policy for \`${step}\`: ${resolution.resolved ? "not set" : `unresolvable (${resolution.reason})`} \u2014 nothing blocks you, but ask a human before treating this as approved.`
29341
+ };
29342
+ }
29343
+ }
29151
29344
  function registerRevisionLifecycleTools(server, ctx, client) {
29152
29345
  server.tool(
29153
29346
  "create_revision",
@@ -29424,6 +29617,10 @@ Accepts the full PR metadata (number, url, branch, state, optional mergeSha). Fi
29424
29617
  return { content: [{ type: "text", text: `Revision not found: "${revisionId}"` }], isError: true };
29425
29618
  }
29426
29619
  await assertBeatInOrg(client, revision.beatId, ctx.orgId);
29620
+ const latitude = await checkDeliveryStep(client, revisionId, "openPullRequest");
29621
+ if ("refusal" in latitude) {
29622
+ return { content: [{ type: "text", text: latitude.refusal }], isError: true };
29623
+ }
29427
29624
  if (!confirm) {
29428
29625
  const shortId = revisionId.split("-").slice(0, 2).join("-");
29429
29626
  const lines2 = [
@@ -29445,6 +29642,7 @@ Accepts the full PR metadata (number, url, branch, state, optional mergeSha). Fi
29445
29642
  );
29446
29643
  }
29447
29644
  }
29645
+ lines2.push("", latitude.notice);
29448
29646
  return { content: [{ type: "text", text: lines2.join("\n") }] };
29449
29647
  }
29450
29648
  const result = await client.createPrForRevision(revisionId);
@@ -29458,7 +29656,9 @@ Accepts the full PR metadata (number, url, branch, state, optional mergeSha). Fi
29458
29656
  `**PR:** #${result.result.prNumber} \u2014 ${result.result.prUrl}`,
29459
29657
  `**Branch:** ${result.result.branch}`,
29460
29658
  "",
29461
- "The revision stays in `draft`. Transition it to `open` once the PR is marked ready for review."
29659
+ "The revision stays in `draft`. Transition it to `open` once the PR is marked ready for review.",
29660
+ "",
29661
+ latitude.notice
29462
29662
  ];
29463
29663
  return { content: [{ type: "text", text: lines.join("\n") }] };
29464
29664
  } catch (err) {
@@ -29483,11 +29683,17 @@ Accepts the full PR metadata (number, url, branch, state, optional mergeSha). Fi
29483
29683
  return { content: [{ type: "text", text: `Revision not found: "${revisionId}"` }], isError: true };
29484
29684
  }
29485
29685
  await assertBeatInOrg(client, revision.beatId, ctx.orgId);
29686
+ const latitude = await checkDeliveryStep(client, revisionId, "requestAgentReview");
29687
+ if ("refusal" in latitude) {
29688
+ return { content: [{ type: "text", text: latitude.refusal }], isError: true };
29689
+ }
29486
29690
  const result = await client.requestCodeReview({ revisionId, autoMerge, maxIterations, confirm });
29487
29691
  if (!result.success) {
29488
29692
  return { content: [{ type: "text", text: `Failed to request code review (${result.error.code}): ${result.error.message}` }], isError: true };
29489
29693
  }
29490
- return { content: [{ type: "text", text: result.result.message }] };
29694
+ return { content: [{ type: "text", text: `${result.result.message}
29695
+
29696
+ ${latitude.notice}` }] };
29491
29697
  } catch (err) {
29492
29698
  const message = err instanceof Error ? err.message : String(err);
29493
29699
  return { content: [{ type: "text", text: `Failed to request code review: ${message}` }], isError: true };
@@ -30349,7 +30555,7 @@ function registerProjectLifecycleTools(server, ctx, client) {
30349
30555
  if (system.orgId !== ctx.orgId) {
30350
30556
  return { content: [{ type: "text", text: `System "${systemId}" is not in this organization` }], isError: true };
30351
30557
  }
30352
- const result = await client.transitionProjectLifecycleState(systemId, targetState, {
30558
+ const result = await client.transitionSystemLifecycleState(systemId, targetState, {
30353
30559
  actor: { type: "human", id: ctx.user.userId, name: ctx.user.name },
30354
30560
  reason,
30355
30561
  decisionNoteId
@@ -30388,9 +30594,7 @@ function normalizeCreateAccountId(accountId) {
30388
30594
  }
30389
30595
 
30390
30596
  // ../../libs/harmonica-services/src/mcp/tools/system-tools.ts
30391
- function accountLine(request, resolved) {
30392
- const requested = request["accountId"];
30393
- if (requested === null || requested === "") return "**Account:** (unlinked)";
30597
+ function accountLine(_request, resolved) {
30394
30598
  return resolved ? `**Account:** ${resolved}` : "";
30395
30599
  }
30396
30600
  var PROJECT_EMBEDDING_FIELDS = ["title", "description", "strategy"];
@@ -30452,7 +30656,7 @@ function registerProjectTools(server, ctx, client) {
30452
30656
  description: external_exports.string().optional().describe("New system description"),
30453
30657
  strategy: external_exports.string().optional().describe('System strategy markdown \u2014 Org Strategy + System Strategy ("System Coda")'),
30454
30658
  teamspaceId: external_exports.string().nullable().optional().describe("Teamspace ID to associate this system with; pass null to remove the association"),
30455
- accountId: external_exports.string().nullable().optional().describe("Account that owns this System \u2014 the client or internal org unit. Pass null (or an empty string) to unlink. Reassigning moves the System so it is listed under exactly one Account."),
30659
+ accountId: external_exports.string().min(1).optional().describe("Account that owns this System \u2014 the client or internal org unit. Omit to leave unchanged. A System cannot be unlinked: reassigning moves it so it is listed under exactly one Account."),
30456
30660
  repoOwner: external_exports.string().optional().describe("GitHub repository owner (org or user)"),
30457
30661
  repoName: external_exports.string().optional().describe("GitHub repository name"),
30458
30662
  repoDefaultBranch: external_exports.string().optional().describe('Default branch for code check (defaults to "main")'),
@@ -30472,7 +30676,7 @@ function registerProjectTools(server, ctx, client) {
30472
30676
  return { content: [{ type: "text", text: "No updates provided." }], isError: true };
30473
30677
  }
30474
30678
  await assertProjectInOrg(client, systemId, ctx.orgId);
30475
- if (nonEmpty["accountId"] === "") nonEmpty["accountId"] = null;
30679
+ if (nonEmpty["accountId"] === "") delete nonEmpty["accountId"];
30476
30680
  if (typeof updates.teamspaceId === "string") {
30477
30681
  const teamspace = await client.getTeamspace(updates.teamspaceId);
30478
30682
  if (!teamspace) {
@@ -30546,7 +30750,7 @@ function registerProjectTools(server, ctx, client) {
30546
30750
  description: external_exports.string().optional().describe("System description"),
30547
30751
  strategy: external_exports.string().optional().describe('System strategy markdown \u2014 Org Strategy + System Strategy ("System Coda")'),
30548
30752
  teamspaceId: external_exports.string().optional().describe("Teamspace ID to associate this system with"),
30549
- accountId: external_exports.string().optional().describe("Account that owns this System \u2014 the client or internal org unit. Set it at creation so the System appears in the Account's Systems list; omit only for a System that belongs to no Account."),
30753
+ accountId: external_exports.string().min(1).describe("Account that owns this System \u2014 the client or internal org unit. REQUIRED: the Account is the only parent access follows, so a System without one is reachable by nobody."),
30550
30754
  repoOwner: external_exports.string().optional().describe("GitHub repository owner (org or user)"),
30551
30755
  repoName: external_exports.string().optional().describe("GitHub repository name"),
30552
30756
  repoDefaultBranch: external_exports.string().optional().describe('Default branch for code check (defaults to "main")')
@@ -30594,7 +30798,7 @@ function registerProjectTools(server, ctx, client) {
30594
30798
  return { content: [{ type: "text", text: `Failed to create system: ${message}` }], isError: true };
30595
30799
  }
30596
30800
  };
30597
- server.tool("create_system", "Create a new system in the configured organization. Pass accountId whenever the system belongs to a client \u2014 a system created without it is not listed under any Account.", createSystemSchema, createSystemHandler);
30801
+ server.tool("create_system", "Create a new system in the configured organization. accountId is required \u2014 it is the Account that owns the System, and the only parent access follows.", createSystemSchema, createSystemHandler);
30598
30802
  }
30599
30803
 
30600
30804
  // ../../libs/harmonica-services/src/mcp/tools/teamspace-tools.ts
@@ -31732,6 +31936,7 @@ function registerAllTools(server, ctx, client, profile) {
31732
31936
  registerCadenceScheduleTools(server, ctx, client);
31733
31937
  registerPulseReportTools(server, ctx, client);
31734
31938
  registerDownbeatHarmonyReportTools(server, ctx, client);
31939
+ registerFeatureFlagTools(server, ctx, client);
31735
31940
  }
31736
31941
 
31737
31942
  // ../../libs/harmonica-services/src/mcp/resources/beat-resources.ts
@@ -32094,11 +32299,23 @@ function createHttpClient(config2) {
32094
32299
  },
32095
32300
  updateProject: (projectId, updates) => request("PATCH", `/api/systems/${encodeURIComponent(projectId)}`, updates),
32096
32301
  archiveProject: (projectId) => request("PATCH", `/api/systems/${encodeURIComponent(projectId)}/archive`),
32302
+ getSystemFact: async (projectId) => {
32303
+ const result = await request("GET", `/api/systems/${encodeURIComponent(projectId)}/facts`);
32304
+ return result?.data;
32305
+ },
32097
32306
  getProjectFact: async (projectId) => {
32098
32307
  const result = await request("GET", `/api/systems/${encodeURIComponent(projectId)}/facts`);
32099
32308
  return result?.data;
32100
32309
  },
32310
+ saveSystemFact: (input) => request("PUT", `/api/systems/${encodeURIComponent(String(input.projectId))}/facts`, input),
32101
32311
  saveProjectFact: (input) => request("PUT", `/api/systems/${encodeURIComponent(String(input.projectId))}/facts`, input),
32312
+ transitionSystemLifecycleState: async (projectId, targetState, options) => {
32313
+ return request("POST", `/api/systems/${encodeURIComponent(projectId)}/lifecycle/transitions`, {
32314
+ targetState,
32315
+ reason: options.reason,
32316
+ decisionNoteId: options.decisionNoteId
32317
+ });
32318
+ },
32102
32319
  transitionProjectLifecycleState: async (projectId, targetState, options) => {
32103
32320
  return request("POST", `/api/systems/${encodeURIComponent(projectId)}/lifecycle/transitions`, {
32104
32321
  targetState,
@@ -32299,6 +32516,24 @@ function createHttpClient(config2) {
32299
32516
  const result = await request("GET", `/api/systems/${encodeURIComponent(projectId)}/notes${qs}`);
32300
32517
  return result?.notes ?? [];
32301
32518
  },
32519
+ listAllSystemNotes: async (projectId, filters, limit, cursor) => {
32520
+ const params = new URLSearchParams();
32521
+ if (filters?.noteType) {
32522
+ const types = Array.isArray(filters.noteType) ? filters.noteType : [filters.noteType];
32523
+ types.forEach((t) => params.append("type", t));
32524
+ }
32525
+ if (filters?.excludeNoteType) params.set("excludeType", filters.excludeNoteType);
32526
+ if (filters?.status) params.set("status", filters.status);
32527
+ if (filters?.revisionId) params.set("revisionId", filters.revisionId);
32528
+ if (filters?.beatVersionId) params.set("beatVersionId", filters.beatVersionId);
32529
+ if (filters?.excludeChildren) params.set("excludeChildren", "true");
32530
+ if (filters?.significance) params.set("significance", filters.significance);
32531
+ if (limit !== void 0) params.set("limit", String(limit));
32532
+ if (cursor !== void 0) params.set("cursor", cursor);
32533
+ const qs = params.toString() ? `?${params.toString()}` : "";
32534
+ const result = await request("GET", `/api/systems/${encodeURIComponent(projectId)}/notes/all${qs}`);
32535
+ return { notes: result?.notes ?? [], cursor: result?.cursor, hasMore: result?.hasMore ?? false };
32536
+ },
32302
32537
  listAllProjectNotes: async (projectId, filters, limit, cursor) => {
32303
32538
  const params = new URLSearchParams();
32304
32539
  if (filters?.noteType) {
@@ -32408,6 +32643,7 @@ function createHttpClient(config2) {
32408
32643
  return result?.note ?? result;
32409
32644
  },
32410
32645
  // Gaps are deprecated — replaced by assumption Notes. Return empty results.
32646
+ getSystemGaps: async () => [],
32411
32647
  getProjectGaps: async () => [],
32412
32648
  updateInformationGap: async () => false,
32413
32649
  // Revisions
@@ -32974,6 +33210,20 @@ function createHttpClient(config2) {
32974
33210
  );
32975
33211
  return result.score;
32976
33212
  },
33213
+ rankSystemBeatVersions: async (projectId, lens) => {
33214
+ const result = await request(
33215
+ "GET",
33216
+ `/api/systems/${encodeURIComponent(projectId)}/beat-versions/ranked?lens=${encodeURIComponent(lens)}`
33217
+ );
33218
+ return result.ranked ?? [];
33219
+ },
33220
+ rankSystemBeatVersionsWithLeverage: async (projectId, lens) => {
33221
+ const result = await request(
33222
+ "GET",
33223
+ `/api/systems/${encodeURIComponent(projectId)}/beat-versions/ranked?lens=${encodeURIComponent(lens)}&withLeverage=true`
33224
+ );
33225
+ return result.ranked ?? [];
33226
+ },
32977
33227
  rankProjectBeatVersions: async (projectId, lens) => {
32978
33228
  const result = await request(
32979
33229
  "GET",
@@ -33298,6 +33548,13 @@ function createHttpClient(config2) {
33298
33548
  );
33299
33549
  return { checks: res?.checks ?? [], nextCursor: res?.nextCursor };
33300
33550
  },
33551
+ listLatestSystemChecks: async (projectId) => {
33552
+ const res = await request(
33553
+ "GET",
33554
+ `/api/checks/latest?projectId=${encodeURIComponent(projectId)}`
33555
+ );
33556
+ return res?.checks ?? [];
33557
+ },
33301
33558
  listLatestProjectChecks: async (projectId) => {
33302
33559
  const res = await request(
33303
33560
  "GET",
@@ -33616,6 +33873,17 @@ function createHttpClient(config2) {
33616
33873
  if (!res) return { resolved: false, reason: "revision_not_found" };
33617
33874
  return res;
33618
33875
  },
33876
+ // Mirrors the Revision route. A 404 becomes note_not_found rather than
33877
+ // revision_not_found: reporting the wrong missing entity sends a human
33878
+ // looking for something that was never involved.
33879
+ resolveDeliveryPolicyForNote: async (noteId) => {
33880
+ const res = await request(
33881
+ "GET",
33882
+ `/api/notes/${encodeURIComponent(noteId)}/delivery-policy`
33883
+ );
33884
+ if (!res) return { resolved: false, reason: "note_not_found" };
33885
+ return res;
33886
+ },
33619
33887
  listAccountSystems: async (accountId) => {
33620
33888
  const res = await request("GET", `/api/accounts/${encodeURIComponent(accountId)}/systems`);
33621
33889
  return res?.systems ?? [];
@@ -34001,6 +34269,17 @@ function createHttpClient(config2) {
34001
34269
  `/api/notes/${encodeURIComponent(childNoteId)}/used-by`
34002
34270
  );
34003
34271
  return result?.usedBy;
34272
+ },
34273
+ // Feature flags (B-135 v9) — not available in HTTP transport; the published
34274
+ // npm package has no DynamoDB credentials.
34275
+ listFlags: async () => {
34276
+ throw new Error("Feature flag queries are not available in HTTP transport mode");
34277
+ },
34278
+ getFlag: async () => {
34279
+ throw new Error("Feature flag queries are not available in HTTP transport mode");
34280
+ },
34281
+ checkFlag: async () => {
34282
+ throw new Error("Feature flag queries are not available in HTTP transport mode");
34004
34283
  }
34005
34284
  };
34006
34285
  return client;
@@ -34051,7 +34330,7 @@ function loadConfig() {
34051
34330
  };
34052
34331
  }
34053
34332
  async function main() {
34054
- console.error(`[harmonica-mcp] v${"3.3.0"} starting\u2026`);
34333
+ console.error(`[harmonica-mcp] v${"3.4.0"} starting\u2026`);
34055
34334
  const config2 = loadConfig();
34056
34335
  const client = createHttpClient({
34057
34336
  apiBaseUrl: config2.apiBaseUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codazen/harmonica-mcp",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "description": "MCP server for Harmonica — connect any MCP-compatible AI assistant to Harmonica",
5
5
  "license": "MIT",
6
6
  "bin": {