@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.
- package/dist/index.js +99 -64
- 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
|
-
|
|
22975
|
-
|
|
22976
|
-
|
|
22977
|
-
|
|
22978
|
-
|
|
22979
|
-
|
|
22980
|
-
|
|
22981
|
-
|
|
22982
|
-
|
|
22983
|
-
|
|
22984
|
-
|
|
22985
|
-
|
|
22986
|
-
|
|
22987
|
-
`
|
|
22988
|
-
|
|
22989
|
-
|
|
22990
|
-
|
|
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
|
-
|
|
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,
|
|
24954
|
+
assertSnapshotShape(parsed, "");
|
|
24905
24955
|
const normalized = normalizeSnapshot(parsed);
|
|
24906
|
-
const result = await client.importProjectSnapshot(normalized,
|
|
24907
|
-
|
|
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.
|
|
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,
|