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');
    }
Behavior3/5

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

With no annotations provided, the description carries the full burden of behavioral disclosure. It mentions the tool 'Returns formatted Markdown or structured JSON data,' which gives some output behavior, but lacks details on permissions, rate limits, side effects, or error handling. It adequately describes the core operation but misses deeper behavioral traits.

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 appropriately sized with two sentences that efficiently convey purpose and output options. It's front-loaded with the main action and avoids unnecessary details, though it could be slightly more structured by explicitly listing key features.

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 the tool's moderate complexity (5 parameters, no output schema, no annotations), the description is adequate but incomplete. It covers the core functionality and output formats but lacks details on error cases, data scope limitations, or integration with sibling tools. It meets minimum viability but has clear gaps for a tool with filtering capabilities.

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%, so the schema already documents all 5 parameters thoroughly. The description adds minimal value beyond the schema by mentioning 'filtering options' and output formats, but doesn't provide additional syntax, examples, or contextual meaning. Baseline 3 is appropriate as the schema does the heavy lifting.

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

Purpose4/5

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

The description clearly states the tool's purpose: 'Generate a summary of your Things database with filtering options.' It specifies the verb (generate), resource (Things database), and scope (summary with filtering). However, it doesn't explicitly differentiate from sibling tools like 'export_json' which might also provide data exports, leaving room for ambiguity.

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 through 'filtering options' and mentions output formats, but provides no explicit guidance on when to use this tool versus alternatives like 'export_json' or other siblings. It suggests context for generating summaries but lacks clear when/when-not directives or named alternatives.

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