#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const API_BASE_URL =
process.env.MCP_TODO_API_URL ||
"https://pub_1769256237434_c2uwy4p-prod.mvpstar.ai";
const WORKSPACE_ID = process.env.MCP_TODO_WORKSPACE_ID || "";
interface ApiResponse<T> {
success?: boolean;
data?: T;
error?: string;
}
async function callApi<T>(
action: string,
input: Record<string, unknown>
): Promise<ApiResponse<T>> {
const response = await fetch(`${API_BASE_URL}/api/mcp`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
type: "action",
workspaceId: WORKSPACE_ID,
action,
input,
}),
});
return response.json() as Promise<ApiResponse<T>>;
}
const server = new McpServer({
name: "mcp-todo-server",
version: "1.0.0",
});
// Todo Tools
server.tool(
"list_todos",
"일정 목록을 조회합니다. 카테고리나 날짜로 필터링할 수 있습니다.",
{
category: z
.enum(["scheduled", "pending", "archived"])
.optional()
.describe("카테고리 필터 (scheduled: 예정됨, pending: 미정, archived: 보관됨)"),
date: z
.string()
.optional()
.describe("날짜 필터 (YYYY-MM-DD 형식)"),
},
async ({ category, date }) => {
const input: Record<string, string> = {};
if (category) input.category = category;
if (date) input.date = date;
const result = await callApi("todo.list", input);
if (result.error) {
return {
content: [{ type: "text" as const, text: `Error: ${result.error}` }],
isError: true,
};
}
return {
content: [
{ type: "text" as const, text: JSON.stringify(result.data, null, 2) },
],
};
}
);
server.tool(
"create_todo",
"새로운 일정을 생성합니다.",
{
title: z.string().describe("일정 제목"),
category: z
.enum(["scheduled", "pending"])
.describe("카테고리 (scheduled: 예정됨, pending: 미정)"),
date: z
.string()
.optional()
.describe("날짜 (YYYY-MM-DD 형식), scheduled인 경우 권장"),
time: z.string().optional().describe("시간 (HH:mm 형식)"),
},
async ({ title, category, date, time }) => {
const input: Record<string, string> = { title, category };
if (date) input.date = date;
if (time) input.time = time;
const result = await callApi("todo.create", input);
if (result.error) {
return {
content: [{ type: "text" as const, text: `Error: ${result.error}` }],
isError: true,
};
}
return {
content: [
{
type: "text" as const,
text: `일정이 생성되었습니다:\n${JSON.stringify(result.data, null, 2)}`,
},
],
};
}
);
server.tool(
"update_todo",
"기존 일정을 수정합니다.",
{
id: z.string().describe("일정 ID"),
title: z.string().optional().describe("새로운 제목"),
category: z
.enum(["scheduled", "pending", "archived"])
.optional()
.describe("새로운 카테고리"),
isCompleted: z.boolean().optional().describe("완료 여부"),
date: z
.string()
.nullable()
.optional()
.describe("새로운 날짜 (YYYY-MM-DD 형식), null이면 미정"),
time: z
.string()
.nullable()
.optional()
.describe("새로운 시간 (HH:mm 형식)"),
},
async ({ id, title, category, isCompleted, date, time }) => {
const input: Record<string, unknown> = { id };
if (title !== undefined) input.title = title;
if (category !== undefined) input.category = category;
if (isCompleted !== undefined) input.isCompleted = isCompleted;
if (date !== undefined) input.date = date;
if (time !== undefined) input.time = time;
const result = await callApi("todo.update", input);
if (result.error) {
return {
content: [{ type: "text" as const, text: `Error: ${result.error}` }],
isError: true,
};
}
return {
content: [
{
type: "text" as const,
text: `일정이 수정되었습니다:\n${JSON.stringify(result.data, null, 2)}`,
},
],
};
}
);
server.tool(
"delete_todo",
"일정을 삭제합니다.",
{
id: z.string().describe("삭제할 일정 ID"),
},
async ({ id }) => {
const result = await callApi("todo.delete", { id });
if (result.error) {
return {
content: [{ type: "text" as const, text: `Error: ${result.error}` }],
isError: true,
};
}
return {
content: [{ type: "text" as const, text: "일정이 삭제되었습니다." }],
};
}
);
// Memo Tools
server.tool(
"list_memos",
"메모 목록을 조회합니다. 최근 수정순으로 정렬됩니다.",
{},
async () => {
const result = await callApi("memo.list", {});
if (result.error) {
return {
content: [{ type: "text" as const, text: `Error: ${result.error}` }],
isError: true,
};
}
return {
content: [
{ type: "text" as const, text: JSON.stringify(result.data, null, 2) },
],
};
}
);
server.tool(
"get_memo",
"특정 메모의 상세 내용을 조회합니다.",
{
id: z.string().describe("메모 ID"),
},
async ({ id }) => {
const result = await callApi("memo.get", { id });
if (result.error) {
return {
content: [{ type: "text" as const, text: `Error: ${result.error}` }],
isError: true,
};
}
if (!result.data) {
return {
content: [
{ type: "text" as const, text: "메모를 찾을 수 없습니다." },
],
isError: true,
};
}
return {
content: [
{ type: "text" as const, text: JSON.stringify(result.data, null, 2) },
],
};
}
);
server.tool(
"create_memo",
"새로운 메모를 생성합니다.",
{
title: z.string().describe("메모 제목"),
content: z.string().optional().describe("메모 내용"),
},
async ({ title, content }) => {
const input: Record<string, string> = { title };
if (content) input.content = content;
const result = await callApi("memo.create", input);
if (result.error) {
return {
content: [{ type: "text" as const, text: `Error: ${result.error}` }],
isError: true,
};
}
return {
content: [
{
type: "text" as const,
text: `메모가 생성되었습니다:\n${JSON.stringify(result.data, null, 2)}`,
},
],
};
}
);
server.tool(
"update_memo",
"기존 메모를 수정합니다.",
{
id: z.string().describe("메모 ID"),
title: z.string().optional().describe("새로운 제목"),
content: z.string().optional().describe("새로운 내용"),
},
async ({ id, title, content }) => {
const input: Record<string, string> = { id };
if (title !== undefined) input.title = title;
if (content !== undefined) input.content = content;
const result = await callApi("memo.update", input);
if (result.error) {
return {
content: [{ type: "text" as const, text: `Error: ${result.error}` }],
isError: true,
};
}
if (!result.data) {
return {
content: [
{ type: "text" as const, text: "메모를 찾을 수 없습니다." },
],
isError: true,
};
}
return {
content: [
{
type: "text" as const,
text: `메모가 수정되었습니다:\n${JSON.stringify(result.data, null, 2)}`,
},
],
};
}
);
server.tool(
"delete_memo",
"메모를 삭제합니다.",
{
id: z.string().describe("삭제할 메모 ID"),
},
async ({ id }) => {
const result = await callApi("memo.delete", { id });
if (result.error) {
return {
content: [{ type: "text" as const, text: `Error: ${result.error}` }],
isError: true,
};
}
return {
content: [{ type: "text" as const, text: "메모가 삭제되었습니다." }],
};
}
);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("mcp-todo MCP Server running on stdio");
}
main().catch(console.error);