Skip to main content
Glama
cristip73

MCP Server for Asana

by cristip73

asana_get_project_hierarchy

Retrieve the complete hierarchical structure of an Asana project, including sections, tasks, and subtasks, with support for both automatic and manual pagination.

Instructions

Get the complete hierarchical structure of an Asana project, including its sections, tasks, and subtasks. Supports both manual and automatic pagination.

PAGINATION GUIDE:

  1. Get all data at once: Use auto_paginate=true

  2. Manual pagination: First request with limit=N, then use the returned 'next_offset' tokens in subsequent requests

  3. Tips for large projects: Specify only needed fields, set include_subtasks=false if subtasks aren't needed

EXAMPLES:

  • For all data: {project_id:"123", auto_paginate:true}

  • For first page: {project_id:"123", limit:10}

  • For next page: {project_id:"123", limit:10, offset:"eyJ0a..."}

  • For deep subtasks: {project_id:"123", include_subtasks:true, max_subtask_depth:3} Note: offset must be a token from previous response (section.pagination_info.next_offset)

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
project_idYesID of the project to get hierarchy for
include_completed_tasksNoInclude completed tasks (default: false)
include_subtasksNoInclude subtasks for each task (default: true)
include_completed_subtasksNoInclude completed subtasks (default: follows include_completed_tasks)
max_subtask_depthNoMaximum depth of subtasks to retrieve (default: 1, meaning only direct subtasks)
opt_fields_tasksNoOptional fields for tasks (e.g. 'name,notes,assignee,due_on,completed')
opt_fields_subtasksNoOptional fields for subtasks (if not specified, uses same as tasks)
opt_fields_sectionsNoOptional fields for sections (e.g. 'name,created_at')
opt_fields_projectNoOptional fields for project (e.g. 'name,created_at,owner')
limitNoMax results per page (1-100). For pagination, set this and don't use auto_paginate
offsetNoPagination token from previous response. MUST be valid token from section.pagination_info.next_offset
auto_paginateNoIf true, automatically gets all pages and combines results (limited by max_pages)
max_pagesNoMaximum pages to fetch when auto_paginate is true (protects against infinite loops)

Implementation Reference

  • Core handler function implementing the full logic to fetch project hierarchy: project info, sections, tasks per section, subtasks recursively up to max depth, with full pagination support (auto/manual). Handles stats, errors, and returns structured data.
    // Metodă pentru obținerea structurii ierarhice complete a unui proiect
    async getProjectHierarchy(projectId: string, opts: any = {}) {
      /**
       * Get the complete hierarchical structure of an Asana project
       * Pagination features:
       * 1. Auto pagination: Set auto_paginate=true to get all results automatically
       * 2. Manual pagination: 
       *    - First request: Set limit=N (without offset)
       *    - Subsequent requests: Use limit=N with offset token from previous response
       * 3. Pagination metadata is provided at multiple levels:
       *    - Global: result.pagination_info
       *    - Section level: section.pagination_info (contains next_offset token)
       *    - Task level: task.subtasks_pagination_info (for subtasks pagination)
       * 
       * @param projectId - The project GID
       * @param opts - Options including pagination params (limit, offset, auto_paginate)
       */
      try {
        // Extrage opțiunile de paginare
        const { 
          auto_paginate = false, 
          max_pages = 10,
          limit,
          offset,
          include_subtasks = true,
          include_completed_subtasks,
          max_subtask_depth = 1,
          ...otherOpts 
        } = opts;
    
        // Initialize stats object for tracking counts
        const stats = {
          total_sections: 0,
          total_tasks: 0,
          total_subtasks: 0
        };
    
        // Pasul 1: Obține informații despre proiect
        const projectFields = "name,gid" + (opts.opt_fields_project ? `,${opts.opt_fields_project}` : "");
        const project = await this.getProject(projectId, { opt_fields: projectFields });
        
        // Pasul 2: Obține secțiunile proiectului
        const sectionFields = "name,gid" + (opts.opt_fields_sections ? `,${opts.opt_fields_sections}` : "");
        const sections = await this.getProjectSections(projectId, { opt_fields: sectionFields });
        
        // Update stats with section count
        stats.total_sections = sections ? sections.length : 0;
        
        // Verifică dacă avem secțiuni
        if (!sections || sections.length === 0) {
          return {
            project: project,
            sections: [],
            stats
          };
        }
        
        // Calculăm limita efectivă pentru task-uri, dacă este specificată
        // Dacă nu este specificată, folosim o valoare implicită care va permite API-ului să decidă
        const effectiveLimit = limit ? Math.min(Math.max(1, Number(limit)), 100) : undefined;
        
        // Pasul 3: Pentru fiecare secțiune, obține task-urile
        const sectionsWithTasks = await Promise.all(sections.map(async (section: any) => {
          const taskFields = "name,gid,completed,resource_subtype,num_subtasks" + (opts.opt_fields_tasks ? `,${opts.opt_fields_tasks}` : "");
          
          // Pregătim parametrii pentru task-uri
          const taskOpts: any = { 
            opt_fields: taskFields
          };
          
          // Adăugăm limita doar dacă este specificată
          if (effectiveLimit) {
            taskOpts.limit = effectiveLimit;
          }
          
          // Adăugăm offset doar dacă este specificat și pare valid (începe cu 'eyJ')
          if (offset && typeof offset === 'string' && offset.startsWith('eyJ')) {
            taskOpts.offset = offset;
          }
          
          // Include sau exclude task-urile completate
          if (opts.include_completed_tasks === false) {
            taskOpts.completed_since = "now";
          }
          
          // Obținem task-urile din secțiune cu sau fără paginare
          let tasks;
          if (auto_paginate) {
            // Folosim handlePaginatedResults pentru a obține toate task-urile cu paginare automată
            tasks = await this.handlePaginatedResults(
              // Initial fetch function
              () => this.tasks.getTasksForSection(section.gid, taskOpts),
              // Next page fetch function
              (nextOffset) => this.tasks.getTasksForSection(section.gid, { ...taskOpts, offset: nextOffset }),
              // Pagination options
              { auto_paginate, max_pages }
            );
          } else {
            // Obținem doar o pagină de task-uri
            try {
              const response = await this.tasks.getTasksForSection(section.gid, taskOpts);
              tasks = response.data || [];
              
              // Includem informații despre paginare în rezultat
              if (response.next_page) {
                section.pagination_info = {
                  has_more: true,
                  next_offset: response.next_page.offset
                };
              } else {
                section.pagination_info = {
                  has_more: false
                };
              }
            } catch (error) {
              console.error(`Error fetching tasks for section ${section.gid}:`, error);
              tasks = [];
              section.error = "Could not fetch tasks for this section";
            }
          }
          
          // Update total tasks count
          stats.total_tasks += tasks ? tasks.length : 0;
          
          // Pasul 4: Pentru fiecare task, obține subtask-urile dacă acestea există și dacă utilizatorul dorește
          const tasksWithSubtasks = await Promise.all(tasks.map(async (task: any) => {
            // Verifică dacă avem nevoie de subtask-uri și dacă task-ul are subtask-uri
            if (include_subtasks && task.num_subtasks && task.num_subtasks > 0) {
              try {
                // Pregătim câmpurile pentru subtask-uri
                const subtaskFields = "name,gid,completed,resource_subtype,num_subtasks" + 
                  (opts.opt_fields_subtasks ? `,${opts.opt_fields_subtasks}` : 
                   opts.opt_fields_tasks ? `,${opts.opt_fields_tasks}` : "");
                
                // Pregătim parametrii pentru subtask-uri
                const subtaskOpts: any = { 
                  opt_fields: subtaskFields
                };
                
                // Adăugăm limita doar dacă este specificată
                if (effectiveLimit) {
                  subtaskOpts.limit = effectiveLimit;
                }
                
                // Aplicăm filtrarea pentru task-uri completate (dacă este specificată)
                if (include_completed_subtasks === false) {
                  subtaskOpts.completed_since = "now";
                }
                
                // Folosim metoda corectă pentru a obține subtask-urile
                let subtasks;
                if (auto_paginate) {
                  // Cu paginare automată
                  subtasks = await this.handlePaginatedResults(
                    // Initial fetch function
                    () => this.tasks.getSubtasksForTask(task.gid, subtaskOpts),
                    // Next page fetch function
                    (nextOffset) => this.tasks.getSubtasksForTask(task.gid, { ...subtaskOpts, offset: nextOffset }),
                    // Pagination options
                    { auto_paginate, max_pages }
                  );
                } else {
                  // Fără paginare automată, doar o singură pagină
                  try {
                    const response = await this.tasks.getSubtasksForTask(task.gid, subtaskOpts);
                    subtasks = response.data || [];
                    
                    // Includem informații despre paginare în rezultat
                    if (response.next_page) {
                      task.subtasks_pagination_info = {
                        has_more: true,
                        next_offset: response.next_page.offset
                      };
                    } else {
                      task.subtasks_pagination_info = {
                        has_more: false
                      };
                    }
                  } catch (error) {
                    console.error(`Error fetching subtasks for task ${task.gid}:`, error);
                    subtasks = [];
                    task.subtasks_error = "Could not fetch subtasks for this task";
                  }
                }
                
                // Update subtasks count
                stats.total_subtasks += subtasks ? subtasks.length : 0;
                
                // If max_subtask_depth > 1, recursively fetch deeper subtasks
                if (max_subtask_depth > 1 && subtasks && subtasks.length > 0) {
                  await this.fetchSubtasksRecursively(
                    subtasks,
                    max_subtask_depth,
                    1, // Current depth
                    subtaskOpts,
                    auto_paginate,
                    max_pages,
                    stats
                  );
                }
                
                return { ...task, subtasks };
              } catch (error) {
                console.error(`Error fetching subtasks for task ${task.gid}:`, error);
                return { ...task, subtasks: [], subtasks_error: "Error fetching subtasks" };
              }
            }
            return { ...task, subtasks: [] };
          }));
          
          return { ...section, tasks: tasksWithSubtasks };
        }));
        
        // Adăugăm informații despre paginare la nivelul rezultatului
        const result = {
          project: project,
          sections: sectionsWithTasks,
          stats, // Include the statistics in the result
          pagination_info: {
            auto_paginate_used: auto_paginate,
            effective_limit: effectiveLimit,
            offset_provided: offset ? true : false
          }
        };
        
        // Returnează structura ierarhică completă
        return result;
      } catch (error: any) {
        // Oferim un mesaj de eroare mai util pentru probleme comune
        if (error.message && error.message.includes('offset')) {
          console.error("Error in getProjectHierarchy with pagination:", error);
          throw new Error(`Invalid pagination parameters: ${error.message}. Asana requires offset tokens to be obtained from previous responses.`);
        } else {
          console.error("Error in getProjectHierarchy:", error);
          throw error;
        }
      }
    }
  • Tool schema definition with name, description, and comprehensive inputSchema for validating tool call parameters including pagination, subtasks options, and optional fields.
    export const getProjectHierarchyTool: Tool = {
      name: "asana_get_project_hierarchy",
      description: "Get the complete hierarchical structure of an Asana project, including its sections, tasks, and subtasks. Supports both manual and automatic pagination.\n\n" +
      "PAGINATION GUIDE:\n" +
      "1. Get all data at once: Use auto_paginate=true\n" +
      "2. Manual pagination: First request with limit=N, then use the returned 'next_offset' tokens in subsequent requests\n" +
      "3. Tips for large projects: Specify only needed fields, set include_subtasks=false if subtasks aren't needed\n\n" +
      "EXAMPLES:\n" +
      "- For all data: {project_id:\"123\", auto_paginate:true}\n" +
      "- For first page: {project_id:\"123\", limit:10}\n" +
      "- For next page: {project_id:\"123\", limit:10, offset:\"eyJ0a...\"}\n" +
      "- For deep subtasks: {project_id:\"123\", include_subtasks:true, max_subtask_depth:3}\n" +
      "Note: offset must be a token from previous response (section.pagination_info.next_offset)",
      inputSchema: {
        type: "object",
        properties: {
          project_id: {
            type: "string",
            description: "ID of the project to get hierarchy for"
          },
          include_completed_tasks: {
            type: "boolean",
            description: "Include completed tasks (default: false)"
          },
          include_subtasks: {
            type: "boolean",
            description: "Include subtasks for each task (default: true)"
          },
          include_completed_subtasks: {
            type: "boolean",
            description: "Include completed subtasks (default: follows include_completed_tasks)"
          },
          max_subtask_depth: {
            type: "number",
            description: "Maximum depth of subtasks to retrieve (default: 1, meaning only direct subtasks)",
            minimum: 1,
            maximum: 10,
            default: 1
          },
          opt_fields_tasks: {
            type: "string",
            description: "Optional fields for tasks (e.g. 'name,notes,assignee,due_on,completed')"
          },
          opt_fields_subtasks: {
            type: "string",
            description: "Optional fields for subtasks (if not specified, uses same as tasks)"
          },
          opt_fields_sections: {
            type: "string",
            description: "Optional fields for sections (e.g. 'name,created_at')"
          },
          opt_fields_project: {
            type: "string",
            description: "Optional fields for project (e.g. 'name,created_at,owner')"
          },
          limit: {
            type: "number",
            description: "Max results per page (1-100). For pagination, set this and don't use auto_paginate",
            minimum: 1,
            maximum: 100
          },
          offset: {
            type: "string",
            description: "Pagination token from previous response. MUST be valid token from section.pagination_info.next_offset"
          },
          auto_paginate: {
            type: "boolean",
            description: "If true, automatically gets all pages and combines results (limited by max_pages)",
            default: false
          },
          max_pages: {
            type: "number",
            description: "Maximum pages to fetch when auto_paginate is true (protects against infinite loops)",
            default: 10
          }
        },
        required: ["project_id"]
      }
    };
  • Tool registration: getProjectHierarchyTool is included in the exported tools array used for MCP tool discovery.
    export const tools: Tool[] = [
      listWorkspacesTool,
      searchProjectsTool,
      getProjectTool,
      getProjectTaskCountsTool,
      getProjectSectionsTool,
      createSectionForProjectTool,
      createProjectForWorkspaceTool,
      updateProjectTool,
      reorderSectionsTool,
      getProjectStatusTool,
      getProjectStatusesForProjectTool,
      createProjectStatusTool,
      deleteProjectStatusTool,
      searchTasksTool,
      getTaskTool,
      createTaskTool,
      updateTaskTool,
      createSubtaskTool,
      getMultipleTasksByGidTool,
      addTaskToSectionTool,
      getTasksForSectionTool,
      getProjectHierarchyTool,
      getSubtasksForTaskTool,
      getTasksForProjectTool,
      getTasksForTagTool,
      getTagsForWorkspaceTool,
      addTagsToTaskTool,
      addTaskDependenciesTool,
      addTaskDependentsTool,
      setParentForTaskTool,
      addFollowersToTaskTool,
      getStoriesForTaskTool,
      createTaskStoryTool,
      getTeamsForUserTool,
      getTeamsForWorkspaceTool,
      addMembersForProjectTool,
      addFollowersForProjectTool,
      getUsersForWorkspaceTool,
      getAttachmentsForObjectTool,
      uploadAttachmentForObjectTool,
      downloadAttachmentTool
  • Switch case dispatcher in tool_handler that extracts parameters and calls the AsanaClientWrapper.getProjectHierarchy method.
    case "asana_get_project_hierarchy": {
      const { project_id, ...opts } = args;
      const response = await asanaClient.getProjectHierarchy(project_id, opts);
      return {
        content: [{ type: "text", text: JSON.stringify(response) }],
      };
    }
  • Private recursive helper method used by getProjectHierarchy to fetch deeper levels of subtasks beyond direct level.
    // Helper method to recursively fetch subtasks to a specified depth
    private async fetchSubtasksRecursively(
      tasks: any[],
      maxDepth: number,
      currentDepth: number,
      opts: any,
      auto_paginate: boolean,
      max_pages: number,
      stats: { total_subtasks: number }
    ) {
      // If we've reached the maximum depth, stop recursion
      if (currentDepth >= maxDepth) {
        return;
      }
      
      // For each task at the current depth
      for (const task of tasks) {
        // Skip if task has no subtasks
        if (!task.num_subtasks || task.num_subtasks <= 0) {
          continue;
        }
        
        try {
          // Fetch subtasks for this task
          let subtasks;
          if (auto_paginate) {
            // With auto pagination
            subtasks = await this.handlePaginatedResults(
              // Initial fetch function
              () => this.tasks.getSubtasksForTask(task.gid, opts),
              // Next page fetch function
              (nextOffset) => this.tasks.getSubtasksForTask(task.gid, { ...opts, offset: nextOffset }),
              // Pagination options
              { auto_paginate, max_pages }
            );
          } else {
            // Without auto pagination, just get one page
            const response = await this.tasks.getSubtasksForTask(task.gid, opts);
            subtasks = response.data || [];
            
            // Add pagination info to the task
            if (response.next_page) {
              task.subtasks_pagination_info = {
                has_more: true,
                next_offset: response.next_page.offset
              };
            } else {
              task.subtasks_pagination_info = {
                has_more: false
              };
            }
          }
          
          // Update subtasks count
          stats.total_subtasks += subtasks ? subtasks.length : 0;
          
          // Add subtasks to the current task
          task.subtasks = subtasks;
          
          // Recursively fetch the next level of subtasks
          if (subtasks && subtasks.length > 0) {
            await this.fetchSubtasksRecursively(
              subtasks,
              maxDepth,
              currentDepth + 1,
              opts,
              auto_paginate,
              max_pages,
              stats
            );
          }
        } catch (error) {
          console.error(`Error recursively fetching subtasks for task ${task.gid}:`, error);
          task.subtasks = [];
          task.subtasks_error = "Error fetching subtasks recursively";
        }
      }
    }

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/cristip73/mcp-server-asana'

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