@codazen/harmonica-mcp 0.22.0 → 0.24.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 +99 -64
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -22966,28 +22966,45 @@ function registerDropTools(server, ctx, client) {
22966
22966
  );
22967
22967
  server.tool(
22968
22968
  "get_drop",
22969
- "Get full details of a Drop by ID \u2014 state, member Revisions, target date, and audit trail.",
22969
+ "Get full details of a Drop by ID \u2014 state, member Revisions, target date, and audit trail. Accepts either a UUID (drop-abc123) or a drop code (D-086). When passing a code, teamspaceId is required.",
22970
22970
  {
22971
- dropId: external_exports.string().describe("The Drop ID (UUID)")
22971
+ dropId: external_exports.string().describe("The Drop ID (UUID, e.g. drop-abc123) or drop code (e.g. D-086). When passing a code, teamspaceId is required."),
22972
+ teamspaceId: external_exports.string().optional().describe("Required when dropId is a drop code like D-086")
22972
22973
  },
22973
- async ({ dropId }) => {
22974
- const d = await client.getDrop(dropId);
22975
- if (!d) return { content: [{ type: "text", text: `Drop not found: ${dropId}` }], isError: true };
22976
- const lines = [
22977
- `ID: ${d.dropId}`,
22978
- `Code: ${d.dropCode}`,
22979
- `Name: ${d.name}`,
22980
- `Teamspace: ${d.teamspaceId}`,
22981
- `State: ${d.state}`,
22982
- d.description ? `Description: ${d.description}` : null,
22983
- d.targetDate ? `Target: ${d.targetDate}` : null,
22984
- d.releasedAt ? `Released: ${d.releasedAt}` : null,
22985
- d.rolledBackAt ? `Rolled back: ${d.rolledBackAt}` : null,
22986
- `Revisions (${d.revisionIds.length}): ${d.revisionIds.length === 0 ? "(none)" : d.revisionIds.join(", ")}`,
22987
- `Version: ${d.version}`,
22988
- `Created: ${d.createdAt}`
22989
- ].filter(Boolean);
22990
- return { content: [{ type: "text", text: lines.join("\n") }] };
22974
+ async ({ dropId, teamspaceId }) => {
22975
+ try {
22976
+ let resolvedId = dropId;
22977
+ if (/^D-\d+$/i.test(dropId)) {
22978
+ if (!teamspaceId) {
22979
+ return { content: [{ type: "text", text: `teamspaceId is required when dropId is a drop code (e.g. D-086)` }], isError: true };
22980
+ }
22981
+ const normalizedCode = dropId.toUpperCase();
22982
+ const drops = await client.listTeamspaceDrops(teamspaceId, void 0);
22983
+ const match = drops.find((d2) => d2.dropCode.toUpperCase() === normalizedCode);
22984
+ if (!match) return { content: [{ type: "text", text: `Drop not found: ${dropId}` }], isError: true };
22985
+ resolvedId = match.dropId;
22986
+ }
22987
+ const d = await client.getDrop(resolvedId);
22988
+ const notFoundMsg = resolvedId !== dropId ? `Drop not found: ${resolvedId} (code: ${dropId})` : `Drop not found: ${dropId}`;
22989
+ if (!d) return { content: [{ type: "text", text: notFoundMsg }], isError: true };
22990
+ const lines = [
22991
+ `ID: ${d.dropId}`,
22992
+ `Code: ${d.dropCode}`,
22993
+ `Name: ${d.name}`,
22994
+ `Teamspace: ${d.teamspaceId}`,
22995
+ `State: ${d.state}`,
22996
+ d.description ? `Description: ${d.description}` : null,
22997
+ d.targetDate ? `Target: ${d.targetDate}` : null,
22998
+ d.releasedAt ? `Released: ${d.releasedAt}` : null,
22999
+ d.rolledBackAt ? `Rolled back: ${d.rolledBackAt}` : null,
23000
+ `Revisions (${d.revisionIds.length}): ${d.revisionIds.length === 0 ? "(none)" : d.revisionIds.join(", ")}`,
23001
+ `Version: ${d.version}`,
23002
+ `Created: ${d.createdAt}`
23003
+ ].filter(Boolean);
23004
+ return { content: [{ type: "text", text: lines.join("\n") }] };
23005
+ } catch (err) {
23006
+ return { content: [{ type: "text", text: `Failed to get drop: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
23007
+ }
22991
23008
  }
22992
23009
  );
22993
23010
  server.tool(
@@ -24398,7 +24415,7 @@ function registerPlanQualityTools(server, ctx, client) {
24398
24415
  "check_plan_quality",
24399
24416
  "Enqueue a Revision plan quality check across 6 dimensions (Scoped, Directional, Testable, Traceable, Assignable, Risk-aware). By default returns a taskId immediately (fire-and-forget) \u2014 use get_task_status or list_checks to retrieve results. Set wait=true to block until the check completes and receive the scorecard inline.",
24400
24417
  {
24401
- revisionId: external_exports.string().describe("The revision ID (e.g., rev-abc123)"),
24418
+ revisionId: external_exports.string().describe("The revision or Beat Version ID (e.g., rev-abc123 or bv-abc123)"),
24402
24419
  projectId: external_exports.string().describe("The project ID"),
24403
24420
  wait: external_exports.boolean().optional().describe("If true, block until the check completes and return the scorecard inline. Default: false (returns taskId immediately).")
24404
24421
  },
@@ -24679,24 +24696,35 @@ function registerRevisionLifecycleTools(server, ctx, client) {
24679
24696
  title: external_exports.string().describe("Title for this revision"),
24680
24697
  description: external_exports.string().describe("What this revision delivers"),
24681
24698
  changeSummary: external_exports.string().describe("Brief summary of the change"),
24699
+ beatVersionId: external_exports.string().min(1).optional().describe("Optional Beat Version ID to associate this revision with"),
24682
24700
  tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization"),
24683
24701
  priority: external_exports.coerce.number().optional().describe("Priority (lower = higher priority)"),
24684
24702
  estimatedEffort: external_exports.string().optional().describe("Estimated effort (e.g., S, M, L, XL)"),
24685
24703
  humanAssignee: humanAssigneeSchema.optional().describe('Human assignee (e.g. { email: "mmerchant@codazen.com", name: "Mike Merchant" })'),
24686
24704
  agentAssignee: agentAssigneeSchema.optional().describe('Agent assignee (e.g. { agentId: "maya", name: "Maya" })')
24687
24705
  },
24688
- async ({ beatId, title, description, changeSummary, tags, priority, estimatedEffort, humanAssignee, agentAssignee }) => {
24706
+ async ({ beatId, title, description, changeSummary, beatVersionId, tags, priority, estimatedEffort, humanAssignee, agentAssignee }) => {
24689
24707
  try {
24690
24708
  await assertBeatInOrg(client, beatId, ctx.orgId);
24691
24709
  const beat = await client.getBeat(beatId);
24692
24710
  if (!beat) {
24693
24711
  return { content: [{ type: "text", text: `Beat not found: "${beatId}"` }], isError: true };
24694
24712
  }
24713
+ if (beatVersionId !== void 0) {
24714
+ const bv = await client.getBeatVersion(beatVersionId);
24715
+ if (!bv) {
24716
+ return { content: [{ type: "text", text: `Beat Version not found: "${beatVersionId}"` }], isError: true };
24717
+ }
24718
+ if (bv.beatId !== beatId || bv.projectId !== beat.projectId) {
24719
+ return { content: [{ type: "text", text: `Beat Version "${beatVersionId}" belongs to a different Beat.` }], isError: true };
24720
+ }
24721
+ }
24695
24722
  const { revision, warnings } = await client.createRevision(beat.projectId, beatId, {
24696
24723
  title,
24697
24724
  description,
24698
24725
  changeSummary,
24699
24726
  changeImportance: "moderate",
24727
+ beatVersionId,
24700
24728
  tags,
24701
24729
  priority,
24702
24730
  estimatedEffort,
@@ -24733,23 +24761,34 @@ function registerRevisionLifecycleTools(server, ctx, client) {
24733
24761
  changeSummary: external_exports.string().describe("Why this change is being made (recorded in audit trail)"),
24734
24762
  title: external_exports.string().optional().describe("New title"),
24735
24763
  description: external_exports.string().optional().describe("New description (what this revision delivers)"),
24764
+ beatVersionId: external_exports.string().min(1).nullable().optional().describe("Beat Version ID to associate \u2014 set null to clear"),
24736
24765
  tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization"),
24737
24766
  priority: external_exports.coerce.number().optional().describe("Priority (lower = higher priority)"),
24738
24767
  estimatedEffort: external_exports.string().optional().describe("Effort estimate (e.g., S, M, L, XL)"),
24739
24768
  humanAssignee: humanAssigneeSchema.nullable().optional().describe("Human assignee \u2014 set null to clear"),
24740
24769
  agentAssignee: agentAssigneeSchema.nullable().optional().describe("Agent assignee \u2014 set null to clear")
24741
24770
  },
24742
- async ({ revisionId, changeSummary, ...changes }) => {
24771
+ async ({ revisionId, changeSummary, beatVersionId, ...changes }) => {
24743
24772
  try {
24744
24773
  const revision = await client.getRevision(revisionId);
24745
24774
  if (!revision) {
24746
24775
  return { content: [{ type: "text", text: `Revision not found: "${revisionId}"` }], isError: true };
24747
24776
  }
24748
24777
  await assertBeatInOrg(client, revision.beatId, ctx.orgId);
24778
+ if (typeof beatVersionId === "string") {
24779
+ const bv = await client.getBeatVersion(beatVersionId);
24780
+ if (!bv) {
24781
+ return { content: [{ type: "text", text: `Beat Version not found: "${beatVersionId}"` }], isError: true };
24782
+ }
24783
+ if (bv.beatId !== revision.beatId || bv.projectId !== revision.projectId) {
24784
+ return { content: [{ type: "text", text: `Beat Version "${beatVersionId}" belongs to a different Beat.` }], isError: true };
24785
+ }
24786
+ }
24749
24787
  const updates = { changeSummary };
24750
24788
  for (const [k, v] of Object.entries(changes)) {
24751
24789
  if (v !== void 0) updates[k] = v;
24752
24790
  }
24791
+ if (beatVersionId !== void 0) updates.beatVersionId = beatVersionId;
24753
24792
  if (Object.keys(updates).length <= 1) {
24754
24793
  return { content: [{ type: "text", text: "No fields provided to update." }], isError: true };
24755
24794
  }
@@ -24893,37 +24932,29 @@ function registerSnapshotTools(server, ctx, client) {
24893
24932
  isError: true
24894
24933
  };
24895
24934
  }
24935
+ const trimmed = snapshotInput.trim();
24936
+ const importOptions = { targetOrgId: ctx.orgId, targetUserId: ctx.user.userId };
24937
+ if (trimmed.startsWith("https://")) {
24938
+ if (isPrivateHost(trimmed)) {
24939
+ throw new Error("Snapshot URL must point to a public host \u2014 private, link-local, and loopback addresses are not permitted.");
24940
+ }
24941
+ const result2 = await client.importProjectSnapshotFromUrl(trimmed, importOptions);
24942
+ return { content: [{ type: "text", text: formatImportSummary(result2) }] };
24943
+ }
24944
+ if (trimmed.startsWith("http://")) {
24945
+ throw new Error("Snapshot URL must use https:// \u2014 http:// is not permitted.");
24946
+ }
24896
24947
  const snapshotJson = await resolveSnapshotInput(snapshotInput);
24897
24948
  let parsed;
24898
24949
  try {
24899
24950
  parsed = JSON.parse(snapshotJson);
24900
24951
  } catch {
24901
- const urlHint = snapshotInput.trim().startsWith("https://") ? ` fetched from ${snapshotInput.trim().split("?")[0]}` : "";
24902
- throw new Error(`Snapshot is not valid JSON${urlHint}. The URL may have returned an error page instead of snapshot data.`);
24952
+ throw new Error("Snapshot is not valid JSON. The content may be corrupted.");
24903
24953
  }
24904
- assertSnapshotShape(parsed, snapshotInput.trim().startsWith("https://") ? ` fetched from ${snapshotInput.trim().split("?")[0]}` : "");
24954
+ assertSnapshotShape(parsed, "");
24905
24955
  const normalized = normalizeSnapshot(parsed);
24906
- const result = await client.importProjectSnapshot(normalized, {
24907
- targetOrgId: ctx.orgId,
24908
- targetUserId: ctx.user.userId
24909
- });
24910
- const summary = [
24911
- `Imported project: ${result.projectId}`,
24912
- ` Beats: ${result.counts.beats}`,
24913
- ` Proposals: ${result.counts.proposals}`,
24914
- ` Revisions: ${result.counts.revisions}`,
24915
- ` Activities: ${result.counts.activities}`,
24916
- ` Questions: ${result.counts.questions}`,
24917
- ` Notes: ${result.counts.notes}`,
24918
- ` Tasks: ${result.counts.tasks}`,
24919
- ` Submissions: ${result.counts.submissions}`,
24920
- ` PromptLogs: ${result.counts.promptLogs}`
24921
- ];
24922
- if (result.errors.length > 0) {
24923
- summary.push("", `Errors (${result.errors.length}):`);
24924
- result.errors.forEach((e) => summary.push(` - ${e}`));
24925
- }
24926
- return { content: [{ type: "text", text: summary.join("\n") }] };
24956
+ const result = await client.importProjectSnapshot(normalized, importOptions);
24957
+ return { content: [{ type: "text", text: formatImportSummary(result) }] };
24927
24958
  }
24928
24959
  );
24929
24960
  }
@@ -24952,27 +24983,30 @@ function normalizeSnapshot(raw) {
24952
24983
  }
24953
24984
  async function resolveSnapshotInput(input) {
24954
24985
  const trimmed = input.trim();
24955
- if (trimmed.startsWith("https://")) {
24956
- if (isPrivateHost(trimmed)) {
24957
- throw new Error("Snapshot URL must point to a public host \u2014 private, link-local, and loopback addresses are not permitted.");
24958
- }
24959
- let res;
24960
- try {
24961
- res = await fetch(trimmed, { redirect: "error", signal: AbortSignal.timeout(3e4) });
24962
- } catch (err) {
24963
- throw new Error(`Failed to fetch snapshot URL: ${err instanceof Error ? err.message : String(err)}`);
24964
- }
24965
- if (!res.ok) throw new Error(`Failed to fetch snapshot from URL: ${res.status}`);
24966
- return res.text();
24967
- }
24968
- if (trimmed.startsWith("http://")) {
24969
- throw new Error("Snapshot URL must use https:// \u2014 http:// is not permitted.");
24970
- }
24971
24986
  if (trimmed.startsWith("/") || trimmed.startsWith("~")) {
24972
24987
  return (0, import_promises.readFile)(trimmed, "utf-8");
24973
24988
  }
24974
24989
  return trimmed;
24975
24990
  }
24991
+ function formatImportSummary(result) {
24992
+ const lines = [
24993
+ `Imported project: ${result.projectId}`,
24994
+ ` Beats: ${result.counts.beats}`,
24995
+ ` Proposals: ${result.counts.proposals}`,
24996
+ ` Revisions: ${result.counts.revisions}`,
24997
+ ` Activities: ${result.counts.activities}`,
24998
+ ` Questions: ${result.counts.questions}`,
24999
+ ` Notes: ${result.counts.notes}`,
25000
+ ` Tasks: ${result.counts.tasks}`,
25001
+ ` Submissions: ${result.counts.submissions}`,
25002
+ ` PromptLogs: ${result.counts.promptLogs}`
25003
+ ];
25004
+ if (result.errors.length > 0) {
25005
+ lines.push("", `Errors (${result.errors.length}):`);
25006
+ result.errors.forEach((e) => lines.push(` - ${e}`));
25007
+ }
25008
+ return lines.join("\n");
25009
+ }
24976
25010
  function isPrivateHost(url) {
24977
25011
  let hostname2;
24978
25012
  try {
@@ -26401,6 +26435,7 @@ function createHttpClient(config2) {
26401
26435
  return download;
26402
26436
  },
26403
26437
  importProjectSnapshot: (snapshot, options) => request("POST", "/api/projects/import", { ...snapshot, importOptions: options }, LONG_RUNNING_TIMEOUT_MS),
26438
+ importProjectSnapshotFromUrl: (url2, options) => request("POST", "/api/projects/import", { snapshotUrl: url2, importOptions: options }, LONG_RUNNING_TIMEOUT_MS),
26404
26439
  // Embedding similarity
26405
26440
  embedProjectEntities: (projectId) => request("POST", `/api/projects/${encodeURIComponent(projectId)}/embeddings/generate`),
26406
26441
  findSimilarNotes: async (noteId, projectId, options) => {
@@ -26708,7 +26743,7 @@ function loadConfig() {
26708
26743
  };
26709
26744
  }
26710
26745
  async function main() {
26711
- console.error(`[harmonica-mcp] v${"0.22.0"} starting\u2026`);
26746
+ console.error(`[harmonica-mcp] v${"0.24.0"} starting\u2026`);
26712
26747
  const config2 = loadConfig();
26713
26748
  const client = createHttpClient({
26714
26749
  apiBaseUrl: config2.apiBaseUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codazen/harmonica-mcp",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "description": "MCP server for Harmonica — connect Claude to your Harmonica projects",
5
5
  "license": "MIT",
6
6
  "bin": {