* feat: kan mcp server initial attempt * fix: card was missing options and added default fallbacks * fix: label creating with better information for colors and presets
145 lines
5.5 KiB
TypeScript
145 lines
5.5 KiB
TypeScript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
import { z } from "zod";
|
|
import { kanRequest } from "../client.js";
|
|
|
|
export function registerBoardTools(server: McpServer): void {
|
|
server.tool(
|
|
"list_boards",
|
|
"List all boards in a workspace. Requires the workspace publicId — use find_workspace_by_name first if you only know the workspace name.",
|
|
{ workspacePublicId: z.string().min(12).describe("The workspace's 12-character public ID (not the name). Get it from list_workspaces or find_workspace_by_name first.") },
|
|
async ({ workspacePublicId }) => {
|
|
const data = await kanRequest("GET", `/workspaces/${workspacePublicId}/boards`);
|
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
},
|
|
);
|
|
|
|
server.tool(
|
|
"find_board_by_name",
|
|
"Find a board by workspace name and board name (both case-insensitive). Resolves workspace name → publicId, then board name → publicId automatically. Use this when you only know names.",
|
|
{
|
|
workspaceName: z.string().describe("The workspace name (e.g. 'UC Roleplay')"),
|
|
boardName: z.string().describe("The board name (e.g. 'Mechanics Rework')"),
|
|
},
|
|
async ({ workspaceName, boardName }) => {
|
|
const workspaces = await kanRequest<{ publicId: string; name: string }[]>("GET", "/workspaces");
|
|
const workspace = workspaces.find(
|
|
(w) => w.name.toLowerCase() === workspaceName.toLowerCase(),
|
|
);
|
|
if (!workspace) {
|
|
const names = workspaces.map((w) => w.name).join(", ");
|
|
return {
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: `No workspace found with name "${workspaceName}". Available: ${names}`,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
const boards = await kanRequest<{ publicId: string; name: string }[]>(
|
|
"GET",
|
|
`/workspaces/${workspace.publicId}/boards`,
|
|
);
|
|
const board = boards.find(
|
|
(b) => b.name.toLowerCase() === boardName.toLowerCase(),
|
|
);
|
|
if (!board) {
|
|
const names = boards.map((b) => b.name).join(", ");
|
|
return {
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: `No board found with name "${boardName}" in workspace "${workspaceName}". Available boards: ${names}`,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
return { content: [{ type: "text", text: JSON.stringify(board, null, 2) }] };
|
|
},
|
|
);
|
|
|
|
server.tool(
|
|
"get_board",
|
|
"Get a board by its public ID, including its lists and cards",
|
|
{
|
|
boardPublicId: z.string().describe("The board's public ID"),
|
|
labelPublicId: z.string().optional().describe("Filter cards by label public ID"),
|
|
memberPublicId: z.string().optional().describe("Filter cards by member public ID"),
|
|
},
|
|
async ({ boardPublicId, labelPublicId, memberPublicId }) => {
|
|
const params = new URLSearchParams();
|
|
if (labelPublicId) params.set("labelPublicId", labelPublicId);
|
|
if (memberPublicId) params.set("memberPublicId", memberPublicId);
|
|
const qs = params.toString() ? `?${params}` : "";
|
|
const data = await kanRequest("GET", `/boards/${boardPublicId}${qs}`);
|
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
},
|
|
);
|
|
|
|
server.tool(
|
|
"get_board_by_slug",
|
|
"Get a board by workspace slug and board slug",
|
|
{
|
|
workspaceSlug: z.string().describe("The workspace slug"),
|
|
boardSlug: z.string().describe("The board slug"),
|
|
},
|
|
async ({ workspaceSlug, boardSlug }) => {
|
|
const data = await kanRequest("GET", `/workspaces/${workspaceSlug}/boards/${boardSlug}`);
|
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
},
|
|
);
|
|
|
|
server.tool(
|
|
"create_board",
|
|
"Create a new board in a workspace",
|
|
{
|
|
workspacePublicId: z.string().describe("The workspace's public ID"),
|
|
name: z.string().describe("Board name"),
|
|
slug: z.string().optional().describe("URL-friendly slug (auto-generated if omitted)"),
|
|
visibility: z
|
|
.enum(["public", "private"])
|
|
.optional()
|
|
.describe("Board visibility (default: private)"),
|
|
},
|
|
async ({ workspacePublicId, name, slug, visibility }) => {
|
|
const data = await kanRequest("POST", `/workspaces/${workspacePublicId}/boards`, {
|
|
name,
|
|
slug,
|
|
visibility,
|
|
});
|
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
},
|
|
);
|
|
|
|
server.tool(
|
|
"update_board",
|
|
"Update a board's name, slug, visibility, or favorite status",
|
|
{
|
|
boardPublicId: z.string().describe("The board's public ID"),
|
|
name: z.string().optional().describe("New board name"),
|
|
slug: z.string().optional().describe("New board slug"),
|
|
visibility: z.enum(["public", "private"]).optional().describe("New visibility"),
|
|
isFavorite: z.boolean().optional().describe("Whether the board is favorited"),
|
|
},
|
|
async ({ boardPublicId, name, slug, visibility, isFavorite }) => {
|
|
const data = await kanRequest("PUT", `/boards/${boardPublicId}`, {
|
|
name,
|
|
slug,
|
|
visibility,
|
|
isFavorite,
|
|
});
|
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
},
|
|
);
|
|
|
|
server.tool(
|
|
"delete_board",
|
|
"Delete a board (soft delete)",
|
|
{ boardPublicId: z.string().describe("The board's public ID") },
|
|
async ({ boardPublicId }) => {
|
|
const data = await kanRequest("DELETE", `/boards/${boardPublicId}`);
|
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
},
|
|
);
|
|
}
|