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): 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) : 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); }, };