@codazen/harmonica-mcp 0.23.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 +61 -43
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -24696,24 +24696,35 @@ function registerRevisionLifecycleTools(server, ctx, client) {
24696
24696
  title: external_exports.string().describe("Title for this revision"),
24697
24697
  description: external_exports.string().describe("What this revision delivers"),
24698
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"),
24699
24700
  tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization"),
24700
24701
  priority: external_exports.coerce.number().optional().describe("Priority (lower = higher priority)"),
24701
24702
  estimatedEffort: external_exports.string().optional().describe("Estimated effort (e.g., S, M, L, XL)"),
24702
24703
  humanAssignee: humanAssigneeSchema.optional().describe('Human assignee (e.g. { email: "mmerchant@codazen.com", name: "Mike Merchant" })'),
24703
24704
  agentAssignee: agentAssigneeSchema.optional().describe('Agent assignee (e.g. { agentId: "maya", name: "Maya" })')
24704
24705
  },
24705
- async ({ beatId, title, description, changeSummary, tags, priority, estimatedEffort, humanAssignee, agentAssignee }) => {
24706
+ async ({ beatId, title, description, changeSummary, beatVersionId, tags, priority, estimatedEffort, humanAssignee, agentAssignee }) => {
24706
24707
  try {
24707
24708
  await assertBeatInOrg(client, beatId, ctx.orgId);
24708
24709
  const beat = await client.getBeat(beatId);
24709
24710
  if (!beat) {
24710
24711
  return { content: [{ type: "text", text: `Beat not found: "${beatId}"` }], isError: true };
24711
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
+ }
24712
24722
  const { revision, warnings } = await client.createRevision(beat.projectId, beatId, {
24713
24723
  title,
24714
24724
  description,
24715
24725
  changeSummary,
24716
24726
  changeImportance: "moderate",
24727
+ beatVersionId,
24717
24728
  tags,
24718
24729
  priority,
24719
24730
  estimatedEffort,
@@ -24750,23 +24761,34 @@ function registerRevisionLifecycleTools(server, ctx, client) {
24750
24761
  changeSummary: external_exports.string().describe("Why this change is being made (recorded in audit trail)"),
24751
24762
  title: external_exports.string().optional().describe("New title"),
24752
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"),
24753
24765
  tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization"),
24754
24766
  priority: external_exports.coerce.number().optional().describe("Priority (lower = higher priority)"),
24755
24767
  estimatedEffort: external_exports.string().optional().describe("Effort estimate (e.g., S, M, L, XL)"),
24756
24768
  humanAssignee: humanAssigneeSchema.nullable().optional().describe("Human assignee \u2014 set null to clear"),
24757
24769
  agentAssignee: agentAssigneeSchema.nullable().optional().describe("Agent assignee \u2014 set null to clear")
24758
24770
  },
24759
- async ({ revisionId, changeSummary, ...changes }) => {
24771
+ async ({ revisionId, changeSummary, beatVersionId, ...changes }) => {
24760
24772
  try {
24761
24773
  const revision = await client.getRevision(revisionId);
24762
24774
  if (!revision) {
24763
24775
  return { content: [{ type: "text", text: `Revision not found: "${revisionId}"` }], isError: true };
24764
24776
  }
24765
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
+ }
24766
24787
  const updates = { changeSummary };
24767
24788
  for (const [k, v] of Object.entries(changes)) {
24768
24789
  if (v !== void 0) updates[k] = v;
24769
24790
  }
24791
+ if (beatVersionId !== void 0) updates.beatVersionId = beatVersionId;
24770
24792
  if (Object.keys(updates).length <= 1) {
24771
24793
  return { content: [{ type: "text", text: "No fields provided to update." }], isError: true };
24772
24794
  }
@@ -24910,37 +24932,29 @@ function registerSnapshotTools(server, ctx, client) {
24910
24932
  isError: true
24911
24933
  };
24912
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
+ }
24913
24947
  const snapshotJson = await resolveSnapshotInput(snapshotInput);
24914
24948
  let parsed;
24915
24949
  try {
24916
24950
  parsed = JSON.parse(snapshotJson);
24917
24951
  } catch {
24918
- const urlHint = snapshotInput.trim().startsWith("https://") ? ` fetched from ${snapshotInput.trim().split("?")[0]}` : "";
24919
- 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.");
24920
24953
  }
24921
- assertSnapshotShape(parsed, snapshotInput.trim().startsWith("https://") ? ` fetched from ${snapshotInput.trim().split("?")[0]}` : "");
24954
+ assertSnapshotShape(parsed, "");
24922
24955
  const normalized = normalizeSnapshot(parsed);
24923
- const result = await client.importProjectSnapshot(normalized, {
24924
- targetOrgId: ctx.orgId,
24925
- targetUserId: ctx.user.userId
24926
- });
24927
- const summary = [
24928
- `Imported project: ${result.projectId}`,
24929
- ` Beats: ${result.counts.beats}`,
24930
- ` Proposals: ${result.counts.proposals}`,
24931
- ` Revisions: ${result.counts.revisions}`,
24932
- ` Activities: ${result.counts.activities}`,
24933
- ` Questions: ${result.counts.questions}`,
24934
- ` Notes: ${result.counts.notes}`,
24935
- ` Tasks: ${result.counts.tasks}`,
24936
- ` Submissions: ${result.counts.submissions}`,
24937
- ` PromptLogs: ${result.counts.promptLogs}`
24938
- ];
24939
- if (result.errors.length > 0) {
24940
- summary.push("", `Errors (${result.errors.length}):`);
24941
- result.errors.forEach((e) => summary.push(` - ${e}`));
24942
- }
24943
- return { content: [{ type: "text", text: summary.join("\n") }] };
24956
+ const result = await client.importProjectSnapshot(normalized, importOptions);
24957
+ return { content: [{ type: "text", text: formatImportSummary(result) }] };
24944
24958
  }
24945
24959
  );
24946
24960
  }
@@ -24969,27 +24983,30 @@ function normalizeSnapshot(raw) {
24969
24983
  }
24970
24984
  async function resolveSnapshotInput(input) {
24971
24985
  const trimmed = input.trim();
24972
- if (trimmed.startsWith("https://")) {
24973
- if (isPrivateHost(trimmed)) {
24974
- throw new Error("Snapshot URL must point to a public host \u2014 private, link-local, and loopback addresses are not permitted.");
24975
- }
24976
- let res;
24977
- try {
24978
- res = await fetch(trimmed, { redirect: "error", signal: AbortSignal.timeout(3e4) });
24979
- } catch (err) {
24980
- throw new Error(`Failed to fetch snapshot URL: ${err instanceof Error ? err.message : String(err)}`);
24981
- }
24982
- if (!res.ok) throw new Error(`Failed to fetch snapshot from URL: ${res.status}`);
24983
- return res.text();
24984
- }
24985
- if (trimmed.startsWith("http://")) {
24986
- throw new Error("Snapshot URL must use https:// \u2014 http:// is not permitted.");
24987
- }
24988
24986
  if (trimmed.startsWith("/") || trimmed.startsWith("~")) {
24989
24987
  return (0, import_promises.readFile)(trimmed, "utf-8");
24990
24988
  }
24991
24989
  return trimmed;
24992
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
+ }
24993
25010
  function isPrivateHost(url) {
24994
25011
  let hostname2;
24995
25012
  try {
@@ -26418,6 +26435,7 @@ function createHttpClient(config2) {
26418
26435
  return download;
26419
26436
  },
26420
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),
26421
26439
  // Embedding similarity
26422
26440
  embedProjectEntities: (projectId) => request("POST", `/api/projects/${encodeURIComponent(projectId)}/embeddings/generate`),
26423
26441
  findSimilarNotes: async (noteId, projectId, options) => {
@@ -26725,7 +26743,7 @@ function loadConfig() {
26725
26743
  };
26726
26744
  }
26727
26745
  async function main() {
26728
- console.error(`[harmonica-mcp] v${"0.23.0"} starting\u2026`);
26746
+ console.error(`[harmonica-mcp] v${"0.24.0"} starting\u2026`);
26729
26747
  const config2 = loadConfig();
26730
26748
  const client = createHttpClient({
26731
26749
  apiBaseUrl: config2.apiBaseUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codazen/harmonica-mcp",
3
- "version": "0.23.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": {