@codazen/harmonica-mcp 5.1.0 → 5.2.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 +94 -12
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -22480,20 +22480,30 @@ var POLL_MAX_CONSECUTIVE_ERRORS = 3;
22480
22480
  async function pollForCheck(client, taskId) {
22481
22481
  const deadline = Date.now() + POLL_TIMEOUT_MS;
22482
22482
  let consecutiveErrors = 0;
22483
+ let consecutiveMissing = 0;
22483
22484
  while (Date.now() < deadline) {
22484
22485
  const [task, err] = await client.getTask(taskId).then(
22485
22486
  (t) => [t, null],
22486
22487
  (e) => [null, e]
22487
22488
  );
22488
22489
  if (err) {
22490
+ consecutiveMissing = 0;
22489
22491
  consecutiveErrors++;
22490
22492
  if (consecutiveErrors >= POLL_MAX_CONSECUTIVE_ERRORS)
22491
22493
  throw new Error(`getTask failed ${consecutiveErrors} consecutive times: ${err instanceof Error ? err.message : String(err)}`);
22492
22494
  await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
22493
22495
  continue;
22494
22496
  }
22497
+ if (!task) {
22498
+ consecutiveErrors = 0;
22499
+ consecutiveMissing++;
22500
+ if (consecutiveMissing >= POLL_MAX_CONSECUTIVE_ERRORS)
22501
+ throw new Error(`Task ${taskId} not found after ${consecutiveMissing} consecutive attempts`);
22502
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
22503
+ continue;
22504
+ }
22495
22505
  consecutiveErrors = 0;
22496
- if (!task) throw new Error(`Task ${taskId} not found`);
22506
+ consecutiveMissing = 0;
22497
22507
  if (task.status === "completed") {
22498
22508
  if (!task.result) throw new CheckResultPendingError(taskId);
22499
22509
  return task.result;
@@ -22801,6 +22811,18 @@ ${revision.description}`);
22801
22811
  }
22802
22812
  return sections.join("\n");
22803
22813
  }
22814
+ function formatRevisionPreview(revisionId, preview) {
22815
+ if (!preview) return `No preview recorded for revision: ${revisionId}`;
22816
+ const freshness = preview.isCurrent ? "Current \u2014 built from the head commit." : `Stale \u2014 built from ${preview.commitSha?.slice(0, 7) ?? "an unknown commit"}, which is no longer the head. Still opens.`;
22817
+ return [
22818
+ `# Preview: ${revisionId}`,
22819
+ `**URL:** ${preview.url}`,
22820
+ `**Built from:** ${preview.commitSha}`,
22821
+ `**Recorded:** ${preview.recordedAt}`,
22822
+ `
22823
+ ${freshness}`
22824
+ ].join("\n");
22825
+ }
22804
22826
 
22805
22827
  // ../../libs/harmonica-services/src/mcp/formatters/note-formatter.ts
22806
22828
  var TYPE_LABELS = {
@@ -23309,6 +23331,26 @@ ${formatDeliveryPolicy(
23309
23331
  }
23310
23332
  }
23311
23333
  );
23334
+ server.tool(
23335
+ "get_revision_preview",
23336
+ "Get the preview link recorded for a Revision \u2014 the running environment its own pipeline published, and whether that build is still the pull request's head. A Revision with no recorded preview is reported plainly, not as an error: one whose change artifact is a document never gets one. A stale link still opens; it was built before the latest commit.",
23337
+ {
23338
+ revisionId: external_exports.string().describe("The revision ID (e.g., rev-abc123)")
23339
+ },
23340
+ async ({ revisionId }) => {
23341
+ try {
23342
+ const revision = await client.getRevision(revisionId);
23343
+ if (!revision) {
23344
+ return { content: [{ type: "text", text: `Revision not found: ${revisionId}` }], isError: true };
23345
+ }
23346
+ const preview = await client.resolveRevisionPreview(revisionId);
23347
+ return { content: [{ type: "text", text: formatRevisionPreview(revisionId, preview) }] };
23348
+ } catch (err) {
23349
+ const message = err instanceof Error ? err.message : String(err);
23350
+ return { content: [{ type: "text", text: `Failed to get revision preview: ${message}` }], isError: true };
23351
+ }
23352
+ }
23353
+ );
23312
23354
  }
23313
23355
  async function logBeatActivity(client, params) {
23314
23356
  try {
@@ -24227,12 +24269,14 @@ var POLL_MAX_CONSECUTIVE_ERRORS2 = 3;
24227
24269
  async function pollForCheck2(client, taskId) {
24228
24270
  const deadline = Date.now() + POLL_TIMEOUT_MS2;
24229
24271
  let consecutiveErrors = 0;
24272
+ let consecutiveMissing = 0;
24230
24273
  while (Date.now() < deadline) {
24231
24274
  const [task, err] = await client.getTask(taskId).then(
24232
24275
  (t) => [t, null],
24233
24276
  (e) => [null, e]
24234
24277
  );
24235
24278
  if (err) {
24279
+ consecutiveMissing = 0;
24236
24280
  consecutiveErrors++;
24237
24281
  if (consecutiveErrors >= POLL_MAX_CONSECUTIVE_ERRORS2) {
24238
24282
  throw new Error(`getTask failed ${consecutiveErrors} times: ${err instanceof Error ? err.message : String(err)}`);
@@ -24240,8 +24284,16 @@ async function pollForCheck2(client, taskId) {
24240
24284
  await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS2));
24241
24285
  continue;
24242
24286
  }
24287
+ if (!task) {
24288
+ consecutiveErrors = 0;
24289
+ consecutiveMissing++;
24290
+ if (consecutiveMissing >= POLL_MAX_CONSECUTIVE_ERRORS2)
24291
+ throw new Error(`Task ${taskId} not found after ${consecutiveMissing} consecutive attempts`);
24292
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS2));
24293
+ continue;
24294
+ }
24243
24295
  consecutiveErrors = 0;
24244
- if (!task) throw new Error(`Task ${taskId} not found`);
24296
+ consecutiveMissing = 0;
24245
24297
  if (task.status === "completed") {
24246
24298
  if (!task.result) throw new CheckResultPendingError(taskId);
24247
24299
  return task.result;
@@ -25344,6 +25396,14 @@ var RevisionScmSchema = external_exports.object({
25344
25396
  state: external_exports.enum(["draft", "open", "merged", "closed"]),
25345
25397
  mergeSha: external_exports.string().optional()
25346
25398
  });
25399
+ var RevisionPreviewSchema = external_exports.object({
25400
+ url: external_exports.string(),
25401
+ commitSha: external_exports.string(),
25402
+ recordedAt: external_exports.string()
25403
+ });
25404
+ var RevisionPreviewApiSchema = RevisionPreviewSchema.extend({
25405
+ isCurrent: external_exports.boolean()
25406
+ });
25347
25407
  var RevisionApiSchema = external_exports.object({
25348
25408
  revisionId: external_exports.string(),
25349
25409
  beatId: external_exports.string(),
@@ -28362,7 +28422,7 @@ ${formatTree(tree.root)}` }] };
28362
28422
  );
28363
28423
  server.tool(
28364
28424
  "update_notebook_collection",
28365
- "Reconfigure a placed live-collection section in place \u2014 change what it collects (note type, project scope, limit, order, significance tier) or its heading, without removing and re-adding it. Only the fields you pass change; the rest keep their current values. Identify the section by its elementId (from get_notebook_tree) and the page it sits on.",
28425
+ "Reconfigure a placed live-collection section in place \u2014 change what it collects (note type, system scope, limit, order, significance tier) or its heading, without removing and re-adding it. Only the fields you pass change; the rest keep their current values. Identify the section by its elementId (from get_notebook_tree) and the page it sits on.",
28366
28426
  {
28367
28427
  notebookId: external_exports.string().describe("The Notebook ID"),
28368
28428
  elementId: external_exports.string().describe("The collection section's elementId (from get_notebook_tree)"),
@@ -28503,9 +28563,9 @@ ${scopeText}` }] };
28503
28563
  );
28504
28564
  server.tool(
28505
28565
  "list_notebooks",
28506
- "List Notebooks reachable from a scope (org, teamspace, or system). Returns all notebooks bound to that scope.",
28566
+ "List Notebooks reachable from a scope (org, teamspace, system, or account). Returns all notebooks bound to that scope.",
28507
28567
  {
28508
- scopeType: external_exports.enum(SCOPE_TYPE_VALUES).describe("Scope type \u2014 org, teamspace, or project"),
28568
+ scopeType: external_exports.enum(SCOPE_TYPE_VALUES).describe("Scope type \u2014 org, teamspace, system, or account"),
28509
28569
  scopeId: external_exports.string().min(1).describe("ID of the scope entity"),
28510
28570
  includeArchived: external_exports.boolean().optional().describe("Include archived Notebooks in the results. Default: false (archived excluded).")
28511
28571
  },
@@ -28566,10 +28626,10 @@ ${formatNotebook(updated)}` }] };
28566
28626
  );
28567
28627
  server.tool(
28568
28628
  "add_notebook_shortcut",
28569
- "Add a curated, read-only shortcut from a scope (org, teamspace, project, or account) to a Notebook owned elsewhere in the SAME organization. Unlike set_notebook_scopes, a shortcut grants no rename/archive/delete rights over the Notebook in the consuming scope \u2014 it is a link, not ownership.",
28629
+ "Add a curated, read-only shortcut from a scope (org, teamspace, system, or account) to a Notebook owned elsewhere in the SAME organization. Unlike set_notebook_scopes, a shortcut grants no rename/archive/delete rights over the Notebook in the consuming scope \u2014 it is a link, not ownership.",
28570
28630
  {
28571
28631
  notebookId: external_exports.string().describe("The Notebook ID to shortcut"),
28572
- scopeType: external_exports.enum(SCOPE_TYPE_VALUES).describe("Consuming scope type \u2014 org, teamspace, project, or account"),
28632
+ scopeType: external_exports.enum(SCOPE_TYPE_VALUES).describe("Consuming scope type \u2014 org, teamspace, system, or account"),
28573
28633
  scopeId: external_exports.string().min(1).describe("ID of the consuming scope entity"),
28574
28634
  createdBy: external_exports.string().optional().describe("Actor ID \u2014 defaults to the session user")
28575
28635
  },
@@ -28598,7 +28658,7 @@ ${formatNotebook(updated)}` }] };
28598
28658
  "Remove a Notebook shortcut from a scope. Only removes the shortcut reference \u2014 never affects the source Notebook or its real scope bindings.",
28599
28659
  {
28600
28660
  notebookId: external_exports.string().describe("The Notebook ID"),
28601
- scopeType: external_exports.enum(SCOPE_TYPE_VALUES).describe("Consuming scope type \u2014 org, teamspace, project, or account"),
28661
+ scopeType: external_exports.enum(SCOPE_TYPE_VALUES).describe("Consuming scope type \u2014 org, teamspace, system, or account"),
28602
28662
  scopeId: external_exports.string().min(1).describe("ID of the consuming scope entity")
28603
28663
  },
28604
28664
  async ({ notebookId, scopeType, scopeId }) => {
@@ -28621,7 +28681,7 @@ ${formatNotebook(updated)}` }] };
28621
28681
  "list_notebook_shortcuts",
28622
28682
  "List a scope\u2019s curated Notebook shortcuts \u2014 Notebooks owned elsewhere in the org that this scope has chosen to surface, hydrated with each source Notebook\u2019s live title/status.",
28623
28683
  {
28624
- scopeType: external_exports.enum(SCOPE_TYPE_VALUES).describe("Scope type \u2014 org, teamspace, project, or account"),
28684
+ scopeType: external_exports.enum(SCOPE_TYPE_VALUES).describe("Scope type \u2014 org, teamspace, system, or account"),
28625
28685
  scopeId: external_exports.string().min(1).describe("ID of the scope entity")
28626
28686
  },
28627
28687
  async ({ scopeType, scopeId }) => {
@@ -28976,12 +29036,14 @@ var ROOT_CODA_POLL_MAX_CONSECUTIVE_ERRORS = 3;
28976
29036
  async function pollForRootCodaCheck(client, taskId) {
28977
29037
  const deadline = Date.now() + ROOT_CODA_POLL_TIMEOUT_MS;
28978
29038
  let consecutiveErrors = 0;
29039
+ let consecutiveMissing = 0;
28979
29040
  while (Date.now() < deadline) {
28980
29041
  const [task, err] = await client.getTask(taskId).then(
28981
29042
  (t) => [t, null],
28982
29043
  (e) => [null, e]
28983
29044
  );
28984
29045
  if (err) {
29046
+ consecutiveMissing = 0;
28985
29047
  consecutiveErrors++;
28986
29048
  if (consecutiveErrors >= ROOT_CODA_POLL_MAX_CONSECUTIVE_ERRORS) {
28987
29049
  throw new Error(`getTask failed ${consecutiveErrors} times: ${err instanceof Error ? err.message : String(err)}`);
@@ -28989,8 +29051,16 @@ async function pollForRootCodaCheck(client, taskId) {
28989
29051
  await new Promise((r) => setTimeout(r, ROOT_CODA_POLL_INTERVAL_MS));
28990
29052
  continue;
28991
29053
  }
29054
+ if (!task) {
29055
+ consecutiveErrors = 0;
29056
+ consecutiveMissing++;
29057
+ if (consecutiveMissing >= ROOT_CODA_POLL_MAX_CONSECUTIVE_ERRORS)
29058
+ throw new Error(`Task ${taskId} not found after ${consecutiveMissing} consecutive attempts`);
29059
+ await new Promise((r) => setTimeout(r, ROOT_CODA_POLL_INTERVAL_MS));
29060
+ continue;
29061
+ }
28992
29062
  consecutiveErrors = 0;
28993
- if (!task) throw new Error(`Task ${taskId} not found`);
29063
+ consecutiveMissing = 0;
28994
29064
  if (task.status === "completed") {
28995
29065
  if (!task.result) throw new CheckResultPendingError(taskId);
28996
29066
  const result = task.result;
@@ -29333,12 +29403,14 @@ var POLL_MAX_CONSECUTIVE_ERRORS4 = 3;
29333
29403
  async function pollForCheck4(client, taskId) {
29334
29404
  const deadline = Date.now() + POLL_TIMEOUT_MS4;
29335
29405
  let consecutiveErrors = 0;
29406
+ let consecutiveMissing = 0;
29336
29407
  while (Date.now() < deadline) {
29337
29408
  const [task, err] = await client.getTask(taskId).then(
29338
29409
  (t) => [t, null],
29339
29410
  (e) => [null, e]
29340
29411
  );
29341
29412
  if (err) {
29413
+ consecutiveMissing = 0;
29342
29414
  consecutiveErrors++;
29343
29415
  if (consecutiveErrors >= POLL_MAX_CONSECUTIVE_ERRORS4) {
29344
29416
  throw new Error(`getTask failed ${consecutiveErrors} times: ${err instanceof Error ? err.message : String(err)}`);
@@ -29346,8 +29418,16 @@ async function pollForCheck4(client, taskId) {
29346
29418
  await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS4));
29347
29419
  continue;
29348
29420
  }
29421
+ if (!task) {
29422
+ consecutiveErrors = 0;
29423
+ consecutiveMissing++;
29424
+ if (consecutiveMissing >= POLL_MAX_CONSECUTIVE_ERRORS4)
29425
+ throw new Error(`Task ${taskId} not found after ${consecutiveMissing} consecutive attempts`);
29426
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS4));
29427
+ continue;
29428
+ }
29349
29429
  consecutiveErrors = 0;
29350
- if (!task) throw new Error(`Task ${taskId} not found`);
29430
+ consecutiveMissing = 0;
29351
29431
  if (task.status === "completed") {
29352
29432
  if (!task.result) throw new CheckResultPendingError(taskId);
29353
29433
  return task.result;
@@ -33664,6 +33744,8 @@ function createHttpClient(config2) {
33664
33744
  return { revision: result.revision, warnings: result.warnings ?? [] };
33665
33745
  },
33666
33746
  getRevision: (revisionId) => request("GET", `/api/revisions/${encodeURIComponent(revisionId)}`),
33747
+ // `request` maps 404 to undefined, which is what absence must be here.
33748
+ resolveRevisionPreview: (revisionId) => request("GET", `/api/revisions/${encodeURIComponent(revisionId)}/preview`),
33667
33749
  listConceptRevisions: async (systemId, options) => {
33668
33750
  const qs = options?.includeArchived ? "?includeArchived=true" : "";
33669
33751
  const result = await request("GET", `/api/systems/${encodeURIComponent(systemId)}/revisions/pending${qs}`);
@@ -35471,7 +35553,7 @@ function loadConfig() {
35471
35553
  };
35472
35554
  }
35473
35555
  async function main() {
35474
- console.error(`[harmonica-mcp] v${"5.1.0"} starting\u2026`);
35556
+ console.error(`[harmonica-mcp] v${"5.2.0"} starting\u2026`);
35475
35557
  const config2 = loadConfig();
35476
35558
  const client = createHttpClient({
35477
35559
  apiBaseUrl: config2.apiBaseUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codazen/harmonica-mcp",
3
- "version": "5.1.0",
3
+ "version": "5.2.0",
4
4
  "description": "MCP server for Harmonica — connect any MCP-compatible AI assistant to Harmonica",
5
5
  "license": "MIT",
6
6
  "bin": {