@codazen/harmonica-mcp 0.31.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.
Files changed (2) hide show
  1. package/dist/index.js +79 -3
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -25360,6 +25360,63 @@ function registerRevisionLifecycleTools(server, ctx, client) {
25360
25360
  }
25361
25361
  }
25362
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
+ );
25363
25420
  }
25364
25421
 
25365
25422
  // ../../libs/harmonica-services/src/mcp/tools/session-tools.ts
@@ -26582,9 +26639,10 @@ function createHttpClient(config2) {
26582
26639
  }
26583
26640
  const DEFAULT_TIMEOUT_MS = parseInt(process.env.MCP_HTTP_TIMEOUT_MS || "30000", 10);
26584
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;
26585
26643
  async function request(method, path, body, timeoutMs) {
26586
26644
  const headers = {
26587
- Authorization: `Bearer ${config2.apiKey}`
26645
+ Authorization: `Bearer ${getApiKey()}`
26588
26646
  };
26589
26647
  if (body !== void 0) {
26590
26648
  headers["Content-Type"] = "application/json";
@@ -26991,6 +27049,24 @@ function createHttpClient(config2) {
26991
27049
  return { success: false, error: { code: "TRANSITION_FAILED", message } };
26992
27050
  }
26993
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
+ },
26994
27070
  reconcileRevisionToState: async (revisionId, targetState, options) => {
26995
27071
  const forwardPath = ["planning", "building", "ready_for_drop", "in_staging", "ready_for_production", "live"];
26996
27072
  const revision = await request("GET", `/api/revisions/${encodeURIComponent(revisionId)}`);
@@ -27097,7 +27173,7 @@ function createHttpClient(config2) {
27097
27173
  res = await fetch(url2, {
27098
27174
  method: "GET",
27099
27175
  headers: {
27100
- Authorization: `Bearer ${config2.apiKey}`,
27176
+ Authorization: `Bearer ${getApiKey()}`,
27101
27177
  Accept: "text/event-stream"
27102
27178
  },
27103
27179
  signal: controller.signal
@@ -27783,7 +27859,7 @@ function loadConfig() {
27783
27859
  };
27784
27860
  }
27785
27861
  async function main() {
27786
- console.error(`[harmonica-mcp] v${"0.31.0"} starting\u2026`);
27862
+ console.error(`[harmonica-mcp] v${"0.32.0"} starting\u2026`);
27787
27863
  const config2 = loadConfig();
27788
27864
  const client = createHttpClient({
27789
27865
  apiBaseUrl: config2.apiBaseUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codazen/harmonica-mcp",
3
- "version": "0.31.0",
3
+ "version": "0.32.0",
4
4
  "description": "MCP server for Harmonica — connect Claude to your Harmonica projects",
5
5
  "license": "MIT",
6
6
  "bin": {