Skip to main content
Glama

things_summary

Generate a filtered summary of your Things database, returning tasks, projects, areas, and tags as Markdown or JSON. Filter by areas, tags, or projects to focus on specific work.

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

  • src/index.ts:25-26 (registration)
    Registration of the things_summary tool via registerThingsSummaryTool(server)
    registerThingsSummaryTool(server);
    registerExportJsonTool(server);
  • Exported registerThingsSummaryTool function that registers the 'things_summary' tool with MCP server, defines the async handler that calls getThingsSummary and returns markdown or JSON.
    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;
          }
        }
      );
    }
  • Core logic function getThingsSummary that queries the Things SQLite database and builds the ThingsSummary data structure.
    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 schema 'summarySchema' defining input parameters: format, includeCompleted, 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'),
    });
  • Helper function generateMarkdownSummary that formats the ThingsSummary data into a human-readable Markdown string.
    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');
    }
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden. It mentions filtering and output formats but does not disclose whether the tool is read-only, if it modifies data, or any authorization needs. Basic safety is implied by the verb 'Generate' but not explicit.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single sentence that covers purpose and key capabilities efficiently. However, it lacks structural elements like bullet points or separation of concerns.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given no output schema and no annotations, the description is somewhat sparse. It mentions tasks, projects, areas, and tags but does not explain the summary's structure or depth. Adequate for basic understanding but not fully complete.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100% with detailed parameter docs. The description adds no new meaning beyond the schema, so baseline 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the verb 'Generate', the resource 'summary of your Things database', and mentions filtering options and output formats. It distinguishes well from sibling CRUD tools like add_project and update_todo.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies usage for generating summaries but does not explicitly state when to use this tool versus alternatives like export_json. No when-not or comparative guidance is provided.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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