Patchpass V1
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
import {
|
||||
cancelChangeRequest,
|
||||
consumeApproval,
|
||||
createChangeRequest,
|
||||
getChangeRequestForAgent,
|
||||
listChangeRequestsForAgent,
|
||||
updateChangeRequest,
|
||||
CreateRequestInput,
|
||||
} from "../../ChangeRequestService";
|
||||
import { Change } from "../../ChangeNormalization";
|
||||
import { CHANGES_SCHEMA, McpToolDef, errResult, fromService, json } from "../shared";
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
function coerceChanges(raw: unknown): Change[] | { error: string } {
|
||||
if (!Array.isArray(raw) || raw.length === 0) {
|
||||
return { error: "changes must be a non-empty array" };
|
||||
}
|
||||
const out: Change[] = [];
|
||||
for (const [i, item] of raw.entries()) {
|
||||
if (!item || typeof item !== "object") return { error: `changes[${i}] must be an object` };
|
||||
const c = item as any;
|
||||
if (c.type === "unified_diff") {
|
||||
if (typeof c.path !== "string" || typeof c.content !== "string") {
|
||||
return { error: `changes[${i}] (unified_diff) needs string 'path' and 'content'` };
|
||||
}
|
||||
out.push({ type: "unified_diff", path: c.path, content: c.content });
|
||||
} else if (c.type === "config") {
|
||||
if (typeof c.path !== "string") return { error: `changes[${i}] (config) needs a string 'path'` };
|
||||
out.push({
|
||||
type: "config",
|
||||
path: c.path,
|
||||
before: c.before,
|
||||
after: c.after,
|
||||
content_type: typeof c.content_type === "string" ? c.content_type : undefined,
|
||||
});
|
||||
} else if (c.type === "custom") {
|
||||
if (typeof c.label !== "string") return { error: `changes[${i}] (custom) needs a string 'label'` };
|
||||
out.push({ type: "custom", label: c.label, before: c.before, after: c.after });
|
||||
} else {
|
||||
return { error: `changes[${i}] has invalid type '${c.type}'` };
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function buildInput(args: Record<string, unknown>): CreateRequestInput | { error: string } {
|
||||
const title = typeof args.title === "string" ? args.title : undefined;
|
||||
if (!title) return { error: "title is required" };
|
||||
const changes = coerceChanges(args.changes);
|
||||
if ("error" in changes) return { error: changes.error };
|
||||
return {
|
||||
title,
|
||||
description: typeof args.description === "string" ? args.description : null,
|
||||
changes,
|
||||
expires_in: typeof args.expires_in === "number" ? args.expires_in : undefined,
|
||||
metadata:
|
||||
args.metadata && typeof args.metadata === "object"
|
||||
? (args.metadata as Record<string, unknown>)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const commonProps = {
|
||||
title: { type: "string", description: "Short, human-readable title (1–200 chars)" },
|
||||
description: { type: "string", description: "Optional context for the reviewer" },
|
||||
changes: CHANGES_SCHEMA,
|
||||
expires_in: {
|
||||
type: "integer",
|
||||
description: "Seconds until the request expires (60–43200; default 1800 = 30 min)",
|
||||
},
|
||||
metadata: {
|
||||
type: "object",
|
||||
description: "Optional free-form context, e.g. { repository, environment }",
|
||||
},
|
||||
};
|
||||
|
||||
export const createRequest: McpToolDef = {
|
||||
name: "create_request",
|
||||
description:
|
||||
"Submit a change request for human approval. Returns request_id, approval_url, and state (PENDING). Poll get_request until decided.",
|
||||
inputSchema: { type: "object", required: ["title", "changes"], properties: commonProps },
|
||||
async handler(args, { agent }) {
|
||||
const input = buildInput(args);
|
||||
if ("error" in input) return errResult(input.error);
|
||||
return fromService(await createChangeRequest(agent, input));
|
||||
},
|
||||
};
|
||||
|
||||
export const updateRequest: McpToolDef = {
|
||||
name: "update_request",
|
||||
description:
|
||||
"Update a PENDING or CHANGES_REQUESTED request with revised content (e.g. after the human requested changes). Resets the request to PENDING for re-review.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
required: ["request_id", "title", "changes"],
|
||||
properties: { request_id: { type: "string" }, ...commonProps },
|
||||
},
|
||||
async handler(args, { agent }) {
|
||||
const requestId = typeof args.request_id === "string" ? args.request_id : undefined;
|
||||
if (!requestId) return errResult("request_id is required");
|
||||
const input = buildInput(args);
|
||||
if ("error" in input) return errResult(input.error);
|
||||
return fromService(await updateChangeRequest(agent, requestId, input));
|
||||
},
|
||||
};
|
||||
|
||||
export const getRequest: McpToolDef = {
|
||||
name: "get_request",
|
||||
description:
|
||||
"Fetch a request by id: its current state, comment, and (once decided) the platform-signed receipt.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
required: ["request_id"],
|
||||
properties: { request_id: { type: "string" } },
|
||||
},
|
||||
async handler(args, { agent }) {
|
||||
const requestId = typeof args.request_id === "string" ? args.request_id : undefined;
|
||||
if (!requestId) return errResult("request_id is required");
|
||||
return fromService(await getChangeRequestForAgent(agent, requestId));
|
||||
},
|
||||
};
|
||||
|
||||
export const cancelRequest: McpToolDef = {
|
||||
name: "cancel_request",
|
||||
description: "Cancel your own request before a decision is made (PENDING or CHANGES_REQUESTED).",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
required: ["request_id"],
|
||||
properties: { request_id: { type: "string" } },
|
||||
},
|
||||
async handler(args, { agent }) {
|
||||
const requestId = typeof args.request_id === "string" ? args.request_id : undefined;
|
||||
if (!requestId) return errResult("request_id is required");
|
||||
return fromService(await cancelChangeRequest(agent, requestId));
|
||||
},
|
||||
};
|
||||
|
||||
export const consumeApprovalTool: McpToolDef = {
|
||||
name: "consume_approval",
|
||||
description:
|
||||
"Consume an APPROVED request (single-use) before applying changes. Returns the signed receipt. Fails if not APPROVED or already consumed.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
required: ["request_id"],
|
||||
properties: { request_id: { type: "string" } },
|
||||
},
|
||||
async handler(args, { agent }) {
|
||||
const requestId = typeof args.request_id === "string" ? args.request_id : undefined;
|
||||
if (!requestId) return errResult("request_id is required");
|
||||
return fromService(await consumeApproval(agent, requestId));
|
||||
},
|
||||
};
|
||||
|
||||
export const listRequests: McpToolDef = {
|
||||
name: "list_requests",
|
||||
description: "List your recent change requests, optionally filtered by state.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
state: {
|
||||
type: "string",
|
||||
enum: [
|
||||
"PENDING",
|
||||
"CHANGES_REQUESTED",
|
||||
"APPROVED",
|
||||
"REJECTED",
|
||||
"EXPIRED",
|
||||
"CONSUMED",
|
||||
"CANCELLED",
|
||||
],
|
||||
},
|
||||
page: { type: "integer" },
|
||||
page_size: { type: "integer" },
|
||||
},
|
||||
},
|
||||
async handler(args, { agent }) {
|
||||
const data = await listChangeRequestsForAgent(agent, {
|
||||
state: typeof args.state === "string" ? args.state : undefined,
|
||||
page: typeof args.page === "number" ? args.page : 1,
|
||||
pageSize: typeof args.page_size === "number" ? args.page_size : 20,
|
||||
});
|
||||
return json(data);
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user