Skip to main content
Glama
index.js19.5 kB
#!/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);

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Mieluoxxx/things_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server