@codazen/harmonica-mcp 0.30.0 → 0.32.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 +167 -5
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -22877,20 +22877,98 @@ function registerCheckTools(server, ctx, client) {
|
|
|
22877
22877
|
);
|
|
22878
22878
|
server.tool(
|
|
22879
22879
|
"list_checks",
|
|
22880
|
-
"List persisted checks for a project, beat, or revision. Shows score trends over time. Filter by check type.",
|
|
22880
|
+
"List persisted checks for a project, beat, beat version, or revision. Shows score trends over time. Filter by check type.",
|
|
22881
22881
|
{
|
|
22882
22882
|
projectId: external_exports.string().describe("The project ID"),
|
|
22883
22883
|
beatId: external_exports.string().optional().describe("List checks for this specific beat"),
|
|
22884
|
+
beatVersionId: external_exports.string().optional().describe("List checks for this specific beat version"),
|
|
22884
22885
|
revisionId: external_exports.string().optional().describe("List checks for this specific revision"),
|
|
22885
22886
|
checkType: external_exports.enum(["beat_quality", "plan_quality", "build_quality", "pii_scan", "portfolio_coherence"]).optional().describe("Filter by check type")
|
|
22886
22887
|
},
|
|
22887
|
-
async ({ projectId, beatId, revisionId, checkType }) => {
|
|
22888
|
+
async ({ projectId, beatId, beatVersionId, revisionId, checkType }) => {
|
|
22888
22889
|
try {
|
|
22890
|
+
const filters = [
|
|
22891
|
+
revisionId ? "revisionId" : null,
|
|
22892
|
+
beatVersionId ? "beatVersionId" : null,
|
|
22893
|
+
beatId ? "beatId" : null
|
|
22894
|
+
].filter(Boolean);
|
|
22895
|
+
if (filters.length > 1) {
|
|
22896
|
+
return {
|
|
22897
|
+
content: [{
|
|
22898
|
+
type: "text",
|
|
22899
|
+
text: `Only one of revisionId, beatVersionId, beatId may be supplied (got: ${filters.join(", ")}). Pick the target scope and resend.`
|
|
22900
|
+
}],
|
|
22901
|
+
isError: true
|
|
22902
|
+
};
|
|
22903
|
+
}
|
|
22904
|
+
if (revisionId) {
|
|
22905
|
+
if (revisionId.startsWith("bv-")) {
|
|
22906
|
+
return {
|
|
22907
|
+
content: [{
|
|
22908
|
+
type: "text",
|
|
22909
|
+
text: "revisionId must start with 'rev-', got a Beat Version ID (bv-prefix). Use beatVersionId parameter instead."
|
|
22910
|
+
}],
|
|
22911
|
+
isError: true
|
|
22912
|
+
};
|
|
22913
|
+
}
|
|
22914
|
+
if (!revisionId.startsWith("rev-")) {
|
|
22915
|
+
return {
|
|
22916
|
+
content: [{
|
|
22917
|
+
type: "text",
|
|
22918
|
+
text: `revisionId must start with 'rev-', got '${revisionId}'. Pass the full Revision UUID (rev-xxxxxxxx-...).`
|
|
22919
|
+
}],
|
|
22920
|
+
isError: true
|
|
22921
|
+
};
|
|
22922
|
+
}
|
|
22923
|
+
}
|
|
22924
|
+
if (beatVersionId) {
|
|
22925
|
+
if (beatVersionId.startsWith("rev-")) {
|
|
22926
|
+
return {
|
|
22927
|
+
content: [{
|
|
22928
|
+
type: "text",
|
|
22929
|
+
text: "beatVersionId must start with 'bv-', got a Revision ID (rev-prefix). Use revisionId parameter instead."
|
|
22930
|
+
}],
|
|
22931
|
+
isError: true
|
|
22932
|
+
};
|
|
22933
|
+
}
|
|
22934
|
+
if (!beatVersionId.startsWith("bv-")) {
|
|
22935
|
+
return {
|
|
22936
|
+
content: [{
|
|
22937
|
+
type: "text",
|
|
22938
|
+
text: `beatVersionId must start with 'bv-', got '${beatVersionId}'. Pass the full Beat Version UUID (bv-xxxxxxxx-...).`
|
|
22939
|
+
}],
|
|
22940
|
+
isError: true
|
|
22941
|
+
};
|
|
22942
|
+
}
|
|
22943
|
+
}
|
|
22944
|
+
if (beatId) {
|
|
22945
|
+
if (beatId.startsWith("rev-")) {
|
|
22946
|
+
return {
|
|
22947
|
+
content: [{
|
|
22948
|
+
type: "text",
|
|
22949
|
+
text: "beatId must not be a Revision ID (rev-prefix). Use revisionId parameter instead."
|
|
22950
|
+
}],
|
|
22951
|
+
isError: true
|
|
22952
|
+
};
|
|
22953
|
+
}
|
|
22954
|
+
if (beatId.startsWith("bv-")) {
|
|
22955
|
+
return {
|
|
22956
|
+
content: [{
|
|
22957
|
+
type: "text",
|
|
22958
|
+
text: "beatId must not be a Beat Version ID (bv-prefix). Use beatVersionId parameter instead."
|
|
22959
|
+
}],
|
|
22960
|
+
isError: true
|
|
22961
|
+
};
|
|
22962
|
+
}
|
|
22963
|
+
}
|
|
22889
22964
|
let checks;
|
|
22890
22965
|
let scope;
|
|
22891
22966
|
if (revisionId) {
|
|
22892
22967
|
checks = await client.listRevisionChecks(revisionId, projectId, checkType);
|
|
22893
22968
|
scope = `revision ${revisionId}`;
|
|
22969
|
+
} else if (beatVersionId) {
|
|
22970
|
+
checks = await client.listBeatVersionChecks(beatVersionId, projectId, checkType);
|
|
22971
|
+
scope = `beat_version ${beatVersionId}`;
|
|
22894
22972
|
} else if (beatId) {
|
|
22895
22973
|
checks = await client.listBeatChecks(beatId, projectId, checkType);
|
|
22896
22974
|
scope = `beat ${beatId}`;
|
|
@@ -25282,6 +25360,63 @@ function registerRevisionLifecycleTools(server, ctx, client) {
|
|
|
25282
25360
|
}
|
|
25283
25361
|
}
|
|
25284
25362
|
);
|
|
25363
|
+
server.tool(
|
|
25364
|
+
"create_pr_for_revision",
|
|
25365
|
+
"Open a Harmonica-initiated DRAFT pull request for a revision and link it back via revision.scm. Cuts a branch (agent/rev-<short>-<slug>), bootstraps it with an empty commit, opens a draft PR whose body starts with the `Harmonica: rev-<uuid>` marker, then records the PR on the revision. Rejects if the revision already has a linked PR, or if the project has no repo configured. Dry-run by default \u2014 call with `confirm: true` to actually create the branch and PR. Leaves the revision in `draft`; transition it to `open` when the PR is marked ready for review.",
|
|
25366
|
+
{
|
|
25367
|
+
revisionId: external_exports.string().describe("The revision ID (e.g., rev-abc123)"),
|
|
25368
|
+
confirm: external_exports.boolean().optional().describe("Set true to create the branch + draft PR. Omitted/false performs a dry-run that only describes what would happen \u2014 no GitHub or DB writes.")
|
|
25369
|
+
},
|
|
25370
|
+
async ({ revisionId, confirm }) => {
|
|
25371
|
+
try {
|
|
25372
|
+
const revision = await client.getRevision(revisionId);
|
|
25373
|
+
if (!revision) {
|
|
25374
|
+
return { content: [{ type: "text", text: `Revision not found: "${revisionId}"` }], isError: true };
|
|
25375
|
+
}
|
|
25376
|
+
await assertBeatInOrg(client, revision.beatId, ctx.orgId);
|
|
25377
|
+
if (!confirm) {
|
|
25378
|
+
const shortId = revisionId.split("-").slice(0, 2).join("-");
|
|
25379
|
+
const lines2 = [
|
|
25380
|
+
"**Dry run** \u2014 no PR created. Re-run with `confirm: true` to proceed.",
|
|
25381
|
+
"",
|
|
25382
|
+
`**Revision:** ${revisionId}`,
|
|
25383
|
+
`**Title:** ${revision.title}`
|
|
25384
|
+
];
|
|
25385
|
+
if (revision.scm) {
|
|
25386
|
+
lines2.push("", `\u26A0 This revision already has a linked PR (#${revision.scm.number} \u2014 ${revision.scm.url}). create_pr_for_revision would be rejected.`);
|
|
25387
|
+
} else {
|
|
25388
|
+
const project = await client.getProject(revision.projectId);
|
|
25389
|
+
if (!project?.repoOwner || !project?.repoName || !project?.repoDefaultBranch) {
|
|
25390
|
+
lines2.push("", "\u26A0 The project has no repo configured (repoOwner/repoName/repoDefaultBranch). create_pr_for_revision would be rejected.");
|
|
25391
|
+
} else {
|
|
25392
|
+
lines2.push(
|
|
25393
|
+
"",
|
|
25394
|
+
`Would open a draft PR against \`${project.repoOwner}/${project.repoName}:${project.repoDefaultBranch}\` on a new \`agent/${shortId}-\u2026\` branch, with body starting \`Harmonica: ${revisionId}\`.`
|
|
25395
|
+
);
|
|
25396
|
+
}
|
|
25397
|
+
}
|
|
25398
|
+
return { content: [{ type: "text", text: lines2.join("\n") }] };
|
|
25399
|
+
}
|
|
25400
|
+
const result = await client.createPrForRevision(revisionId);
|
|
25401
|
+
if (!result.success) {
|
|
25402
|
+
return { content: [{ type: "text", text: `Failed to create PR (${result.error.code}): ${result.error.message}` }], isError: true };
|
|
25403
|
+
}
|
|
25404
|
+
const lines = [
|
|
25405
|
+
"Draft PR created and linked to the revision.",
|
|
25406
|
+
"",
|
|
25407
|
+
`**Revision:** ${result.result.revisionId}`,
|
|
25408
|
+
`**PR:** #${result.result.prNumber} \u2014 ${result.result.prUrl}`,
|
|
25409
|
+
`**Branch:** ${result.result.branch}`,
|
|
25410
|
+
"",
|
|
25411
|
+
"The revision stays in `draft`. Transition it to `open` once the PR is marked ready for review."
|
|
25412
|
+
];
|
|
25413
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
25414
|
+
} catch (err) {
|
|
25415
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
25416
|
+
return { content: [{ type: "text", text: `Failed to create PR for revision: ${message}` }], isError: true };
|
|
25417
|
+
}
|
|
25418
|
+
}
|
|
25419
|
+
);
|
|
25285
25420
|
}
|
|
25286
25421
|
|
|
25287
25422
|
// ../../libs/harmonica-services/src/mcp/tools/session-tools.ts
|
|
@@ -26504,9 +26639,10 @@ function createHttpClient(config2) {
|
|
|
26504
26639
|
}
|
|
26505
26640
|
const DEFAULT_TIMEOUT_MS = parseInt(process.env.MCP_HTTP_TIMEOUT_MS || "30000", 10);
|
|
26506
26641
|
const LONG_RUNNING_TIMEOUT_MS = parseInt(process.env.MCP_HTTP_LONG_TIMEOUT_MS || "180000", 10);
|
|
26642
|
+
const getApiKey = () => typeof config2.apiKey === "function" ? config2.apiKey() : config2.apiKey;
|
|
26507
26643
|
async function request(method, path, body, timeoutMs) {
|
|
26508
26644
|
const headers = {
|
|
26509
|
-
Authorization: `Bearer ${
|
|
26645
|
+
Authorization: `Bearer ${getApiKey()}`
|
|
26510
26646
|
};
|
|
26511
26647
|
if (body !== void 0) {
|
|
26512
26648
|
headers["Content-Type"] = "application/json";
|
|
@@ -26913,6 +27049,24 @@ function createHttpClient(config2) {
|
|
|
26913
27049
|
return { success: false, error: { code: "TRANSITION_FAILED", message } };
|
|
26914
27050
|
}
|
|
26915
27051
|
},
|
|
27052
|
+
createPrForRevision: async (revisionId) => {
|
|
27053
|
+
try {
|
|
27054
|
+
const result = await request(
|
|
27055
|
+
"POST",
|
|
27056
|
+
`/api/revisions/${encodeURIComponent(revisionId)}/pr`
|
|
27057
|
+
);
|
|
27058
|
+
if (result === void 0) {
|
|
27059
|
+
return { success: false, error: { code: "REVISION_NOT_FOUND", message: "Revision not found" } };
|
|
27060
|
+
}
|
|
27061
|
+
return { success: true, result };
|
|
27062
|
+
} catch (err) {
|
|
27063
|
+
if (err instanceof ApiError) {
|
|
27064
|
+
const parsed = safeParseErrorBody(err.body);
|
|
27065
|
+
return { success: false, error: { code: parsed?.code ?? "PR_CREATE_FAILED", message: parsed?.error ?? err.message } };
|
|
27066
|
+
}
|
|
27067
|
+
return { success: false, error: { code: "PR_CREATE_FAILED", message: err instanceof Error ? err.message : String(err) } };
|
|
27068
|
+
}
|
|
27069
|
+
},
|
|
26916
27070
|
reconcileRevisionToState: async (revisionId, targetState, options) => {
|
|
26917
27071
|
const forwardPath = ["planning", "building", "ready_for_drop", "in_staging", "ready_for_production", "live"];
|
|
26918
27072
|
const revision = await request("GET", `/api/revisions/${encodeURIComponent(revisionId)}`);
|
|
@@ -27019,7 +27173,7 @@ function createHttpClient(config2) {
|
|
|
27019
27173
|
res = await fetch(url2, {
|
|
27020
27174
|
method: "GET",
|
|
27021
27175
|
headers: {
|
|
27022
|
-
Authorization: `Bearer ${
|
|
27176
|
+
Authorization: `Bearer ${getApiKey()}`,
|
|
27023
27177
|
Accept: "text/event-stream"
|
|
27024
27178
|
},
|
|
27025
27179
|
signal: controller.signal
|
|
@@ -27450,6 +27604,14 @@ function createHttpClient(config2) {
|
|
|
27450
27604
|
);
|
|
27451
27605
|
return res.checks;
|
|
27452
27606
|
},
|
|
27607
|
+
listBeatVersionChecks: async (beatVersionId, projectId, checkType) => {
|
|
27608
|
+
const qs = checkType ? `?type=${encodeURIComponent(checkType)}` : "";
|
|
27609
|
+
const res = await request(
|
|
27610
|
+
"GET",
|
|
27611
|
+
`/api/projects/${encodeURIComponent(projectId)}/beat-versions/${encodeURIComponent(beatVersionId)}/checks${qs}`
|
|
27612
|
+
);
|
|
27613
|
+
return res.checks;
|
|
27614
|
+
},
|
|
27453
27615
|
listDropChecks: async (dropId, _projectId, checkType) => {
|
|
27454
27616
|
const qs = checkType ? `?type=${encodeURIComponent(checkType)}` : "";
|
|
27455
27617
|
const res = await request(
|
|
@@ -27697,7 +27859,7 @@ function loadConfig() {
|
|
|
27697
27859
|
};
|
|
27698
27860
|
}
|
|
27699
27861
|
async function main() {
|
|
27700
|
-
console.error(`[harmonica-mcp] v${"0.
|
|
27862
|
+
console.error(`[harmonica-mcp] v${"0.32.0"} starting\u2026`);
|
|
27701
27863
|
const config2 = loadConfig();
|
|
27702
27864
|
const client = createHttpClient({
|
|
27703
27865
|
apiBaseUrl: config2.apiBaseUrl,
|