/** * mcp/deck.ts — Nextcloud Deck board/card management tools */ const DECK_BASE = (process.env.DECK_BASE_URL ?? "https://next.jqxp.org") + "/index.php/apps/deck/api/v1"; const OCS_BASE = (process.env.DECK_BASE_URL ?? "https://next.jqxp.org") + "/ocs/v2.php/apps/deck/api/v1.0"; const AUTH = "Basic " + btoa(`${process.env.DECK_USER ?? "claude"}:${process.env.DECK_TOKEN ?? "AH25o-cBZxD-8tMZ6-yWDKt-q9874"}`); const HEADERS: Record = { Authorization: AUTH, "OCS-APIRequest": "true", "Content-Type": "application/json", }; async function deckFetch(path: string, method = "GET", body?: any) { const res = await fetch(`${DECK_BASE}${path}`, { method, headers: HEADERS, body: body ? JSON.stringify(body) : undefined, }); if (!res.ok) { const text = await res.text(); throw new Error(`Deck ${method} ${path}: ${res.status} ${text.slice(0, 200)}`); } const text = await res.text(); if (!text) return {}; return JSON.parse(text); } async function ocsFetch(path: string, method = "GET", body?: any) { const res = await fetch(`${OCS_BASE}${path}`, { method, headers: { ...HEADERS, Accept: "application/json" }, body: body ? JSON.stringify(body) : undefined, }); if (!res.ok) { const text = await res.text(); throw new Error(`OCS ${method} ${path}: ${res.status} ${text.slice(0, 200)}`); } return res.json(); } function sanitize(text: string): string { return text .replace(/\u2014/g, "--") .replace(/\u2013/g, "-") .replace(/[\u201C\u201D]/g, '"') .replace(/[\u2018\u2019]/g, "'"); } export const tools = [ { name: "deck_get_stacks", description: "Get all stacks and cards from a Deck board.", inputSchema: { type: "object" as const, properties: { boardId: { type: "number", description: "Board ID" }, }, required: ["boardId"], }, }, { name: "deck_get_cards", description: "Get cards from a board, optionally filtered by label name.", inputSchema: { type: "object" as const, properties: { boardId: { type: "number", description: "Board ID" }, label: { type: "string", description: "Filter by label name (e.g. 'smoke')" }, }, required: ["boardId"], }, }, { name: "deck_create_board", description: "Create a new Deck board. Auto-shared with nico.", inputSchema: { type: "object" as const, properties: { title: { type: "string", description: "Board title" }, color: { type: "string", description: "Hex color without # (default: 28a745)" }, }, required: ["title"], }, }, { name: "deck_create_stack", description: "Create a stack (column) on a board.", inputSchema: { type: "object" as const, properties: { boardId: { type: "number" }, title: { type: "string" }, order: { type: "number" }, }, required: ["boardId", "title", "order"], }, }, { name: "deck_create_card", description: "Create a card in a stack.", inputSchema: { type: "object" as const, properties: { boardId: { type: "number" }, stackId: { type: "number" }, title: { type: "string" }, description: { type: "string" }, order: { type: "number" }, }, required: ["boardId", "stackId", "title"], }, }, { name: "deck_move_card", description: "Move a card to a different stack.", inputSchema: { type: "object" as const, properties: { boardId: { type: "number" }, cardId: { type: "number" }, targetStackId: { type: "number" }, order: { type: "number" }, }, required: ["boardId", "cardId", "targetStackId"], }, }, { name: "deck_add_comment", description: "Add a comment to a card. Sanitizes special characters.", inputSchema: { type: "object" as const, properties: { cardId: { type: "number" }, text: { type: "string" }, }, required: ["cardId", "text"], }, }, { name: "deck_delete_board", description: "Delete a Deck board permanently.", inputSchema: { type: "object" as const, properties: { boardId: { type: "number", description: "Board ID to delete" }, }, required: ["boardId"], }, }, { name: "deck_delete_card", description: "Delete a card from a board.", inputSchema: { type: "object" as const, properties: { boardId: { type: "number" }, stackId: { type: "number" }, cardId: { type: "number" }, }, required: ["boardId", "stackId", "cardId"], }, }, { name: "deck_create_label", description: "Create a label on a board. Returns the label with its ID.", inputSchema: { type: "object" as const, properties: { boardId: { type: "number" }, title: { type: "string", description: "Label name (e.g. 'smoke')" }, color: { type: "string", description: "Hex color without # (e.g. 'e67e22')" }, }, required: ["boardId", "title", "color"], }, }, { name: "deck_assign_label", description: "Assign a label to a card.", inputSchema: { type: "object" as const, properties: { boardId: { type: "number" }, stackId: { type: "number" }, cardId: { type: "number" }, labelId: { type: "number" }, }, required: ["boardId", "stackId", "cardId", "labelId"], }, }, ]; export async function handle(name: string, args: any): Promise { switch (name) { case "deck_get_stacks": { const stacks = await deckFetch(`/boards/${args.boardId}/stacks`); const slim = stacks.map((s: any) => ({ id: s.id, title: s.title, order: s.order, cards: (s.cards || []).map((c: any) => ({ id: c.id, title: c.title, stackId: c.stackId, order: c.order, labels: (c.labels || []).map((l: any) => l.title), description: c.description?.slice(0, 200), })), })); return { content: [{ type: "text", text: JSON.stringify(slim, null, 2) }] }; } case "deck_get_cards": { const stacks = await deckFetch(`/boards/${args.boardId}/stacks`); let cards: any[] = []; for (const stack of stacks) { if (stack.cards) cards.push(...stack.cards.map((c: any) => ({ id: c.id, title: c.title, stackId: c.stackId, stackTitle: stack.title, labels: (c.labels || []).map((l: any) => l.title), description: c.description, }))); } if (args.label) { cards = cards.filter((c: any) => c.labels?.some((l: string) => l.toLowerCase() === args.label.toLowerCase()) ); } return { content: [{ type: "text", text: JSON.stringify(cards, null, 2) }] }; } case "deck_create_board": { const board = await deckFetch("/boards", "POST", { title: args.title, color: args.color || "28a745", }); await deckFetch(`/boards/${board.id}/acl`, "POST", { type: 0, participant: "nico", permissionEdit: true, permissionShare: false, permissionManage: true, }); return { content: [{ type: "text", text: JSON.stringify(board, null, 2) }] }; } case "deck_create_stack": { const stack = await deckFetch(`/boards/${args.boardId}/stacks`, "POST", { title: args.title, order: args.order, }); return { content: [{ type: "text", text: JSON.stringify(stack, null, 2) }] }; } case "deck_create_card": { const card = await deckFetch(`/boards/${args.boardId}/stacks/${args.stackId}/cards`, "POST", { title: args.title, type: "plain", order: args.order ?? 0, description: args.description || "", }); return { content: [{ type: "text", text: JSON.stringify(card, null, 2) }] }; } case "deck_move_card": { const result = await deckFetch( `/boards/${args.boardId}/stacks/${args.targetStackId}/cards/${args.cardId}/reorder`, "PUT", { order: args.order ?? 0, stackId: args.targetStackId }, ); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } case "deck_add_comment": { const result = await ocsFetch(`/cards/${args.cardId}/comments`, "POST", { message: sanitize(args.text), }); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } case "deck_delete_board": { const result = await deckFetch(`/boards/${args.boardId}`, "DELETE"); return { content: [{ type: "text", text: `Board ${args.boardId} deleted` }] }; } case "deck_delete_card": { const result = await deckFetch(`/boards/${args.boardId}/stacks/${args.stackId}/cards/${args.cardId}`, "DELETE"); return { content: [{ type: "text", text: `Card ${args.cardId} deleted` }] }; } case "deck_create_label": { const label = await deckFetch(`/boards/${args.boardId}/labels`, "POST", { title: args.title, color: args.color, }); return { content: [{ type: "text", text: JSON.stringify(label, null, 2) }] }; } case "deck_assign_label": { await deckFetch( `/boards/${args.boardId}/stacks/${args.stackId}/cards/${args.cardId}/assignLabel`, "PUT", { labelId: args.labelId }, ); return { content: [{ type: "text", text: `Label ${args.labelId} assigned to card ${args.cardId}` }] }; } default: throw new Error(`Unknown deck tool: ${name}`); } }