@codazen/harmonica-mcp 0.31.0 → 1.0.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 +96 -20
- package/package.json +2 -2
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
|
|
@@ -26229,16 +26286,16 @@ ${JSON.stringify(task.result, null, 2)}
|
|
|
26229
26286
|
}
|
|
26230
26287
|
);
|
|
26231
26288
|
server.tool(
|
|
26232
|
-
"
|
|
26233
|
-
|
|
26289
|
+
"start_session",
|
|
26290
|
+
'Start an agent session from a free-form message \u2014 like opening a Claude Code conversation pointed at your project. The message is the only instruction: describe what you want ("investigate why login fails", "implement revision rev-abc123", "what does the auth flow do?") and the agent resolves any entity references itself and does whatever the work needs \u2014 investigate and report findings, or change code and open a PR. A findings-only result with no PR is a valid outcome. Returns a job ID \u2014 use get_job_status to track progress. Requires agent sessions enabled on the project.',
|
|
26234
26291
|
{
|
|
26235
|
-
|
|
26236
|
-
|
|
26292
|
+
projectId: external_exports.string().min(1).describe("The project ID \u2014 the trusted scope the session is rooted at"),
|
|
26293
|
+
message: external_exports.string().min(1).describe("Free-form first message describing what the session should do")
|
|
26237
26294
|
},
|
|
26238
|
-
async ({
|
|
26295
|
+
async ({ projectId, message }) => {
|
|
26239
26296
|
try {
|
|
26240
26297
|
await assertProjectInOrg(client, projectId, ctx.orgId);
|
|
26241
|
-
const task = await client.
|
|
26298
|
+
const task = await client.startSession(projectId, message, {
|
|
26242
26299
|
name: ctx.user.name,
|
|
26243
26300
|
email: ctx.user.email
|
|
26244
26301
|
});
|
|
@@ -26246,20 +26303,20 @@ ${JSON.stringify(task.result, null, 2)}
|
|
|
26246
26303
|
content: [{
|
|
26247
26304
|
type: "text",
|
|
26248
26305
|
text: [
|
|
26249
|
-
"
|
|
26306
|
+
"Agent session queued.",
|
|
26250
26307
|
"",
|
|
26251
|
-
`**
|
|
26308
|
+
`**Project:** ${projectId}`,
|
|
26252
26309
|
`**Task ID:** ${task.taskId}`,
|
|
26253
26310
|
`**Status:** ${task.status}`,
|
|
26254
26311
|
"",
|
|
26255
|
-
"The agent
|
|
26256
|
-
"Use `get_job_status` with the job ID to check progress and see
|
|
26312
|
+
"The agent runs as a single thread: it resolves any entity references in your message and produces whatever the work needs \u2014 findings, file changes, and/or a PR.",
|
|
26313
|
+
"Use `get_job_status` with the job ID to check progress and see what it produced."
|
|
26257
26314
|
].join("\n")
|
|
26258
26315
|
}]
|
|
26259
26316
|
};
|
|
26260
26317
|
} catch (err) {
|
|
26261
|
-
const
|
|
26262
|
-
return { content: [{ type: "text", text: `Failed to start
|
|
26318
|
+
const errMessage = err instanceof Error ? err.message : String(err);
|
|
26319
|
+
return { content: [{ type: "text", text: `Failed to start session: ${errMessage}` }], isError: true };
|
|
26263
26320
|
}
|
|
26264
26321
|
}
|
|
26265
26322
|
);
|
|
@@ -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 ${
|
|
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 ${
|
|
27176
|
+
Authorization: `Bearer ${getApiKey()}`,
|
|
27101
27177
|
Accept: "text/event-stream"
|
|
27102
27178
|
},
|
|
27103
27179
|
signal: controller.signal
|
|
@@ -27340,14 +27416,14 @@ function createHttpClient(config2) {
|
|
|
27340
27416
|
return pollTaskResult(enqueued.taskId);
|
|
27341
27417
|
},
|
|
27342
27418
|
// Agent sessions
|
|
27343
|
-
|
|
27419
|
+
startSession: async (projectId, message, triggeredBy) => {
|
|
27344
27420
|
const result = await request(
|
|
27345
27421
|
"POST",
|
|
27346
|
-
`/api/
|
|
27347
|
-
triggeredBy
|
|
27422
|
+
`/api/sessions`,
|
|
27423
|
+
{ projectId, message, ...triggeredBy && { triggeredBy } }
|
|
27348
27424
|
);
|
|
27349
|
-
if (!result?.taskId) throw new Error("
|
|
27350
|
-
return { taskId: result.taskId, taskType: "
|
|
27425
|
+
if (!result?.taskId) throw new Error("Start session enqueue failed: no taskId returned");
|
|
27426
|
+
return { taskId: result.taskId, taskType: "generic_session", status: result.status ?? "pending", projectId, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
27351
27427
|
},
|
|
27352
27428
|
planRevisionBatch: async (projectId, revisionIds) => {
|
|
27353
27429
|
const result = await request(
|
|
@@ -27783,7 +27859,7 @@ function loadConfig() {
|
|
|
27783
27859
|
};
|
|
27784
27860
|
}
|
|
27785
27861
|
async function main() {
|
|
27786
|
-
console.error(`[harmonica-mcp] v${"0.
|
|
27862
|
+
console.error(`[harmonica-mcp] v${"1.0.0"} starting\u2026`);
|
|
27787
27863
|
const config2 = loadConfig();
|
|
27788
27864
|
const client = createHttpClient({
|
|
27789
27865
|
apiBaseUrl: config2.apiBaseUrl,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codazen/harmonica-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server for Harmonica — connect
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for Harmonica — connect any MCP-compatible AI assistant to Harmonica",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
7
7
|
"harmonica-mcp": "./dist/index.js"
|