#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { exec } from 'child_process';
import { promisify } from 'util';
import { buildThingsUrl } from './utils.js';
const execAsync = promisify(exec);
// 获取环境变量中的授权令牌
const DEFAULT_AUTH_TOKEN = process.env.THINGS_AUTH_TOKEN || '';
class ThingsMCPServer {
constructor() {
this.server = new Server(
{
name: 'things-mcp',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
this.setupHandlers();
this.setupErrorHandling();
}
setupErrorHandling() {
this.server.onerror = (error) => {
console.error('[MCP Error]', error);
};
process.on('SIGINT', async () => {
await this.server.close();
process.exit(0);
});
}
setupHandlers() {
// 列出所有可用工具
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'add_todo',
description: '创建新的待办事项。支持标题、备注、标签、清单、截止日期等。',
inputSchema: {
type: 'object',
properties: {
title: {
type: 'string',
description: '待办事项标题',
},
titles: {
type: 'string',
description: '批量创建待办事项,用换行符分隔',
},
notes: {
type: 'string',
description: '备注内容',
},
when: {
type: 'string',
description: '时间安排: today, tomorrow, evening, anytime, someday, 日期(yyyy-mm-dd)或日期时间(yyyy-mm-dd@HH:mm)',
},
deadline: {
type: 'string',
description: '截止日期(yyyy-mm-dd)',
},
tags: {
type: 'string',
description: '标签,逗号分隔',
},
checklistItems: {
type: 'array',
items: { type: 'string' },
description: '清单项列表',
},
listId: {
type: 'string',
description: '项目或区域的ID',
},
list: {
type: 'string',
description: '项目或区域的标题',
},
headingId: {
type: 'string',
description: '项目内标题的ID',
},
heading: {
type: 'string',
description: '项目内标题的名称',
},
completed: {
type: 'boolean',
description: '是否标记为完成',
},
canceled: {
type: 'boolean',
description: '是否标记为取消',
},
reveal: {
type: 'boolean',
description: '是否导航并显示',
},
},
},
},
{
name: 'add_project',
description: '创建新的项目。支持标题、备注、区域、标签、子任务等。',
inputSchema: {
type: 'object',
properties: {
title: {
type: 'string',
description: '项目标题',
},
notes: {
type: 'string',
description: '备注内容',
},
when: {
type: 'string',
description: '时间安排: today, tomorrow, evening, anytime, someday, 日期或日期时间',
},
deadline: {
type: 'string',
description: '截止日期(yyyy-mm-dd)',
},
tags: {
type: 'string',
description: '标签,逗号分隔',
},
areaId: {
type: 'string',
description: '区域ID',
},
area: {
type: 'string',
description: '区域标题',
},
todos: {
type: 'array',
items: { type: 'string' },
description: '子待办事项列表',
},
completed: {
type: 'boolean',
description: '是否标记为完成',
},
canceled: {
type: 'boolean',
description: '是否标记为取消',
},
reveal: {
type: 'boolean',
description: '是否导航进入项目',
},
},
},
},
{
name: 'update_todo',
description: '更新现有的待办事项。需要提供待办事项ID和授权令牌。',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: '待办事项的ID(必需)',
},
authToken: {
type: 'string',
description: '授权令牌(如未提供,将使用环境变量THINGS_AUTH_TOKEN)',
},
title: {
type: 'string',
description: '新标题',
},
notes: {
type: 'string',
description: '新备注(替换现有)',
},
prependNotes: {
type: 'string',
description: '在现有备注前添加',
},
appendNotes: {
type: 'string',
description: '在现有备注后添加',
},
when: {
type: 'string',
description: '时间安排',
},
deadline: {
type: 'string',
description: '截止日期',
},
tags: {
type: 'string',
description: '标签(替换所有)',
},
addTags: {
type: 'string',
description: '添加标签',
},
checklistItems: {
type: 'array',
items: { type: 'string' },
description: '清单项(替换所有)',
},
appendChecklistItems: {
type: 'array',
items: { type: 'string' },
description: '追加清单项',
},
prependChecklistItems: {
type: 'array',
items: { type: 'string' },
description: '前置清单项',
},
completed: {
type: 'boolean',
description: '完成状态',
},
canceled: {
type: 'boolean',
description: '取消状态',
},
reveal: {
type: 'boolean',
description: '是否显示',
},
},
required: ['id'],
},
},
{
name: 'update_project',
description: '更新现有的项目。需要提供项目ID和授权令牌。',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: '项目的ID(必需)',
},
authToken: {
type: 'string',
description: '授权令牌(如未提供,将使用环境变量THINGS_AUTH_TOKEN)',
},
title: {
type: 'string',
description: '新标题',
},
notes: {
type: 'string',
description: '新备注(替换现有)',
},
prependNotes: {
type: 'string',
description: '在现有备注前添加',
},
appendNotes: {
type: 'string',
description: '在现有备注后添加',
},
when: {
type: 'string',
description: '时间安排',
},
deadline: {
type: 'string',
description: '截止日期',
},
tags: {
type: 'string',
description: '标签(替换所有)',
},
addTags: {
type: 'string',
description: '添加标签',
},
areaId: {
type: 'string',
description: '区域ID',
},
area: {
type: 'string',
description: '区域标题',
},
completed: {
type: 'boolean',
description: '完成状态',
},
canceled: {
type: 'boolean',
description: '取消状态',
},
reveal: {
type: 'boolean',
description: '是否显示',
},
},
required: ['id'],
},
},
{
name: 'show',
description: '导航到并显示区域、项目、标签、待办事项或内置列表。',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'ID或内置列表名(inbox, today, anytime, upcoming, someday, logbook等)',
},
query: {
type: 'string',
description: '查询名称(如果未提供id)',
},
filter: {
type: 'string',
description: '按标签筛选,逗号分隔',
},
},
},
},
{
name: 'search',
description: '搜索Things中的待办事项、项目等。',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: '搜索查询文本',
},
},
},
},
{
name: 'get_version',
description: '获取Things应用和URL Scheme的版本信息。',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'json_import',
description: 'JSON批量导入待办事项和项目。支持复杂的嵌套结构。',
inputSchema: {
type: 'object',
properties: {
data: {
type: 'array',
description: 'JSON数据数组,包含to-do和project对象',
},
authToken: {
type: 'string',
description: '授权令牌(如包含更新操作则必需)',
},
reveal: {
type: 'boolean',
description: '是否显示第一个创建的项',
},
},
required: ['data'],
},
},
{
name: 'delete_todo',
description: '删除待办事项(通过将其标记为已取消)。需要提供待办事项ID和授权令牌。',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: '待办事项的ID(必需)',
},
authToken: {
type: 'string',
description: '授权令牌(如未提供,将使用环境变量THINGS_AUTH_TOKEN)',
},
},
required: ['id'],
},
},
{
name: 'delete_project',
description: '删除项目(通过将其标记为已取消)。需要提供项目ID和授权令牌。',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: '项目的ID(必需)',
},
authToken: {
type: 'string',
description: '授权令牌(如未提供,将使用环境变量THINGS_AUTH_TOKEN)',
},
},
required: ['id'],
},
},
],
}));
// 处理工具调用
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const { name, arguments: args } = request.params;
switch (name) {
case 'add_todo':
return await this.handleAddTodo(args);
case 'add_project':
return await this.handleAddProject(args);
case 'update_todo':
return await this.handleUpdateTodo(args);
case 'update_project':
return await this.handleUpdateProject(args);
case 'delete_todo':
return await this.handleDeleteTodo(args);
case 'delete_project':
return await this.handleDeleteProject(args);
case 'show':
return await this.handleShow(args);
case 'search':
return await this.handleSearch(args);
case 'get_version':
return await this.handleGetVersion();
case 'json_import':
return await this.handleJsonImport(args);
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `错误: ${error.message}`,
},
],
isError: true,
};
}
});
}
async openThingsUrl(url) {
try {
await execAsync(`open "${url}"`);
return { success: true };
} catch (error) {
throw new Error(`无法打开Things URL: ${error.message}`);
}
}
async handleAddTodo(args) {
const url = buildThingsUrl('add', args);
await this.openThingsUrl(url);
return {
content: [
{
type: 'text',
text: `✅ 待办事项创建命令已发送${args.title ? `: ${args.title}` : ''}`,
},
],
};
}
async handleAddProject(args) {
const url = buildThingsUrl('add-project', args);
await this.openThingsUrl(url);
return {
content: [
{
type: 'text',
text: `✅ 项目创建命令已发送${args.title ? `: ${args.title}` : ''}`,
},
],
};
}
async handleUpdateTodo(args) {
const authToken = args.authToken || DEFAULT_AUTH_TOKEN;
if (!authToken) {
throw new Error('需要授权令牌。请设置环境变量THINGS_AUTH_TOKEN或在参数中提供authToken');
}
const params = { ...args, authToken };
const url = buildThingsUrl('update', params);
await this.openThingsUrl(url);
return {
content: [
{
type: 'text',
text: `✅ 待办事项更新命令已发送 (ID: ${args.id})`,
},
],
};
}
async handleUpdateProject(args) {
const authToken = args.authToken || DEFAULT_AUTH_TOKEN;
if (!authToken) {
throw new Error('需要授权令牌。请设置环境变量THINGS_AUTH_TOKEN或在参数中提供authToken');
}
const params = { ...args, authToken };
const url = buildThingsUrl('update-project', params);
await this.openThingsUrl(url);
return {
content: [
{
type: 'text',
text: `✅ 项目更新命令已发送 (ID: ${args.id})`,
},
],
};
}
async handleShow(args) {
const url = buildThingsUrl('show', args);
await this.openThingsUrl(url);
return {
content: [
{
type: 'text',
text: `✅ 导航命令已发送${args.id ? ` (${args.id})` : args.query ? ` (${args.query})` : ''}`,
},
],
};
}
async handleSearch(args) {
const url = buildThingsUrl('search', args);
await this.openThingsUrl(url);
return {
content: [
{
type: 'text',
text: `🔍 搜索命令已发送${args.query ? `: ${args.query}` : ''}`,
},
],
};
}
async handleGetVersion() {
const url = buildThingsUrl('version', {});
await this.openThingsUrl(url);
return {
content: [
{
type: 'text',
text: '✅ 版本信息命令已发送。请查看Things应用获取版本信息。',
},
],
};
}
async handleJsonImport(args) {
const authToken = args.authToken || DEFAULT_AUTH_TOKEN;
const params = {
data: JSON.stringify(args.data),
reveal: args.reveal,
};
// 如果数据中包含更新操作,需要授权令牌
const hasUpdate = args.data.some(item => item.operation === 'update');
if (hasUpdate && !authToken) {
throw new Error('JSON数据包含更新操作,需要授权令牌');
}
if (authToken) {
params.authToken = authToken;
}
const url = buildThingsUrl('json', params);
await this.openThingsUrl(url);
return {
content: [
{
type: 'text',
text: `✅ JSON导入命令已发送 (${args.data.length}个项目)`,
},
],
};
}
async handleDeleteTodo(args) {
const authToken = args.authToken || DEFAULT_AUTH_TOKEN;
if (!authToken) {
throw new Error('需要授权令牌。请设置环境变量THINGS_AUTH_TOKEN或在参数中提供authToken');
}
// 通过将 canceled 设为 true 来实现删除效果
const params = {
id: args.id,
authToken,
canceled: true,
};
const url = buildThingsUrl('update', params);
await this.openThingsUrl(url);
return {
content: [
{
type: 'text',
text: `🗑️ 待办事项已删除 (ID: ${args.id})`,
},
],
};
}
async handleDeleteProject(args) {
const authToken = args.authToken || DEFAULT_AUTH_TOKEN;
if (!authToken) {
throw new Error('需要授权令牌。请设置环境变量THINGS_AUTH_TOKEN或在参数中提供authToken');
}
// 通过将 canceled 设为 true 来实现删除效果
const params = {
id: args.id,
authToken,
canceled: true,
};
const url = buildThingsUrl('update-project', params);
await this.openThingsUrl(url);
return {
content: [
{
type: 'text',
text: `🗑️ 项目已删除 (ID: ${args.id})`,
},
],
};
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Things MCP Server 已启动');
}
}
const server = new ThingsMCPServer();
server.run().catch(console.error);