import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import { KanbanManager } from "./kanban.js";
import * as path from 'path';
import * as os from 'os';
const VAULT_PATH = process.env.VAULT_PATH || path.join(os.homedir(), 'workspace', 'cursor-vault');
const DEFAULT_BOARD_NAME = process.env.OBSIDIAN_BOARD_NAME;
const kanban = new KanbanManager(VAULT_PATH);
const server = new Server(
{
name: "obsidian-kanban-mcp",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
server.setRequestHandler(ListToolsRequestSchema, async () => {
const tools: Tool[] = [
{
name: "list_boards",
description: "List all Kanban boards in the Obsidian vault",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "get_board_content",
description: "Get the columns and tasks of a Kanban board",
inputSchema: {
type: "object",
properties: {
board_name: {
type: "string",
description: "The filename of the board (e.g., 'Board.md'). Optional if OBSIDIAN_BOARD_NAME env var is set.",
},
},
},
},
{
name: "add_task",
description: "Add a new task to a specific column in a Kanban board",
inputSchema: {
type: "object",
properties: {
board_name: { type: "string", description: "Optional if OBSIDIAN_BOARD_NAME env var is set." },
column_name: { type: "string" },
task_text: { type: "string" },
description: { type: "string", description: "Longer description of the task (multiline supported)" },
labels: {
type: "array",
items: { type: "string" },
description: "List of tags/labels (e.g. ['urgent', 'bug'])"
},
acceptance_criteria: {
type: "array",
items: { type: "string" },
description: "List of acceptance criteria checklist items"
},
create_note: {
type: "boolean",
description: "If true (default), creates a new Markdown note for the task with the description, labels, and criteria, and links it on the board."
}
},
required: ["column_name", "task_text"],
},
},
{
name: "move_task",
description: "Move a task from one column to another",
inputSchema: {
type: "object",
properties: {
board_name: { type: "string", description: "Optional if OBSIDIAN_BOARD_NAME env var is set." },
task_text: { type: "string" },
from_column: { type: "string" },
to_column: { type: "string" },
},
required: ["task_text", "from_column", "to_column"],
},
},
{
name: "create_board",
description: "Create a new Kanban board",
inputSchema: {
type: "object",
properties: {
board_name: { type: "string" },
columns: {
type: "array",
items: { type: "string" }
},
},
required: ["board_name", "columns"],
},
},
];
return { tools };
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const { name, arguments: args } = request.params;
if (name === "list_boards") {
const boards = await kanban.listBoards();
return {
content: [{ type: "text", text: JSON.stringify(boards, null, 2) }],
};
}
if (name === "create_board") {
const { board_name, columns } = args as { board_name: string; columns: string[] };
await kanban.createBoard(board_name, columns);
return {
content: [{ type: "text", text: `Board '${board_name}' created.` }]
};
}
if (name === "get_board_content") {
const argsTyped = args as { board_name?: string };
const board_name = argsTyped.board_name || DEFAULT_BOARD_NAME;
if (!board_name) {
throw new Error("board_name is required (either as argument or OBSIDIAN_BOARD_NAME env var)");
}
const board = await kanban.parseBoard(board_name);
return {
content: [{ type: "text", text: JSON.stringify(board.columns, null, 2) }]
};
}
if (name === "add_task") {
const argsTyped = args as {
board_name?: string;
column_name: string;
task_text: string;
description?: string;
labels?: string[];
acceptance_criteria?: string[];
create_note?: boolean;
};
const board_name = argsTyped.board_name || DEFAULT_BOARD_NAME;
if (!board_name) {
throw new Error("board_name is required (either as argument or OBSIDIAN_BOARD_NAME env var)");
}
const { column_name, task_text, description, labels, acceptance_criteria, create_note } = argsTyped;
// Default to true if not specified
const shouldCreateNote = create_note !== undefined ? create_note : true;
await kanban.addTask(board_name, column_name, task_text, { description, labels, acceptance_criteria, create_note: shouldCreateNote });
return {
content: [{ type: "text", text: `Task added to '${column_name}'` }]
};
}
if (name === "move_task") {
const argsTyped = args as { board_name?: string; task_text: string; from_column: string; to_column: string };
const board_name = argsTyped.board_name || DEFAULT_BOARD_NAME;
if (!board_name) {
throw new Error("board_name is required (either as argument or OBSIDIAN_BOARD_NAME env var)");
}
const { task_text, from_column, to_column } = argsTyped;
await kanban.moveTaskByText(board_name, task_text, from_column, to_column);
return {
content: [{ type: "text", text: `Task moved from '${from_column}' to '${to_column}'` }]
};
}
throw new Error(`Unknown tool: ${name}`);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{ type: "text", text: `Error: ${errorMessage}` }],
isError: true,
};
}
});
async function runServer() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Obsidian Kanban MCP Server running on stdio");
}
runServer().catch((error) => {
console.error("Fatal error in main loop:", error);
process.exit(1);
});