Skip to main content
Glama

things_summary

Generate a summary of your Things database with filtering options for tasks, projects, areas, and tags. Output formatted Markdown or structured JSON data to organize and review your tasks.

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
NameRequiredDescriptionDefault
formatNoOutput format for the summary. Use "markdown" for readable formatted summary (default) or "json" for structured data that can be processed by other toolsmarkdown
includeCompletedNoInclude completed tasks and projects in the summary (default: false). When true, shows recently completed items for reference
areasNoFilter to show only specific areas by name (e.g., ["Work", "Personal"]). If not provided, shows all areas
tagsNoFilter to show only tasks/projects with specific tags (e.g., ["urgent", "review"]). If not provided, shows all items
projectsNoFilter to show only specific projects by name. If not provided, shows all projects

Implementation Reference

  • Core handler function that locates the Things database, executes SQL queries to fetch areas, tags, tasks, and projects, applies filters based on parameters, organizes data hierarchically, and returns a structured ThingsSummary object.
    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); }
  • Zod input schema for the things_summary tool parameters: format (markdown/json), includeCompleted, areas, tags, projects filters.
    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'), });
  • Registration function that calls server.tool('things_summary', ..., summarySchema.shape, async handler) to register the tool with the MCP server.
    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/index.ts:25-25 (registration)
    Invocation of registerThingsSummaryTool during server initialization to enable the things_summary tool.
    registerThingsSummaryTool(server);
  • Helper function to generate human-readable Markdown summary from the structured data, used when format='markdown'.
    function generateMarkdownSummary(data: ThingsSummary): string { const lines: string[] = []; // Header lines.push('# Things Database Summary'); lines.push(''); const now = new Date(); lines.push(`**Generated:** ${now.toLocaleDateString('en-US')} ${now.toLocaleTimeString('en-US')}`); lines.push(`**Last Updated:** ${data.summary.lastUpdated}`); lines.push(''); // Overview Statistics lines.push('## Overview'); lines.push(''); lines.push(`- **Open Tasks:** ${data.summary.totalOpenTasks}`); lines.push(`- **Active Projects:** ${data.summary.totalActiveProjects}`); lines.push(`- **Areas:** ${data.summary.totalAreas}`); lines.push(`- **Tags in Use:** ${data.summary.totalTags}`); lines.push(''); // Today Tasks (high priority section) if (data.todayTasks && data.todayTasks.length > 0) { lines.push('## Today'); lines.push(''); data.todayTasks.forEach(task => { let taskLine = `- [ ] **${task.title}**`; if (task.deadline) taskLine += ` (due: ${task.deadline})`; lines.push(taskLine); lines.push(` - *ID: ${task.id}*`); if (task.notes) { lines.push(` - ${task.notes}`); } if (task.area) { lines.push(` - Area: ${task.area.name}`); } if (task.project) { lines.push(` - Project: ${task.project.name}`); } if (task.tags && task.tags.length > 0) { lines.push(` - Tags: ${task.tags.map(t => `#${t.name}`).join(', ')}`); } if (task.checklistItems && task.checklistItems.total > 0) { lines.push(` - Checklist: ${task.checklistItems.open}/${task.checklistItems.total} remaining`); } }); lines.push(''); } // Inbox Tasks if (data.inboxTasks && data.inboxTasks.length > 0) { lines.push('## Inbox'); lines.push(''); data.inboxTasks.forEach(task => { let taskLine = `- [ ] **${task.title}**`; if (task.startDate) taskLine += ` (scheduled: ${task.startDate})`; if (task.deadline) taskLine += ` (due: ${task.deadline})`; lines.push(taskLine); lines.push(` - *ID: ${task.id}*`); if (task.notes) { lines.push(` - ${task.notes}`); } if (task.tags && task.tags.length > 0) { lines.push(` - Tags: ${task.tags.map(t => `#${t.name}`).join(', ')}`); } if (task.checklistItems && task.checklistItems.total > 0) { lines.push(` - Checklist: ${task.checklistItems.open}/${task.checklistItems.total} remaining`); } }); lines.push(''); } // Areas if (data.areas && data.areas.length > 0) { lines.push('## Areas'); lines.push(''); data.areas.forEach(area => { lines.push(`### ${area.name}`); lines.push(`*ID: ${area.id}*`); if (area.visible === false) { lines.push('*Status: Hidden area*'); } lines.push(''); // Area projects if (area.projects && area.projects.length > 0) { lines.push('**Projects:**'); area.projects.forEach(project => { lines.push(`- [ ] **${project.title}**`); lines.push(` - *ID: ${project.id}*`); if (project.notes) { lines.push(` - ${project.notes}`); } if (project.startDate) { lines.push(` - Scheduled: ${project.startDate}`); } if (project.deadline) { lines.push(` - Due: ${project.deadline}`); } if (project.tags && project.tags.length > 0) { lines.push(` - Tags: ${project.tags.map(t => `#${t.name}`).join(', ')}`); } // Project tasks if (project.tasks && project.tasks.length > 0) { lines.push(' - **Tasks:**'); project.tasks.forEach(task => { let taskLine = ` - [ ] ${task.title}`; if (task.startDate) taskLine += ` (${task.startDate})`; if (task.deadline) taskLine += ` (due: ${task.deadline})`; lines.push(taskLine); if (task.notes) { lines.push(` - ${task.notes}`); } if (task.tags && task.tags.length > 0) { lines.push(` - ${task.tags.map(t => `#${t.name}`).join(', ')}`); } if (task.checklistItems && task.checklistItems.total > 0) { lines.push(` - ${task.checklistItems.open}/${task.checklistItems.total} remaining`); } }); } }); lines.push(''); } // Area tasks if (area.tasks && area.tasks.length > 0) { lines.push('**Tasks:**'); area.tasks.forEach(task => { let taskLine = `- [ ] ${task.title}`; if (task.startDate) taskLine += ` (${task.startDate})`; if (task.deadline) taskLine += ` (due: ${task.deadline})`; lines.push(taskLine); if (task.notes) { lines.push(` - ${task.notes}`); } if (task.tags && task.tags.length > 0) { lines.push(` - Tags: ${task.tags.map(t => `#${t.name}`).join(', ')}`); } if (task.checklistItems && task.checklistItems.total > 0) { lines.push(` - Checklist: ${task.checklistItems.open}/${task.checklistItems.total} remaining`); } }); lines.push(''); } }); } // Standalone Projects if (data.projects && data.projects.length > 0) { const standaloneProjects = data.projects.filter(p => !p.area); if (standaloneProjects.length > 0) { lines.push('## Projects'); lines.push(''); standaloneProjects.forEach(project => { lines.push(`### ${project.title}`); lines.push(`*ID: ${project.id}*`); if (project.notes) { lines.push(`${project.notes}`); } if (project.startDate) { lines.push(`**Start:** ${project.startDate}`); } if (project.deadline) { lines.push(`**Due:** ${project.deadline}`); } if (project.tags && project.tags.length > 0) { lines.push(`**Tags:** ${project.tags.map(t => `#${t.name}`).join(', ')}`); } lines.push(''); // Project tasks if (project.tasks && project.tasks.length > 0) { lines.push('**Tasks:**'); project.tasks.forEach(task => { let taskLine = `- [ ] ${task.title}`; if (task.startDate) taskLine += ` (${task.startDate})`; if (task.deadline) taskLine += ` (due: ${task.deadline})`; lines.push(taskLine); if (task.notes) { lines.push(` - ${task.notes}`); } if (task.tags && task.tags.length > 0) { lines.push(` - ${task.tags.map(t => `#${t.name}`).join(', ')}`); } if (task.checklistItems && task.checklistItems.total > 0) { lines.push(` - Checklist: ${task.checklistItems.open}/${task.checklistItems.total} remaining`); } }); lines.push(''); } }); } } // Tags if (data.tags && data.tags.length > 0) { lines.push('## Tags'); lines.push(''); const sortedTags = [...data.tags].sort((a, b) => b.taskCount - a.taskCount); sortedTags.forEach(tag => { lines.push(`### #${tag.name}`); lines.push(`- **Task Count:** ${tag.taskCount}`); if (tag.shortcut) { lines.push(`- **Shortcut:** ${tag.shortcut}`); } lines.push(`- *ID: ${tag.id}*`); lines.push(''); }); } // Navigation URLs lines.push('## Quick Navigation'); lines.push(''); lines.push(`- [Today](${data.urls.showToday})`); lines.push(`- [Inbox](${data.urls.showInbox})`); lines.push(`- [Projects](${data.urls.showProjects})`); lines.push(`- [Areas](${data.urls.showAreas})`); lines.push(''); // Footer lines.push('---'); lines.push(''); return lines.join('\n'); }

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/wbopan/things-mcp'

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