things_summary
Create summaries of your Things database with customizable filters for areas, projects, and tags. Export results as Markdown for readability or JSON for further processing.
Instructions
Generate a summary of your Things database with filtering options. Returns formatted Markdown or structured JSON data for tasks, projects, areas, and tags.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| areas | No | Filter to show only specific areas by name (e.g., ["Work", "Personal"]). If not provided, shows all areas | |
| format | No | Output format for the summary. Use "markdown" for readable formatted summary (default) or "json" for structured data that can be processed by other tools | markdown |
| includeCompleted | No | Include completed tasks and projects in the summary (default: false). When true, shows recently completed items for reference | |
| projects | No | Filter to show only specific projects by name. If not provided, shows all projects | |
| tags | No | Filter to show only tasks/projects with specific tags (e.g., ["urgent", "review"]). If not provided, shows all items |
Implementation Reference
- src/tools/things-summary.ts:657-698 (handler)The main handler function for the 'things_summary' tool. It checks for macOS platform, logs the request, fetches summary data using getThingsSummary, and returns either JSON or Markdown formatted response.async (params) => { try { // Validate macOS platform if (process.platform !== 'darwin') { throw new Error('Things database access is only available on macOS'); } logger.info('Generating Things summary', { format: params.format, filters: { areas: params.areas?.length || 0, tags: params.tags?.length || 0, projects: params.projects?.length || 0, includeCompleted: params.includeCompleted } }); const data = getThingsSummary(params); if (params.format === 'json') { return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; } else { const markdown = generateMarkdownSummary(data); return { content: [{ type: "text", text: markdown }] }; } } catch (error) { logger.error('Failed to generate Things summary', { error: error instanceof Error ? error.message : error }); throw error; } }
- src/tools/things-summary.ts:75-93 (schema)Zod input schema for the things_summary tool parameters: format (markdown/json), includeCompleted, filters for areas, tags, projects.const summarySchema = z.object({ format: z.enum(['markdown', 'json']) .optional() .default('markdown') .describe('Output format for the summary. Use "markdown" for readable formatted summary (default) or "json" for structured data that can be processed by other tools'), includeCompleted: z.boolean() .optional() .default(false) .describe('Include completed tasks and projects in the summary (default: false). When true, shows recently completed items for reference'), areas: z.array(z.string()) .optional() .describe('Filter to show only specific areas by name (e.g., ["Work", "Personal"]). If not provided, shows all areas'), tags: z.array(z.string()) .optional() .describe('Filter to show only tasks/projects with specific tags (e.g., ["urgent", "review"]). If not provided, shows all items'), projects: z.array(z.string()) .optional() .describe('Filter to show only specific projects by name. If not provided, shows all projects'), });
- src/tools/things-summary.ts:652-700 (registration)The registration function that calls server.tool('things_summary', ...) to register the tool with MCP server, providing name, description, schema, and handler.export function registerThingsSummaryTool(server: McpServer): void { server.tool( 'things_summary', 'Generate a summary of your Things database with filtering options. Returns formatted Markdown or structured JSON data for tasks, projects, areas, and tags.', summarySchema.shape, async (params) => { try { // Validate macOS platform if (process.platform !== 'darwin') { throw new Error('Things database access is only available on macOS'); } logger.info('Generating Things summary', { format: params.format, filters: { areas: params.areas?.length || 0, tags: params.tags?.length || 0, projects: params.projects?.length || 0, includeCompleted: params.includeCompleted } }); const data = getThingsSummary(params); if (params.format === 'json') { return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; } else { const markdown = generateMarkdownSummary(data); return { content: [{ type: "text", text: markdown }] }; } } catch (error) { logger.error('Failed to generate Things summary', { error: error instanceof Error ? error.message : error }); throw error; } } ); }
- src/tools/things-summary.ts:235-411 (helper)Core helper function that locates Things SQLite database, executes SQL queries to fetch tasks/projects/areas/tags, applies filters, structures data into ThingsSummary interface, and compresses empty fields.function getThingsSummary(params: any): ThingsSummary { const dbPath = findThingsDatabase(); logger.info('Found Things database', { path: dbPath }); // Build status filter let statusFilter = 'status = 0 AND trashed = 0'; if (params.includeCompleted) { statusFilter = 'trashed = 0'; } // Get all areas const areasData = executeSqlQuery(dbPath, "SELECT uuid, title, visible FROM TMArea"); const areas: ThingsArea[] = areasData.map(row => { const area: any = { id: row[0], name: row[1] || 'Unnamed Area', thingsUrl: generateThingsUrl('show', row[0]) }; if (row[2] === '1') area.visible = true; return area; }); // Filter areas if specified let filteredAreas = areas; if (params.areas && params.areas.length > 0) { filteredAreas = areas.filter(area => params.areas.includes(area.name)); } // Get all tags const tagsData = executeSqlQuery(dbPath, "SELECT uuid, title, shortcut FROM TMTag"); const tags: ThingsTag[] = tagsData.map(row => ({ id: row[0], name: row[1] || 'Unnamed Tag', shortcut: row[2] || undefined, taskCount: 0, thingsUrl: generateThingsUrl('show', undefined, { filter: row[1] }) })); // Get open tasks and projects const tasksData = executeSqlQuery(dbPath, `SELECT uuid, title, notes, type, creationDate, startDate, deadline, area, project, checklistItemsCount, openChecklistItemsCount FROM TMTask WHERE ${statusFilter}` ); // Create a map for quick lookup of project names const projectMap = new Map(); tasksData.forEach(row => { if (row[3] === '1') { projectMap.set(row[0], row[1]); } }); const allTasks: ThingsTask[] = tasksData.map(row => { const taskType = row[3] === '0' ? 'task' : row[3] === '1' ? 'project' : 'heading'; const areaInfo = areas.find(area => area.id === row[7]); const projectName = row[8] ? projectMap.get(row[8]) : null; const task: any = { id: row[0], title: row[1] || 'Untitled', type: taskType, thingsUrl: generateThingsUrl('show', row[0]) }; if (row[2]) task.notes = row[2]; const creationDate = formatDate(row[4]); if (creationDate) task.creationDate = creationDate; const startDate = formatDate(row[5], true); if (startDate) task.startDate = startDate; const deadline = formatDate(row[6], true); if (deadline) task.deadline = deadline; if (areaInfo) task.area = { id: areaInfo.id, name: areaInfo.name }; if (projectName) task.project = { id: row[8], name: projectName }; const checklistTotal = parseInt(row[9]) || 0; const checklistOpen = parseInt(row[10]) || 0; if (checklistTotal > 0 || checklistOpen > 0) { task.checklistItems = { total: checklistTotal, open: checklistOpen }; } return task; }); // Get task-tag relationships const taskTagData = executeSqlQuery(dbPath, "SELECT tasks, tags FROM TMTaskTag"); taskTagData.forEach(row => { const task = allTasks.find(t => t.id === row[0]); const tag = tags.find(t => t.id === row[1]); if (task && tag) { if (!task.tags) task.tags = []; task.tags.push({ id: tag.id, name: tag.name }); tag.taskCount++; } }); // Apply tag filtering let filteredTasks = allTasks; if (params.tags && params.tags.length > 0) { filteredTasks = allTasks.filter(task => task.tags && task.tags.some(tag => params.tags.includes(tag.name)) ); } // Apply project filtering if (params.projects && params.projects.length > 0) { filteredTasks = filteredTasks.filter(task => (task.type === 'project' && params.projects.includes(task.title)) || (task.project && params.projects.includes(task.project.name)) ); } // Separate tasks by type const projects = filteredTasks.filter(task => task.type === 'project'); const tasks = filteredTasks.filter(task => task.type === 'task'); const inboxTasks = tasks.filter(task => !task.area && !task.project); const todayTasks = tasks.filter(task => { const today = new Date().toISOString().split('T')[0]; return task.startDate === today; }); // Organize tasks by area and add project tasks filteredAreas.forEach(area => { const areaProjects = projects.filter(project => project.area?.id === area.id); const areaTasks = tasks.filter(task => task.area?.id === area.id && !task.project); if (areaProjects.length > 0) area.projects = areaProjects; if (areaTasks.length > 0) area.tasks = areaTasks; }); // Add tasks to their respective projects projects.forEach(project => { const projectTasks = tasks.filter(task => task.project?.id === project.id); if (projectTasks.length > 0) { (project as any).tasks = projectTasks; } }); // Filter areas and tags to show only active ones const activeAreas = filteredAreas.filter(area => (area.projects && area.projects.length > 0) || (area.tasks && area.tasks.length > 0) ); const activeTags = tags.filter(tag => tag.taskCount > 0); const summary: any = { summary: { totalOpenTasks: tasks.length, totalActiveProjects: projects.length, totalAreas: activeAreas.length, totalTags: activeTags.length, lastUpdated: new Date().toISOString() }, urls: { showToday: generateThingsUrl('show', undefined, { list: 'today' }), showInbox: generateThingsUrl('show', undefined, { list: 'inbox' }), showProjects: generateThingsUrl('show', undefined, { list: 'projects' }), showAreas: generateThingsUrl('show', undefined, { list: 'areas' }) } }; // Only add sections that have content if (activeAreas.length > 0) summary.areas = activeAreas; if (inboxTasks.length > 0) summary.inboxTasks = inboxTasks; if (todayTasks.length > 0) summary.todayTasks = todayTasks; if (projects.length > 0) summary.projects = projects; if (activeTags.length > 0) summary.tags = activeTags; return compressObject(summary); }
- src/index.ts:12-25 (helper)Import and invocation of registerThingsSummaryTool in the main server entrypoint to register the tool with the MCP server instance.import { registerThingsSummaryTool } from './tools/things-summary.js'; import { registerExportJsonTool } from './tools/export-json.js'; const server = new McpServer({ name: 'things-mcp', version: '1.0.0' }); // Register all tools registerAddTodoTool(server); registerAddProjectTool(server); registerUpdateTodoTool(server); registerUpdateProjectTool(server); registerThingsSummaryTool(server);